mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Enforce no-inline-qualified-paths via clippy absolute_paths lint
Add clippy.toml with absolute-paths-max-segments = 2 (allowing std/core/alloc) and enable the absolute_paths = "warn" lint workspace-wide. Fix all ~300 violations across the codebase: replace 3+-segment inline paths with use statements so call sites read as operations::create() rather than fabro_workflows::operations::create(). The demo module gets an allow attribute since it constructs many API types by design. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
41e1809b21
commit
0b90305432
163 changed files with 1787 additions and 1644 deletions
|
|
@ -67,6 +67,7 @@ object_store = "0.12.5"
|
|||
|
||||
[workspace.lints.clippy]
|
||||
wildcard_imports = "warn"
|
||||
absolute_paths = "warn"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
|
|
|
|||
2
clippy.toml
Normal file
2
clippy.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
absolute-paths-max-segments = 2
|
||||
absolute-paths-allowed-crates = ["std", "core", "alloc"]
|
||||
|
|
@ -2,13 +2,14 @@ use crate::profiles::EnvContext;
|
|||
use crate::sandbox::Sandbox;
|
||||
use crate::skills::Skill;
|
||||
use crate::subagent::{
|
||||
make_close_agent_tool, make_send_input_tool, make_spawn_agent_tool, SessionFactory,
|
||||
SubAgentManager,
|
||||
make_close_agent_tool, make_send_input_tool, make_spawn_agent_tool, make_wait_tool,
|
||||
SessionFactory, SubAgentManager,
|
||||
};
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
use fabro_llm::types::ToolDefinition;
|
||||
use fabro_model::{Catalog, Provider};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub trait AgentProfile: Send + Sync {
|
||||
fn provider(&self) -> Provider;
|
||||
|
|
@ -43,7 +44,7 @@ pub trait AgentProfile: Send + Sync {
|
|||
|
||||
fn register_subagent_tools(
|
||||
&mut self,
|
||||
manager: Arc<tokio::sync::Mutex<SubAgentManager>>,
|
||||
manager: Arc<Mutex<SubAgentManager>>,
|
||||
session_factory: SessionFactory,
|
||||
current_depth: usize,
|
||||
) {
|
||||
|
|
@ -55,7 +56,7 @@ pub trait AgentProfile: Send + Sync {
|
|||
self.tool_registry_mut()
|
||||
.register(make_send_input_tool(manager.clone()));
|
||||
self.tool_registry_mut()
|
||||
.register(crate::subagent::make_wait_tool(manager.clone()));
|
||||
.register(make_wait_tool(manager.clone()));
|
||||
self.tool_registry_mut()
|
||||
.register(make_close_agent_tool(manager));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
use crate::config::ToolApprovalFn;
|
||||
use crate::config::{ToolApprovalAdapter, ToolApprovalFn, ToolHookCallback};
|
||||
use crate::error::AbortReason;
|
||||
use crate::tools::WebFetchSummarizer;
|
||||
use crate::truncation;
|
||||
use crate::{
|
||||
subagent::{SessionFactory, SubAgentManager},
|
||||
AgentEvent, AgentProfile, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile,
|
||||
Session, SessionConfig, Turn,
|
||||
Sandbox, Session, SessionConfig, Turn,
|
||||
};
|
||||
use clap::{Args, Parser};
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::error::SdkError;
|
||||
use fabro_llm::middleware::{Middleware, NextFn, NextStreamFn};
|
||||
use fabro_llm::provider::StreamEventStream;
|
||||
use fabro_llm::types::{Request, Response};
|
||||
use fabro_mcp::config::McpServerConfig;
|
||||
use fabro_model::{Catalog, ModelRef, Provider};
|
||||
use fabro_util::terminal::Styles;
|
||||
use std::io::{IsTerminal, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::signal;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
|
||||
/// Public arguments for the agent command, usable from an external CLI.
|
||||
#[derive(Args)]
|
||||
|
|
@ -173,12 +183,9 @@ fn summarizer_model_id(provider: Provider) -> ModelRef {
|
|||
}
|
||||
}
|
||||
|
||||
fn build_summarizer(
|
||||
provider: Provider,
|
||||
llm_client: Option<Client>,
|
||||
) -> Option<crate::tools::WebFetchSummarizer> {
|
||||
fn build_summarizer(provider: Provider, llm_client: Option<Client>) -> Option<WebFetchSummarizer> {
|
||||
let client = llm_client?;
|
||||
Some(crate::tools::WebFetchSummarizer {
|
||||
Some(WebFetchSummarizer {
|
||||
client,
|
||||
model_id: summarizer_model_id(provider),
|
||||
})
|
||||
|
|
@ -218,7 +225,7 @@ fn format_tool_args(args: &serde_json::Value, cwd: &str) -> String {
|
|||
serde_json::Value::String(s) => {
|
||||
let s = s.strip_prefix(&cwd_prefix).unwrap_or(s);
|
||||
let display = if s.len() > 80 {
|
||||
format!("{}...", &s[..crate::truncation::floor_char_boundary(s, 77)])
|
||||
format!("{}...", &s[..truncation::floor_char_boundary(s, 77)])
|
||||
} else {
|
||||
s.to_string()
|
||||
};
|
||||
|
|
@ -273,12 +280,8 @@ struct DebugMiddleware {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl fabro_llm::middleware::Middleware for DebugMiddleware {
|
||||
async fn handle_complete(
|
||||
&self,
|
||||
request: fabro_llm::types::Request,
|
||||
next: fabro_llm::middleware::NextFn,
|
||||
) -> Result<fabro_llm::types::Response, fabro_llm::error::SdkError> {
|
||||
impl Middleware for DebugMiddleware {
|
||||
async fn handle_complete(&self, request: Request, next: NextFn) -> Result<Response, SdkError> {
|
||||
let s = self.styles;
|
||||
eprintln!(
|
||||
"{}",
|
||||
|
|
@ -306,9 +309,9 @@ impl fabro_llm::middleware::Middleware for DebugMiddleware {
|
|||
|
||||
async fn handle_stream(
|
||||
&self,
|
||||
request: fabro_llm::types::Request,
|
||||
next: fabro_llm::middleware::NextStreamFn,
|
||||
) -> Result<fabro_llm::provider::StreamEventStream, fabro_llm::error::SdkError> {
|
||||
request: Request,
|
||||
next: NextStreamFn,
|
||||
) -> Result<StreamEventStream, SdkError> {
|
||||
next(request).await
|
||||
}
|
||||
}
|
||||
|
|
@ -319,12 +322,8 @@ struct VerboseMiddleware {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl fabro_llm::middleware::Middleware for VerboseMiddleware {
|
||||
async fn handle_complete(
|
||||
&self,
|
||||
request: fabro_llm::types::Request,
|
||||
next: fabro_llm::middleware::NextFn,
|
||||
) -> Result<fabro_llm::types::Response, fabro_llm::error::SdkError> {
|
||||
impl Middleware for VerboseMiddleware {
|
||||
async fn handle_complete(&self, request: Request, next: NextFn) -> Result<Response, SdkError> {
|
||||
let s = self.styles;
|
||||
eprintln!(
|
||||
"{}\n{}",
|
||||
|
|
@ -344,16 +343,16 @@ impl fabro_llm::middleware::Middleware for VerboseMiddleware {
|
|||
|
||||
async fn handle_stream(
|
||||
&self,
|
||||
request: fabro_llm::types::Request,
|
||||
next: fabro_llm::middleware::NextStreamFn,
|
||||
) -> Result<fabro_llm::provider::StreamEventStream, fabro_llm::error::SdkError> {
|
||||
request: Request,
|
||||
next: NextStreamFn,
|
||||
) -> Result<StreamEventStream, SdkError> {
|
||||
next(request).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_with_args(
|
||||
args: AgentArgs,
|
||||
mcp_servers: Vec<fabro_mcp::config::McpServerConfig>,
|
||||
mcp_servers: Vec<McpServerConfig>,
|
||||
) -> anyhow::Result<()> {
|
||||
run_with_args_and_client(args, None, mcp_servers).await
|
||||
}
|
||||
|
|
@ -361,7 +360,7 @@ pub async fn run_with_args(
|
|||
pub async fn run_with_args_and_client(
|
||||
args: AgentArgs,
|
||||
llm_client: Option<Client>,
|
||||
mcp_servers: Vec<fabro_mcp::config::McpServerConfig>,
|
||||
mcp_servers: Vec<McpServerConfig>,
|
||||
) -> anyhow::Result<()> {
|
||||
// Resolve color support once, leak to get 'static lifetime for use across threads
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
|
|
@ -407,7 +406,7 @@ pub async fn run_with_args_and_client(
|
|||
// Build sandbox
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let cwd_str = cwd.to_string_lossy().to_string();
|
||||
let env: Arc<dyn crate::Sandbox> = Arc::new(crate::ReadBeforeWriteSandbox::new(Arc::new(
|
||||
let env: Arc<dyn Sandbox> = Arc::new(crate::ReadBeforeWriteSandbox::new(Arc::new(
|
||||
LocalSandbox::new(cwd),
|
||||
)));
|
||||
|
||||
|
|
@ -415,8 +414,7 @@ pub async fn run_with_args_and_client(
|
|||
let permissions = args.permissions.unwrap_or(PermissionLevel::ReadWrite);
|
||||
let is_interactive = std::io::stdin().is_terminal() && !args.auto_approve;
|
||||
let tool_approval = build_tool_approval(permissions, is_interactive, styles);
|
||||
let tool_hooks: Arc<dyn crate::config::ToolHookCallback> =
|
||||
Arc::new(crate::config::ToolApprovalAdapter(tool_approval));
|
||||
let tool_hooks: Arc<dyn ToolHookCallback> = Arc::new(ToolApprovalAdapter(tool_approval));
|
||||
|
||||
let config = SessionConfig {
|
||||
tool_hooks: Some(tool_hooks.clone()),
|
||||
|
|
@ -426,7 +424,7 @@ pub async fn run_with_args_and_client(
|
|||
};
|
||||
|
||||
// Register subagent tools
|
||||
let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(
|
||||
let manager = Arc::new(AsyncMutex::new(SubAgentManager::new(
|
||||
config.max_subagent_depth,
|
||||
)));
|
||||
let manager_for_callback = manager.clone();
|
||||
|
|
@ -490,11 +488,11 @@ pub async fn run_with_args_and_client(
|
|||
let cancel_token = session.cancel_token();
|
||||
let abort_reason = session.abort_reason_handle();
|
||||
tokio::spawn(async move {
|
||||
tokio::signal::ctrl_c().await.ok();
|
||||
signal::ctrl_c().await.ok();
|
||||
{
|
||||
let mut guard = abort_reason.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if guard.is_none() {
|
||||
*guard = Some(crate::error::AbortReason::Cancelled);
|
||||
*guard = Some(AbortReason::Cancelled);
|
||||
}
|
||||
}
|
||||
cancel_token.cancel();
|
||||
|
|
@ -562,7 +560,7 @@ pub async fn run_with_args_and_client(
|
|||
..
|
||||
} => {
|
||||
let task_preview = if task.len() > 60 {
|
||||
&task[..crate::truncation::floor_char_boundary(task, 60)]
|
||||
&task[..truncation::floor_char_boundary(task, 60)]
|
||||
} else {
|
||||
task
|
||||
};
|
||||
|
|
@ -773,7 +771,7 @@ mod tests {
|
|||
#[test]
|
||||
fn build_profile_can_register_subagent_tools() {
|
||||
let mut profile = build_profile(Provider::Anthropic, "model", None);
|
||||
let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(1)));
|
||||
let manager = Arc::new(AsyncMutex::new(SubAgentManager::new(1)));
|
||||
let factory: SessionFactory = Arc::new(|| {
|
||||
panic!("factory should not be called in this test");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use crate::error::AgentError;
|
|||
use crate::event::EventEmitter;
|
||||
use crate::file_tracker::FileTracker;
|
||||
use crate::history::History;
|
||||
use crate::truncation;
|
||||
use crate::types::{AgentEvent, Turn};
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::types::{Message, Request};
|
||||
|
|
@ -212,7 +213,7 @@ pub fn render_turns_for_summary(turns: &[Turn]) -> String {
|
|||
let truncated = if args_str.len() > 500 {
|
||||
format!(
|
||||
"{}...",
|
||||
&args_str[..crate::truncation::floor_char_boundary(&args_str, 500)]
|
||||
&args_str[..truncation::floor_char_boundary(&args_str, 500)]
|
||||
)
|
||||
} else {
|
||||
args_str
|
||||
|
|
@ -226,8 +227,7 @@ pub fn render_turns_for_summary(turns: &[Turn]) -> String {
|
|||
let truncated = if content_str.len() > 500 {
|
||||
format!(
|
||||
"{}...",
|
||||
&content_str
|
||||
[..crate::truncation::floor_char_boundary(&content_str, 500)]
|
||||
&content_str[..truncation::floor_char_boundary(&content_str, 500)]
|
||||
)
|
||||
} else {
|
||||
content_str
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::collections::HashMap;
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use fabro_llm::types::ReasoningEffort;
|
||||
use fabro_mcp::config::McpServerConfig;
|
||||
|
||||
/// Callback invoked before each tool execution. Return `Ok(())` to allow,
|
||||
|
|
@ -63,7 +64,7 @@ pub struct SessionConfig {
|
|||
pub max_tool_rounds_per_input: usize,
|
||||
pub default_command_timeout_ms: u64,
|
||||
pub max_command_timeout_ms: u64,
|
||||
pub reasoning_effort: Option<fabro_llm::types::ReasoningEffort>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
pub speed: Option<String>,
|
||||
pub tool_output_limits: HashMap<String, usize>,
|
||||
pub tool_line_limits: HashMap<String, usize>,
|
||||
|
|
@ -187,14 +188,11 @@ mod tests {
|
|||
fn config_with_custom_values() {
|
||||
let config = SessionConfig {
|
||||
max_turns: 50,
|
||||
reasoning_effort: Some(fabro_llm::types::ReasoningEffort::High),
|
||||
reasoning_effort: Some(ReasoningEffort::High),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(config.max_turns, 50);
|
||||
assert_eq!(
|
||||
config.reasoning_effort,
|
||||
Some(fabro_llm::types::ReasoningEffort::High)
|
||||
);
|
||||
assert_eq!(config.reasoning_effort, Some(ReasoningEffort::High));
|
||||
assert_eq!(config.max_tool_rounds_per_input, 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,37 @@
|
|||
use crate::agent_profile::AgentProfile;
|
||||
use crate::compaction::{check_context_usage, compact_context};
|
||||
use crate::config::SessionConfig;
|
||||
use crate::error::{AbortReason, AgentError};
|
||||
use crate::event::EventEmitter;
|
||||
use crate::file_tracker::FileTracker;
|
||||
use crate::history::History;
|
||||
use crate::loop_detection::detect_loop;
|
||||
use crate::mcp_integration;
|
||||
use crate::memory::discover_memory;
|
||||
use crate::profiles::EnvContext;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::skills::{
|
||||
default_skill_dirs, discover_skills, expand_skill, make_use_skill_tool, Skill,
|
||||
default_skill_dirs, discover_skills, expand_skill, make_use_skill_tool, ExpandedInput, Skill,
|
||||
};
|
||||
use crate::types::{AgentEvent, SessionState, Turn};
|
||||
use crate::subagent::{SubAgentEventCallback, SubAgentManager};
|
||||
use crate::tool_execution::execute_tool_calls;
|
||||
use crate::types::{AgentEvent, SessionEvent, SessionState, Turn};
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::error::{ProviderErrorKind, SdkError};
|
||||
use fabro_llm::generate::StreamAccumulator;
|
||||
use fabro_llm::provider::StreamEventStream;
|
||||
use fabro_llm::types::{Message, Request, StreamEvent, ToolChoice};
|
||||
use fabro_mcp::config::McpServerConfig;
|
||||
use fabro_llm::retry;
|
||||
use fabro_llm::types::{
|
||||
ContentPart, Message, ReasoningEffort, Request, RetryPolicy, StreamEvent, ToolChoice,
|
||||
};
|
||||
use fabro_mcp::config::{McpServerConfig, McpTransport};
|
||||
use fabro_mcp::connection_manager::McpConnectionManager;
|
||||
use futures::StreamExt;
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
use tokio::sync::{broadcast, Mutex as AsyncMutex};
|
||||
use tokio::time;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
|
|
@ -43,8 +53,8 @@ pub struct Session {
|
|||
skills: Vec<Skill>,
|
||||
system_prompt: String,
|
||||
file_tracker: FileTracker,
|
||||
tool_env: Option<std::collections::HashMap<String, String>>,
|
||||
subagent_manager: Option<Arc<tokio::sync::Mutex<crate::subagent::SubAgentManager>>>,
|
||||
tool_env: Option<HashMap<String, String>>,
|
||||
subagent_manager: Option<Arc<AsyncMutex<SubAgentManager>>>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
|
|
@ -54,7 +64,7 @@ impl Session {
|
|||
provider_profile: Arc<dyn AgentProfile>,
|
||||
sandbox: Arc<dyn Sandbox>,
|
||||
config: SessionConfig,
|
||||
subagent_manager: Option<Arc<tokio::sync::Mutex<crate::subagent::SubAgentManager>>>,
|
||||
subagent_manager: Option<Arc<AsyncMutex<SubAgentManager>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
|
|
@ -79,7 +89,7 @@ impl Session {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_tool_env(&mut self, env: std::collections::HashMap<String, String>) {
|
||||
pub fn set_tool_env(&mut self, env: HashMap<String, String>) {
|
||||
self.tool_env = Some(env);
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +139,7 @@ impl Session {
|
|||
// then rewrite the config to Http using the sandbox's preview URL.
|
||||
let mcp_servers = self.resolve_sandbox_mcp_servers().await;
|
||||
|
||||
let mut manager = fabro_mcp::connection_manager::McpConnectionManager::new();
|
||||
let mut manager = McpConnectionManager::new();
|
||||
let results = manager.start_servers(&mcp_servers).await;
|
||||
|
||||
for (server_name, result) in &results {
|
||||
|
|
@ -156,7 +166,7 @@ impl Session {
|
|||
}
|
||||
|
||||
let manager = Arc::new(manager);
|
||||
let mcp_tools = crate::mcp_integration::make_mcp_tools(manager);
|
||||
let mcp_tools = mcp_integration::make_mcp_tools(manager);
|
||||
if let Some(profile) = Arc::get_mut(&mut self.provider_profile) {
|
||||
for tool in mcp_tools {
|
||||
profile.tool_registry_mut().register(tool);
|
||||
|
|
@ -189,7 +199,7 @@ impl Session {
|
|||
|
||||
for config in &self.config.mcp_servers {
|
||||
match &config.transport {
|
||||
fabro_mcp::config::McpTransport::Sandbox { command, port, env } => {
|
||||
McpTransport::Sandbox { command, port, env } => {
|
||||
let port = *port;
|
||||
match self.start_sandbox_mcp_server(command, port, env).await {
|
||||
Ok((url, headers)) => {
|
||||
|
|
@ -200,7 +210,7 @@ impl Session {
|
|||
);
|
||||
resolved.push(McpServerConfig {
|
||||
name: config.name.clone(),
|
||||
transport: fabro_mcp::config::McpTransport::Http { url, headers },
|
||||
transport: McpTransport::Http { url, headers },
|
||||
startup_timeout_secs: config.startup_timeout_secs,
|
||||
tool_timeout_secs: config.tool_timeout_secs,
|
||||
});
|
||||
|
|
@ -347,7 +357,7 @@ impl Session {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<crate::types::SessionEvent> {
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<SessionEvent> {
|
||||
self.event_emitter.subscribe()
|
||||
}
|
||||
|
||||
|
|
@ -410,9 +420,9 @@ impl Session {
|
|||
&mut self,
|
||||
client: &Client,
|
||||
request: &Request,
|
||||
retry_policy: &fabro_llm::types::RetryPolicy,
|
||||
retry_policy: &RetryPolicy,
|
||||
) -> Result<StreamEventStream, AgentError> {
|
||||
let stream_result = fabro_llm::retry::retry(retry_policy, || {
|
||||
let stream_result = retry::retry(retry_policy, || {
|
||||
let client = client.clone();
|
||||
let request = request.clone();
|
||||
async move { client.stream(&request).await }
|
||||
|
|
@ -442,7 +452,7 @@ impl Session {
|
|||
|
||||
/// Build a callback that forwards `AgentEvent`s through this session's emitter.
|
||||
#[must_use]
|
||||
pub fn event_callback(&self) -> crate::subagent::SubAgentEventCallback {
|
||||
pub fn event_callback(&self) -> SubAgentEventCallback {
|
||||
let emitter = self.event_emitter.clone();
|
||||
let session_id = self.id.clone();
|
||||
Arc::new(move |event| {
|
||||
|
|
@ -498,7 +508,7 @@ impl Session {
|
|||
self.transition(SessionState::Closed);
|
||||
}
|
||||
|
||||
pub fn set_reasoning_effort(&mut self, effort: Option<fabro_llm::types::ReasoningEffort>) {
|
||||
pub fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffort>) {
|
||||
self.config.reasoning_effort = effort;
|
||||
}
|
||||
|
||||
|
|
@ -530,7 +540,7 @@ impl Session {
|
|||
let token = self.cancel_token.clone();
|
||||
let reason_handle = self.abort_reason.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(duration).await;
|
||||
time::sleep(duration).await;
|
||||
{
|
||||
let mut guard = reason_handle.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if guard.is_none() {
|
||||
|
|
@ -581,7 +591,7 @@ impl Session {
|
|||
|
||||
// Expand skill references in input
|
||||
let expanded = if self.skills.is_empty() {
|
||||
crate::skills::ExpandedInput {
|
||||
ExpandedInput {
|
||||
text: input.to_string(),
|
||||
skill_name: None,
|
||||
}
|
||||
|
|
@ -661,7 +671,7 @@ impl Session {
|
|||
let retry_session_id = self.id.clone();
|
||||
let retry_provider = self.provider_profile.provider().as_str().to_string();
|
||||
let retry_model = self.provider_profile.model().to_string();
|
||||
let retry_policy = fabro_llm::types::RetryPolicy {
|
||||
let retry_policy = RetryPolicy {
|
||||
max_retries: 3,
|
||||
on_retry: Some(std::sync::Arc::new(move |err, attempt, delay| {
|
||||
retry_emitter.emit(
|
||||
|
|
@ -782,13 +792,7 @@ impl Session {
|
|||
.message
|
||||
.content
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
matches!(
|
||||
p,
|
||||
fabro_llm::types::ContentPart::Other { .. }
|
||||
| fabro_llm::types::ContentPart::Thinking(_)
|
||||
)
|
||||
})
|
||||
.filter(|p| matches!(p, ContentPart::Other { .. } | ContentPart::Thinking(_)))
|
||||
.cloned()
|
||||
.collect();
|
||||
let usage = response.usage.clone();
|
||||
|
|
@ -824,7 +828,7 @@ impl Session {
|
|||
round_count += 1;
|
||||
|
||||
// Execute tool calls (parallel or sequential based on provider)
|
||||
let results = crate::tool_execution::execute_tool_calls(
|
||||
let results = execute_tool_calls(
|
||||
&tool_calls,
|
||||
true,
|
||||
self.provider_profile.tool_registry(),
|
||||
|
|
@ -878,7 +882,7 @@ impl Session {
|
|||
}
|
||||
|
||||
async fn compact_if_needed(&mut self) {
|
||||
let over_threshold = crate::compaction::check_context_usage(
|
||||
let over_threshold = check_context_usage(
|
||||
&self.system_prompt,
|
||||
&self.history,
|
||||
self.provider_profile.as_ref(),
|
||||
|
|
@ -887,7 +891,7 @@ impl Session {
|
|||
&self.id,
|
||||
);
|
||||
if over_threshold && self.config.enable_context_compaction {
|
||||
if let Err(e) = crate::compaction::compact_context(
|
||||
if let Err(e) = compact_context(
|
||||
&mut self.history,
|
||||
&self.llm_client,
|
||||
self.provider_profile.as_ref(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use crate::types::{AgentEvent, Turn};
|
|||
use fabro_llm::types::ToolDefinition;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub type SessionFactory = Arc<dyn Fn() -> Session + Send + Sync>;
|
||||
|
|
@ -26,7 +28,7 @@ pub enum SubAgentStatus {
|
|||
}
|
||||
|
||||
pub struct SubAgent {
|
||||
task: Option<tokio::task::JoinHandle<Result<SubAgentResult, AgentError>>>,
|
||||
task: Option<JoinHandle<Result<SubAgentResult, AgentError>>>,
|
||||
followup_queue: Arc<Mutex<VecDeque<String>>>,
|
||||
cancel_token: CancellationToken,
|
||||
depth: usize,
|
||||
|
|
@ -303,7 +305,7 @@ impl SubAgentManager {
|
|||
}
|
||||
|
||||
pub fn make_spawn_agent_tool(
|
||||
manager: Arc<tokio::sync::Mutex<SubAgentManager>>,
|
||||
manager: Arc<AsyncMutex<SubAgentManager>>,
|
||||
session_factory: SessionFactory,
|
||||
current_depth: usize,
|
||||
) -> RegisteredTool {
|
||||
|
|
@ -358,7 +360,7 @@ pub fn make_spawn_agent_tool(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn make_send_input_tool(manager: Arc<tokio::sync::Mutex<SubAgentManager>>) -> RegisteredTool {
|
||||
pub fn make_send_input_tool(manager: Arc<AsyncMutex<SubAgentManager>>) -> RegisteredTool {
|
||||
RegisteredTool {
|
||||
definition: ToolDefinition {
|
||||
name: "send_input".into(),
|
||||
|
|
@ -393,7 +395,7 @@ pub fn make_send_input_tool(manager: Arc<tokio::sync::Mutex<SubAgentManager>>) -
|
|||
}
|
||||
}
|
||||
|
||||
pub fn make_wait_tool(manager: Arc<tokio::sync::Mutex<SubAgentManager>>) -> RegisteredTool {
|
||||
pub fn make_wait_tool(manager: Arc<AsyncMutex<SubAgentManager>>) -> RegisteredTool {
|
||||
RegisteredTool {
|
||||
definition: ToolDefinition {
|
||||
name: "wait".into(),
|
||||
|
|
@ -425,7 +427,7 @@ pub fn make_wait_tool(manager: Arc<tokio::sync::Mutex<SubAgentManager>>) -> Regi
|
|||
}
|
||||
}
|
||||
|
||||
pub fn make_close_agent_tool(manager: Arc<tokio::sync::Mutex<SubAgentManager>>) -> RegisteredTool {
|
||||
pub fn make_close_agent_tool(manager: Arc<AsyncMutex<SubAgentManager>>) -> RegisteredTool {
|
||||
RegisteredTool {
|
||||
definition: ToolDefinition {
|
||||
name: "close_agent".into(),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use crate::config::{SessionConfig, ToolHookCallback, ToolHookDecision};
|
||||
use crate::event::EventEmitter;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
use crate::tool_registry::{RegisteredTool, ToolContext, ToolRegistry};
|
||||
use crate::truncation::truncate_tool_output;
|
||||
use crate::types::AgentEvent;
|
||||
use fabro_llm::types::ToolResult;
|
||||
use fabro_llm::types::{ToolCall, ToolResult};
|
||||
use futures::future;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
|
@ -13,7 +14,7 @@ use tracing::debug;
|
|||
/// Execute tool calls, choosing parallel or sequential based on `parallel` flag.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute_tool_calls(
|
||||
tool_calls: &[fabro_llm::types::ToolCall],
|
||||
tool_calls: &[ToolCall],
|
||||
parallel: bool,
|
||||
registry: &ToolRegistry,
|
||||
env: Arc<dyn Sandbox>,
|
||||
|
|
@ -55,7 +56,7 @@ pub async fn execute_tool_calls(
|
|||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn execute_tool_calls_sequential(
|
||||
tool_calls: &[fabro_llm::types::ToolCall],
|
||||
tool_calls: &[ToolCall],
|
||||
registry: &ToolRegistry,
|
||||
env: Arc<dyn Sandbox>,
|
||||
tool_hooks: Option<&Arc<dyn ToolHookCallback>>,
|
||||
|
|
@ -91,7 +92,7 @@ async fn execute_tool_calls_sequential(
|
|||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn execute_tool_calls_parallel(
|
||||
tool_calls: &[fabro_llm::types::ToolCall],
|
||||
tool_calls: &[ToolCall],
|
||||
registry: &ToolRegistry,
|
||||
env: Arc<dyn Sandbox>,
|
||||
tool_hooks: Option<&Arc<dyn ToolHookCallback>>,
|
||||
|
|
@ -132,13 +133,13 @@ async fn execute_tool_calls_parallel(
|
|||
})
|
||||
.collect();
|
||||
|
||||
futures::future::join_all(futures).await
|
||||
future::join_all(futures).await
|
||||
}
|
||||
|
||||
/// Execute a single tool call with event emission and output truncation.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute_and_emit_one_tool(
|
||||
tc: &fabro_llm::types::ToolCall,
|
||||
tc: &ToolCall,
|
||||
registry: &ToolRegistry,
|
||||
env: Arc<dyn Sandbox>,
|
||||
tool_hooks: Option<&Arc<dyn ToolHookCallback>>,
|
||||
|
|
@ -165,8 +166,8 @@ pub async fn execute_and_emit_one_tool(
|
|||
/// Execute a single tool call with event emission, using a pre-looked-up tool reference.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn execute_and_emit_one_tool_with_lookup(
|
||||
tc: &fabro_llm::types::ToolCall,
|
||||
registered_tool: Option<&crate::tool_registry::RegisteredTool>,
|
||||
tc: &ToolCall,
|
||||
registered_tool: Option<&RegisteredTool>,
|
||||
env: Arc<dyn Sandbox>,
|
||||
tool_hooks: Option<&Arc<dyn ToolHookCallback>>,
|
||||
cancel_token: CancellationToken,
|
||||
|
|
@ -262,8 +263,8 @@ async fn execute_and_emit_one_tool_with_lookup(
|
|||
|
||||
/// Execute a single tool call: argument validation and execution.
|
||||
async fn execute_one_tool(
|
||||
tc: &fabro_llm::types::ToolCall,
|
||||
registered_tool: Option<&crate::tool_registry::RegisteredTool>,
|
||||
tc: &ToolCall,
|
||||
registered_tool: Option<&RegisteredTool>,
|
||||
env: Arc<dyn Sandbox>,
|
||||
cancel_token: CancellationToken,
|
||||
tool_env: Option<&HashMap<String, String>>,
|
||||
|
|
@ -276,7 +277,7 @@ async fn execute_one_tool(
|
|||
return ToolResult::error(&tc.id, validation_error);
|
||||
}
|
||||
|
||||
let ctx = crate::tool_registry::ToolContext {
|
||||
let ctx = ToolContext {
|
||||
env,
|
||||
cancel: cancel_token,
|
||||
tool_env: tool_env.cloned(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::config::SessionConfig;
|
||||
use crate::sandbox::GrepOptions;
|
||||
use crate::tool_registry::RegisteredTool;
|
||||
use crate::tool_registry::{RegisteredTool, ToolRegistry};
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::types::{Message, Request, ToolDefinition};
|
||||
use fabro_model::ModelRef;
|
||||
|
|
@ -47,7 +47,7 @@ fn html_to_markdown(text: &str) -> String {
|
|||
/// `SessionConfig` (e.g. with a longer `default_command_timeout_ms`) for providers
|
||||
/// that need non-default shell behavior.
|
||||
pub fn register_core_tools(
|
||||
registry: &mut crate::tool_registry::ToolRegistry,
|
||||
registry: &mut ToolRegistry,
|
||||
config: &SessionConfig,
|
||||
summarizer: Option<WebFetchSummarizer>,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
use crate::error::AgentError;
|
||||
use fabro_llm::error::SdkError;
|
||||
use fabro_llm::types::{ContentPart, ThinkingData, ToolCall, ToolResult, Usage};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::SystemTime;
|
||||
|
||||
mod system_time_iso8601 {
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, SecondsFormat, Utc};
|
||||
use serde::de::Error as DeError;
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use std::time::SystemTime;
|
||||
|
||||
|
|
@ -12,7 +15,7 @@ mod system_time_iso8601 {
|
|||
S: Serializer,
|
||||
{
|
||||
let dt: DateTime<Utc> = (*time).into();
|
||||
serializer.serialize_str(&dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true))
|
||||
serializer.serialize_str(&dt.to_rfc3339_opts(SecondsFormat::Millis, true))
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
|
||||
|
|
@ -20,7 +23,7 @@ mod system_time_iso8601 {
|
|||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let dt = DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom)?;
|
||||
let dt = DateTime::parse_from_rfc3339(&s).map_err(DeError::custom)?;
|
||||
Ok(dt.with_timezone(&Utc).into())
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +131,7 @@ pub enum AgentEvent {
|
|||
is_error: bool,
|
||||
},
|
||||
Error {
|
||||
error: crate::error::AgentError,
|
||||
error: AgentError,
|
||||
},
|
||||
Warning {
|
||||
kind: String,
|
||||
|
|
@ -160,7 +163,7 @@ pub enum AgentEvent {
|
|||
model: String,
|
||||
attempt: usize,
|
||||
delay_secs: f64,
|
||||
error: fabro_llm::error::SdkError,
|
||||
error: SdkError,
|
||||
},
|
||||
SubAgentSpawned {
|
||||
agent_id: String,
|
||||
|
|
@ -176,7 +179,7 @@ pub enum AgentEvent {
|
|||
SubAgentFailed {
|
||||
agent_id: String,
|
||||
depth: usize,
|
||||
error: crate::error::AgentError,
|
||||
error: AgentError,
|
||||
},
|
||||
SubAgentClosed {
|
||||
agent_id: String,
|
||||
|
|
@ -481,7 +484,7 @@ mod tests {
|
|||
let event = AgentEvent::SubAgentFailed {
|
||||
agent_id: "sa-1".into(),
|
||||
depth: 0,
|
||||
error: crate::error::AgentError::ToolExecution("timeout".into()),
|
||||
error: AgentError::ToolExecution("timeout".into()),
|
||||
};
|
||||
assert!(matches!(event, AgentEvent::SubAgentFailed { depth: 0, .. }));
|
||||
}
|
||||
|
|
@ -527,7 +530,7 @@ mod tests {
|
|||
AgentEvent::SubAgentFailed {
|
||||
agent_id: "sa-1".into(),
|
||||
depth: 0,
|
||||
error: crate::error::AgentError::ToolExecution("oops".into()),
|
||||
error: AgentError::ToolExecution("oops".into()),
|
||||
},
|
||||
AgentEvent::SubAgentClosed {
|
||||
agent_id: "sa-1".into(),
|
||||
|
|
@ -669,7 +672,7 @@ mod tests {
|
|||
#[test]
|
||||
fn error_event_serde_roundtrip_with_agent_error() {
|
||||
let event = AgentEvent::Error {
|
||||
error: crate::error::AgentError::Llm(fabro_llm::error::SdkError::Network {
|
||||
error: AgentError::Llm(fabro_llm::error::SdkError::Network {
|
||||
message: "refused".into(),
|
||||
source: None,
|
||||
}),
|
||||
|
|
@ -720,7 +723,7 @@ mod tests {
|
|||
let event = AgentEvent::SubAgentFailed {
|
||||
agent_id: "sa-1".into(),
|
||||
depth: 0,
|
||||
error: crate::error::AgentError::ToolExecution("cmd failed".into()),
|
||||
error: AgentError::ToolExecution("cmd failed".into()),
|
||||
};
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
let deserialized: AgentEvent = serde_json::from_str(&json).unwrap();
|
||||
|
|
@ -735,7 +738,7 @@ mod tests {
|
|||
#[test]
|
||||
fn error_event_preserves_error_type_through_json() {
|
||||
let event = AgentEvent::Error {
|
||||
error: crate::error::AgentError::ToolExecution("cmd failed".into()),
|
||||
error: AgentError::ToolExecution("cmd failed".into()),
|
||||
};
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::{env, fs, path::Path};
|
||||
|
||||
use schemars::schema::Schema;
|
||||
use typify::{TypeSpace, TypeSpaceSettings};
|
||||
|
||||
fn main() {
|
||||
|
|
@ -23,10 +24,10 @@ fn main() {
|
|||
.as_object()
|
||||
.expect("no components/schemas in spec");
|
||||
|
||||
let named_schemas: Vec<(String, schemars::schema::Schema)> = schemas
|
||||
let named_schemas: Vec<(String, Schema)> = schemas
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
let schema: schemars::schema::Schema = serde_json::from_value(value.clone())
|
||||
let schema: Schema = serde_json::from_value(value.clone())
|
||||
.unwrap_or_else(|e| panic!("failed to parse schema {name}: {e}"));
|
||||
(name.clone(), schema)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#[allow(clippy::derivable_impls)]
|
||||
#[allow(clippy::absolute_paths, clippy::derivable_impls)]
|
||||
mod generated {
|
||||
include!(concat!(env!("OUT_DIR"), "/openapi_types.rs"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use axum::Router;
|
|||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::oneshot;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
|
@ -108,7 +110,7 @@ fn parse_event_metadata(body: &[u8]) -> (String, String) {
|
|||
/// A running webhook listener that can be shut down.
|
||||
pub struct WebhookListener {
|
||||
port: u16,
|
||||
shutdown_tx: tokio::sync::oneshot::Sender<()>,
|
||||
shutdown_tx: oneshot::Sender<()>,
|
||||
}
|
||||
|
||||
impl WebhookListener {
|
||||
|
|
@ -132,7 +134,7 @@ pub async fn spawn_webhook_listener(secret: Vec<u8>) -> anyhow::Result<WebhookLi
|
|||
.route("/webhooks/github", post(webhook_handler))
|
||||
.with_state(state);
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, router)
|
||||
|
|
@ -198,7 +200,7 @@ impl WebhookManager {
|
|||
}
|
||||
|
||||
async fn enable_tailscale_funnel(port: u16) -> anyhow::Result<String> {
|
||||
let output = tokio::process::Command::new("tailscale")
|
||||
let output = Command::new("tailscale")
|
||||
.args(["funnel", &port.to_string()])
|
||||
.output()
|
||||
.await?;
|
||||
|
|
@ -209,7 +211,7 @@ async fn enable_tailscale_funnel(port: u16) -> anyhow::Result<String> {
|
|||
}
|
||||
|
||||
// Get the funnel URL from `tailscale funnel status`
|
||||
let status_output = tokio::process::Command::new("tailscale")
|
||||
let status_output = Command::new("tailscale")
|
||||
.args(["funnel", "status"])
|
||||
.output()
|
||||
.await?;
|
||||
|
|
@ -233,7 +235,7 @@ async fn enable_tailscale_funnel(port: u16) -> anyhow::Result<String> {
|
|||
}
|
||||
|
||||
async fn disable_tailscale_funnel(port: u16) {
|
||||
match tokio::process::Command::new("tailscale")
|
||||
match Command::new("tailscale")
|
||||
.args(["funnel", "off", &port.to_string()])
|
||||
.output()
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ use std::sync::Arc;
|
|||
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::request::Parts;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
|
||||
use rustls_pki_types::CertificateDer;
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::error::ApiError;
|
||||
use fabro_config::server::ApiSettings;
|
||||
|
||||
/// JWT claims for service-to-service authentication.
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -57,7 +59,7 @@ pub fn decode_pem_env(name: &str, value: &str) -> String {
|
|||
if value.starts_with("-----") {
|
||||
return value.to_string();
|
||||
}
|
||||
let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, value)
|
||||
let bytes = base64::Engine::decode(&BASE64_STANDARD, value)
|
||||
.unwrap_or_else(|e| panic!("{name} is not valid PEM or base64: {e}"));
|
||||
String::from_utf8(bytes)
|
||||
.unwrap_or_else(|e| panic!("{name} base64 decoded to invalid UTF-8: {e}"))
|
||||
|
|
@ -67,10 +69,7 @@ pub fn decode_pem_env(name: &str, value: &str) -> String {
|
|||
///
|
||||
/// Call this once at startup before serving requests. Panics if the
|
||||
/// configuration is invalid (JWT strategy but no public key, or mTLS without TLS config).
|
||||
pub fn resolve_auth_mode(
|
||||
api_config: &fabro_config::server::ApiSettings,
|
||||
allowed_usernames: Vec<String>,
|
||||
) -> AuthMode {
|
||||
pub fn resolve_auth_mode(api_config: &ApiSettings, allowed_usernames: Vec<String>) -> AuthMode {
|
||||
use fabro_config::server::ApiAuthStrategy;
|
||||
|
||||
if api_config.authentication_strategies.is_empty() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#[allow(clippy::wildcard_imports)]
|
||||
#[allow(clippy::wildcard_imports, clippy::absolute_paths)]
|
||||
mod demo;
|
||||
pub mod error;
|
||||
pub mod github_webhooks;
|
||||
|
|
|
|||
|
|
@ -2,18 +2,23 @@ use std::path::PathBuf;
|
|||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use fabro_config::server::{load_server_settings, resolve_storage_dir};
|
||||
use fabro_model::{Catalog, Provider};
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::git::GitAuthor;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::time::interval;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use clap::Args;
|
||||
|
||||
use fabro_config::FabroSettings;
|
||||
|
||||
use crate::jwt_auth::{AuthMode, AuthStrategy};
|
||||
use crate::server::build_router;
|
||||
use crate::tls::ClientAuth;
|
||||
use crate::github_webhooks::WebhookManager;
|
||||
use crate::jwt_auth::{decode_pem_env, resolve_auth_mode, AuthMode, AuthStrategy};
|
||||
use crate::server::{build_router, create_app_state_with_options, spawn_scheduler};
|
||||
use crate::tls::{build_rustls_config, serve_tls, ClientAuth};
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_workflows::pipeline::LlmSpec;
|
||||
|
||||
|
|
@ -62,7 +67,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
let dry_run_mode = if args.dry_run {
|
||||
true
|
||||
} else {
|
||||
match fabro_llm::client::Client::from_env().await {
|
||||
match LlmClient::from_env().await {
|
||||
Ok(c) if c.provider_names().is_empty() => {
|
||||
eprintln!(
|
||||
"{} No LLM providers configured. Running in dry-run mode.",
|
||||
|
|
@ -83,8 +88,8 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
|
||||
// Initialize data directory and SQLite database
|
||||
let config_path = args.config;
|
||||
let server_settings = fabro_config::server::load_server_settings(config_path.as_deref())?;
|
||||
let data_dir = fabro_config::server::resolve_storage_dir(&server_settings);
|
||||
let server_settings = load_server_settings(config_path.as_deref())?;
|
||||
let data_dir = resolve_storage_dir(&server_settings);
|
||||
|
||||
// Shared config for live reloading
|
||||
let shared_config = Arc::new(RwLock::new(server_settings));
|
||||
|
|
@ -121,7 +126,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
.as_ref()
|
||||
.map(|w| w.auth.allowed_usernames.clone())
|
||||
.unwrap_or_default();
|
||||
let auth_mode = crate::jwt_auth::resolve_auth_mode(&api, allowed_usernames);
|
||||
let auth_mode = resolve_auth_mode(&api, allowed_usernames);
|
||||
let client_auth = api.tls.as_ref().map(|_| client_auth_from_mode(&auth_mode));
|
||||
let max_concurrent_runs = args
|
||||
.max_concurrent_runs
|
||||
|
|
@ -133,7 +138,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
let git_author = {
|
||||
let cfg = shared_config.read().expect("config lock poisoned");
|
||||
let author = cfg.git_author();
|
||||
fabro_workflows::git::GitAuthor::from_options(
|
||||
GitAuthor::from_options(
|
||||
author.and_then(|a| a.name.clone()),
|
||||
author.and_then(|a| a.email.clone()),
|
||||
)
|
||||
|
|
@ -142,7 +147,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
let cfg = shared_config.read().expect("config lock poisoned");
|
||||
cfg.hooks.clone()
|
||||
};
|
||||
let state = crate::server::create_app_state_with_options(
|
||||
let state = create_app_state_with_options(
|
||||
db,
|
||||
factory,
|
||||
dry_run_mode,
|
||||
|
|
@ -150,7 +155,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
git_author,
|
||||
hooks,
|
||||
);
|
||||
crate::server::spawn_scheduler(Arc::clone(&state));
|
||||
spawn_scheduler(Arc::clone(&state));
|
||||
let router = build_router(state, auth_mode);
|
||||
|
||||
let addr = format!("{}:{}", args.host, args.port);
|
||||
|
|
@ -183,13 +188,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
let private_key_pem = read_github_private_key();
|
||||
match (secret, private_key_pem) {
|
||||
(Some(secret), Some(pem)) => {
|
||||
match crate::github_webhooks::WebhookManager::start(
|
||||
secret.into_bytes(),
|
||||
&app_id,
|
||||
&pem,
|
||||
)
|
||||
.await
|
||||
{
|
||||
match WebhookManager::start(secret.into_bytes(), &app_id, &pem).await {
|
||||
Ok(manager) => Some(manager),
|
||||
Err(err) => {
|
||||
error!(error = %err, "Failed to start webhook listener");
|
||||
|
|
@ -210,11 +209,11 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
let config_for_poll = Arc::clone(&shared_config);
|
||||
let config_path_for_poll = config_path.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(5));
|
||||
let mut interval = interval(Duration::from_secs(5));
|
||||
interval.tick().await; // skip first immediate tick
|
||||
loop {
|
||||
interval.tick().await;
|
||||
match fabro_config::server::load_server_settings(config_path_for_poll.as_deref()) {
|
||||
match load_server_settings(config_path_for_poll.as_deref()) {
|
||||
Ok(new_config) => {
|
||||
let changed = {
|
||||
let cfg = config_for_poll.read().expect("config lock poisoned");
|
||||
|
|
@ -243,12 +242,12 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
if let Some(ref tls_config) = tls_config {
|
||||
let client_auth = client_auth.unwrap();
|
||||
|
||||
let rustls_config = crate::tls::build_rustls_config(tls_config, client_auth);
|
||||
let rustls_config = build_rustls_config(tls_config, client_auth);
|
||||
let tls_acceptor = tokio_rustls::TlsAcceptor::from(rustls_config);
|
||||
|
||||
info!("TLS enabled");
|
||||
|
||||
crate::tls::serve_tls(listener, tls_acceptor, router).await?;
|
||||
serve_tls(listener, tls_acceptor, router).await?;
|
||||
} else {
|
||||
axum::serve(listener, router).await?;
|
||||
}
|
||||
|
|
@ -308,10 +307,7 @@ fn resolve_model_provider(
|
|||
/// Read the GitHub App private key from the environment, decoding base64 if needed.
|
||||
fn read_github_private_key() -> Option<String> {
|
||||
let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok()?;
|
||||
Some(crate::jwt_auth::decode_pem_env(
|
||||
"GITHUB_APP_PRIVATE_KEY",
|
||||
&raw,
|
||||
))
|
||||
Some(decode_pem_env("GITHUB_APP_PRIVATE_KEY", &raw))
|
||||
}
|
||||
|
||||
/// Derive client certificate verification mode from the resolved auth strategies.
|
||||
|
|
|
|||
|
|
@ -3,21 +3,41 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::extract::{self as axum_extract, Path, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::sse::{Event, Sse};
|
||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use fabro_config::sandbox::SandboxSettings;
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_llm::generate::{generate, generate_object, GenerateParams};
|
||||
use fabro_llm::types::{
|
||||
ContentPart, FinishReason, Message as LlmMessage, Request as LlmRequest,
|
||||
Response as LlmResponse, Role, StreamEvent, ToolChoice, ToolDefinition, Usage,
|
||||
};
|
||||
use fabro_retro::retro::{derive_retro, extract_stage_durations, Retro};
|
||||
use fabro_util::redact::redact_jsonl_line;
|
||||
use fabro_workflows::error::FabroError;
|
||||
use fabro_workflows::git::GitAuthor;
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use futures_util::stream;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::{Notify, OnceCell};
|
||||
use tokio::task::spawn_blocking;
|
||||
use tokio::time::{sleep, timeout};
|
||||
use tokio_stream::wrappers::BroadcastStream;
|
||||
use tokio_stream::StreamExt;
|
||||
use tower::ServiceExt;
|
||||
use tower::{service_fn, ServiceExt};
|
||||
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::demo;
|
||||
use crate::error::ApiError;
|
||||
use crate::jwt_auth::{AuthMode, AuthenticatedService, AuthenticatedUser};
|
||||
use crate::sessions as sessions_mod;
|
||||
use crate::sessions::{new_session_store, SessionStore};
|
||||
use fabro_interview::{Answer, Interviewer, QuestionType, WebInterviewer};
|
||||
use fabro_retro::RetroExt;
|
||||
use fabro_workflows::context::Context;
|
||||
|
|
@ -75,7 +95,7 @@ struct ManagedRun {
|
|||
event_tx: Option<broadcast::Sender<WorkflowRunEvent>>,
|
||||
context: Option<Context>,
|
||||
checkpoint: Option<Checkpoint>,
|
||||
cancel_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
cancel_tx: Option<oneshot::Sender<()>>,
|
||||
cancel_token: Option<Arc<AtomicBool>>,
|
||||
run_dir: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
|
@ -98,8 +118,7 @@ struct AggregateUsageTotals {
|
|||
}
|
||||
|
||||
type LlmSpecFactory = dyn Fn() -> LlmSpec + Send + Sync;
|
||||
type RegistryFactoryOverride =
|
||||
dyn Fn(Arc<dyn Interviewer>) -> fabro_workflows::handler::HandlerRegistry + Send + Sync;
|
||||
type RegistryFactoryOverride = dyn Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync;
|
||||
|
||||
/// Shared application state for the server.
|
||||
pub struct AppState {
|
||||
|
|
@ -110,11 +129,11 @@ pub struct AppState {
|
|||
pub dry_run: bool,
|
||||
pub db: sqlx::SqlitePool,
|
||||
max_concurrent_runs: usize,
|
||||
scheduler_notify: tokio::sync::Notify,
|
||||
scheduler_notify: Notify,
|
||||
pub hooks: Vec<fabro_hooks::HookDefinition>,
|
||||
git_author: fabro_workflows::git::GitAuthor,
|
||||
pub sessions: crate::sessions::SessionStore,
|
||||
llm_client: tokio::sync::OnceCell<fabro_llm::client::Client>,
|
||||
git_author: GitAuthor,
|
||||
pub sessions: SessionStore,
|
||||
llm_client: OnceCell<LlmClient>,
|
||||
}
|
||||
|
||||
/// Build the axum Router with all run endpoints.
|
||||
|
|
@ -140,7 +159,7 @@ pub fn build_router(state: Arc<AppState>, auth_mode: AuthMode) -> Router {
|
|||
.layer(axum::Extension(auth_mode))
|
||||
.with_state(state);
|
||||
|
||||
let dispatch = tower::service_fn(move |req: axum::extract::Request| {
|
||||
let dispatch = service_fn(move |req: axum_extract::Request| {
|
||||
let demo = demo_router.clone();
|
||||
let real = real_router.clone();
|
||||
async move {
|
||||
|
|
@ -157,99 +176,78 @@ pub fn build_router(state: Arc<AppState>, auth_mode: AuthMode) -> Router {
|
|||
|
||||
fn demo_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/runs",
|
||||
get(crate::demo::list_runs).post(crate::demo::start_run_stub),
|
||||
)
|
||||
.route("/runs/{id}", get(crate::demo::get_run_status))
|
||||
.route("/runs/{id}/questions", get(crate::demo::get_questions_stub))
|
||||
.route(
|
||||
"/runs/{id}/questions/{qid}/answer",
|
||||
post(crate::demo::answer_stub),
|
||||
)
|
||||
.route("/runs/{id}/events", get(crate::demo::run_events_stub))
|
||||
.route("/runs/{id}/checkpoint", get(crate::demo::checkpoint_stub))
|
||||
.route("/runs/{id}/context", get(crate::demo::context_stub))
|
||||
.route("/runs/{id}/cancel", post(crate::demo::cancel_stub))
|
||||
.route("/runs/{id}/pause", post(crate::demo::pause_stub))
|
||||
.route("/runs/{id}/unpause", post(crate::demo::unpause_stub))
|
||||
.route("/runs/{id}/graph", get(crate::demo::get_run_graph))
|
||||
.route("/runs/{id}/retro", get(crate::demo::get_run_retro))
|
||||
.route("/runs/{id}/stages", get(crate::demo::get_run_stages))
|
||||
.route("/runs", get(demo::list_runs).post(demo::start_run_stub))
|
||||
.route("/runs/{id}", get(demo::get_run_status))
|
||||
.route("/runs/{id}/questions", get(demo::get_questions_stub))
|
||||
.route("/runs/{id}/questions/{qid}/answer", post(demo::answer_stub))
|
||||
.route("/runs/{id}/events", get(demo::run_events_stub))
|
||||
.route("/runs/{id}/checkpoint", get(demo::checkpoint_stub))
|
||||
.route("/runs/{id}/context", get(demo::context_stub))
|
||||
.route("/runs/{id}/cancel", post(demo::cancel_stub))
|
||||
.route("/runs/{id}/pause", post(demo::pause_stub))
|
||||
.route("/runs/{id}/unpause", post(demo::unpause_stub))
|
||||
.route("/runs/{id}/graph", get(demo::get_run_graph))
|
||||
.route("/runs/{id}/retro", get(demo::get_run_retro))
|
||||
.route("/runs/{id}/stages", get(demo::get_run_stages))
|
||||
.route(
|
||||
"/runs/{id}/stages/{stageId}/turns",
|
||||
get(crate::demo::get_stage_turns),
|
||||
)
|
||||
.route("/runs/{id}/files", get(crate::demo::get_run_files))
|
||||
.route("/runs/{id}/usage", get(crate::demo::get_run_usage))
|
||||
.route(
|
||||
"/runs/{id}/verification",
|
||||
get(crate::demo::get_run_verification),
|
||||
)
|
||||
.route("/runs/{id}/settings", get(crate::demo::get_run_settings))
|
||||
.route("/runs/{id}/steer", post(crate::demo::steer_run_stub))
|
||||
.route(
|
||||
"/runs/{id}/preview",
|
||||
post(crate::demo::generate_preview_url_stub),
|
||||
)
|
||||
.route("/workflows", get(crate::demo::list_workflows))
|
||||
.route("/workflows/{name}", get(crate::demo::get_workflow))
|
||||
.route(
|
||||
"/workflows/{name}/runs",
|
||||
get(crate::demo::list_workflow_runs),
|
||||
get(demo::get_stage_turns),
|
||||
)
|
||||
.route("/runs/{id}/files", get(demo::get_run_files))
|
||||
.route("/runs/{id}/usage", get(demo::get_run_usage))
|
||||
.route("/runs/{id}/verification", get(demo::get_run_verification))
|
||||
.route("/runs/{id}/settings", get(demo::get_run_settings))
|
||||
.route("/runs/{id}/steer", post(demo::steer_run_stub))
|
||||
.route("/runs/{id}/preview", post(demo::generate_preview_url_stub))
|
||||
.route("/workflows", get(demo::list_workflows))
|
||||
.route("/workflows/{name}", get(demo::get_workflow))
|
||||
.route("/workflows/{name}/runs", get(demo::list_workflow_runs))
|
||||
.route(
|
||||
"/verification/criteria",
|
||||
get(crate::demo::list_verification_criteria),
|
||||
get(demo::list_verification_criteria),
|
||||
)
|
||||
.route(
|
||||
"/verification/criteria/{id}",
|
||||
get(crate::demo::get_verification_criterion),
|
||||
get(demo::get_verification_criterion),
|
||||
)
|
||||
.route(
|
||||
"/verification/controls",
|
||||
get(crate::demo::list_verification_controls),
|
||||
get(demo::list_verification_controls),
|
||||
)
|
||||
.route(
|
||||
"/verification/controls/{id}",
|
||||
get(crate::demo::get_verification_control),
|
||||
get(demo::get_verification_control),
|
||||
)
|
||||
.route(
|
||||
"/verification/signoffs",
|
||||
get(crate::demo::list_signoffs).post(crate::demo::create_signoff_stub),
|
||||
get(demo::list_signoffs).post(demo::create_signoff_stub),
|
||||
)
|
||||
.route("/verification/signoffs/{id}", get(crate::demo::get_signoff))
|
||||
.route("/retros", get(crate::demo::list_retros))
|
||||
.route("/verification/signoffs/{id}", get(demo::get_signoff))
|
||||
.route("/retros", get(demo::list_retros))
|
||||
.route(
|
||||
"/sessions",
|
||||
get(crate::demo::list_sessions).post(crate::demo::create_session_stub),
|
||||
)
|
||||
.route("/sessions/{id}", get(crate::demo::get_session))
|
||||
.route(
|
||||
"/sessions/{id}/messages",
|
||||
post(crate::demo::send_message_stub),
|
||||
)
|
||||
.route(
|
||||
"/sessions/{id}/events",
|
||||
get(crate::demo::session_events_stub),
|
||||
get(demo::list_sessions).post(demo::create_session_stub),
|
||||
)
|
||||
.route("/sessions/{id}", get(demo::get_session))
|
||||
.route("/sessions/{id}/messages", post(demo::send_message_stub))
|
||||
.route("/sessions/{id}/events", get(demo::session_events_stub))
|
||||
.route(
|
||||
"/insights/queries",
|
||||
get(crate::demo::list_saved_queries).post(crate::demo::save_query_stub),
|
||||
get(demo::list_saved_queries).post(demo::save_query_stub),
|
||||
)
|
||||
.route(
|
||||
"/insights/queries/{id}",
|
||||
get(crate::demo::get_saved_query)
|
||||
.put(crate::demo::update_query_stub)
|
||||
.delete(crate::demo::delete_query_stub),
|
||||
get(demo::get_saved_query)
|
||||
.put(demo::update_query_stub)
|
||||
.delete(demo::delete_query_stub),
|
||||
)
|
||||
.route("/insights/execute", post(crate::demo::execute_query_stub))
|
||||
.route("/insights/history", get(crate::demo::list_query_history))
|
||||
.route("/models", get(crate::demo::list_models))
|
||||
.route("/insights/execute", post(demo::execute_query_stub))
|
||||
.route("/insights/history", get(demo::list_query_history))
|
||||
.route("/models", get(demo::list_models))
|
||||
.route("/models/{id}/test", post(test_model))
|
||||
.route("/completions", post(create_completion))
|
||||
.route("/settings", get(crate::demo::get_server_settings))
|
||||
.route("/usage", get(crate::demo::get_aggregate_usage))
|
||||
.route("/settings", get(demo::get_server_settings))
|
||||
.route("/usage", get(demo::get_aggregate_usage))
|
||||
}
|
||||
|
||||
fn real_routes() -> Router<Arc<AppState>> {
|
||||
|
|
@ -289,16 +287,13 @@ fn real_routes() -> Router<Arc<AppState>> {
|
|||
.route("/retros", get(not_implemented))
|
||||
.route(
|
||||
"/sessions",
|
||||
get(crate::sessions::list_sessions).post(crate::sessions::create_session),
|
||||
)
|
||||
.route("/sessions/{id}", get(crate::sessions::retrieve_session))
|
||||
.route(
|
||||
"/sessions/{id}/messages",
|
||||
post(crate::sessions::send_message),
|
||||
get(sessions_mod::list_sessions).post(sessions_mod::create_session),
|
||||
)
|
||||
.route("/sessions/{id}", get(sessions_mod::retrieve_session))
|
||||
.route("/sessions/{id}/messages", post(sessions_mod::send_message))
|
||||
.route(
|
||||
"/sessions/{id}/events",
|
||||
get(crate::sessions::stream_session_events),
|
||||
get(sessions_mod::stream_session_events),
|
||||
)
|
||||
.route(
|
||||
"/insights/queries",
|
||||
|
|
@ -312,7 +307,7 @@ fn real_routes() -> Router<Arc<AppState>> {
|
|||
)
|
||||
.route("/insights/execute", post(not_implemented))
|
||||
.route("/insights/history", get(not_implemented))
|
||||
.route("/models", get(crate::demo::list_models))
|
||||
.route("/models", get(demo::list_models))
|
||||
.route("/models/{id}/test", post(test_model))
|
||||
.route("/completions", post(create_completion))
|
||||
.route("/settings", get(not_implemented))
|
||||
|
|
@ -393,7 +388,7 @@ pub fn create_app_state(
|
|||
llm_spec_factory,
|
||||
false,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
GitAuthor::default(),
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
|
@ -402,10 +397,7 @@ pub fn create_app_state(
|
|||
pub fn create_app_state_with_registry_factory(
|
||||
db: sqlx::SqlitePool,
|
||||
llm_spec_factory: impl Fn() -> LlmSpec + Send + Sync + 'static,
|
||||
registry_factory_override: impl Fn(Arc<dyn Interviewer>) -> fabro_workflows::handler::HandlerRegistry
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
registry_factory_override: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
|
||||
) -> Arc<AppState> {
|
||||
build_app_state(
|
||||
db,
|
||||
|
|
@ -413,7 +405,7 @@ pub fn create_app_state_with_registry_factory(
|
|||
Some(Box::new(registry_factory_override)),
|
||||
false,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
GitAuthor::default(),
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
|
@ -424,7 +416,7 @@ pub fn create_app_state_with_options(
|
|||
llm_spec_factory: impl Fn() -> LlmSpec + Send + Sync + 'static,
|
||||
dry_run: bool,
|
||||
max_concurrent_runs: usize,
|
||||
git_author: fabro_workflows::git::GitAuthor,
|
||||
git_author: GitAuthor,
|
||||
hooks: Vec<fabro_hooks::HookDefinition>,
|
||||
) -> Arc<AppState> {
|
||||
build_app_state(
|
||||
|
|
@ -444,7 +436,7 @@ fn build_app_state(
|
|||
registry_factory_override: Option<Box<RegistryFactoryOverride>>,
|
||||
dry_run: bool,
|
||||
max_concurrent_runs: usize,
|
||||
git_author: fabro_workflows::git::GitAuthor,
|
||||
git_author: GitAuthor,
|
||||
hooks: Vec<fabro_hooks::HookDefinition>,
|
||||
) -> Arc<AppState> {
|
||||
Arc::new(AppState {
|
||||
|
|
@ -455,11 +447,11 @@ fn build_app_state(
|
|||
dry_run,
|
||||
db,
|
||||
max_concurrent_runs,
|
||||
scheduler_notify: tokio::sync::Notify::new(),
|
||||
scheduler_notify: Notify::new(),
|
||||
hooks,
|
||||
git_author,
|
||||
sessions: crate::sessions::new_session_store(),
|
||||
llm_client: tokio::sync::OnceCell::new(),
|
||||
sessions: new_session_store(),
|
||||
llm_client: OnceCell::new(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -524,7 +516,7 @@ async fn start_run(
|
|||
let settings = fabro_config::FabroSettings {
|
||||
dry_run: Some(state.dry_run),
|
||||
hooks: state.hooks.clone(),
|
||||
sandbox: Some(fabro_config::sandbox::SandboxSettings {
|
||||
sandbox: Some(SandboxSettings {
|
||||
provider: Some("local".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
|
|
@ -544,7 +536,7 @@ async fn start_run(
|
|||
base_branch: None,
|
||||
}) {
|
||||
Ok(created) => created,
|
||||
Err(ref err @ fabro_workflows::error::FabroError::ValidationFailed { ref diagnostics }) => {
|
||||
Err(ref err @ FabroError::ValidationFailed { ref diagnostics }) => {
|
||||
let message = if diagnostics.is_empty() {
|
||||
err.to_string()
|
||||
} else {
|
||||
|
|
@ -556,7 +548,7 @@ async fn start_run(
|
|||
};
|
||||
return ApiError::bad_request(message).into_response();
|
||||
}
|
||||
Err(err @ fabro_workflows::error::FabroError::Parse(_)) => {
|
||||
Err(err @ FabroError::Parse(_)) => {
|
||||
return ApiError::bad_request(err.to_string()).into_response();
|
||||
}
|
||||
Err(err) => {
|
||||
|
|
@ -619,7 +611,7 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
|
|||
None => return,
|
||||
};
|
||||
|
||||
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let (cancel_tx, cancel_rx) = oneshot::channel::<()>();
|
||||
let cancel_token = Arc::new(AtomicBool::new(false));
|
||||
let (event_tx, _) = broadcast::channel(256);
|
||||
|
||||
|
|
@ -751,7 +743,7 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
|
|||
},
|
||||
)
|
||||
.await?;
|
||||
Ok::<_, fabro_workflows::error::FabroError>(pipeline::execute(initialized).await)
|
||||
Ok::<_, FabroError>(pipeline::execute(initialized).await)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -778,8 +770,8 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
|
|||
if let Some(ref cp) = checkpoint {
|
||||
let failed = result.is_err();
|
||||
let completed_stages = fabro_workflows::build_completed_stages(cp, failed);
|
||||
let stage_durations = fabro_retro::retro::extract_stage_durations(&run_options.run_dir);
|
||||
let retro = fabro_retro::retro::derive_retro(
|
||||
let stage_durations = extract_stage_durations(&run_options.run_dir);
|
||||
let retro = derive_retro(
|
||||
&run_id,
|
||||
"workflow",
|
||||
"",
|
||||
|
|
@ -817,7 +809,7 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
|
|||
info!(run_id = %run_id, "Run completed");
|
||||
managed_run.status = RunStatus::Completed;
|
||||
}
|
||||
Err(fabro_workflows::error::FabroError::Cancelled) => {
|
||||
Err(FabroError::Cancelled) => {
|
||||
info!(run_id = %run_id, "Run cancelled");
|
||||
managed_run.status = RunStatus::Cancelled;
|
||||
}
|
||||
|
|
@ -844,7 +836,7 @@ pub fn spawn_scheduler(state: Arc<AppState>) {
|
|||
loop {
|
||||
tokio::select! {
|
||||
_ = state.scheduler_notify.notified() => {},
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {},
|
||||
_ = sleep(std::time::Duration::from_secs(1)) => {},
|
||||
}
|
||||
// Promote as many queued runs as capacity allows
|
||||
loop {
|
||||
|
|
@ -1045,7 +1037,7 @@ async fn get_events(
|
|||
let stream = BroadcastStream::new(rx).filter_map(|result| match result {
|
||||
Ok(event) => {
|
||||
let data = serde_json::to_string(&event).unwrap_or_default();
|
||||
let data = fabro_util::redact::redact_jsonl_line(&data);
|
||||
let data = redact_jsonl_line(&data);
|
||||
Some(Ok::<Event, std::convert::Infallible>(
|
||||
Event::default().data(data),
|
||||
))
|
||||
|
|
@ -1196,16 +1188,12 @@ async fn test_model(
|
|||
.into_response();
|
||||
}
|
||||
|
||||
let params = fabro_llm::generate::GenerateParams::new(&info.id)
|
||||
let params = GenerateParams::new(&info.id)
|
||||
.provider(info.provider.as_str())
|
||||
.prompt("Say OK")
|
||||
.max_tokens(16);
|
||||
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
fabro_llm::generate::generate(params),
|
||||
)
|
||||
.await;
|
||||
let result = timeout(Duration::from_secs(30), generate(params)).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(_)) => Json(serde_json::json!({
|
||||
|
|
@ -1228,26 +1216,26 @@ async fn test_model(
|
|||
}
|
||||
}
|
||||
|
||||
fn finish_reason_to_api_stop_reason(reason: &fabro_llm::types::FinishReason) -> String {
|
||||
fn finish_reason_to_api_stop_reason(reason: &FinishReason) -> String {
|
||||
match reason {
|
||||
fabro_llm::types::FinishReason::Stop => "end_turn".to_string(),
|
||||
fabro_llm::types::FinishReason::Length => "max_tokens".to_string(),
|
||||
fabro_llm::types::FinishReason::ToolCalls => "tool_calls".to_string(),
|
||||
fabro_llm::types::FinishReason::ContentFilter => "content_filter".to_string(),
|
||||
fabro_llm::types::FinishReason::Error => "error".to_string(),
|
||||
fabro_llm::types::FinishReason::Other(s) => s.clone(),
|
||||
FinishReason::Stop => "end_turn".to_string(),
|
||||
FinishReason::Length => "max_tokens".to_string(),
|
||||
FinishReason::ToolCalls => "tool_calls".to_string(),
|
||||
FinishReason::ContentFilter => "content_filter".to_string(),
|
||||
FinishReason::Error => "error".to_string(),
|
||||
FinishReason::Other(s) => s.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> fabro_llm::types::Message {
|
||||
fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> LlmMessage {
|
||||
let role = match msg.role {
|
||||
fabro_api_types::CompletionMessageRole::System => fabro_llm::types::Role::System,
|
||||
fabro_api_types::CompletionMessageRole::User => fabro_llm::types::Role::User,
|
||||
fabro_api_types::CompletionMessageRole::Assistant => fabro_llm::types::Role::Assistant,
|
||||
fabro_api_types::CompletionMessageRole::Tool => fabro_llm::types::Role::Tool,
|
||||
fabro_api_types::CompletionMessageRole::Developer => fabro_llm::types::Role::Developer,
|
||||
fabro_api_types::CompletionMessageRole::System => Role::System,
|
||||
fabro_api_types::CompletionMessageRole::User => Role::User,
|
||||
fabro_api_types::CompletionMessageRole::Assistant => Role::Assistant,
|
||||
fabro_api_types::CompletionMessageRole::Tool => Role::Tool,
|
||||
fabro_api_types::CompletionMessageRole::Developer => Role::Developer,
|
||||
};
|
||||
let content: Vec<fabro_llm::types::ContentPart> = msg
|
||||
let content: Vec<ContentPart> = msg
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|part| {
|
||||
|
|
@ -1255,7 +1243,7 @@ fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> fabro_llm::t
|
|||
serde_json::from_value(json).ok()
|
||||
})
|
||||
.collect();
|
||||
fabro_llm::types::Message {
|
||||
LlmMessage {
|
||||
role,
|
||||
content,
|
||||
name: msg.name.clone(),
|
||||
|
|
@ -1263,13 +1251,13 @@ fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> fabro_llm::t
|
|||
}
|
||||
}
|
||||
|
||||
fn convert_llm_message(msg: &fabro_llm::types::Message) -> fabro_api_types::CompletionMessage {
|
||||
fn convert_llm_message(msg: &LlmMessage) -> fabro_api_types::CompletionMessage {
|
||||
let role = match msg.role {
|
||||
fabro_llm::types::Role::System => fabro_api_types::CompletionMessageRole::System,
|
||||
fabro_llm::types::Role::User => fabro_api_types::CompletionMessageRole::User,
|
||||
fabro_llm::types::Role::Assistant => fabro_api_types::CompletionMessageRole::Assistant,
|
||||
fabro_llm::types::Role::Tool => fabro_api_types::CompletionMessageRole::Tool,
|
||||
fabro_llm::types::Role::Developer => fabro_api_types::CompletionMessageRole::Developer,
|
||||
Role::System => fabro_api_types::CompletionMessageRole::System,
|
||||
Role::User => fabro_api_types::CompletionMessageRole::User,
|
||||
Role::Assistant => fabro_api_types::CompletionMessageRole::Assistant,
|
||||
Role::Tool => fabro_api_types::CompletionMessageRole::Tool,
|
||||
Role::Developer => fabro_api_types::CompletionMessageRole::Developer,
|
||||
};
|
||||
let content: Vec<fabro_api_types::CompletionContentPart> = msg
|
||||
.content
|
||||
|
|
@ -1310,22 +1298,22 @@ async fn create_completion(
|
|||
info!(model = %model_id, provider = ?provider_name, "Completion request received");
|
||||
|
||||
// Build messages list
|
||||
let mut messages: Vec<fabro_llm::types::Message> = Vec::new();
|
||||
let mut messages: Vec<LlmMessage> = Vec::new();
|
||||
if let Some(system) = req.system {
|
||||
messages.push(fabro_llm::types::Message::system(system));
|
||||
messages.push(LlmMessage::system(system));
|
||||
}
|
||||
for msg in &req.messages {
|
||||
messages.push(convert_api_message(msg));
|
||||
}
|
||||
|
||||
// Convert tools
|
||||
let tools: Option<Vec<fabro_llm::types::ToolDefinition>> = if req.tools.is_empty() {
|
||||
let tools: Option<Vec<ToolDefinition>> = if req.tools.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
req.tools
|
||||
.into_iter()
|
||||
.map(|t| fabro_llm::types::ToolDefinition {
|
||||
.map(|t| ToolDefinition {
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: t.parameters,
|
||||
|
|
@ -1335,20 +1323,17 @@ async fn create_completion(
|
|||
};
|
||||
|
||||
// Convert tool_choice
|
||||
let tool_choice: Option<fabro_llm::types::ToolChoice> =
|
||||
req.tool_choice.map(|tc| match tc.mode {
|
||||
fabro_api_types::CompletionToolChoiceMode::Auto => fabro_llm::types::ToolChoice::Auto,
|
||||
fabro_api_types::CompletionToolChoiceMode::None => fabro_llm::types::ToolChoice::None,
|
||||
fabro_api_types::CompletionToolChoiceMode::Required => {
|
||||
fabro_llm::types::ToolChoice::Required
|
||||
}
|
||||
fabro_api_types::CompletionToolChoiceMode::Named => {
|
||||
fabro_llm::types::ToolChoice::named(tc.tool_name.unwrap_or_default())
|
||||
}
|
||||
});
|
||||
let tool_choice: Option<ToolChoice> = req.tool_choice.map(|tc| match tc.mode {
|
||||
fabro_api_types::CompletionToolChoiceMode::Auto => ToolChoice::Auto,
|
||||
fabro_api_types::CompletionToolChoiceMode::None => ToolChoice::None,
|
||||
fabro_api_types::CompletionToolChoiceMode::Required => ToolChoice::Required,
|
||||
fabro_api_types::CompletionToolChoiceMode::Named => {
|
||||
ToolChoice::named(tc.tool_name.unwrap_or_default())
|
||||
}
|
||||
});
|
||||
|
||||
// Build the LLM request
|
||||
let request = fabro_llm::types::Request {
|
||||
let request = LlmRequest {
|
||||
model: model_id.clone(),
|
||||
messages,
|
||||
provider: provider_name,
|
||||
|
|
@ -1376,23 +1361,23 @@ async fn create_completion(
|
|||
if state.dry_run {
|
||||
let msg_id = ulid::Ulid::new().to_string();
|
||||
if use_stream {
|
||||
let finish_event = fabro_llm::types::StreamEvent::finish(
|
||||
fabro_llm::types::FinishReason::Stop,
|
||||
fabro_llm::types::Usage::default(),
|
||||
fabro_llm::types::Response {
|
||||
let finish_event = StreamEvent::finish(
|
||||
FinishReason::Stop,
|
||||
Usage::default(),
|
||||
LlmResponse {
|
||||
id: msg_id.clone(),
|
||||
model: model_id.clone(),
|
||||
provider: String::new(),
|
||||
message: fabro_llm::types::Message::assistant(""),
|
||||
finish_reason: fabro_llm::types::FinishReason::Stop,
|
||||
usage: fabro_llm::types::Usage::default(),
|
||||
message: LlmMessage::assistant(""),
|
||||
finish_reason: FinishReason::Stop,
|
||||
usage: Usage::default(),
|
||||
raw: None,
|
||||
warnings: vec![],
|
||||
rate_limit: None,
|
||||
},
|
||||
);
|
||||
let json = serde_json::to_string(&finish_event).unwrap_or_default();
|
||||
let sse_stream = futures_util::stream::iter(vec![Ok::<_, std::convert::Infallible>(
|
||||
let sse_stream = stream::iter(vec![Ok::<_, std::convert::Infallible>(
|
||||
Event::default().event("stream_event").data(json),
|
||||
)]);
|
||||
return Sse::new(sse_stream).into_response();
|
||||
|
|
@ -1418,11 +1403,7 @@ async fn create_completion(
|
|||
}
|
||||
|
||||
// Get or create LLM client (cached in AppState)
|
||||
let client = match state
|
||||
.llm_client
|
||||
.get_or_try_init(fabro_llm::client::Client::from_env)
|
||||
.await
|
||||
{
|
||||
let client = match state.llm_client.get_or_try_init(LlmClient::from_env).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return ApiError::new(
|
||||
|
|
@ -1469,13 +1450,11 @@ async fn create_completion(
|
|||
|
||||
Sse::new(sse_stream)
|
||||
.keep_alive(
|
||||
axum::response::sse::KeepAlive::new()
|
||||
.interval(Duration::from_secs(15))
|
||||
.event(
|
||||
Event::default()
|
||||
.event("ping")
|
||||
.data(serde_json::json!({"type": "ping"}).to_string()),
|
||||
),
|
||||
KeepAlive::new().interval(Duration::from_secs(15)).event(
|
||||
Event::default()
|
||||
.event("ping")
|
||||
.data(serde_json::json!({"type": "ping"}).to_string()),
|
||||
),
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
|
|
@ -1484,7 +1463,7 @@ async fn create_completion(
|
|||
|
||||
if let Some(schema) = req.schema {
|
||||
// Structured output uses generate_object for JSON parsing logic
|
||||
let mut params = fabro_llm::generate::GenerateParams::new(&request.model)
|
||||
let mut params = GenerateParams::new(&request.model)
|
||||
.messages(request.messages)
|
||||
.client(std::sync::Arc::new(client.clone()));
|
||||
if let Some(ref p) = request.provider {
|
||||
|
|
@ -1499,7 +1478,7 @@ async fn create_completion(
|
|||
if let Some(top_p) = request.top_p {
|
||||
params = params.top_p(top_p);
|
||||
}
|
||||
match fabro_llm::generate::generate_object(params, schema).await {
|
||||
match generate_object(params, schema).await {
|
||||
Ok(result) => Json(fabro_api_types::CompletionResponse {
|
||||
id: msg_id,
|
||||
model: model_id,
|
||||
|
|
@ -1553,7 +1532,7 @@ async fn get_retro(
|
|||
return (StatusCode::OK, Json(serde_json::json!(null))).into_response();
|
||||
};
|
||||
|
||||
match fabro_retro::retro::Retro::load(&run_dir) {
|
||||
match Retro::load(&run_dir) {
|
||||
Ok(retro) => (StatusCode::OK, Json(retro)).into_response(),
|
||||
Err(_) => (StatusCode::OK, Json(serde_json::json!(null))).into_response(),
|
||||
}
|
||||
|
|
@ -1564,7 +1543,7 @@ pub(crate) async fn render_dot_svg(dot_source: &str) -> Response {
|
|||
use fabro_graphviz::render::{render_dot, GraphFormat};
|
||||
|
||||
let source = dot_source.to_owned();
|
||||
match tokio::task::spawn_blocking(move || render_dot(&source, GraphFormat::Svg)).await {
|
||||
match spawn_blocking(move || render_dot(&source, GraphFormat::Svg)).await {
|
||||
Ok(Ok(bytes)) => {
|
||||
(StatusCode::OK, [("content-type", "image/svg+xml")], bytes).into_response()
|
||||
}
|
||||
|
|
@ -1675,7 +1654,7 @@ mod tests {
|
|||
test_llm_spec,
|
||||
true,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
GitAuthor::default(),
|
||||
Vec::new(),
|
||||
);
|
||||
let app = build_router(state, AuthMode::Disabled);
|
||||
|
|
@ -1702,7 +1681,7 @@ mod tests {
|
|||
test_llm_spec,
|
||||
true,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
GitAuthor::default(),
|
||||
Vec::new(),
|
||||
);
|
||||
let app = build_router(state, AuthMode::Disabled);
|
||||
|
|
@ -2393,7 +2372,7 @@ mod tests {
|
|||
test_llm_spec,
|
||||
false,
|
||||
1,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
GitAuthor::default(),
|
||||
Vec::new(),
|
||||
);
|
||||
let app = test_app_with_scheduler(state);
|
||||
|
|
@ -2481,7 +2460,7 @@ mod tests {
|
|||
test_llm_spec,
|
||||
true,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
GitAuthor::default(),
|
||||
Vec::new(),
|
||||
);
|
||||
let app = build_router(state, AuthMode::Disabled);
|
||||
|
|
@ -2518,7 +2497,7 @@ mod tests {
|
|||
test_llm_spec,
|
||||
true,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
GitAuthor::default(),
|
||||
Vec::new(),
|
||||
);
|
||||
let app = build_router(state, AuthMode::Disabled);
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@ use axum::http::StatusCode;
|
|||
use axum::response::sse::{Event, Sse};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use fabro_llm::generate::{stream as llm_stream, GenerateParams};
|
||||
use fabro_llm::types::{Message as LlmMessage, StreamEvent};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_stream::wrappers::BroadcastStream;
|
||||
|
||||
use crate::error::ApiError;
|
||||
use crate::jwt_auth::AuthenticatedService;
|
||||
use crate::server::PaginationParams;
|
||||
use crate::server::{AppState, PaginationParams};
|
||||
|
||||
pub type SessionStore = Arc<RwLock<HashMap<uuid::Uuid, SessionState>>>;
|
||||
|
||||
|
|
@ -72,15 +75,13 @@ fn resolve_model(model_arg: Option<String>) -> (String, Option<String>) {
|
|||
}
|
||||
}
|
||||
|
||||
fn turns_to_messages(turns: &[fabro_api_types::SessionTurn]) -> Vec<fabro_llm::types::Message> {
|
||||
fn turns_to_messages(turns: &[fabro_api_types::SessionTurn]) -> Vec<LlmMessage> {
|
||||
turns
|
||||
.iter()
|
||||
.filter_map(|turn| match turn {
|
||||
fabro_api_types::SessionTurn::UserTurn(t) => {
|
||||
Some(fabro_llm::types::Message::user(&t.content))
|
||||
}
|
||||
fabro_api_types::SessionTurn::UserTurn(t) => Some(LlmMessage::user(&t.content)),
|
||||
fabro_api_types::SessionTurn::AssistantTurn(t) => {
|
||||
Some(fabro_llm::types::Message::assistant(&t.content))
|
||||
Some(LlmMessage::assistant(&t.content))
|
||||
}
|
||||
fabro_api_types::SessionTurn::ToolTurn(_) => None,
|
||||
})
|
||||
|
|
@ -136,7 +137,7 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool,
|
|||
return;
|
||||
}
|
||||
|
||||
let mut params = fabro_llm::generate::GenerateParams::new(&model_id)
|
||||
let mut params = GenerateParams::new(&model_id)
|
||||
.messages(messages)
|
||||
.max_tokens(4096);
|
||||
if let Some(ref provider) = model_provider {
|
||||
|
|
@ -146,7 +147,7 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool,
|
|||
params = params.system(system);
|
||||
}
|
||||
|
||||
let stream_result = match fabro_llm::generate::stream(params).await {
|
||||
let stream_result = match llm_stream(params).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
let _ = event_tx.send(SessionEvent::Error {
|
||||
|
|
@ -165,7 +166,7 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool,
|
|||
return;
|
||||
}
|
||||
match event {
|
||||
Ok(fabro_llm::types::StreamEvent::TextDelta { delta, .. }) => {
|
||||
Ok(StreamEvent::TextDelta { delta, .. }) => {
|
||||
full_text.push_str(&delta);
|
||||
let _ = event_tx.send(SessionEvent::TextDelta { delta });
|
||||
}
|
||||
|
|
@ -207,7 +208,7 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool,
|
|||
|
||||
pub async fn create_session(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<crate::server::AppState>>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<fabro_api_types::CreateSessionRequest>,
|
||||
) -> Response {
|
||||
let (model_id, model_provider) = resolve_model(req.model);
|
||||
|
|
@ -259,7 +260,7 @@ pub async fn create_session(
|
|||
|
||||
pub async fn retrieve_session(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<crate::server::AppState>>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Response {
|
||||
let store = state.sessions.read().expect("session store lock poisoned");
|
||||
|
|
@ -284,7 +285,7 @@ pub async fn retrieve_session(
|
|||
|
||||
pub async fn send_message(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<crate::server::AppState>>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
Json(req): Json<fabro_api_types::SendMessageRequest>,
|
||||
) -> Response {
|
||||
|
|
@ -318,7 +319,7 @@ pub async fn send_message(
|
|||
|
||||
pub async fn stream_session_events(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<crate::server::AppState>>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Response {
|
||||
let rx = {
|
||||
|
|
@ -331,46 +332,45 @@ pub async fn stream_session_events(
|
|||
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
let stream =
|
||||
tokio_stream::wrappers::BroadcastStream::new(rx).filter_map(|result| match result {
|
||||
Ok(event) => {
|
||||
let sse: Option<Event> = match event {
|
||||
SessionEvent::TextDelta { delta } => Some(
|
||||
Event::default()
|
||||
.event("content_delta")
|
||||
.data(serde_json::json!({"delta": delta}).to_string()),
|
||||
let stream = BroadcastStream::new(rx).filter_map(|result| match result {
|
||||
Ok(event) => {
|
||||
let sse: Option<Event> = match event {
|
||||
SessionEvent::TextDelta { delta } => Some(
|
||||
Event::default()
|
||||
.event("content_delta")
|
||||
.data(serde_json::json!({"delta": delta}).to_string()),
|
||||
),
|
||||
SessionEvent::AssistantTurnComplete {
|
||||
content,
|
||||
created_at,
|
||||
} => Some(
|
||||
Event::default().event("assistant_turn").data(
|
||||
serde_json::json!({
|
||||
"kind": "assistant",
|
||||
"content": content,
|
||||
"created_at": created_at,
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
SessionEvent::AssistantTurnComplete {
|
||||
content,
|
||||
created_at,
|
||||
} => Some(
|
||||
Event::default().event("assistant_turn").data(
|
||||
serde_json::json!({
|
||||
"kind": "assistant",
|
||||
"content": content,
|
||||
"created_at": created_at,
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
SessionEvent::Done => Some(Event::default().event("done").data("{}")),
|
||||
SessionEvent::Error { message } => Some(
|
||||
Event::default()
|
||||
.event("error")
|
||||
.data(serde_json::json!({"message": message}).to_string()),
|
||||
),
|
||||
};
|
||||
sse.map(Ok::<_, std::convert::Infallible>)
|
||||
}
|
||||
Err(_) => None,
|
||||
});
|
||||
),
|
||||
SessionEvent::Done => Some(Event::default().event("done").data("{}")),
|
||||
SessionEvent::Error { message } => Some(
|
||||
Event::default()
|
||||
.event("error")
|
||||
.data(serde_json::json!({"message": message}).to_string()),
|
||||
),
|
||||
};
|
||||
sse.map(Ok::<_, std::convert::Infallible>)
|
||||
}
|
||||
Err(_) => None,
|
||||
});
|
||||
|
||||
Sse::new(stream).into_response()
|
||||
}
|
||||
|
||||
pub async fn list_sessions(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<crate::server::AppState>>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> Response {
|
||||
let store = state.sessions.read().expect("session store lock poisoned");
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ pub async fn serve_tls(
|
|||
tls_acceptor: tokio_rustls::TlsAcceptor,
|
||||
router: axum::Router,
|
||||
) -> anyhow::Result<()> {
|
||||
use hyper::body::Incoming;
|
||||
use hyper::service::service_fn;
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use hyper_util::server::conn::auto::Builder;
|
||||
use tower_service::Service;
|
||||
|
|
@ -94,13 +96,11 @@ pub async fn serve_tls(
|
|||
|
||||
let io = TokioIo::new(tls_stream);
|
||||
|
||||
let service = hyper::service::service_fn(
|
||||
move |mut req: hyper::Request<hyper::body::Incoming>| {
|
||||
req.extensions_mut().insert(peer_certs.clone());
|
||||
let mut router = router.clone();
|
||||
async move { router.call(req).await }
|
||||
},
|
||||
);
|
||||
let service = service_fn(move |mut req: hyper::Request<Incoming>| {
|
||||
req.extensions_mut().insert(peer_certs.clone());
|
||||
let mut router = router.clone();
|
||||
async move { router.call(req).await }
|
||||
});
|
||||
|
||||
if let Err(e) = builder.serve_connection(io, service).await {
|
||||
error!(%remote_addr, "connection error: {e}");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ use std::fmt;
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Args, Subcommand, ValueEnum};
|
||||
use fabro_agent::cli::AgentArgs;
|
||||
use fabro_graphviz::render::GraphFormat;
|
||||
use fabro_llm::cli::{ChatArgs, ModelsCommand, PromptArgs};
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use crate::cli_config;
|
||||
|
|
@ -273,7 +276,7 @@ pub(crate) enum GraphOutputFormat {
|
|||
Png,
|
||||
}
|
||||
|
||||
impl From<GraphOutputFormat> for fabro_graphviz::render::GraphFormat {
|
||||
impl From<GraphOutputFormat> for GraphFormat {
|
||||
fn from(value: GraphOutputFormat) -> Self {
|
||||
match value {
|
||||
GraphOutputFormat::Svg => Self::Svg,
|
||||
|
|
@ -726,7 +729,7 @@ pub(crate) enum Commands {
|
|||
Llm(LlmNamespace),
|
||||
/// Run an agentic coding session
|
||||
#[command(hide = true)]
|
||||
Exec(fabro_agent::cli::AgentArgs),
|
||||
Exec(AgentArgs),
|
||||
#[command(flatten)]
|
||||
RunCmd(RunCommands),
|
||||
/// Validate run configuration without executing
|
||||
|
|
@ -745,7 +748,7 @@ pub(crate) enum Commands {
|
|||
/// List and test LLM models
|
||||
Model {
|
||||
#[command(subcommand)]
|
||||
command: Option<fabro_llm::cli::ModelsCommand>,
|
||||
command: Option<ModelsCommand>,
|
||||
},
|
||||
/// Start the HTTP API server
|
||||
#[cfg(feature = "server")]
|
||||
|
|
@ -825,8 +828,8 @@ impl Commands {
|
|||
Self::Parse(_) => "parse",
|
||||
Self::RunsCmd(cmd) => cmd.name(),
|
||||
Self::Model { command } => match command {
|
||||
Some(fabro_llm::cli::ModelsCommand::List { .. }) => "model list",
|
||||
Some(fabro_llm::cli::ModelsCommand::Test { .. }) => "model test",
|
||||
Some(ModelsCommand::List { .. }) => "model list",
|
||||
Some(ModelsCommand::Test { .. }) => "model test",
|
||||
None => "model",
|
||||
},
|
||||
#[cfg(feature = "server")]
|
||||
|
|
@ -1009,9 +1012,9 @@ pub(crate) struct LlmNamespace {
|
|||
#[derive(Subcommand)]
|
||||
pub(crate) enum LlmCommand {
|
||||
/// Execute a prompt
|
||||
Prompt(fabro_llm::cli::PromptArgs),
|
||||
Prompt(PromptArgs),
|
||||
/// Interactive multi-turn chat
|
||||
Chat(fabro_llm::cli::ChatArgs),
|
||||
Chat(ChatArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ pub use fabro_config::cli::*;
|
|||
|
||||
use std::path::Path;
|
||||
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::FabroSettings;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use tracing::debug;
|
||||
|
||||
pub fn load_cli_settings(path: Option<&Path>) -> anyhow::Result<FabroSettings> {
|
||||
fabro_config::cli::load_cli_config(path)?.try_into()
|
||||
load_cli_config(path)?.try_into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
|
|
|
|||
|
|
@ -3,18 +3,20 @@ use std::path::{Path, PathBuf};
|
|||
use anyhow::{bail, Context, Result};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_workflows::assets::{scan_assets, AssetEntry};
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
|
||||
use crate::args::AssetCpArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::split_run_path;
|
||||
|
||||
pub fn cp_command(args: &AssetCpArgs) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let (run_id, asset_path) = parse_source(&args.source);
|
||||
let run = fabro_workflows::run_lookup::resolve_run(&base, run_id)?;
|
||||
let run = resolve_run(&base, run_id)?;
|
||||
let runtime_state = RuntimeState::new(&run.path);
|
||||
let entries =
|
||||
fabro_workflows::assets::scan_assets(&runtime_state.assets_dir(), args.node.as_deref())?;
|
||||
let entries = scan_assets(&runtime_state.assets_dir(), args.node.as_deref())?;
|
||||
|
||||
if entries.is_empty() {
|
||||
bail!("No assets found for this run");
|
||||
|
|
@ -80,8 +82,7 @@ pub fn cp_command(args: &AssetCpArgs) -> Result<()> {
|
|||
})?;
|
||||
}
|
||||
} else {
|
||||
let mut by_filename: Vec<(String, &fabro_workflows::assets::AssetEntry)> =
|
||||
Vec::with_capacity(entries.len());
|
||||
let mut by_filename: Vec<(String, &AssetEntry)> = Vec::with_capacity(entries.len());
|
||||
for entry in &entries {
|
||||
let filename = Path::new(&entry.relative_path)
|
||||
.file_name()
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
use anyhow::Result;
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_workflows::assets::scan_assets;
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
|
||||
use crate::args::AssetListArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::format_size;
|
||||
|
||||
pub fn list_command(args: &AssetListArgs) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let run = fabro_workflows::run_lookup::resolve_run(&base, &args.run_id)?;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let run = resolve_run(&base, &args.run_id)?;
|
||||
let runtime_state = RuntimeState::new(&run.path);
|
||||
let entries =
|
||||
fabro_workflows::assets::scan_assets(&runtime_state.assets_dir(), args.node.as_deref())?;
|
||||
let entries = scan_assets(&runtime_state.assets_dir(), args.node.as_deref())?;
|
||||
|
||||
if args.json {
|
||||
println!("{}", serde_json::to_string_pretty(&entries)?);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use std::io::Write;
|
|||
use std::path::Path;
|
||||
|
||||
use crate::args::{ConfigCommand, ConfigNamespace, ConfigShowArgs};
|
||||
use fabro_config::project::ResolveSettingsInput;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::project::{discover_project_config, resolve_settings, ResolveSettingsInput};
|
||||
use fabro_config::{FabroConfig, FabroSettings};
|
||||
|
||||
pub fn dispatch(ns: ConfigNamespace) -> anyhow::Result<()> {
|
||||
|
|
@ -13,9 +14,9 @@ pub fn dispatch(ns: ConfigNamespace) -> anyhow::Result<()> {
|
|||
|
||||
fn merged_config(workflow: Option<&Path>) -> anyhow::Result<FabroSettings> {
|
||||
if let Some(workflow) = workflow {
|
||||
let cli_config = fabro_config::cli::load_cli_config(None)?;
|
||||
let cli_config = load_cli_config(None)?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
return fabro_config::project::resolve_settings(ResolveSettingsInput {
|
||||
return resolve_settings(ResolveSettingsInput {
|
||||
workflow_path: workflow.to_path_buf(),
|
||||
cwd,
|
||||
defaults: cli_config,
|
||||
|
|
@ -25,10 +26,10 @@ fn merged_config(workflow: Option<&Path>) -> anyhow::Result<FabroSettings> {
|
|||
}
|
||||
|
||||
let cwd = std::env::current_dir()?;
|
||||
let project_config = fabro_config::project::discover_project_config(&cwd)?
|
||||
let project_config = discover_project_config(&cwd)?
|
||||
.map(|(_, config)| config)
|
||||
.unwrap_or_default();
|
||||
let cli_config = fabro_config::cli::load_cli_config(None)?;
|
||||
let cli_config = load_cli_config(None)?;
|
||||
FabroConfig::combine(project_config, cli_config).try_into()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,18 +4,25 @@ use std::process::Command;
|
|||
#[cfg(feature = "server")]
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use base64::Engine as _;
|
||||
#[cfg(feature = "server")]
|
||||
use fabro_config::server::{ApiAuthStrategy, AuthProvider};
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_llm::types::{Message, Request};
|
||||
use fabro_model::{Catalog, Provider};
|
||||
pub use fabro_util::check_report::{
|
||||
CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus,
|
||||
};
|
||||
use fabro_util::terminal::Styles;
|
||||
use futures::future::join_all;
|
||||
#[cfg(feature = "server")]
|
||||
use regex::Regex;
|
||||
#[cfg(feature = "server")]
|
||||
use semver::Version;
|
||||
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System dependency types and parsers (server mode only)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -863,12 +870,12 @@ pub(crate) fn probe_model(provider: Provider) -> String {
|
|||
}
|
||||
|
||||
async fn probe_llm_provider(
|
||||
client: &fabro_llm::client::Client,
|
||||
client: &LlmClient,
|
||||
provider: Provider,
|
||||
) -> (Provider, Result<(), String>) {
|
||||
let request = fabro_llm::types::Request {
|
||||
let request = Request {
|
||||
model: probe_model(provider),
|
||||
messages: vec![fabro_llm::types::Message::user("hi")],
|
||||
messages: vec![Message::user("hi")],
|
||||
provider: Some(provider.as_str().to_string()),
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
|
|
@ -928,7 +935,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 {
|
|||
spinner.enable_steady_tick(std::time::Duration::from_millis(80));
|
||||
|
||||
// Gather state
|
||||
let cli_config = crate::cli_config::load_cli_settings(None).unwrap_or_default();
|
||||
let cli_config = load_cli_settings(None).unwrap_or_default();
|
||||
|
||||
let config_path = dirs::home_dir().map(|h| h.join(".fabro").join("cli.toml"));
|
||||
let config_exists = config_path.as_ref().is_some_and(|p| p.exists());
|
||||
|
|
@ -980,7 +987,8 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 {
|
|||
let pem = if raw.starts_with("-----") {
|
||||
Ok(raw.clone())
|
||||
} else {
|
||||
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, raw)
|
||||
BASE64_STANDARD
|
||||
.decode(raw)
|
||||
.map_err(|e| format!("base64 decode failed: {e}"))
|
||||
.and_then(|bytes| {
|
||||
String::from_utf8(bytes)
|
||||
|
|
@ -1058,7 +1066,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 {
|
|||
let http = reqwest::Client::new();
|
||||
|
||||
// Build LLM client — may fail if no keys are set
|
||||
let llm_client = fabro_llm::client::Client::from_env().await.ok();
|
||||
let llm_client = LlmClient::from_env().await.ok();
|
||||
|
||||
let configured_providers: Vec<Provider> = llm_statuses
|
||||
.iter()
|
||||
|
|
@ -1072,7 +1080,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 {
|
|||
.iter()
|
||||
.map(|p| probe_llm_provider(client, *p))
|
||||
.collect();
|
||||
Some(futures::future::join_all(futures).await)
|
||||
Some(join_all(futures).await)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
use anyhow::Result;
|
||||
#[cfg(feature = "server")]
|
||||
use fabro_agent::cli::run_with_args_and_client;
|
||||
use fabro_agent::cli::{run_with_args, AgentArgs};
|
||||
use fabro_config::mcp::McpServerEntry;
|
||||
use fabro_mcp::config::McpServerConfig;
|
||||
|
||||
use crate::args::GlobalArgs;
|
||||
use crate::cli_config;
|
||||
|
||||
pub async fn execute(mut args: fabro_agent::cli::AgentArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
pub async fn execute(mut args: AgentArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_config = cli_config::load_cli_settings(None)?;
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = crate::sleep_inhibitor::guard(cli_config.prevent_idle_sleep_enabled());
|
||||
|
|
@ -20,10 +25,10 @@ pub async fn execute(mut args: fabro_agent::cli::AgentArgs, globals: &GlobalArgs
|
|||
globals.server_url.as_deref(),
|
||||
&cli_config,
|
||||
);
|
||||
let mcp_servers: Vec<fabro_mcp::config::McpServerConfig> = cli_config
|
||||
let mcp_servers: Vec<McpServerConfig> = cli_config
|
||||
.mcp_servers
|
||||
.into_iter()
|
||||
.map(|(name, entry): (String, fabro_config::mcp::McpServerEntry)| entry.into_config(name))
|
||||
.map(|(name, entry): (String, McpServerEntry)| entry.into_config(name))
|
||||
.collect();
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
|
|
@ -46,11 +51,11 @@ pub async fn execute(mut args: fabro_agent::cli::AgentArgs, globals: &GlobalArgs
|
|||
.register_provider(adapter)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to register fabro server adapter: {e}"))?;
|
||||
fabro_agent::cli::run_with_args_and_client(args, Some(client), mcp_servers).await?
|
||||
run_with_args_and_client(args, Some(client), mcp_servers).await?
|
||||
}
|
||||
cli_config::ExecutionMode::Standalone => {
|
||||
tracing::info!(mode = "standalone", "Agent session starting");
|
||||
fabro_agent::cli::run_with_args(args, mcp_servers).await?
|
||||
run_with_args(args, mcp_servers).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +63,7 @@ pub async fn execute(mut args: fabro_agent::cli::AgentArgs, globals: &GlobalArgs
|
|||
{
|
||||
let _ = globals;
|
||||
tracing::info!(mode = "standalone", "Agent session starting");
|
||||
fabro_agent::cli::run_with_args(args, mcp_servers).await?
|
||||
run_with_args(args, mcp_servers).await?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ use std::io::Write;
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::bail;
|
||||
use fabro_config::project::ResolveSettingsInput;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::project::{resolve_settings, resolve_workflow_path, ResolveSettingsInput};
|
||||
use fabro_graphviz::render::render_dot;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_validate::Severity;
|
||||
use fabro_workflows::operations::{validate, ValidateInput, WorkflowInput};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::args::{GraphArgs, GraphDirection};
|
||||
|
|
@ -16,22 +19,21 @@ static RANKDIR_RE: LazyLock<regex::Regex> =
|
|||
|
||||
pub fn run(args: &GraphArgs, styles: &Styles) -> anyhow::Result<()> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let cli_defaults = fabro_config::cli::load_cli_config(None)?;
|
||||
let settings = fabro_config::project::resolve_settings(ResolveSettingsInput {
|
||||
let cli_defaults = load_cli_config(None)?;
|
||||
let settings = resolve_settings(ResolveSettingsInput {
|
||||
workflow_path: args.workflow.clone(),
|
||||
cwd: cwd.clone(),
|
||||
defaults: cli_defaults,
|
||||
overrides: fabro_config::FabroConfig::default(),
|
||||
apply_project_config: true,
|
||||
})?;
|
||||
let resolution = fabro_config::project::resolve_workflow_path(&args.workflow, &cwd)?;
|
||||
let validated =
|
||||
fabro_workflows::operations::validate(fabro_workflows::operations::ValidateInput {
|
||||
workflow: fabro_workflows::operations::WorkflowInput::Path(args.workflow.clone()),
|
||||
settings,
|
||||
cwd,
|
||||
custom_transforms: Vec::new(),
|
||||
})?;
|
||||
let resolution = resolve_workflow_path(&args.workflow, &cwd)?;
|
||||
let validated = validate(ValidateInput {
|
||||
workflow: WorkflowInput::Path(args.workflow.clone()),
|
||||
settings,
|
||||
cwd,
|
||||
custom_transforms: Vec::new(),
|
||||
})?;
|
||||
let diagnostics = validated.diagnostics();
|
||||
|
||||
print_diagnostics(diagnostics, styles);
|
||||
|
|
@ -42,7 +44,7 @@ pub fn run(args: &GraphArgs, styles: &Styles) -> anyhow::Result<()> {
|
|||
|
||||
let source = read_workflow_file(&resolution.dot_path)?;
|
||||
let source = apply_direction(&source, args.direction);
|
||||
let rendered = fabro_graphviz::render::render_dot(&source, args.format.into())?;
|
||||
let rendered = render_dot(&source, args.format.into())?;
|
||||
|
||||
if let Some(ref output_path) = args.output {
|
||||
std::fs::write(output_path, &rendered)?;
|
||||
|
|
|
|||
|
|
@ -8,12 +8,17 @@ use anyhow::{bail, Context, Result};
|
|||
use axum::extract::Query;
|
||||
use axum::response::Html;
|
||||
use axum::routing::get;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use base64::Engine as _;
|
||||
use dialoguer::console::Term;
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use dialoguer::{MultiSelect, Select};
|
||||
use fabro_model::Provider;
|
||||
use fabro_util::terminal::Styles;
|
||||
use rand::Rng;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
use super::doctor;
|
||||
use crate::shared::provider_auth::{
|
||||
|
|
@ -224,29 +229,23 @@ fn detect_binary_on_path(binary: &str) -> bool {
|
|||
|
||||
#[cfg(feature = "server")]
|
||||
fn prompt_input(prompt: &str) -> Result<String> {
|
||||
Ok(
|
||||
dialoguer::Input::with_theme(&dialoguer::theme::ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.interact_on(&dialoguer::console::Term::stderr())?,
|
||||
)
|
||||
Ok(dialoguer::Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.interact_on(&Term::stderr())?)
|
||||
}
|
||||
|
||||
fn prompt_select(prompt: &str, items: &[String]) -> Result<usize> {
|
||||
Ok(
|
||||
Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.items(items)
|
||||
.interact_on(&dialoguer::console::Term::stderr())?,
|
||||
)
|
||||
Ok(Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.items(items)
|
||||
.interact_on(&Term::stderr())?)
|
||||
}
|
||||
|
||||
fn prompt_multiselect(prompt: &str, items: &[String]) -> Result<Vec<usize>> {
|
||||
Ok(
|
||||
MultiSelect::with_theme(&dialoguer::theme::ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.items(items)
|
||||
.interact_on(&dialoguer::console::Term::stderr())?,
|
||||
)
|
||||
Ok(MultiSelect::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.items(items)
|
||||
.interact_on(&Term::stderr())?)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -468,8 +467,7 @@ async fn setup_github_app(
|
|||
);
|
||||
|
||||
// Return secrets as env pairs
|
||||
let pem_b64 =
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, pem.as_bytes());
|
||||
let pem_b64 = BASE64_STANDARD.encode(pem.as_bytes());
|
||||
|
||||
let mut env_pairs = vec![
|
||||
("GITHUB_APP_PRIVATE_KEY".to_string(), pem_b64),
|
||||
|
|
@ -524,7 +522,7 @@ pub async fn run_install(web_url: &str) -> Result<()> {
|
|||
let dot_idx = doctor::DEP_SPECS.iter().position(|s| s.name == "dot");
|
||||
if let Some(idx) = dot_idx {
|
||||
if matches!(dep_outcomes[idx], doctor::ProbeOutcome::NotFound) {
|
||||
let install = tokio::task::spawn_blocking(|| {
|
||||
let install = spawn_blocking(|| {
|
||||
prompt_confirm("Graphviz (dot) not found. Install via Homebrew?", true)
|
||||
})
|
||||
.await??;
|
||||
|
|
@ -560,7 +558,7 @@ pub async fn run_install(web_url: &str) -> Result<()> {
|
|||
|
||||
if codex_detected {
|
||||
tracing::debug!("Codex binary detected on PATH");
|
||||
let use_oauth = tokio::task::spawn_blocking(|| {
|
||||
let use_oauth = spawn_blocking(|| {
|
||||
prompt_confirm(
|
||||
"OpenAI (Codex) detected. Set up OpenAI via browser login?",
|
||||
true,
|
||||
|
|
@ -584,7 +582,7 @@ pub async fn run_install(web_url: &str) -> Result<()> {
|
|||
.map(|p| provider_display_name(*p).to_string())
|
||||
.collect();
|
||||
|
||||
let primary_idx: usize = tokio::task::spawn_blocking({
|
||||
let primary_idx: usize = spawn_blocking({
|
||||
let labels = primary_labels.clone();
|
||||
move || prompt_select("Choose your first LLM provider", &labels)
|
||||
})
|
||||
|
|
@ -601,8 +599,7 @@ pub async fn run_install(web_url: &str) -> Result<()> {
|
|||
// Additional providers
|
||||
eprintln!();
|
||||
let add_more =
|
||||
tokio::task::spawn_blocking(|| prompt_confirm("Set up additional LLM providers?", false))
|
||||
.await??;
|
||||
spawn_blocking(|| prompt_confirm("Set up additional LLM providers?", false)).await??;
|
||||
|
||||
if add_more {
|
||||
let remaining_labels: Vec<String> = Provider::ALL
|
||||
|
|
@ -619,7 +616,7 @@ pub async fn run_install(web_url: &str) -> Result<()> {
|
|||
.copied()
|
||||
.collect();
|
||||
|
||||
let selected_indices: Vec<usize> = tokio::task::spawn_blocking({
|
||||
let selected_indices: Vec<usize> = spawn_blocking({
|
||||
let labels = remaining_labels.clone();
|
||||
move || prompt_multiselect("Which additional LLM providers?", &labels)
|
||||
})
|
||||
|
|
@ -644,10 +641,8 @@ pub async fn run_install(web_url: &str) -> Result<()> {
|
|||
eprintln!();
|
||||
|
||||
{
|
||||
let setup_github = tokio::task::spawn_blocking(|| {
|
||||
prompt_confirm("Set up a GitHub App? (Recommended)", true)
|
||||
})
|
||||
.await??;
|
||||
let setup_github =
|
||||
spawn_blocking(|| prompt_confirm("Set up a GitHub App? (Recommended)", true)).await??;
|
||||
|
||||
if setup_github {
|
||||
let github_env_pairs = setup_github_app(&arc_dir, &s, web_url).await?;
|
||||
|
|
@ -686,7 +681,7 @@ pub async fn run_install(web_url: &str) -> Result<()> {
|
|||
|
||||
let config_path = arc_dir.join("server.toml");
|
||||
let write_config = if config_path.exists() {
|
||||
tokio::task::spawn_blocking(|| {
|
||||
spawn_blocking(|| {
|
||||
prompt_confirm("~/.fabro/server.toml already exists. Overwrite?", false)
|
||||
})
|
||||
.await??
|
||||
|
|
@ -696,8 +691,7 @@ pub async fn run_install(web_url: &str) -> Result<()> {
|
|||
|
||||
if write_config {
|
||||
let username: String =
|
||||
tokio::task::spawn_blocking(|| prompt_input("GitHub username for allowed access"))
|
||||
.await??;
|
||||
spawn_blocking(|| prompt_input("GitHub username for allowed access")).await??;
|
||||
|
||||
let toml_content = format_config_toml(&username);
|
||||
std::fs::write(&config_path, &toml_content)?;
|
||||
|
|
@ -732,14 +726,8 @@ pub async fn run_install(web_url: &str) -> Result<()> {
|
|||
s.green.apply_to("✔")
|
||||
);
|
||||
|
||||
let jwt_private_b64 = base64::Engine::encode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
jwt_private_pem.as_bytes(),
|
||||
);
|
||||
let jwt_public_b64 = base64::Engine::encode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
jwt_public_pem.as_bytes(),
|
||||
);
|
||||
let jwt_private_b64 = BASE64_STANDARD.encode(jwt_private_pem.as_bytes());
|
||||
let jwt_public_b64 = BASE64_STANDARD.encode(jwt_public_pem.as_bytes());
|
||||
|
||||
let server_env_pairs = vec![
|
||||
("FABRO_JWT_PRIVATE_KEY".to_string(), jwt_private_b64),
|
||||
|
|
@ -759,8 +747,7 @@ pub async fn run_install(web_url: &str) -> Result<()> {
|
|||
// Verify setup
|
||||
let env_path = arc_dir.join(".env");
|
||||
let run_doctor =
|
||||
tokio::task::spawn_blocking(|| prompt_confirm("Run fabro doctor to verify?", true))
|
||||
.await??;
|
||||
spawn_blocking(|| prompt_confirm("Run fabro doctor to verify?", true)).await??;
|
||||
|
||||
if run_doctor {
|
||||
// Reload .env so doctor sees the values we just wrote
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
use anyhow::Result;
|
||||
use fabro_config::FabroSettings;
|
||||
use fabro_llm::cli::{run_chat, ChatArgs};
|
||||
#[cfg(feature = "server")]
|
||||
use fabro_llm::cli::{run_chat_via_server, ServerConnection};
|
||||
|
||||
use crate::args::GlobalArgs;
|
||||
|
||||
pub async fn execute(
|
||||
mut args: fabro_llm::cli::ChatArgs,
|
||||
mut args: ChatArgs,
|
||||
cli_config: &FabroSettings,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
|
|
@ -23,14 +26,14 @@ pub async fn execute(
|
|||
match resolved.mode {
|
||||
crate::cli_config::ExecutionMode::Server => {
|
||||
let client = crate::cli_config::build_server_client(resolved.tls.as_ref())?;
|
||||
let server = fabro_llm::cli::ServerConnection {
|
||||
let server = ServerConnection {
|
||||
client,
|
||||
base_url: resolved.server_base_url,
|
||||
};
|
||||
fabro_llm::cli::run_chat_via_server(args, &server).await?;
|
||||
run_chat_via_server(args, &server).await?;
|
||||
}
|
||||
crate::cli_config::ExecutionMode::Standalone => {
|
||||
fabro_llm::cli::run_chat(args).await?;
|
||||
run_chat(args).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +41,7 @@ pub async fn execute(
|
|||
#[cfg(not(feature = "server"))]
|
||||
{
|
||||
let _ = globals;
|
||||
fabro_llm::cli::run_chat(args).await?;
|
||||
run_chat(args).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ mod prompt;
|
|||
use anyhow::Result;
|
||||
|
||||
use crate::args::{GlobalArgs, LlmCommand, LlmNamespace};
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub async fn dispatch(ns: LlmNamespace, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
|
||||
match ns.command {
|
||||
LlmCommand::Prompt(args) => prompt::execute(args, &cli_config, globals).await,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
use anyhow::Result;
|
||||
use fabro_config::FabroSettings;
|
||||
use fabro_llm::cli::{run_prompt, PromptArgs};
|
||||
#[cfg(feature = "server")]
|
||||
use fabro_llm::cli::{run_prompt_via_server, ServerConnection};
|
||||
|
||||
use crate::args::GlobalArgs;
|
||||
|
||||
pub async fn execute(
|
||||
mut args: fabro_llm::cli::PromptArgs,
|
||||
mut args: PromptArgs,
|
||||
cli_config: &FabroSettings,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
|
|
@ -23,14 +26,14 @@ pub async fn execute(
|
|||
match resolved.mode {
|
||||
crate::cli_config::ExecutionMode::Server => {
|
||||
let client = crate::cli_config::build_server_client(resolved.tls.as_ref())?;
|
||||
let server = fabro_llm::cli::ServerConnection {
|
||||
let server = ServerConnection {
|
||||
client,
|
||||
base_url: resolved.server_base_url,
|
||||
};
|
||||
fabro_llm::cli::run_prompt_via_server(args, &server).await?;
|
||||
run_prompt_via_server(args, &server).await?;
|
||||
}
|
||||
crate::cli_config::ExecutionMode::Standalone => {
|
||||
fabro_llm::cli::run_prompt(args).await?;
|
||||
run_prompt(args).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +41,7 @@ pub async fn execute(
|
|||
#[cfg(not(feature = "server"))]
|
||||
{
|
||||
let _ = globals;
|
||||
fabro_llm::cli::run_prompt(args).await?;
|
||||
run_prompt(args).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use anyhow::Result;
|
||||
#[cfg(feature = "server")]
|
||||
use fabro_llm::cli::ServerConnection;
|
||||
use fabro_llm::cli::{run_models, ModelsCommand};
|
||||
|
||||
use crate::args::GlobalArgs;
|
||||
#[cfg(feature = "server")]
|
||||
use crate::cli_config;
|
||||
|
||||
pub async fn execute(
|
||||
command: Option<fabro_llm::cli::ModelsCommand>,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
pub async fn execute(command: Option<ModelsCommand>, globals: &GlobalArgs) -> Result<()> {
|
||||
let server = {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
|
|
@ -20,7 +20,7 @@ pub async fn execute(
|
|||
match resolved.mode {
|
||||
cli_config::ExecutionMode::Server => {
|
||||
let client = cli_config::build_server_client(resolved.tls.as_ref())?;
|
||||
Some(fabro_llm::cli::ServerConnection {
|
||||
Some(ServerConnection {
|
||||
client,
|
||||
base_url: resolved.server_base_url,
|
||||
})
|
||||
|
|
@ -35,5 +35,5 @@ pub async fn execute(
|
|||
}
|
||||
};
|
||||
|
||||
fabro_llm::cli::run_models(command, server).await
|
||||
run_models(command, server).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
use std::io::Write;
|
||||
|
||||
use fabro_config::project::resolve_workflow;
|
||||
use fabro_graphviz::parser::parse_ast;
|
||||
|
||||
use crate::args::ParseArgs;
|
||||
use crate::shared::read_workflow_file;
|
||||
use std::io::Write;
|
||||
|
||||
pub fn run(args: &ParseArgs) -> anyhow::Result<()> {
|
||||
let stdout = std::io::stdout();
|
||||
|
|
@ -8,9 +12,9 @@ pub fn run(args: &ParseArgs) -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
fn run_to(args: &ParseArgs, mut out: impl Write) -> anyhow::Result<()> {
|
||||
let (dot_path, _cfg) = fabro_config::project::resolve_workflow(&args.workflow)?;
|
||||
let (dot_path, _cfg) = resolve_workflow(&args.workflow)?;
|
||||
let source = read_workflow_file(&dot_path)?;
|
||||
let ast = fabro_graphviz::parser::parse_ast(&source)?;
|
||||
let ast = parse_ast(&source)?;
|
||||
serde_json::to_writer_pretty(&mut out, &ast)?;
|
||||
writeln!(out)?;
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ use std::path::Path;
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_workflows::run_lookup::runs_base;
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::PrCloseArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub async fn close_command(
|
||||
args: PrCloseArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
close_from(&base, args, github_app).await
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,17 +3,24 @@ use std::path::Path;
|
|||
use anyhow::{bail, Context, Result};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_model::Catalog;
|
||||
use fabro_workflows::records::{ConclusionExt, RunRecordExt, StartRecordExt};
|
||||
use fabro_sandbox::daytona::detect_repo_info;
|
||||
use fabro_workflows::outcome::StageStatus;
|
||||
use fabro_workflows::pull_request::maybe_open_pull_request;
|
||||
use fabro_workflows::records::{
|
||||
Conclusion, ConclusionExt, RunRecord, RunRecordExt, StartRecord, StartRecordExt,
|
||||
};
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::PrCreateArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub async fn create_command(
|
||||
args: PrCreateArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
create_from(&base, args, github_app).await
|
||||
}
|
||||
|
||||
|
|
@ -22,20 +29,17 @@ async fn create_from(
|
|||
args: PrCreateArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let run_dir = fabro_workflows::run_lookup::resolve_run(base, &args.run_id)?.path;
|
||||
let run_dir = resolve_run(base, &args.run_id)?.path;
|
||||
|
||||
let record =
|
||||
fabro_workflows::records::RunRecord::load(&run_dir).context("Failed to load run.json")?;
|
||||
let record = RunRecord::load(&run_dir).context("Failed to load run.json")?;
|
||||
|
||||
let start = fabro_workflows::records::StartRecord::load(&run_dir)
|
||||
.context("Failed to load start.json")?;
|
||||
let start = StartRecord::load(&run_dir).context("Failed to load start.json")?;
|
||||
|
||||
let conclusion = fabro_workflows::records::Conclusion::load(&run_dir.join("conclusion.json"))
|
||||
let conclusion = Conclusion::load(&run_dir.join("conclusion.json"))
|
||||
.context("Failed to load conclusion.json — is the run finished?")?;
|
||||
|
||||
match conclusion.status {
|
||||
fabro_workflows::outcome::StageStatus::Success
|
||||
| fabro_workflows::outcome::StageStatus::PartialSuccess => {}
|
||||
StageStatus::Success | StageStatus::PartialSuccess => {}
|
||||
status => bail!("Run status is '{status}', expected success or partial_success"),
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +56,7 @@ async fn create_from(
|
|||
|
||||
let cwd = std::env::current_dir().context("Failed to get current directory")?;
|
||||
let (origin_url, detected_branch) =
|
||||
fabro_sandbox::daytona::detect_repo_info(&cwd).map_err(|err| anyhow::anyhow!("{err}"))?;
|
||||
detect_repo_info(&cwd).map_err(|err| anyhow::anyhow!("{err}"))?;
|
||||
|
||||
let base_branch = record
|
||||
.base_branch
|
||||
|
|
@ -89,7 +93,7 @@ async fn create_from(
|
|||
.model
|
||||
.unwrap_or_else(|| Catalog::builtin().default_from_env().id.clone());
|
||||
|
||||
let record = fabro_workflows::pull_request::maybe_open_pull_request(
|
||||
let record = maybe_open_pull_request(
|
||||
&creds,
|
||||
&origin_url,
|
||||
base_branch,
|
||||
|
|
|
|||
|
|
@ -2,16 +2,20 @@ use std::path::Path;
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_workflows::pull_request::PullRequestRecord;
|
||||
use fabro_workflows::run_lookup::{runs_base, scan_runs};
|
||||
use futures::future::join_all;
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::PrListArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub async fn list_command(
|
||||
args: PrListArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
list_from(&base, args, github_app).await
|
||||
}
|
||||
|
||||
|
|
@ -24,15 +28,13 @@ async fn list_from(
|
|||
"GitHub App credentials required — set GITHUB_APP_PRIVATE_KEY and configure app_id",
|
||||
)?;
|
||||
|
||||
let runs = fabro_workflows::run_lookup::scan_runs(base).context("Failed to scan runs")?;
|
||||
let runs = scan_runs(base).context("Failed to scan runs")?;
|
||||
|
||||
let mut entries: Vec<(String, fabro_workflows::pull_request::PullRequestRecord)> = Vec::new();
|
||||
let mut entries: Vec<(String, PullRequestRecord)> = Vec::new();
|
||||
for run in &runs {
|
||||
let pr_path = run.path.join("pull_request.json");
|
||||
if let Ok(content) = std::fs::read_to_string(&pr_path) {
|
||||
if let Ok(record) =
|
||||
serde_json::from_str::<fabro_workflows::pull_request::PullRequestRecord>(&content)
|
||||
{
|
||||
if let Ok(record) = serde_json::from_str::<PullRequestRecord>(&content) {
|
||||
entries.push((run.run_id.clone(), record));
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +95,7 @@ async fn list_from(
|
|||
})
|
||||
.collect();
|
||||
|
||||
let all_rows = futures::future::join_all(futures).await;
|
||||
let all_rows = join_all(futures).await;
|
||||
let rows: Vec<_> = if args.all {
|
||||
all_rows
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ use anyhow::{Context, Result};
|
|||
use fabro_config::FabroSettingsExt;
|
||||
use tracing::info;
|
||||
|
||||
use fabro_workflows::run_lookup::runs_base;
|
||||
|
||||
use crate::args::PrMergeArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub async fn merge_command(
|
||||
args: PrMergeArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
merge_from(&base, args, github_app).await
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,16 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use fabro_workflows::pull_request::PullRequestRecord;
|
||||
use fabro_workflows::run_lookup::resolve_run;
|
||||
|
||||
use crate::args::{PrCommand, PrNamespace};
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::github::build_github_app_credentials;
|
||||
|
||||
pub async fn dispatch(ns: PrNamespace) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let github_app = crate::shared::github::build_github_app_credentials(cli_config.app_id());
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let github_app = build_github_app_credentials(cli_config.app_id());
|
||||
|
||||
match ns.command {
|
||||
PrCommand::Create(args) => create::create_command(args, github_app).await,
|
||||
|
|
@ -23,11 +28,8 @@ pub async fn dispatch(ns: PrNamespace) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_pr_record(
|
||||
base: &Path,
|
||||
run_id: &str,
|
||||
) -> Result<(fabro_workflows::pull_request::PullRequestRecord, PathBuf)> {
|
||||
let run_dir = fabro_workflows::run_lookup::resolve_run(base, run_id)?.path;
|
||||
pub(crate) fn load_pr_record(base: &Path, run_id: &str) -> Result<(PullRequestRecord, PathBuf)> {
|
||||
let run_dir = resolve_run(base, run_id)?.path;
|
||||
let pr_path = run_dir.join("pull_request.json");
|
||||
let content = std::fs::read_to_string(&pr_path).with_context(|| {
|
||||
format!(
|
||||
|
|
@ -35,7 +37,7 @@ pub(crate) fn load_pr_record(
|
|||
Create one first with: fabro pr create {run_id}"
|
||||
)
|
||||
})?;
|
||||
let record: fabro_workflows::pull_request::PullRequestRecord =
|
||||
let record: PullRequestRecord =
|
||||
serde_json::from_str(&content).context("Failed to parse pull_request.json")?;
|
||||
Ok((record, run_dir))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ use anyhow::{Context, Result};
|
|||
use fabro_config::FabroSettingsExt;
|
||||
use tracing::info;
|
||||
|
||||
use fabro_workflows::run_lookup::runs_base;
|
||||
|
||||
use crate::args::PrViewArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub async fn view_command(
|
||||
args: PrViewArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
view_from(&base, args, github_app).await
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,39 +3,47 @@ use std::sync::Arc;
|
|||
|
||||
use anyhow::bail;
|
||||
use fabro_agent::{DockerSandbox, DockerSandboxConfig, LocalSandbox, Sandbox};
|
||||
use fabro_config::project::ResolveSettingsInput;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::project::{
|
||||
resolve_settings, resolve_workflow_path, resolve_working_directory, ResolveSettingsInput,
|
||||
};
|
||||
use fabro_config::{FabroConfig, FabroSettings};
|
||||
use fabro_graphviz::graph::{is_llm_handler_type, Graph};
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_model::{Catalog, Provider};
|
||||
use fabro_sandbox::daytona::{detect_repo_info, DaytonaConfig, DaytonaSandbox};
|
||||
use fabro_sandbox::ssh::{GitCloneParams as SshGitCloneParams, SshConfig, SshSandbox};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::git::GitSyncStatus;
|
||||
use fabro_workflows::git::{sync_status, GitSyncStatus};
|
||||
use fabro_workflows::operations::{validate, ValidateInput, WorkflowInput};
|
||||
|
||||
use crate::args::PreflightArgs;
|
||||
use crate::shared::github::build_github_app_credentials;
|
||||
|
||||
pub async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let cli_defaults = fabro_config::cli::load_cli_config(None)?;
|
||||
let cli_defaults = load_cli_config(None)?;
|
||||
let cli_config: FabroSettings = cli_defaults.clone().try_into()?;
|
||||
args.verbose = args.verbose || cli_config.verbose_enabled();
|
||||
|
||||
let github_app = crate::shared::github::build_github_app_credentials(cli_config.app_id());
|
||||
let github_app = build_github_app_credentials(cli_config.app_id());
|
||||
let cli_args_config = FabroConfig::try_from(&args)?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
let settings = fabro_config::project::resolve_settings(ResolveSettingsInput {
|
||||
let settings = resolve_settings(ResolveSettingsInput {
|
||||
workflow_path: args.workflow.clone(),
|
||||
cwd: cwd.clone(),
|
||||
defaults: cli_defaults,
|
||||
overrides: cli_args_config,
|
||||
apply_project_config: true,
|
||||
})?;
|
||||
let resolution = fabro_config::project::resolve_workflow_path(&args.workflow, &cwd)?;
|
||||
let working_directory = fabro_config::project::resolve_working_directory(&settings, &cwd);
|
||||
let resolution = resolve_workflow_path(&args.workflow, &cwd)?;
|
||||
let working_directory = resolve_working_directory(&settings, &cwd);
|
||||
|
||||
let (origin_url, detected_base_branch) =
|
||||
fabro_sandbox::daytona::detect_repo_info(&working_directory)
|
||||
.map(|(url, branch)| (Some(url), branch))
|
||||
.unwrap_or((None, None));
|
||||
let git_status = fabro_workflows::git::sync_status(
|
||||
let (origin_url, detected_base_branch) = detect_repo_info(&working_directory)
|
||||
.map(|(url, branch)| (Some(url), branch))
|
||||
.unwrap_or((None, None));
|
||||
let git_status = sync_status(
|
||||
&working_directory,
|
||||
"origin",
|
||||
detected_base_branch.as_deref(),
|
||||
|
|
@ -43,13 +51,12 @@ pub async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> {
|
|||
|
||||
let sandbox_provider = resolve_sandbox_provider(args.sandbox.map(Into::into), &settings)?;
|
||||
|
||||
let validated =
|
||||
fabro_workflows::operations::validate(fabro_workflows::operations::ValidateInput {
|
||||
workflow: fabro_workflows::operations::WorkflowInput::Path(args.workflow.clone()),
|
||||
settings: settings.clone(),
|
||||
cwd,
|
||||
custom_transforms: Vec::new(),
|
||||
})?;
|
||||
let validated = validate(ValidateInput {
|
||||
workflow: WorkflowInput::Path(args.workflow.clone()),
|
||||
settings: settings.clone(),
|
||||
cwd,
|
||||
custom_transforms: Vec::new(),
|
||||
})?;
|
||||
super::run::output::print_workflow_report(&validated, Some(&resolution.dot_path), styles);
|
||||
if validated.has_errors() {
|
||||
bail!("Validation failed");
|
||||
|
|
@ -74,7 +81,7 @@ fn resolve_model_provider(
|
|||
cli_model: Option<&str>,
|
||||
cli_provider: Option<&str>,
|
||||
settings: &FabroSettings,
|
||||
graph: &fabro_graphviz::graph::Graph,
|
||||
graph: &Graph,
|
||||
) -> (String, Option<String>) {
|
||||
let configured_model = settings.llm.as_ref().and_then(|llm| llm.model.as_deref());
|
||||
let configured_provider = settings
|
||||
|
|
@ -128,9 +135,7 @@ fn resolve_sandbox_provider(
|
|||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
fn resolve_daytona_config(
|
||||
settings: &FabroSettings,
|
||||
) -> Option<fabro_sandbox::daytona::DaytonaConfig> {
|
||||
fn resolve_daytona_config(settings: &FabroSettings) -> Option<DaytonaConfig> {
|
||||
settings
|
||||
.sandbox_settings()
|
||||
.and_then(|sandbox| sandbox.daytona.clone())
|
||||
|
|
@ -145,7 +150,7 @@ fn resolve_exe_config(settings: &FabroSettings) -> Option<fabro_sandbox::exe::Ex
|
|||
|
||||
#[cfg(feature = "exedev")]
|
||||
fn resolve_exe_clone_params(cwd: &Path) -> Option<fabro_sandbox::exe::GitCloneParams> {
|
||||
let (detected_url, branch) = match fabro_sandbox::daytona::detect_repo_info(cwd) {
|
||||
let (detected_url, branch) = match detect_repo_info(cwd) {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
tracing::warn!("No git repo detected for exe.dev clone: {err}");
|
||||
|
|
@ -156,14 +161,14 @@ fn resolve_exe_clone_params(cwd: &Path) -> Option<fabro_sandbox::exe::GitClonePa
|
|||
Some(fabro_sandbox::exe::GitCloneParams { url, branch })
|
||||
}
|
||||
|
||||
fn resolve_ssh_config(settings: &FabroSettings) -> Option<fabro_sandbox::ssh::SshConfig> {
|
||||
fn resolve_ssh_config(settings: &FabroSettings) -> Option<SshConfig> {
|
||||
settings
|
||||
.sandbox_settings()
|
||||
.and_then(|sandbox| sandbox.ssh.clone())
|
||||
}
|
||||
|
||||
fn resolve_ssh_clone_params(cwd: &Path) -> Option<fabro_sandbox::ssh::GitCloneParams> {
|
||||
let (detected_url, branch) = match fabro_sandbox::daytona::detect_repo_info(cwd) {
|
||||
fn resolve_ssh_clone_params(cwd: &Path) -> Option<SshGitCloneParams> {
|
||||
let (detected_url, branch) = match detect_repo_info(cwd) {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
tracing::warn!("No git repo detected for SSH clone: {err}");
|
||||
|
|
@ -171,7 +176,7 @@ fn resolve_ssh_clone_params(cwd: &Path) -> Option<fabro_sandbox::ssh::GitClonePa
|
|||
}
|
||||
};
|
||||
let url = fabro_github::ssh_url_to_https(&detected_url);
|
||||
Some(fabro_sandbox::ssh::GitCloneParams { url, branch })
|
||||
Some(SshGitCloneParams { url, branch })
|
||||
}
|
||||
|
||||
async fn mint_github_token(
|
||||
|
|
@ -201,7 +206,7 @@ async fn mint_github_token(
|
|||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn run_preflight(
|
||||
graph: &fabro_graphviz::graph::Graph,
|
||||
graph: &Graph,
|
||||
settings: &FabroSettings,
|
||||
cli_model: Option<&str>,
|
||||
cli_provider: Option<&str>,
|
||||
|
|
@ -281,14 +286,7 @@ async fn run_preflight(
|
|||
}
|
||||
SandboxProvider::Daytona => {
|
||||
let config = daytona_config.unwrap_or_default();
|
||||
match fabro_sandbox::daytona::DaytonaSandbox::new(
|
||||
config,
|
||||
github_app.clone(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
match DaytonaSandbox::new(config, github_app.clone(), None, None).await {
|
||||
Ok(env) => Ok(Arc::new(env) as Arc<dyn Sandbox>),
|
||||
Err(e) => Err(format!("Daytona sandbox creation failed: {e}")),
|
||||
}
|
||||
|
|
@ -316,7 +314,7 @@ async fn run_preflight(
|
|||
SandboxProvider::Ssh => match ssh_config {
|
||||
Some(config) => {
|
||||
let clone_params = resolve_ssh_clone_params(working_directory);
|
||||
let env = fabro_sandbox::ssh::SshSandbox::new(config, clone_params, None, None);
|
||||
let env = SshSandbox::new(config, clone_params, None, None);
|
||||
Ok(Arc::new(env) as Arc<dyn Sandbox>)
|
||||
}
|
||||
None => Err("SSH sandbox requires [sandbox.ssh] config".to_string()),
|
||||
|
|
@ -367,14 +365,14 @@ async fn run_preflight(
|
|||
}
|
||||
|
||||
let default_provider = provider.as_deref().unwrap_or("anthropic");
|
||||
let llm_ok = match fabro_llm::client::Client::from_env().await {
|
||||
let llm_ok = match LlmClient::from_env().await {
|
||||
Ok(c) => {
|
||||
let configured: Vec<String> =
|
||||
c.provider_names().iter().map(|s| s.to_string()).collect();
|
||||
|
||||
let mut model_providers = std::collections::BTreeSet::new();
|
||||
for node in graph.nodes.values() {
|
||||
if !fabro_graphviz::graph::is_llm_handler_type(node.handler_type()) {
|
||||
if !is_llm_handler_type(node.handler_type()) {
|
||||
continue;
|
||||
}
|
||||
let node_model = node.model().unwrap_or(&model);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use anyhow::{Context, Result};
|
||||
use fabro_model::Provider;
|
||||
use fabro_util::terminal::Styles;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
use crate::args::ProviderLoginArgs;
|
||||
use crate::shared::provider_auth;
|
||||
|
|
@ -13,10 +14,8 @@ pub async fn login_command(args: ProviderLoginArgs) -> Result<()> {
|
|||
std::fs::create_dir_all(&arc_dir)?;
|
||||
|
||||
let use_oauth = args.provider == Provider::OpenAi
|
||||
&& tokio::task::spawn_blocking(|| {
|
||||
provider_auth::prompt_confirm("Log in via browser (OAuth)?", true)
|
||||
})
|
||||
.await??;
|
||||
&& spawn_blocking(|| provider_auth::prompt_confirm("Log in via browser (OAuth)?", true))
|
||||
.await??;
|
||||
|
||||
let env_pairs = if use_oauth {
|
||||
provider_auth::run_openai_oauth_or_api_key(&s).await?
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::github::build_github_app_credentials;
|
||||
|
||||
pub(super) fn git_repo_root() -> Result<PathBuf> {
|
||||
let output = std::process::Command::new("git")
|
||||
|
|
@ -150,7 +154,7 @@ async fn check_github_app_installation() {
|
|||
};
|
||||
|
||||
// Load CLI config to get app_id and slug
|
||||
let cli_config = match crate::cli_config::load_cli_settings(None) {
|
||||
let cli_config = match load_cli_settings(None) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
|
@ -172,7 +176,7 @@ async fn check_github_app_installation() {
|
|||
let slug = cli_config.slug().map(String::from);
|
||||
|
||||
// Build GitHub App credentials
|
||||
let creds = match crate::shared::github::build_github_app_credentials(Some(&app_id)) {
|
||||
let creds = match build_github_app_credentials(Some(&app_id)) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
eprintln!(
|
||||
|
|
@ -264,7 +268,7 @@ async fn check_github_app_installation() {
|
|||
// Only prompt if stdin is a terminal
|
||||
if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
|
||||
eprintln!(" Press Enter to continue after installing...");
|
||||
let _ = tokio::task::spawn_blocking(|| {
|
||||
let _ = spawn_blocking(|| {
|
||||
let mut buf = String::new();
|
||||
let _ = std::io::stdin().read_line(&mut buf);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@ use anyhow::{bail, Result};
|
|||
use fabro_interview::{AnswerValue, ConsoleInterviewer};
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::records::{ConclusionExt, RunRecordExt};
|
||||
use fabro_workflows::outcome::StageStatus;
|
||||
use fabro_workflows::records::{Conclusion, ConclusionExt, RunRecord, RunRecordExt};
|
||||
use fabro_workflows::run_status::{RunStatus, RunStatusRecord, RunStatusRecordExt};
|
||||
use tokio::signal::ctrl_c;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use super::run_progress;
|
||||
|
||||
|
|
@ -41,7 +44,7 @@ pub async fn attach_run(
|
|||
let mut engine_guard = engine_child.map(EngineChildGuard::new);
|
||||
|
||||
let is_tty = std::io::stderr().is_terminal();
|
||||
let verbose = fabro_workflows::records::RunRecord::load(run_dir)
|
||||
let verbose = RunRecord::load(run_dir)
|
||||
.map(|record| record.settings.verbose_enabled())
|
||||
.unwrap_or(false);
|
||||
let mut progress_ui = run_progress::ProgressUI::new(is_tty, verbose);
|
||||
|
|
@ -51,7 +54,7 @@ pub async fn attach_run(
|
|||
{
|
||||
let cancelled = Arc::clone(&cancelled);
|
||||
tokio::spawn(async move {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
let _ = ctrl_c().await;
|
||||
cancelled.store(true, Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
|
|
@ -62,7 +65,7 @@ pub async fn attach_run(
|
|||
// engine death so we surface the real failure instead of timing out.
|
||||
let mut wait_count = 0;
|
||||
while !progress_path.exists() {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
sleep(std::time::Duration::from_millis(100)).await;
|
||||
wait_count += 1;
|
||||
|
||||
// Check if engine died before writing any progress
|
||||
|
|
@ -140,7 +143,7 @@ pub async fn attach_run(
|
|||
{
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
} else {
|
||||
if let Some(guard) = engine_guard.as_mut() {
|
||||
|
|
@ -246,7 +249,7 @@ pub async fn attach_run(
|
|||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
// Finish progress bars
|
||||
|
|
@ -438,11 +441,10 @@ fn write_interview_response_atomically(
|
|||
|
||||
fn determine_exit_code(conclusion_path: &Path, status_record: Option<RunStatusRecord>) -> ExitCode {
|
||||
if conclusion_path.exists() {
|
||||
if let Ok(conclusion) = fabro_workflows::records::Conclusion::load(conclusion_path) {
|
||||
if let Ok(conclusion) = Conclusion::load(conclusion_path) {
|
||||
let success = matches!(
|
||||
conclusion.status,
|
||||
fabro_workflows::outcome::StageStatus::Success
|
||||
| fabro_workflows::outcome::StageStatus::PartialSuccess
|
||||
StageStatus::Success | StageStatus::PartialSuccess
|
||||
);
|
||||
return if success {
|
||||
ExitCode::from(0)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use anyhow::Result;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::{GlobalArgs, RunArgs};
|
||||
|
||||
pub async fn execute(mut args: RunArgs, _globals: &GlobalArgs) -> Result<()> {
|
||||
let styles: &'static fabro_util::terminal::Styles =
|
||||
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
|
||||
let cli_defaults = fabro_config::cli::load_cli_config(None)?;
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let cli_defaults = load_cli_config(None)?;
|
||||
let cli_config: fabro_config::FabroSettings = cli_defaults.clone().try_into()?;
|
||||
args.verbose = args.verbose || cli_config.verbose_enabled();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use fabro_agent::sandbox::Sandbox;
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_sandbox::reconnect::reconnect;
|
||||
use fabro_sandbox::SandboxRecordExt;
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
use tokio::fs;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::args::CpArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::split_run_path;
|
||||
|
||||
enum CopyDirection {
|
||||
|
|
@ -23,8 +28,8 @@ enum CopyDirection {
|
|||
|
||||
pub async fn cp_command(args: CpArgs) -> Result<()> {
|
||||
let direction = parse_direction(&args.src, &args.dst)?;
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
|
||||
match direction {
|
||||
CopyDirection::Download {
|
||||
|
|
@ -90,11 +95,8 @@ fn parse_direction(src: &str, dst: &str) -> Result<CopyDirection> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn load_sandbox(
|
||||
base: &Path,
|
||||
run_prefix: &str,
|
||||
) -> Result<Box<dyn fabro_agent::sandbox::Sandbox>> {
|
||||
let run_dir = fabro_workflows::run_lookup::resolve_run(base, run_prefix)?.path;
|
||||
async fn load_sandbox(base: &Path, run_prefix: &str) -> Result<Box<dyn Sandbox>> {
|
||||
let run_dir = resolve_run(base, run_prefix)?.path;
|
||||
let sandbox_json = run_dir.join("sandbox.json");
|
||||
debug!(path = %sandbox_json.display(), "Loading sandbox record");
|
||||
let record = fabro_sandbox::SandboxRecord::load(&sandbox_json).context(
|
||||
|
|
@ -102,11 +104,11 @@ async fn load_sandbox(
|
|||
)?;
|
||||
|
||||
info!(run_id = %run_prefix, provider = %record.provider, "Connecting to sandbox");
|
||||
fabro_sandbox::reconnect::reconnect(&record).await
|
||||
reconnect(&record).await
|
||||
}
|
||||
|
||||
async fn download_recursive(
|
||||
sandbox: &dyn fabro_agent::sandbox::Sandbox,
|
||||
sandbox: &dyn Sandbox,
|
||||
remote_path: &str,
|
||||
local_path: &Path,
|
||||
) -> Result<()> {
|
||||
|
|
@ -123,7 +125,7 @@ async fn download_recursive(
|
|||
let remote_file = format!("{remote_path}/{}", entry.name);
|
||||
let local_file = local_path.join(&entry.name);
|
||||
if let Some(parent) = local_file.parent() {
|
||||
tokio::fs::create_dir_all(parent)
|
||||
fs::create_dir_all(parent)
|
||||
.await
|
||||
.with_context(|| format!("Failed to create directory {}", parent.display()))?;
|
||||
}
|
||||
|
|
@ -139,7 +141,7 @@ async fn download_recursive(
|
|||
}
|
||||
|
||||
async fn upload_recursive(
|
||||
sandbox: &dyn fabro_agent::sandbox::Sandbox,
|
||||
sandbox: &dyn Sandbox,
|
||||
local_path: &Path,
|
||||
remote_path: &str,
|
||||
) -> Result<()> {
|
||||
|
|
@ -147,7 +149,7 @@ async fn upload_recursive(
|
|||
let mut stack = vec![(local_path.to_path_buf(), remote_path.to_string())];
|
||||
|
||||
while let Some((dir_path, dir_remote)) = stack.pop() {
|
||||
let mut entries = tokio::fs::read_dir(&dir_path)
|
||||
let mut entries = fs::read_dir(&dir_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to read directory {}", dir_path.display()))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::args::RunArgs;
|
||||
use fabro_config::project::ResolveSettingsInput;
|
||||
use fabro_config::project::{resolve_settings, ResolveSettingsInput};
|
||||
use fabro_config::{FabroConfig, FabroSettings};
|
||||
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::error::FabroError;
|
||||
use fabro_workflows::operations::{create, CreateRunInput, WorkflowInput};
|
||||
|
||||
use super::output::{print_diagnostics_from_error, print_workflow_report_from_persisted};
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ pub async fn create_run(
|
|||
.ok_or_else(|| anyhow::anyhow!("--workflow is required"))?;
|
||||
let cli_args_config = FabroConfig::try_from(args)?;
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let settings: FabroSettings = fabro_config::project::resolve_settings(ResolveSettingsInput {
|
||||
let settings: FabroSettings = resolve_settings(ResolveSettingsInput {
|
||||
workflow_path: workflow_path.clone(),
|
||||
cwd: cwd.clone(),
|
||||
defaults: cli_defaults,
|
||||
|
|
@ -31,26 +32,25 @@ pub async fn create_run(
|
|||
apply_project_config: true,
|
||||
})?;
|
||||
|
||||
let created =
|
||||
match fabro_workflows::operations::create(fabro_workflows::operations::CreateRunInput {
|
||||
workflow: fabro_workflows::operations::WorkflowInput::Path(workflow_path.clone()),
|
||||
settings,
|
||||
cwd,
|
||||
workflow_slug: None,
|
||||
run_dir: None,
|
||||
run_id: args.run_id.clone(),
|
||||
base_branch: None,
|
||||
host_repo_path: None,
|
||||
}) {
|
||||
Ok(created) => created,
|
||||
Err(fabro_workflows::error::FabroError::ValidationFailed { diagnostics }) => {
|
||||
if !quiet {
|
||||
print_diagnostics_from_error(&diagnostics, styles);
|
||||
}
|
||||
anyhow::bail!("Validation failed");
|
||||
let created = match create(CreateRunInput {
|
||||
workflow: WorkflowInput::Path(workflow_path.clone()),
|
||||
settings,
|
||||
cwd,
|
||||
workflow_slug: None,
|
||||
run_dir: None,
|
||||
run_id: args.run_id.clone(),
|
||||
base_branch: None,
|
||||
host_repo_path: None,
|
||||
}) {
|
||||
Ok(created) => created,
|
||||
Err(FabroError::ValidationFailed { diagnostics }) => {
|
||||
if !quiet {
|
||||
print_diagnostics_from_error(&diagnostics, styles);
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
anyhow::bail!("Validation failed");
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
if !quiet {
|
||||
print_workflow_report_from_persisted(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ use anyhow::Result;
|
|||
use fabro_interview::FileInterviewer;
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_workflows::event::EventEmitter;
|
||||
use fabro_workflows::git::GitAuthor;
|
||||
use fabro_workflows::operations::{resume as resume_run, start as start_run, StartServices};
|
||||
|
||||
use crate::cli_config;
|
||||
use crate::shared;
|
||||
|
|
@ -12,7 +14,7 @@ use crate::shared;
|
|||
pub async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bool) -> Result<()> {
|
||||
let cli_config = cli_config::load_cli_settings(None)?;
|
||||
let github_app = shared::github::build_github_app_credentials(cli_config.app_id());
|
||||
let git_author = fabro_workflows::git::GitAuthor::from_options(
|
||||
let git_author = GitAuthor::from_options(
|
||||
cli_config.git_author().and_then(|a| a.name.clone()),
|
||||
cli_config.git_author().and_then(|a| a.email.clone()),
|
||||
);
|
||||
|
|
@ -22,7 +24,7 @@ pub async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bool) ->
|
|||
});
|
||||
let runtime_state = RuntimeState::new(&run_dir);
|
||||
|
||||
let services = fabro_workflows::operations::StartServices {
|
||||
let services = StartServices {
|
||||
cancel_token: None,
|
||||
emitter: Arc::new(EventEmitter::new()),
|
||||
interviewer: Arc::new(FileInterviewer::new(
|
||||
|
|
@ -36,9 +38,9 @@ pub async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bool) ->
|
|||
};
|
||||
|
||||
if resume {
|
||||
let _ = fabro_workflows::operations::resume(&run_dir, services).await?;
|
||||
let _ = resume_run(&run_dir, services).await?;
|
||||
} else {
|
||||
let _ = fabro_workflows::operations::start(&run_dir, services).await?;
|
||||
let _ = start_run(&run_dir, services).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -3,17 +3,21 @@ use std::path::Path;
|
|||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_sandbox::reconnect::reconnect;
|
||||
use fabro_sandbox::SandboxRecordExt;
|
||||
use fabro_workflows::records::StartRecordExt;
|
||||
use fabro_workflows::records::{StartRecord, StartRecordExt};
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
use fabro_workflows::sandbox_git::GIT_REMOTE;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::args::DiffArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub async fn run(args: DiffArgs) -> Result<()> {
|
||||
info!(run_id = %args.run, "Showing diff");
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let run_dir = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?.path;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let run_dir = resolve_run(&base, &args.run)?.path;
|
||||
|
||||
let patch = resolve_diff(&run_dir, &args).await?;
|
||||
|
||||
|
|
@ -38,8 +42,7 @@ async fn resolve_diff(run_dir: &Path, args: &DiffArgs) -> Result<String> {
|
|||
});
|
||||
}
|
||||
|
||||
let start = fabro_workflows::records::StartRecord::load(run_dir)
|
||||
.context("Failed to load start.json")?;
|
||||
let start = StartRecord::load(run_dir).context("Failed to load start.json")?;
|
||||
|
||||
let base_sha = start
|
||||
.base_sha
|
||||
|
|
@ -66,7 +69,7 @@ async fn resolve_diff(run_dir: &Path, args: &DiffArgs) -> Result<String> {
|
|||
)?;
|
||||
|
||||
info!(provider = %record.provider, "Reconnecting to sandbox for live diff");
|
||||
let sandbox = fabro_sandbox::reconnect::reconnect(&record).await?;
|
||||
let sandbox = reconnect(&record).await?;
|
||||
|
||||
let cmd = build_live_diff_cmd(base_sha, args.stat, args.shortstat);
|
||||
debug!(cmd, "Running git diff in sandbox");
|
||||
|
|
@ -98,8 +101,7 @@ fn build_live_diff_cmd(base_sha: &str, stat: bool, shortstat: bool) -> String {
|
|||
);
|
||||
format!(
|
||||
"{} add -N . && {} diff{flags} {quoted_sha}",
|
||||
fabro_workflows::sandbox_git::GIT_REMOTE,
|
||||
fabro_workflows::sandbox_git::GIT_REMOTE
|
||||
GIT_REMOTE, GIT_REMOTE
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,19 @@ use anyhow::Context;
|
|||
use anyhow::Result;
|
||||
use fabro_git_storage::gitobj::Store;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::operations::{
|
||||
build_timeline, find_run_id_by_prefix, fork, ForkRunInput, RewindTarget,
|
||||
};
|
||||
use git2::Repository;
|
||||
|
||||
use crate::args::ForkArgs;
|
||||
|
||||
pub fn run(args: &ForkArgs, styles: &Styles) -> Result<()> {
|
||||
let repo = Repository::discover(".").context("not in a git repository")?;
|
||||
let run_id = fabro_workflows::operations::find_run_id_by_prefix(&repo, &args.run_id)?;
|
||||
let run_id = find_run_id_by_prefix(&repo, &args.run_id)?;
|
||||
let store = Store::new(repo);
|
||||
|
||||
let timeline = fabro_workflows::operations::build_timeline(&store, &run_id)?;
|
||||
let timeline = build_timeline(&store, &run_id)?;
|
||||
|
||||
if args.list {
|
||||
super::rewind::print_timeline(&timeline, styles);
|
||||
|
|
@ -21,11 +24,11 @@ pub fn run(args: &ForkArgs, styles: &Styles) -> Result<()> {
|
|||
let target = args
|
||||
.target
|
||||
.as_deref()
|
||||
.map(str::parse::<fabro_workflows::operations::RewindTarget>)
|
||||
.map(str::parse::<RewindTarget>)
|
||||
.transpose()?;
|
||||
let new_run_id = fabro_workflows::operations::fork(
|
||||
let new_run_id = fork(
|
||||
&store,
|
||||
fabro_workflows::operations::ForkRunInput {
|
||||
ForkRunInput {
|
||||
source_run_id: run_id.clone(),
|
||||
target,
|
||||
push: !args.no_push,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
|||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_workflows::records::RunRecordExt;
|
||||
use fabro_workflows::records::{RunRecord, RunRecordExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -46,7 +46,7 @@ pub(crate) fn remove_launcher_record(path: &Path) {
|
|||
}
|
||||
|
||||
pub(crate) fn active_launcher_record_for_run(run_dir: &Path) -> Option<LauncherRecord> {
|
||||
let run_record = fabro_workflows::records::RunRecord::load(run_dir).ok()?;
|
||||
let run_record = RunRecord::load(run_dir).ok()?;
|
||||
let path = launcher_record_path(&run_record.settings.storage_dir(), &run_record.run_id);
|
||||
let launcher = read_launcher_record(&path)?;
|
||||
if launcher_record_is_running(&launcher) {
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@ use anyhow::{bail, Context, Result};
|
|||
use chrono::{DateTime, Utc};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::args::LogsArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub fn run(args: LogsArgs, styles: &Styles) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let run = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let run = resolve_run(&base, &args.run)?;
|
||||
|
||||
info!(run_id = %run.run_id, "Showing logs");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
use anyhow::Result;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
|
||||
use crate::args::{GlobalArgs, RunCommands};
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub(crate) mod attach;
|
||||
pub(crate) mod command;
|
||||
|
|
@ -26,27 +30,25 @@ pub async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<()> {
|
|||
match cmd {
|
||||
RunCommands::Run(args) => command::execute(args, globals).await,
|
||||
RunCommands::Create(args) => {
|
||||
let styles: &'static fabro_util::terminal::Styles =
|
||||
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
|
||||
let cli_defaults = fabro_config::cli::load_cli_config(None)?;
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let cli_defaults = load_cli_config(None)?;
|
||||
let (run_id, _run_dir) = create::create_run(&args, cli_defaults, styles, true).await?;
|
||||
println!("{run_id}");
|
||||
Ok(())
|
||||
}
|
||||
RunCommands::Start { run } => {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let run_info = resolve_run(&base, &run)?;
|
||||
let child = start::start_run(&run_info.path, false)?;
|
||||
eprintln!("Started engine process (PID {})", child.id());
|
||||
Ok(())
|
||||
}
|
||||
RunCommands::Attach { run } => {
|
||||
let styles: &'static fabro_util::terminal::Styles =
|
||||
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?;
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let run_info = resolve_run(&base, &run)?;
|
||||
let exit_code = attach::attach_run(&run_info.path, false, styles, None).await?;
|
||||
if exit_code != std::process::ExitCode::SUCCESS {
|
||||
std::process::exit(1);
|
||||
|
|
@ -63,29 +65,28 @@ pub async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<()> {
|
|||
RunCommands::Ssh(args) => ssh::run(args).await,
|
||||
RunCommands::Diff(args) => diff::run(args).await,
|
||||
RunCommands::Logs(args) => {
|
||||
let styles = fabro_util::terminal::Styles::detect_stdout();
|
||||
let styles = Styles::detect_stdout();
|
||||
logs::run(args, &styles)
|
||||
}
|
||||
RunCommands::Resume(args) => {
|
||||
let styles: &'static fabro_util::terminal::Styles =
|
||||
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
crate::sleep_inhibitor::guard(cli_config.prevent_idle_sleep_enabled())
|
||||
};
|
||||
resume::resume_command(args, styles).await
|
||||
}
|
||||
RunCommands::Rewind(args) => {
|
||||
let styles = fabro_util::terminal::Styles::detect_stderr();
|
||||
let styles = Styles::detect_stderr();
|
||||
rewind::run(&args, &styles)
|
||||
}
|
||||
RunCommands::Fork(args) => {
|
||||
let styles = fabro_util::terminal::Styles::detect_stderr();
|
||||
let styles = Styles::detect_stderr();
|
||||
fork::run(&args, &styles)
|
||||
}
|
||||
RunCommands::Wait(args) => {
|
||||
let styles = fabro_util::terminal::Styles::detect_stderr();
|
||||
let styles = Styles::detect_stderr();
|
||||
wait::run(args, &styles)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_util::text::strip_goal_decoration;
|
||||
use fabro_workflows::asset_snapshot::collect_asset_paths;
|
||||
use fabro_workflows::outcome::{format_cost, StageStatus};
|
||||
use fabro_workflows::pipeline::{Persisted, Validated};
|
||||
use fabro_workflows::records::{Checkpoint, CheckpointExt, ConclusionExt};
|
||||
use fabro_workflows::pull_request::PullRequestRecord;
|
||||
use fabro_workflows::records::{Checkpoint, CheckpointExt, Conclusion, ConclusionExt};
|
||||
use indicatif::HumanDuration;
|
||||
|
||||
use crate::shared::{format_tokens_human, print_diagnostics, relative_path, tilde_path};
|
||||
|
||||
fn print_workflow_header(
|
||||
graph: &fabro_graphviz::graph::Graph,
|
||||
graph: &Graph,
|
||||
diagnostics: &[fabro_validate::Diagnostic],
|
||||
dot_path: Option<&Path>,
|
||||
styles: &Styles,
|
||||
|
|
@ -37,7 +41,7 @@ fn print_workflow_header(
|
|||
|
||||
let goal = graph.goal();
|
||||
if !goal.is_empty() {
|
||||
let stripped = fabro_util::text::strip_goal_decoration(goal);
|
||||
let stripped = strip_goal_decoration(goal);
|
||||
eprintln!("{} {stripped}\n", styles.bold.apply_to("Goal:"));
|
||||
}
|
||||
|
||||
|
|
@ -69,14 +73,14 @@ pub(crate) fn print_diagnostics_from_error(
|
|||
|
||||
pub(crate) fn print_run_summary(run_dir: &Path, run_id: &str, styles: &Styles) {
|
||||
let conclusion_path = run_dir.join("conclusion.json");
|
||||
let Ok(conclusion) = fabro_workflows::records::Conclusion::load(&conclusion_path) else {
|
||||
let Ok(conclusion) = Conclusion::load(&conclusion_path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let pr_url = std::fs::read_to_string(run_dir.join("pull_request.json"))
|
||||
.ok()
|
||||
.and_then(|content| {
|
||||
serde_json::from_str::<fabro_workflows::pull_request::PullRequestRecord>(&content)
|
||||
serde_json::from_str::<PullRequestRecord>(&content)
|
||||
.ok()
|
||||
.map(|record| record.html_url)
|
||||
});
|
||||
|
|
@ -94,7 +98,7 @@ pub(crate) fn print_run_summary(run_dir: &Path, run_id: &str, styles: &Styles) {
|
|||
}
|
||||
|
||||
pub(crate) fn print_run_conclusion(
|
||||
conclusion: &fabro_workflows::records::Conclusion,
|
||||
conclusion: &Conclusion,
|
||||
run_id: &str,
|
||||
run_dir: &Path,
|
||||
pushed_branch: Option<&str>,
|
||||
|
|
@ -201,7 +205,7 @@ pub(crate) fn print_final_output(run_dir: &Path, styles: &Styles) {
|
|||
|
||||
pub(crate) fn print_assets(run_dir: &Path, styles: &Styles) {
|
||||
let runtime_state = RuntimeState::new(run_dir);
|
||||
let paths = fabro_workflows::asset_snapshot::collect_asset_paths(&runtime_state.assets_dir());
|
||||
let paths = collect_asset_paths(&runtime_state.assets_dir());
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use fabro_config::run::LlmConfig;
|
||||
use fabro_config::{sandbox as sandbox_config, FabroConfig};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ impl TryFrom<&RunArgs> for FabroConfig {
|
|||
|
||||
fn try_from(args: &RunArgs) -> Result<Self, Self::Error> {
|
||||
let llm = if args.model.is_some() || args.provider.is_some() {
|
||||
Some(fabro_config::run::LlmConfig {
|
||||
Some(LlmConfig {
|
||||
model: args.model.clone(),
|
||||
provider: args.provider.clone(),
|
||||
fallbacks: None,
|
||||
|
|
@ -65,7 +66,7 @@ impl TryFrom<&PreflightArgs> for FabroConfig {
|
|||
|
||||
fn try_from(args: &PreflightArgs) -> Result<Self, Self::Error> {
|
||||
let llm = if args.model.is_some() || args.provider.is_some() {
|
||||
Some(fabro_config::run::LlmConfig {
|
||||
Some(LlmConfig {
|
||||
model: args.model.clone(),
|
||||
provider: args.provider.clone(),
|
||||
fallbacks: None,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
use anyhow::{Context, Result};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_sandbox::daytona::DaytonaSandbox;
|
||||
use fabro_sandbox::SandboxRecordExt;
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::PreviewArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::validate_daytona_provider;
|
||||
|
||||
pub async fn run(args: PreviewArgs) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let run_dir = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?.path;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let run_dir = resolve_run(&base, &args.run)?.path;
|
||||
let sandbox_json = run_dir.join("sandbox.json");
|
||||
let record = fabro_sandbox::SandboxRecord::load(&sandbox_json).context(
|
||||
"Failed to load sandbox.json — was this run started with a recent version of arc?",
|
||||
|
|
@ -24,7 +27,7 @@ pub async fn run(args: PreviewArgs) -> Result<()> {
|
|||
|
||||
info!(run_id = %args.run, provider = %record.provider, port = args.port, "Generating preview URL");
|
||||
|
||||
let daytona = fabro_sandbox::daytona::DaytonaSandbox::reconnect(name)
|
||||
let daytona = DaytonaSandbox::reconnect(name)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ use anyhow::bail;
|
|||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::records::{RunRecord, RunRecordExt};
|
||||
use fabro_workflows::run_lookup::{find_run_by_prefix, runs_base};
|
||||
|
||||
use crate::args::ResumeArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
/// Resume an interrupted workflow run.
|
||||
///
|
||||
|
|
@ -11,9 +13,9 @@ use crate::args::ResumeArgs;
|
|||
/// artifacts from the previous execution, then spawns an engine subprocess
|
||||
/// (identical to `fabro run`'s create→start→attach flow).
|
||||
pub async fn resume_command(args: ResumeArgs, styles: &'static Styles) -> anyhow::Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let run_dir = fabro_workflows::run_lookup::find_run_by_prefix(&base, &args.run)?;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let run_dir = find_run_by_prefix(&base, &args.run)?;
|
||||
|
||||
// find_run_by_prefix can match orphan directories (no run.json).
|
||||
if !run_dir.join("run.json").exists() {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ use cli_table::format::{Border, Separator};
|
|||
use cli_table::{print_stderr, Cell, CellStruct, Color, Style, Table};
|
||||
use fabro_git_storage::gitobj::Store;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::operations::{
|
||||
build_timeline, find_run_id_by_prefix, rewind, RewindInput, RewindTarget, RunTimeline,
|
||||
};
|
||||
use git2::Repository;
|
||||
|
||||
use crate::args::RewindArgs;
|
||||
|
|
@ -11,25 +14,21 @@ use crate::shared::color_if;
|
|||
|
||||
pub fn run(args: &RewindArgs, styles: &Styles) -> Result<()> {
|
||||
let repo = Repository::discover(".").context("not in a git repository")?;
|
||||
let run_id = fabro_workflows::operations::find_run_id_by_prefix(&repo, &args.run_id)?;
|
||||
let run_id = find_run_id_by_prefix(&repo, &args.run_id)?;
|
||||
let store = Store::new(repo);
|
||||
|
||||
let timeline = fabro_workflows::operations::build_timeline(&store, &run_id)?;
|
||||
let timeline = build_timeline(&store, &run_id)?;
|
||||
|
||||
if args.list || args.target.is_none() {
|
||||
print_timeline(&timeline, styles);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let target = args
|
||||
.target
|
||||
.as_deref()
|
||||
.unwrap()
|
||||
.parse::<fabro_workflows::operations::RewindTarget>()?;
|
||||
let target = args.target.as_deref().unwrap().parse::<RewindTarget>()?;
|
||||
|
||||
fabro_workflows::operations::rewind(
|
||||
rewind(
|
||||
&store,
|
||||
fabro_workflows::operations::RewindInput {
|
||||
RewindInput {
|
||||
run_id: run_id.clone(),
|
||||
target,
|
||||
push: !args.no_push,
|
||||
|
|
@ -44,7 +43,7 @@ pub fn run(args: &RewindArgs, styles: &Styles) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn print_timeline(timeline: &fabro_workflows::operations::RunTimeline, styles: &Styles) {
|
||||
pub(crate) fn print_timeline(timeline: &RunTimeline, styles: &Styles) {
|
||||
if timeline.entries.is_empty() {
|
||||
eprintln!("No checkpoints found.");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
|
|||
|
||||
use fabro_agent::AgentEvent;
|
||||
use fabro_interview::{Answer, ConsoleInterviewer, Interviewer, Question};
|
||||
use fabro_util::version::FABRO_VERSION;
|
||||
use fabro_workflows::event::{EventEmitter, RunNoticeLevel, WorkflowRunEvent};
|
||||
use fabro_workflows::outcome::StageStatus;
|
||||
|
||||
|
|
@ -1227,7 +1228,7 @@ impl ProgressUI {
|
|||
}
|
||||
|
||||
pub fn show_version(&mut self) {
|
||||
let version = fabro_util::version::FABRO_VERSION;
|
||||
let version = FABRO_VERSION;
|
||||
match &self.renderer {
|
||||
ProgressRenderer::Tty(tty) => {
|
||||
let bar = tty.multi.add(ProgressBar::new_spinner());
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
use anyhow::{bail, Context, Result};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_sandbox::daytona::DaytonaSandbox;
|
||||
use fabro_sandbox::SandboxRecordExt;
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::SshArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::validate_daytona_provider;
|
||||
|
||||
pub async fn run(args: SshArgs) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let run_dir = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?.path;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let run_dir = resolve_run(&base, &args.run)?.path;
|
||||
let sandbox_json = run_dir.join("sandbox.json");
|
||||
let record = fabro_sandbox::SandboxRecord::load(&sandbox_json).context(
|
||||
"Failed to load sandbox.json — was this run started with a recent version of arc?",
|
||||
|
|
@ -24,7 +27,7 @@ pub async fn run(args: SshArgs) -> Result<()> {
|
|||
|
||||
info!(run_id = %args.run, ttl_minutes = args.ttl, "Creating SSH access");
|
||||
|
||||
let daytona = fabro_sandbox::daytona::DaytonaSandbox::reconnect(name)
|
||||
let daytona = DaytonaSandbox::reconnect(name)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::path::Path;
|
|||
use anyhow::{anyhow, Result};
|
||||
use chrono::Utc;
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_workflows::records::RunRecordExt;
|
||||
use fabro_workflows::records::{RunRecord, RunRecordExt};
|
||||
|
||||
use super::launcher::{
|
||||
launcher_log_path, launcher_record_path, remove_launcher_record, write_launcher_record,
|
||||
|
|
@ -15,7 +15,7 @@ use super::launcher::{
|
|||
/// The engine process reads `run.json` from the run directory and executes the
|
||||
/// workflow. Returns the child process handle (use `.id()` for the PID).
|
||||
pub fn start_run(run_dir: &Path, resume: bool) -> Result<std::process::Child> {
|
||||
let record = fabro_workflows::records::RunRecord::load(run_dir)
|
||||
let record = RunRecord::load(run_dir)
|
||||
.map_err(|e| anyhow!("Cannot start run: failed to load run.json: {e}"))?;
|
||||
|
||||
let storage_dir = record.settings.storage_dir();
|
||||
|
|
|
|||
|
|
@ -3,17 +3,19 @@ use std::io::Write;
|
|||
use anyhow::{bail, Result};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::records::ConclusionExt;
|
||||
use fabro_workflows::records::{Conclusion, ConclusionExt};
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
use fabro_workflows::run_status::{RunStatus, RunStatusRecord, RunStatusRecordExt};
|
||||
use tracing::info;
|
||||
|
||||
use crate::args::WaitArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::format_duration_ms;
|
||||
|
||||
pub fn run(args: WaitArgs, styles: &Styles) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let run_info = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let run_info = resolve_run(&base, &args.run)?;
|
||||
|
||||
info!(run_id = %run_info.run_id, "Waiting for run to complete");
|
||||
|
||||
|
|
@ -49,7 +51,7 @@ pub fn run(args: WaitArgs, styles: &Styles) -> Result<()> {
|
|||
};
|
||||
|
||||
let conclusion_path = run_info.path.join("conclusion.json");
|
||||
let conclusion = fabro_workflows::records::Conclusion::load(&conclusion_path).ok();
|
||||
let conclusion = Conclusion::load(&conclusion_path).ok();
|
||||
|
||||
if args.json {
|
||||
let json_value = build_json_output(final_status, &run_info.run_id, conclusion.as_ref());
|
||||
|
|
@ -70,7 +72,7 @@ pub fn run(args: WaitArgs, styles: &Styles) -> Result<()> {
|
|||
fn build_json_output(
|
||||
status: RunStatus,
|
||||
run_id: &str,
|
||||
conclusion: Option<&fabro_workflows::records::Conclusion>,
|
||||
conclusion: Option<&Conclusion>,
|
||||
) -> serde_json::Value {
|
||||
let mut value = serde_json::json!({
|
||||
"run_id": run_id,
|
||||
|
|
@ -88,7 +90,7 @@ fn build_json_output(
|
|||
fn print_human_output(
|
||||
status: RunStatus,
|
||||
run_id: &str,
|
||||
conclusion: Option<&fabro_workflows::records::Conclusion>,
|
||||
conclusion: Option<&Conclusion>,
|
||||
styles: &Styles,
|
||||
) {
|
||||
let (style, label) = match status {
|
||||
|
|
|
|||
|
|
@ -6,13 +6,18 @@ use fabro_sandbox::SandboxRecordExt;
|
|||
use fabro_workflows::records::{CheckpointExt, ConclusionExt, RunRecordExt, StartRecordExt};
|
||||
use serde::Serialize;
|
||||
|
||||
use fabro_workflows::records::{Checkpoint, Conclusion, RunRecord, StartRecord};
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
use fabro_workflows::run_status::RunStatus;
|
||||
|
||||
use crate::args::InspectArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct InspectOutput {
|
||||
pub run_id: String,
|
||||
pub run_dir: PathBuf,
|
||||
pub status: fabro_workflows::run_status::RunStatus,
|
||||
pub status: RunStatus,
|
||||
pub run_record: Option<serde_json::Value>,
|
||||
pub start_record: Option<serde_json::Value>,
|
||||
pub conclusion: Option<serde_json::Value>,
|
||||
|
|
@ -21,30 +26,26 @@ pub struct InspectOutput {
|
|||
}
|
||||
|
||||
pub fn run(args: &InspectArgs) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let run = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let run = resolve_run(&base, &args.run)?;
|
||||
let output = inspect_run_dir(&run.run_id, &run.path, run.status)?;
|
||||
let json = serde_json::to_string_pretty(&[output])?;
|
||||
println!("{json}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn inspect_run_dir(
|
||||
run_id: &str,
|
||||
run_dir: &Path,
|
||||
status: fabro_workflows::run_status::RunStatus,
|
||||
) -> Result<InspectOutput> {
|
||||
let run_record = fabro_workflows::records::RunRecord::load(run_dir)
|
||||
fn inspect_run_dir(run_id: &str, run_dir: &Path, status: RunStatus) -> Result<InspectOutput> {
|
||||
let run_record = RunRecord::load(run_dir)
|
||||
.ok()
|
||||
.and_then(|v| serde_json::to_value(v).ok());
|
||||
let start_record = fabro_workflows::records::StartRecord::load(run_dir)
|
||||
let start_record = StartRecord::load(run_dir)
|
||||
.ok()
|
||||
.and_then(|v| serde_json::to_value(v).ok());
|
||||
let conclusion = fabro_workflows::records::Conclusion::load(&run_dir.join("conclusion.json"))
|
||||
let conclusion = Conclusion::load(&run_dir.join("conclusion.json"))
|
||||
.ok()
|
||||
.and_then(|v| serde_json::to_value(v).ok());
|
||||
let checkpoint = fabro_workflows::records::Checkpoint::load(&run_dir.join("checkpoint.json"))
|
||||
let checkpoint = Checkpoint::load(&run_dir.join("checkpoint.json"))
|
||||
.ok()
|
||||
.and_then(|v| serde_json::to_value(v).ok());
|
||||
let sandbox = fabro_sandbox::SandboxRecord::load(&run_dir.join("sandbox.json"))
|
||||
|
|
|
|||
|
|
@ -7,26 +7,31 @@ use cli_table::{print_stdout, Cell, CellStruct, Color, Style, Table};
|
|||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use fabro_util::text::strip_goal_decoration;
|
||||
use fabro_workflows::run_lookup::{filter_runs, runs_base, scan_runs, StatusFilter};
|
||||
use fabro_workflows::run_status::RunStatus;
|
||||
|
||||
use crate::args::RunsListArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::{color_if, format_duration_ms, tilde_path};
|
||||
|
||||
use super::short_run_id;
|
||||
|
||||
pub fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let runs = fabro_workflows::run_lookup::scan_runs(&base)?;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
let runs = scan_runs(&base)?;
|
||||
let label_filters = parse_label_filters(&args.filter.label);
|
||||
let filtered = fabro_workflows::run_lookup::filter_runs(
|
||||
let filtered = filter_runs(
|
||||
&runs,
|
||||
args.filter.before.as_deref(),
|
||||
args.filter.workflow.as_deref(),
|
||||
&label_filters,
|
||||
args.filter.orphans,
|
||||
if args.all {
|
||||
fabro_workflows::run_lookup::StatusFilter::All
|
||||
StatusFilter::All
|
||||
} else {
|
||||
fabro_workflows::run_lookup::StatusFilter::RunningOnly
|
||||
StatusFilter::RunningOnly
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -110,17 +115,15 @@ pub fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn status_cell(status: fabro_workflows::run_status::RunStatus, use_color: bool) -> CellStruct {
|
||||
fn status_cell(status: RunStatus, use_color: bool) -> CellStruct {
|
||||
let text = status.to_string();
|
||||
let color = match status {
|
||||
fabro_workflows::run_status::RunStatus::Succeeded => Some(Color::Green),
|
||||
fabro_workflows::run_status::RunStatus::Failed => Some(Color::Red),
|
||||
fabro_workflows::run_status::RunStatus::Running
|
||||
| fabro_workflows::run_status::RunStatus::Starting
|
||||
| fabro_workflows::run_status::RunStatus::Submitted => Some(Color::Cyan),
|
||||
fabro_workflows::run_status::RunStatus::Removing => Some(Color::Yellow),
|
||||
fabro_workflows::run_status::RunStatus::Paused => Some(Color::Magenta),
|
||||
fabro_workflows::run_status::RunStatus::Dead => Some(Color::Ansi256(8)),
|
||||
RunStatus::Succeeded => Some(Color::Green),
|
||||
RunStatus::Failed => Some(Color::Red),
|
||||
RunStatus::Running | RunStatus::Starting | RunStatus::Submitted => Some(Color::Cyan),
|
||||
RunStatus::Removing => Some(Color::Yellow),
|
||||
RunStatus::Paused => Some(Color::Magenta),
|
||||
RunStatus::Dead => Some(Color::Ansi256(8)),
|
||||
};
|
||||
text.cell()
|
||||
.bold(use_color && color != Some(Color::Ansi256(8)))
|
||||
|
|
@ -136,7 +139,7 @@ fn parse_label_filters(label_args: &[String]) -> Vec<(String, String)> {
|
|||
}
|
||||
|
||||
fn truncate_goal(goal: &str, max_len: usize) -> String {
|
||||
truncate_str(fabro_util::text::strip_goal_decoration(goal), max_len)
|
||||
truncate_str(strip_goal_decoration(goal), max_len)
|
||||
}
|
||||
|
||||
fn truncate_str(s: &str, max_len: usize) -> String {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::Result;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::RunsCommands;
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ pub(crate) mod rm;
|
|||
pub async fn dispatch(cmd: RunsCommands) -> Result<()> {
|
||||
match cmd {
|
||||
RunsCommands::Ps(args) => {
|
||||
let styles = fabro_util::terminal::Styles::detect_stdout();
|
||||
let styles = Styles::detect_stdout();
|
||||
list::list_command(&args, &styles)
|
||||
}
|
||||
RunsCommands::Rm(args) => rm::remove_command(&args).await,
|
||||
|
|
|
|||
|
|
@ -5,13 +5,18 @@ use fabro_config::FabroSettingsExt;
|
|||
use fabro_sandbox::SandboxRecordExt;
|
||||
use tracing::warn;
|
||||
|
||||
use fabro_sandbox::reconnect::reconnect as reconnect_sandbox;
|
||||
use fabro_workflows::run_lookup::{resolve_run, runs_base};
|
||||
use fabro_workflows::run_status::{write_run_status, RunStatus};
|
||||
|
||||
use crate::args::RunsRemoveArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
use super::short_run_id;
|
||||
|
||||
pub async fn remove_command(args: &RunsRemoveArgs) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
remove_from(args, &base).await
|
||||
}
|
||||
|
||||
|
|
@ -19,7 +24,7 @@ async fn remove_from(args: &RunsRemoveArgs, base: &Path) -> Result<()> {
|
|||
let mut had_errors = false;
|
||||
|
||||
for identifier in &args.runs {
|
||||
let run = match fabro_workflows::run_lookup::resolve_run(base, identifier) {
|
||||
let run = match resolve_run(base, identifier) {
|
||||
Ok(run) => run,
|
||||
Err(err) => {
|
||||
eprintln!("error: {identifier}: {err}");
|
||||
|
|
@ -38,16 +43,12 @@ async fn remove_from(args: &RunsRemoveArgs, base: &Path) -> Result<()> {
|
|||
continue;
|
||||
}
|
||||
|
||||
fabro_workflows::run_status::write_run_status(
|
||||
&run.path,
|
||||
fabro_workflows::run_status::RunStatus::Removing,
|
||||
None,
|
||||
);
|
||||
write_run_status(&run.path, RunStatus::Removing, None);
|
||||
|
||||
let sandbox_path = run.path.join("sandbox.json");
|
||||
if let Ok(record) = fabro_sandbox::SandboxRecord::load(&sandbox_path) {
|
||||
if record.provider != "local" {
|
||||
match fabro_sandbox::reconnect::reconnect(&record).await {
|
||||
match reconnect_sandbox(&record).await {
|
||||
Ok(sandbox) => {
|
||||
if let Err(err) = sandbox.cleanup().await {
|
||||
warn!(run_id = %run.run_id, error = %err, "sandbox cleanup failed");
|
||||
|
|
|
|||
|
|
@ -6,19 +6,23 @@ use cli_table::format::{Border, Justify, Separator};
|
|||
use cli_table::{print_stdout, Cell, CellStruct, Style, Table};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
|
||||
use fabro_workflows::run_lookup::{logs_base, runs_base, scan_runs};
|
||||
use fabro_workflows::run_status::RunStatus;
|
||||
|
||||
use crate::args::DfArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::format_size;
|
||||
|
||||
pub fn df_command(args: &DfArgs) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let data_dir = cli_config.storage_dir();
|
||||
let runs_base = fabro_workflows::run_lookup::runs_base(&data_dir);
|
||||
let logs_base = fabro_workflows::run_lookup::logs_base(&data_dir);
|
||||
df_from(args, &data_dir, &runs_base, &logs_base)
|
||||
let runs_base_dir = runs_base(&data_dir);
|
||||
let logs_base_dir = logs_base(&data_dir);
|
||||
df_from(args, &data_dir, &runs_base_dir, &logs_base_dir)
|
||||
}
|
||||
|
||||
fn df_from(args: &DfArgs, data_dir: &Path, runs_base: &Path, logs_base: &Path) -> Result<()> {
|
||||
let runs = fabro_workflows::run_lookup::scan_runs(runs_base)?;
|
||||
let runs = scan_runs(runs_base)?;
|
||||
let mut active_count = 0u64;
|
||||
let mut total_run_size = 0u64;
|
||||
let mut reclaimable_run_size = 0u64;
|
||||
|
|
@ -26,7 +30,7 @@ fn df_from(args: &DfArgs, data_dir: &Path, runs_base: &Path, logs_base: &Path) -
|
|||
struct RunSizeInfo {
|
||||
run_id: String,
|
||||
workflow_name: String,
|
||||
status: fabro_workflows::run_status::RunStatus,
|
||||
status: RunStatus,
|
||||
start_time_dt: Option<DateTime<Utc>>,
|
||||
size: u64,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@ use chrono::Utc;
|
|||
use fabro_config::FabroSettingsExt;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use fabro_workflows::run_lookup::{filter_runs, runs_base, scan_runs, StatusFilter};
|
||||
|
||||
use crate::args::RunsPruneArgs;
|
||||
use crate::cli_config::load_cli_settings;
|
||||
use crate::shared::format_size;
|
||||
|
||||
pub fn prune_command(args: &RunsPruneArgs) -> Result<()> {
|
||||
let cli_config = crate::cli_config::load_cli_settings(None)?;
|
||||
let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir());
|
||||
let cli_config = load_cli_settings(None)?;
|
||||
let base = runs_base(&cli_config.storage_dir());
|
||||
prune_from(args, &base)
|
||||
}
|
||||
|
||||
|
|
@ -31,15 +34,15 @@ pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {
|
|||
}
|
||||
|
||||
fn prune_from(args: &RunsPruneArgs, base: &Path) -> Result<()> {
|
||||
let runs = fabro_workflows::run_lookup::scan_runs(base)?;
|
||||
let runs = scan_runs(base)?;
|
||||
let label_filters = parse_label_filters(&args.filter.label);
|
||||
let mut filtered = fabro_workflows::run_lookup::filter_runs(
|
||||
let mut filtered = filter_runs(
|
||||
&runs,
|
||||
args.filter.before.as_deref(),
|
||||
args.filter.workflow.as_deref(),
|
||||
&label_filters,
|
||||
args.filter.orphans,
|
||||
fabro_workflows::run_lookup::StatusFilter::All,
|
||||
StatusFilter::All,
|
||||
);
|
||||
|
||||
let has_explicit_filters =
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ use semver::Version;
|
|||
use sha2::{Digest, Sha256};
|
||||
use tracing::debug;
|
||||
|
||||
use tokio::process::Command as TokioCommand;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::args::UpgradeArgs;
|
||||
|
||||
// ── Download backend abstraction ───────────────────────────────────────────
|
||||
|
|
@ -29,7 +32,7 @@ impl Backend {
|
|||
async fn fetch_latest_release_tag(&self) -> Result<String> {
|
||||
match self {
|
||||
Backend::Gh => {
|
||||
let output = tokio::process::Command::new("gh")
|
||||
let output = TokioCommand::new("gh")
|
||||
.args([
|
||||
"release",
|
||||
"view",
|
||||
|
|
@ -75,7 +78,7 @@ impl Backend {
|
|||
let dest = dest_dir.join(asset);
|
||||
match self {
|
||||
Backend::Gh => {
|
||||
let status = tokio::process::Command::new("gh")
|
||||
let status = TokioCommand::new("gh")
|
||||
.args([
|
||||
"release",
|
||||
"download",
|
||||
|
|
@ -117,10 +120,7 @@ impl Backend {
|
|||
|
||||
async fn select_backend() -> Backend {
|
||||
// Check if gh is available
|
||||
let gh_version = tokio::process::Command::new("gh")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await;
|
||||
let gh_version = TokioCommand::new("gh").arg("--version").output().await;
|
||||
let Ok(output) = gh_version else {
|
||||
debug!("gh CLI not found, using HTTP backend");
|
||||
return Backend::Http(http_client().expect("failed to build HTTP client"));
|
||||
|
|
@ -131,7 +131,7 @@ async fn select_backend() -> Backend {
|
|||
}
|
||||
|
||||
// Check if gh is authenticated
|
||||
let auth_status = tokio::process::Command::new("gh")
|
||||
let auth_status = TokioCommand::new("gh")
|
||||
.args(["auth", "status"])
|
||||
.output()
|
||||
.await;
|
||||
|
|
@ -349,7 +349,7 @@ pub async fn run_upgrade(args: UpgradeArgs) -> Result<()> {
|
|||
pub fn spawn_upgrade_check(
|
||||
no_upgrade_check: bool,
|
||||
upgrade_check_enabled: bool,
|
||||
) -> Option<tokio::task::JoinHandle<()>> {
|
||||
) -> Option<JoinHandle<()>> {
|
||||
if no_upgrade_check || !upgrade_check_enabled {
|
||||
return None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
use anyhow::bail;
|
||||
use fabro_config::project::ResolveSettingsInput;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::project::{resolve_settings, resolve_workflow_path, ResolveSettingsInput};
|
||||
use fabro_config::FabroConfig;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_validate::Severity;
|
||||
use fabro_workflows::operations::{validate, ValidateInput, WorkflowInput};
|
||||
|
||||
use crate::args::ValidateArgs;
|
||||
use crate::shared::{print_diagnostics, relative_path};
|
||||
|
||||
pub fn run(args: &ValidateArgs, styles: &Styles) -> anyhow::Result<()> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let cli_defaults = fabro_config::cli::load_cli_config(None)?;
|
||||
let settings = fabro_config::project::resolve_settings(ResolveSettingsInput {
|
||||
let cli_defaults = load_cli_config(None)?;
|
||||
let settings = resolve_settings(ResolveSettingsInput {
|
||||
workflow_path: args.workflow.clone(),
|
||||
cwd: cwd.clone(),
|
||||
defaults: cli_defaults,
|
||||
overrides: fabro_config::FabroConfig::default(),
|
||||
overrides: FabroConfig::default(),
|
||||
apply_project_config: true,
|
||||
})?;
|
||||
let resolution = fabro_config::project::resolve_workflow_path(&args.workflow, &cwd)?;
|
||||
let validated =
|
||||
fabro_workflows::operations::validate(fabro_workflows::operations::ValidateInput {
|
||||
workflow: fabro_workflows::operations::WorkflowInput::Path(args.workflow.clone()),
|
||||
settings,
|
||||
cwd,
|
||||
custom_transforms: Vec::new(),
|
||||
})?;
|
||||
let resolution = resolve_workflow_path(&args.workflow, &cwd)?;
|
||||
let validated = validate(ValidateInput {
|
||||
workflow: WorkflowInput::Path(args.workflow.clone()),
|
||||
settings,
|
||||
cwd,
|
||||
custom_transforms: Vec::new(),
|
||||
})?;
|
||||
let graph = validated.graph();
|
||||
let diagnostics = validated.diagnostics();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@ use std::path::Path;
|
|||
|
||||
use anyhow::{bail, Context, Result};
|
||||
|
||||
use fabro_config::project::{discover_project_config, resolve_fabro_root};
|
||||
|
||||
use crate::args::WorkflowCreateArgs;
|
||||
use crate::shared::relative_path;
|
||||
|
||||
pub fn create_command(args: &WorkflowCreateArgs) -> Result<()> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
|
||||
let (config_path, config) = match fabro_config::project::discover_project_config(&cwd)? {
|
||||
let (config_path, config) = match discover_project_config(&cwd)? {
|
||||
Some(found) => found,
|
||||
None => bail!(
|
||||
"No fabro.toml found in {cwd} or any parent directory",
|
||||
|
|
@ -16,7 +18,7 @@ pub fn create_command(args: &WorkflowCreateArgs) -> Result<()> {
|
|||
),
|
||||
};
|
||||
|
||||
let fabro_root = fabro_config::project::resolve_fabro_root(&config_path, &config);
|
||||
let fabro_root = resolve_fabro_root(&config_path, &config);
|
||||
write_workflow_scaffold(args, &fabro_root)?;
|
||||
|
||||
let workflows_dir = fabro_root.join("workflows").join(&args.name);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
use anyhow::{bail, Result};
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use fabro_config::project::{
|
||||
discover_project_config, list_workflows_detailed, resolve_fabro_root, WorkflowInfo,
|
||||
WorkflowSource,
|
||||
};
|
||||
|
||||
use crate::args::WorkflowListArgs;
|
||||
use crate::shared::relative_path;
|
||||
|
||||
|
|
@ -10,7 +15,7 @@ pub fn list_command(_args: &WorkflowListArgs) -> Result<()> {
|
|||
let styles = Styles::detect_stderr();
|
||||
let cwd = std::env::current_dir()?;
|
||||
|
||||
let (config_path, config) = match fabro_config::project::discover_project_config(&cwd)? {
|
||||
let (config_path, config) = match discover_project_config(&cwd)? {
|
||||
Some(found) => found,
|
||||
None => bail!(
|
||||
"No fabro.toml found in {cwd} or any parent directory",
|
||||
|
|
@ -18,22 +23,19 @@ pub fn list_command(_args: &WorkflowListArgs) -> Result<()> {
|
|||
),
|
||||
};
|
||||
|
||||
let fabro_root = fabro_config::project::resolve_fabro_root(&config_path, &config);
|
||||
let fabro_root = resolve_fabro_root(&config_path, &config);
|
||||
let project_wf_dir = fabro_root.join("workflows");
|
||||
let user_wf_dir = dirs::home_dir().map(|h| h.join(".fabro").join("workflows"));
|
||||
|
||||
let workflows = fabro_config::project::list_workflows_detailed(
|
||||
Some(&project_wf_dir),
|
||||
user_wf_dir.as_deref(),
|
||||
);
|
||||
let workflows = list_workflows_detailed(Some(&project_wf_dir), user_wf_dir.as_deref());
|
||||
|
||||
let project: Vec<_> = workflows
|
||||
.iter()
|
||||
.filter(|w| w.source == fabro_config::project::WorkflowSource::Project)
|
||||
.filter(|w| w.source == WorkflowSource::Project)
|
||||
.collect();
|
||||
let user: Vec<_> = workflows
|
||||
.iter()
|
||||
.filter(|w| w.source == fabro_config::project::WorkflowSource::User)
|
||||
.filter(|w| w.source == WorkflowSource::User)
|
||||
.collect();
|
||||
|
||||
let name_width = workflows.iter().map(|w| w.name.len()).max().unwrap_or(0);
|
||||
|
|
@ -65,7 +67,7 @@ pub fn list_command(_args: &WorkflowListArgs) -> Result<()> {
|
|||
fn print_section(
|
||||
title: &str,
|
||||
path: &str,
|
||||
workflows: &[&fabro_config::project::WorkflowInfo],
|
||||
workflows: &[&WorkflowInfo],
|
||||
name_width: usize,
|
||||
styles: &Styles,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
use anyhow::{Context, Result};
|
||||
use fabro_util::run_log;
|
||||
use tracing_appender::rolling;
|
||||
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
pub fn init_tracing(debug: bool, config_log_level: Option<&str>, log_prefix: &str) -> Result<()> {
|
||||
|
|
@ -20,9 +22,9 @@ pub fn init_tracing(debug: bool, config_log_level: Option<&str>, log_prefix: &st
|
|||
let filename = chrono::Local::now()
|
||||
.format(&format!("{log_prefix}-%Y-%m-%d.log"))
|
||||
.to_string();
|
||||
let file_appender = tracing_appender::rolling::never(&log_dir, &filename);
|
||||
let file_appender = rolling::never(&log_dir, &filename);
|
||||
|
||||
let run_log_writer = fabro_util::run_log::init();
|
||||
let run_log_writer = run_log::init();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ mod sleep_inhibitor;
|
|||
use anyhow::Result;
|
||||
use args::{Commands, GlobalArgs, RunCommands, LONG_VERSION};
|
||||
use clap::Parser;
|
||||
use fabro_telemetry::{git, panic as tel_panic, sanitize, sender};
|
||||
use fabro_util::terminal::Styles;
|
||||
use rustls::crypto::ring::default_provider;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Parser)]
|
||||
|
|
@ -23,7 +26,7 @@ struct Cli {
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
fabro_telemetry::panic::install_panic_hook();
|
||||
tel_panic::install_panic_hook();
|
||||
fabro_telemetry::init_cli();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
|
@ -33,8 +36,8 @@ async fn main() {
|
|||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
let is_error = result.is_err();
|
||||
let command = fabro_telemetry::sanitize::sanitize_command(&raw_args, &command_name);
|
||||
let repository = fabro_telemetry::git::repository_identifier();
|
||||
let command = sanitize::sanitize_command(&raw_args, &command_name);
|
||||
let repository = git::repository_identifier();
|
||||
let ci = std::env::var("CI").is_ok();
|
||||
if is_error {
|
||||
fabro_telemetry::track!("CLI Errored", {
|
||||
|
|
@ -82,7 +85,7 @@ async fn main() {
|
|||
}
|
||||
|
||||
async fn main_inner() -> (String, Result<()>) {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
let _ = default_provider().install_default();
|
||||
|
||||
let cli = Cli::parse();
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
|
|
@ -107,7 +110,7 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
Err(err) => return (command_name, Err(err)),
|
||||
}
|
||||
} else {
|
||||
match crate::cli_config::load_cli_settings(None) {
|
||||
match cli_config::load_cli_settings(None) {
|
||||
Ok(cli_config) => (
|
||||
cli_config.log.as_ref().and_then(|l| l.level.clone()),
|
||||
cli_config.upgrade_check_enabled(),
|
||||
|
|
@ -118,7 +121,7 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
}
|
||||
#[cfg(not(feature = "server"))]
|
||||
{
|
||||
match crate::cli_config::load_cli_settings(None) {
|
||||
match cli_config::load_cli_settings(None) {
|
||||
Ok(cli_config) => (
|
||||
cli_config.log.as_ref().and_then(|l| l.level.clone()),
|
||||
cli_config.upgrade_check_enabled(),
|
||||
|
|
@ -160,11 +163,11 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
Commands::RunCmd(cmd) => commands::run::dispatch(cmd, &globals).await?,
|
||||
Commands::Preflight(args) => commands::preflight::execute(args).await?,
|
||||
Commands::Validate(args) => {
|
||||
let styles = fabro_util::terminal::Styles::detect_stderr();
|
||||
let styles = Styles::detect_stderr();
|
||||
commands::validate::run(&args, &styles)?;
|
||||
}
|
||||
Commands::Graph(args) => {
|
||||
let styles = fabro_util::terminal::Styles::detect_stderr();
|
||||
let styles = Styles::detect_stderr();
|
||||
commands::graph::run(&args, &styles)?;
|
||||
}
|
||||
Commands::Parse(args) => {
|
||||
|
|
@ -175,8 +178,7 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
Commands::Model { command } => commands::model::execute(command, &globals).await?,
|
||||
#[cfg(feature = "server")]
|
||||
Commands::Serve(args) => {
|
||||
let styles: &'static fabro_util::terminal::Styles =
|
||||
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
fabro_api::serve::serve_command(args, styles).await?;
|
||||
}
|
||||
Commands::Doctor { verbose, dry_run } => {
|
||||
|
|
@ -213,12 +215,12 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
Commands::Provider(ns) => commands::provider::dispatch(ns).await?,
|
||||
Commands::System(ns) => commands::system::dispatch(ns)?,
|
||||
Commands::SendAnalytics { path } => {
|
||||
let result = fabro_telemetry::sender::upload(&path).await;
|
||||
let result = sender::upload(&path).await;
|
||||
let _ = std::fs::remove_file(&path);
|
||||
result?;
|
||||
}
|
||||
Commands::SendPanic { path } => {
|
||||
let result = fabro_telemetry::panic::capture(&path).await;
|
||||
let result = tel_panic::capture(&path).await;
|
||||
let _ = std::fs::remove_file(&path);
|
||||
result?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
pub(crate) fn build_github_app_credentials(
|
||||
app_id: Option<&str>,
|
||||
) -> Option<fabro_github::GitHubAppCredentials> {
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use base64::Engine as _;
|
||||
use fabro_github::GitHubAppCredentials;
|
||||
|
||||
pub(crate) fn build_github_app_credentials(app_id: Option<&str>) -> Option<GitHubAppCredentials> {
|
||||
let app_id = app_id?;
|
||||
let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok()?;
|
||||
let private_key_pem = if raw.starts_with("-----") {
|
||||
raw
|
||||
} else {
|
||||
let pem_bytes =
|
||||
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &raw).ok()?;
|
||||
let pem_bytes = BASE64_STANDARD.decode(&raw).ok()?;
|
||||
String::from_utf8(pem_bytes).ok()?
|
||||
};
|
||||
Some(fabro_github::GitHubAppCredentials {
|
||||
Some(GitHubAppCredentials {
|
||||
app_id: app_id.to_string(),
|
||||
private_key_pem,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use dialoguer::console::Term;
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use dialoguer::{Confirm, Password};
|
||||
use fabro_config::dotenv::{merge_env, write_env_file as write_env};
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_llm::generate::{generate, GenerateParams};
|
||||
use fabro_model::Provider;
|
||||
use fabro_util::terminal::Styles;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::commands::doctor;
|
||||
|
||||
|
|
@ -111,20 +118,16 @@ pub(crate) async fn run_openai_oauth_or_api_key(s: &Styles) -> Result<Vec<(Strin
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub(crate) fn prompt_confirm(prompt: &str, default: bool) -> Result<bool> {
|
||||
Ok(
|
||||
Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.default(default)
|
||||
.interact_on(&dialoguer::console::Term::stderr())?,
|
||||
)
|
||||
Ok(Confirm::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.default(default)
|
||||
.interact_on(&Term::stderr())?)
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_password(prompt: &str) -> Result<String> {
|
||||
Ok(
|
||||
Password::with_theme(&dialoguer::theme::ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.interact_on(&dialoguer::console::Term::stderr())?,
|
||||
)
|
||||
Ok(Password::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.interact_on(&Term::stderr())?)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -142,8 +145,8 @@ pub(crate) fn write_env_file(
|
|||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.collect();
|
||||
let merged = fabro_config::dotenv::merge_env(&existing, &refs);
|
||||
fabro_config::dotenv::write_env_file(&env_path, &merged)?;
|
||||
let merged = merge_env(&existing, &refs);
|
||||
write_env(&env_path, &merged)?;
|
||||
eprintln!(
|
||||
" {}",
|
||||
s.dim.apply_to(format!("Wrote {}", env_path.display()))
|
||||
|
|
@ -160,24 +163,19 @@ pub(crate) async fn validate_api_key(provider: Provider, api_key: &str) -> Resul
|
|||
let env_var = provider.api_key_env_vars()[0];
|
||||
std::env::set_var(env_var, api_key);
|
||||
|
||||
let client = fabro_llm::client::Client::from_env()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let client = LlmClient::from_env().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let params = fabro_llm::generate::GenerateParams::new(doctor::probe_model(provider))
|
||||
let params = GenerateParams::new(doctor::probe_model(provider))
|
||||
.provider(provider.as_str())
|
||||
.prompt("Say OK")
|
||||
.max_tokens(16)
|
||||
.client(std::sync::Arc::new(client));
|
||||
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(30),
|
||||
fabro_llm::generate::generate(params),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "timeout (30s)".to_string())?
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
timeout(std::time::Duration::from_secs(30), generate(params))
|
||||
.await
|
||||
.map_err(|_| "timeout (30s)".to_string())?
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub(crate) async fn prompt_and_validate_key(
|
||||
|
|
@ -193,7 +191,7 @@ pub(crate) async fn prompt_and_validate_key(
|
|||
|
||||
loop {
|
||||
let prompt = env_var.to_string();
|
||||
let key: String = tokio::task::spawn_blocking(move || prompt_password(&prompt)).await??;
|
||||
let key: String = spawn_blocking(move || prompt_password(&prompt)).await??;
|
||||
|
||||
eprintln!(" {}", s.dim.apply_to("Validating API key..."));
|
||||
match validate_api_key(provider, &key).await {
|
||||
|
|
@ -203,10 +201,9 @@ pub(crate) async fn prompt_and_validate_key(
|
|||
}
|
||||
Err(e) => {
|
||||
eprintln!(" [error] API key validation failed: {e}");
|
||||
let retry = tokio::task::spawn_blocking(|| {
|
||||
prompt_confirm("Try again with a different key?", true)
|
||||
})
|
||||
.await??;
|
||||
let retry =
|
||||
spawn_blocking(|| prompt_confirm("Try again with a different key?", true))
|
||||
.await??;
|
||||
if !retry {
|
||||
return Ok((env_var.to_string(), key));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ use std::path::{Path, PathBuf};
|
|||
use anyhow::anyhow;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::FabroConfig;
|
||||
|
||||
pub use fabro_types::settings::cli::{
|
||||
ClientTlsSettings, ExecSettings, ExecutionMode, OutputFormat, PermissionLevel, ServerSettings,
|
||||
};
|
||||
|
|
@ -70,6 +72,6 @@ impl From<ExecConfig> for ExecSettings {
|
|||
|
||||
/// Load CLI config from an explicit path or `~/.fabro/cli.toml`, returning defaults if the
|
||||
/// default file doesn't exist. An explicit path that doesn't exist is an error.
|
||||
pub fn load_cli_config(path: Option<&Path>) -> anyhow::Result<crate::config::FabroConfig> {
|
||||
pub fn load_cli_config(path: Option<&Path>) -> anyhow::Result<FabroConfig> {
|
||||
crate::load_config_file(path, "cli.toml")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ pub use settings::{FabroSettings, FabroSettingsExt};
|
|||
|
||||
use std::path::Path;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
/// Load a TOML config from an explicit path or `~/.fabro/{filename}`.
|
||||
///
|
||||
/// Returns `T::default()` when no explicit path is given and the default file
|
||||
/// doesn't exist. An explicit path that doesn't exist is an error.
|
||||
pub fn load_config_file<T>(path: Option<&Path>, filename: &str) -> anyhow::Result<T>
|
||||
where
|
||||
T: Default + serde::de::DeserializeOwned,
|
||||
T: Default + DeserializeOwned,
|
||||
{
|
||||
if let Some(explicit) = path {
|
||||
tracing::debug!(path = %explicit.display(), "Loading config from explicit path");
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use tracing::debug;
|
|||
|
||||
use crate::combine::Combine;
|
||||
use crate::config::FabroConfig;
|
||||
use crate::sandbox::DockerfileSource;
|
||||
pub use fabro_types::settings::run::{
|
||||
AssetsSettings, CheckpointSettings, GitHubSettings, LlmSettings, MergeStrategy,
|
||||
PullRequestSettings, SetupSettings,
|
||||
|
|
@ -179,12 +180,12 @@ fn resolve_dockerfile(config: &mut FabroConfig, config_dir: &Path) -> anyhow::Re
|
|||
.and_then(|d| d.snapshot.as_mut())
|
||||
.and_then(|snap| snap.dockerfile.as_mut());
|
||||
|
||||
if let Some(crate::sandbox::DockerfileSource::Path { path: ref rel }) = source {
|
||||
if let Some(DockerfileSource::Path { path: ref rel }) = source {
|
||||
let path = config_dir.join(rel);
|
||||
let contents = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read dockerfile at {}", path.display()))?;
|
||||
debug!(path = %path.display(), "Resolved dockerfile from path");
|
||||
*source.unwrap() = crate::sandbox::DockerfileSource::Inline(contents);
|
||||
*source.unwrap() = DockerfileSource::Inline(contents);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::time::Instant;
|
|||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::{CoreError, Result};
|
||||
use crate::error::{CoreError, Result, VisitLimitSource};
|
||||
use crate::graph::{EdgeSpec, Graph, NodeSpec};
|
||||
use crate::handler::NodeHandler;
|
||||
use crate::lifecycle::{
|
||||
|
|
@ -14,6 +14,7 @@ use crate::lifecycle::{
|
|||
};
|
||||
use crate::outcome::{NodeResult, NodeResultExt, Outcome, StageStatus};
|
||||
use crate::state::RunState;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExecutorOptions {
|
||||
|
|
@ -147,7 +148,7 @@ impl<G: Graph + 'static> Executor<G> {
|
|||
node_id: node.id().to_string(),
|
||||
visits,
|
||||
limit: max,
|
||||
limit_source: crate::error::VisitLimitSource::Node,
|
||||
limit_source: VisitLimitSource::Node,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -157,7 +158,7 @@ impl<G: Graph + 'static> Executor<G> {
|
|||
node_id: node.id().to_string(),
|
||||
visits,
|
||||
limit: global_max,
|
||||
limit_source: crate::error::VisitLimitSource::Graph,
|
||||
limit_source: VisitLimitSource::Graph,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -276,7 +277,7 @@ impl<G: Graph + 'static> Executor<G> {
|
|||
backoff_delay: Some(delay),
|
||||
};
|
||||
self.lifecycle.after_attempt(&ctx, state).await?;
|
||||
tokio::time::sleep(delay).await;
|
||||
sleep(delay).await;
|
||||
}
|
||||
Ok(outcome) if outcome.status == StageStatus::Retry => {
|
||||
let final_outcome = self.handler.on_retries_exhausted(node, outcome);
|
||||
|
|
@ -321,7 +322,7 @@ impl<G: Graph + 'static> Executor<G> {
|
|||
backoff_delay: Some(delay),
|
||||
};
|
||||
self.lifecycle.after_attempt(&ctx, state).await?;
|
||||
tokio::time::sleep(delay).await;
|
||||
sleep(delay).await;
|
||||
}
|
||||
Err(e) => {
|
||||
// Convert handler error to fail outcome so routing continues
|
||||
|
|
@ -1876,7 +1877,7 @@ mod tests {
|
|||
// Cancel stall token while "running"
|
||||
self.0.cancel();
|
||||
// Simulate long work
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
Ok(Outcome::success())
|
||||
}
|
||||
}
|
||||
|
|
@ -1973,7 +1974,7 @@ mod tests {
|
|||
_s: &RunState,
|
||||
) -> Result<NodeDecision> {
|
||||
self.0.cancel();
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
Ok(NodeDecision::Continue)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::error::CoreError;
|
||||
pub use fabro_types::outcome::{
|
||||
FailureCategory, FailureDetail, NodeResult, Outcome, OutcomeMeta, StageStatus,
|
||||
};
|
||||
|
||||
pub trait NodeResultExt<M: OutcomeMeta = ()> {
|
||||
fn from_error(
|
||||
error: &crate::error::CoreError,
|
||||
duration: Duration,
|
||||
attempts: u32,
|
||||
max_attempts: u32,
|
||||
) -> Self;
|
||||
fn from_error(error: &CoreError, duration: Duration, attempts: u32, max_attempts: u32) -> Self;
|
||||
}
|
||||
|
||||
impl<M: OutcomeMeta> NodeResultExt<M> for NodeResult<M> {
|
||||
fn from_error(
|
||||
error: &crate::error::CoreError,
|
||||
duration: Duration,
|
||||
attempts: u32,
|
||||
max_attempts: u32,
|
||||
) -> Self {
|
||||
fn from_error(error: &CoreError, duration: Duration, attempts: u32, max_attempts: u32) -> Self {
|
||||
Self {
|
||||
outcome: error.to_fail_outcome(),
|
||||
duration,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
|
||||
use tokio::sync::Notify;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// Trait for receiving stall timeout notifications.
|
||||
pub trait ActivityMonitor: Send + Sync {
|
||||
|
|
@ -25,7 +27,7 @@ pub struct StallWatchdog {
|
|||
pub struct StallGuard {
|
||||
activity: Arc<Notify>,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
handle: Option<tokio::task::JoinHandle<()>>,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl StallWatchdog {
|
||||
|
|
@ -55,7 +57,7 @@ impl StallWatchdog {
|
|||
let handle = tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(timeout) => {
|
||||
_ = sleep(timeout) => {
|
||||
if shutdown.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::path::Path;
|
||||
|
||||
use tokio::fs;
|
||||
use tokio::process::Command;
|
||||
use tracing::info;
|
||||
|
||||
use crate::types::{FeatureMetadata, LifecycleCommand};
|
||||
|
|
@ -61,7 +63,7 @@ fn dir_name_from_id(feature_id: &str) -> String {
|
|||
|
||||
/// Ensure `oras` CLI is available, installing it if necessary.
|
||||
async fn ensure_oras() -> crate::Result<()> {
|
||||
let check = tokio::process::Command::new("which")
|
||||
let check = Command::new("which")
|
||||
.arg("oras")
|
||||
.output()
|
||||
.await
|
||||
|
|
@ -74,7 +76,7 @@ async fn ensure_oras() -> crate::Result<()> {
|
|||
info!("oras not found, attempting to install");
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
let status = tokio::process::Command::new("brew")
|
||||
let status = Command::new("brew")
|
||||
.args(["install", "oras"])
|
||||
.status()
|
||||
.await
|
||||
|
|
@ -93,7 +95,7 @@ async fn ensure_oras() -> crate::Result<()> {
|
|||
.map_err(|_| DevcontainerError::OrasInstall("HOME not set".to_string()))?;
|
||||
let bin_dir = format!("{home}/.local/bin");
|
||||
|
||||
tokio::fs::create_dir_all(&bin_dir).await.map_err(|e| {
|
||||
fs::create_dir_all(&bin_dir).await.map_err(|e| {
|
||||
DevcontainerError::OrasInstall(format!("failed to create {bin_dir}: {e}"))
|
||||
})?;
|
||||
|
||||
|
|
@ -107,7 +109,7 @@ async fn ensure_oras() -> crate::Result<()> {
|
|||
"https://github.com/oras-project/oras/releases/download/v{version}/oras_{version}_linux_{arch}.tar.gz"
|
||||
);
|
||||
|
||||
let status = tokio::process::Command::new("sh")
|
||||
let status = Command::new("sh")
|
||||
.args([
|
||||
"-c",
|
||||
&format!("curl -fsSL '{url}' | tar xzf - -C '{bin_dir}' oras"),
|
||||
|
|
@ -128,7 +130,7 @@ async fn ensure_oras() -> crate::Result<()> {
|
|||
|
||||
/// Find the first `.tgz` file in a directory.
|
||||
async fn find_tgz(dir: &Path) -> Option<String> {
|
||||
let mut entries = tokio::fs::read_dir(dir).await.ok()?;
|
||||
let mut entries = fs::read_dir(dir).await.ok()?;
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if name.ends_with(".tgz") {
|
||||
|
|
@ -141,7 +143,7 @@ async fn find_tgz(dir: &Path) -> Option<String> {
|
|||
|
||||
/// Extract a tgz archive in the given directory.
|
||||
async fn extract_tgz(feature_dir: &Path, tgz_name: &str, feature_id: &str) -> crate::Result<()> {
|
||||
let status = tokio::process::Command::new("tar")
|
||||
let status = Command::new("tar")
|
||||
.args(["xzf", tgz_name])
|
||||
.current_dir(feature_dir)
|
||||
.status()
|
||||
|
|
@ -159,11 +161,9 @@ async fn extract_tgz(feature_dir: &Path, tgz_name: &str, feature_id: &str) -> cr
|
|||
/// Read and parse devcontainer-feature.json from a feature directory.
|
||||
async fn read_feature_metadata(feature_dir: &Path) -> crate::Result<FeatureMetadata> {
|
||||
let metadata_path = feature_dir.join("devcontainer-feature.json");
|
||||
let metadata_str = tokio::fs::read_to_string(&metadata_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DevcontainerError::Feature(format!("failed to read {}: {e}", metadata_path.display()))
|
||||
})?;
|
||||
let metadata_str = fs::read_to_string(&metadata_path).await.map_err(|e| {
|
||||
DevcontainerError::Feature(format!("failed to read {}: {e}", metadata_path.display()))
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&metadata_str).map_err(|e| {
|
||||
DevcontainerError::Feature(format!("failed to parse {}: {e}", metadata_path.display()))
|
||||
|
|
@ -177,7 +177,7 @@ async fn create_feature_dir(
|
|||
) -> crate::Result<std::path::PathBuf> {
|
||||
let dir_name = dir_name_from_id(feature_id);
|
||||
let feature_dir = output_dir.join(&dir_name);
|
||||
tokio::fs::create_dir_all(&feature_dir).await.map_err(|e| {
|
||||
fs::create_dir_all(&feature_dir).await.map_err(|e| {
|
||||
DevcontainerError::Feature(format!(
|
||||
"failed to create dir {}: {e}",
|
||||
feature_dir.display()
|
||||
|
|
@ -192,7 +192,7 @@ async fn fetch_feature_oci(feature_id: &str, output_dir: &Path) -> crate::Result
|
|||
|
||||
info!(feature_id, "pulling feature with oras");
|
||||
|
||||
let output = tokio::process::Command::new("oras")
|
||||
let output = Command::new("oras")
|
||||
.args(["pull", feature_id, "-o"])
|
||||
.arg(&feature_dir)
|
||||
.output()
|
||||
|
|
@ -259,7 +259,7 @@ async fn fetch_feature_https(
|
|||
})?;
|
||||
|
||||
let tgz_path = feature_dir.join("devcontainer-feature.tgz");
|
||||
tokio::fs::write(&tgz_path, &bytes).await.map_err(|e| {
|
||||
fs::write(&tgz_path, &bytes).await.map_err(|e| {
|
||||
DevcontainerError::Feature(format!("failed to write {}: {e}", tgz_path.display()))
|
||||
})?;
|
||||
|
||||
|
|
@ -290,11 +290,11 @@ async fn fetch_feature_dispatch(
|
|||
|
||||
/// Recursively copy a directory.
|
||||
async fn copy_dir_recursive(src: &Path, dst: &Path) -> crate::Result<()> {
|
||||
tokio::fs::create_dir_all(dst).await.map_err(|e| {
|
||||
fs::create_dir_all(dst).await.map_err(|e| {
|
||||
DevcontainerError::Feature(format!("failed to create dir {}: {e}", dst.display()))
|
||||
})?;
|
||||
|
||||
let mut entries = tokio::fs::read_dir(src).await.map_err(|e| {
|
||||
let mut entries = fs::read_dir(src).await.map_err(|e| {
|
||||
DevcontainerError::Feature(format!("failed to read dir {}: {e}", src.display()))
|
||||
})?;
|
||||
|
||||
|
|
@ -309,15 +309,13 @@ async fn copy_dir_recursive(src: &Path, dst: &Path) -> crate::Result<()> {
|
|||
if entry_path.is_dir() {
|
||||
Box::pin(copy_dir_recursive(&entry_path, &dest_path)).await?;
|
||||
} else {
|
||||
tokio::fs::copy(&entry_path, &dest_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DevcontainerError::Feature(format!(
|
||||
"failed to copy {} to {}: {e}",
|
||||
entry_path.display(),
|
||||
dest_path.display()
|
||||
))
|
||||
})?;
|
||||
fs::copy(&entry_path, &dest_path).await.map_err(|e| {
|
||||
DevcontainerError::Feature(format!(
|
||||
"failed to copy {} to {}: {e}",
|
||||
entry_path.display(),
|
||||
dest_path.display()
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -548,7 +546,7 @@ pub async fn resolve_features(
|
|||
.as_nanos()
|
||||
);
|
||||
let tmp_dir = std::env::temp_dir().join(unique_id);
|
||||
tokio::fs::create_dir_all(&tmp_dir)
|
||||
fs::create_dir_all(&tmp_dir)
|
||||
.await
|
||||
.map_err(|e| DevcontainerError::Feature(format!("failed to create temp dir: {e}")))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ mod variables;
|
|||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use fabro_util::env::SystemEnv;
|
||||
pub use types::DevcontainerJson;
|
||||
|
||||
/// Lifecycle command — string, array, or object (parallel) form.
|
||||
|
|
@ -174,7 +175,7 @@ impl DevcontainerResolver {
|
|||
.clone()
|
||||
.unwrap_or_else(|| format!("/workspaces/{repo_name}"));
|
||||
|
||||
let system_env = fabro_util::env::SystemEnv;
|
||||
let system_env = SystemEnv;
|
||||
let preliminary_vars = variables::VariableContext {
|
||||
local_workspace_folder: repo_root.to_string_lossy().to_string(),
|
||||
local_workspace_folder_basename: repo_name.clone(),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use fabro_util::env::Env;
|
||||
|
||||
/// Context for variable substitution.
|
||||
pub struct VariableContext<'a> {
|
||||
pub local_workspace_folder: String,
|
||||
pub local_workspace_folder_basename: String,
|
||||
pub container_workspace_folder: String,
|
||||
pub env: &'a dyn fabro_util::env::Env,
|
||||
pub env: &'a dyn Env,
|
||||
}
|
||||
|
||||
/// Replace devcontainer variables in a string value.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use nom::branch::alt;
|
||||
use nom::character::complete::char;
|
||||
use nom::bytes::complete::tag;
|
||||
use nom::character::complete::{char, multispace0};
|
||||
use nom::combinator::opt;
|
||||
use nom::error::{Error, ParseError};
|
||||
use nom::multi::{many0, separated_list0};
|
||||
use nom::sequence::{delimited, preceded, tuple};
|
||||
use nom::IResult;
|
||||
|
|
@ -83,11 +85,11 @@ fn node_or_edge_stmt(input: &str) -> IResult<&str, Statement> {
|
|||
let (rest, first_id) = preceded(ws, identifier)(input)?;
|
||||
|
||||
// Try to parse as edge: first_id (-> id)+ [attrs]? ;?
|
||||
if let Ok((rest2, _)) = arrow::<nom::error::Error<&str>>(rest) {
|
||||
if let Ok((rest2, _)) = arrow::<Error<&str>>(rest) {
|
||||
let (rest2, second_id) = preceded(ws, identifier)(rest2)?;
|
||||
let mut nodes = vec![first_id.to_string(), second_id.to_string()];
|
||||
let mut remaining = rest2;
|
||||
while let Ok((r, _)) = arrow::<nom::error::Error<&str>>(remaining) {
|
||||
while let Ok((r, _)) = arrow::<Error<&str>>(remaining) {
|
||||
let (r, next_id) = preceded(ws, identifier)(r)?;
|
||||
nodes.push(next_id.to_string());
|
||||
remaining = r;
|
||||
|
|
@ -148,11 +150,8 @@ pub fn parse_dot_graph(input: &str) -> IResult<&str, DotGraph> {
|
|||
}
|
||||
|
||||
// We need arrow to work with explicit error types
|
||||
fn arrow<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> {
|
||||
preceded(
|
||||
nom::character::complete::multispace0,
|
||||
nom::bytes::complete::tag("->"),
|
||||
)(input)
|
||||
fn arrow<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> {
|
||||
preceded(multispace0, tag("->"))(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -56,8 +56,9 @@ pub mod combinators {
|
|||
use nom::bytes::complete::{tag, take_while, take_while1};
|
||||
use nom::character::complete::{char, multispace0};
|
||||
use nom::combinator::{map, opt, recognize};
|
||||
use nom::error::{Error, ErrorKind};
|
||||
use nom::sequence::{delimited, pair, preceded};
|
||||
use nom::IResult;
|
||||
use nom::{Err, IResult};
|
||||
|
||||
use crate::parser::ast::AstValue;
|
||||
|
||||
|
|
@ -85,7 +86,7 @@ pub mod combinators {
|
|||
let mut result = first.to_string();
|
||||
let mut remaining = rest;
|
||||
let mut found_dot = false;
|
||||
while let Ok((r, _)) = char::<&str, nom::error::Error<&str>>('.')(remaining) {
|
||||
while let Ok((r, _)) = char::<&str, Error<&str>>('.')(remaining) {
|
||||
if let Ok((r2, segment)) = identifier(r) {
|
||||
result.push('.');
|
||||
result.push_str(segment);
|
||||
|
|
@ -98,10 +99,7 @@ pub mod combinators {
|
|||
if found_dot {
|
||||
Ok((remaining, result))
|
||||
} else {
|
||||
Err(nom::Err::Error(nom::error::Error::new(
|
||||
input,
|
||||
nom::error::ErrorKind::Tag,
|
||||
)))
|
||||
Err(Err::Error(Error::new(input, ErrorKind::Tag)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,10 +146,7 @@ pub mod combinators {
|
|||
consumed += c.len_utf8();
|
||||
}
|
||||
None => {
|
||||
return Err(nom::Err::Error(nom::error::Error::new(
|
||||
input,
|
||||
nom::error::ErrorKind::Char,
|
||||
)));
|
||||
return Err(Err::Error(Error::new(input, ErrorKind::Char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -160,10 +155,7 @@ pub mod combinators {
|
|||
consumed += c.len_utf8();
|
||||
}
|
||||
None => {
|
||||
return Err(nom::Err::Error(nom::error::Error::new(
|
||||
input,
|
||||
nom::error::ErrorKind::Char,
|
||||
)));
|
||||
return Err(Err::Error(Error::new(input, ErrorKind::Char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,10 +167,7 @@ pub mod combinators {
|
|||
match word {
|
||||
"true" => Ok((rest, true)),
|
||||
"false" => Ok((rest, false)),
|
||||
_ => Err(nom::Err::Error(nom::error::Error::new(
|
||||
input,
|
||||
nom::error::ErrorKind::Tag,
|
||||
))),
|
||||
_ => Err(Err::Error(Error::new(input, ErrorKind::Tag))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,9 +177,9 @@ pub mod combinators {
|
|||
pair(opt(char('-')), take_while(|c: char| c.is_ascii_digit())),
|
||||
pair(char('.'), take_while1(|c: char| c.is_ascii_digit())),
|
||||
))(input)?;
|
||||
let val: f64 = raw.parse().map_err(|_| {
|
||||
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Float))
|
||||
})?;
|
||||
let val: f64 = raw
|
||||
.parse()
|
||||
.map_err(|_| Err::Error(Error::new(input, ErrorKind::Float)))?;
|
||||
Ok((rest, val))
|
||||
}
|
||||
|
||||
|
|
@ -201,14 +190,11 @@ pub mod combinators {
|
|||
take_while1(|c: char| c.is_ascii_digit()),
|
||||
))(input)?;
|
||||
if rest.starts_with('.') {
|
||||
return Err(nom::Err::Error(nom::error::Error::new(
|
||||
input,
|
||||
nom::error::ErrorKind::Digit,
|
||||
)));
|
||||
return Err(Err::Error(Error::new(input, ErrorKind::Digit)));
|
||||
}
|
||||
let val: i64 = raw.parse().map_err(|_| {
|
||||
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
|
||||
})?;
|
||||
let val: i64 = raw
|
||||
.parse()
|
||||
.map_err(|_| Err::Error(Error::new(input, ErrorKind::Digit)))?;
|
||||
Ok((rest, val))
|
||||
}
|
||||
|
||||
|
|
@ -224,10 +210,7 @@ pub mod combinators {
|
|||
.next()
|
||||
.is_some_and(|c| c.is_ascii_alphanumeric())
|
||||
{
|
||||
return Err(nom::Err::Error(nom::error::Error::new(
|
||||
input,
|
||||
nom::error::ErrorKind::Tag,
|
||||
)));
|
||||
return Err(Err::Error(Error::new(input, ErrorKind::Tag)));
|
||||
}
|
||||
Ok((rest, AstValue::Str(format!("{num}{unit}"))))
|
||||
}
|
||||
|
|
@ -243,10 +226,7 @@ pub mod combinators {
|
|||
take_while(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.'),
|
||||
))(input)?;
|
||||
if !raw.contains('-') && !raw.contains('.') {
|
||||
return Err(nom::Err::Error(nom::error::Error::new(
|
||||
input,
|
||||
nom::error::ErrorKind::Verify,
|
||||
)));
|
||||
return Err(Err::Error(Error::new(input, ErrorKind::Verify)));
|
||||
}
|
||||
Ok((rest, raw.to_string()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::time::Duration;
|
|||
|
||||
use crate::error::GraphvizError;
|
||||
use crate::graph::types::{AttrValue, Edge, Graph, Node};
|
||||
use crate::parser::ast::{AstValue, AttrBlock, DotGraph, Statement};
|
||||
use crate::parser::ast::{AstValue, AttrBlock, DotGraph, EdgeStmt, NodeStmt, Statement};
|
||||
|
||||
/// Convert an AST `AstValue` to a semantic `AttrValue`.
|
||||
fn convert_value(ast_val: &AstValue) -> AttrValue {
|
||||
|
|
@ -89,11 +89,7 @@ impl SemanticState {
|
|||
}
|
||||
}
|
||||
|
||||
fn process_node(
|
||||
&mut self,
|
||||
node_stmt: &crate::parser::ast::NodeStmt,
|
||||
subgraph_class: Option<&str>,
|
||||
) {
|
||||
fn process_node(&mut self, node_stmt: &NodeStmt, subgraph_class: Option<&str>) {
|
||||
self.ensure_node(&node_stmt.id);
|
||||
let node = self
|
||||
.graph
|
||||
|
|
@ -141,11 +137,7 @@ impl SemanticState {
|
|||
}
|
||||
}
|
||||
|
||||
fn process_edge(
|
||||
&mut self,
|
||||
edge_stmt: &crate::parser::ast::EdgeStmt,
|
||||
subgraph_class: Option<&str>,
|
||||
) {
|
||||
fn process_edge(&mut self, edge_stmt: &EdgeStmt, subgraph_class: Option<&str>) {
|
||||
for id in &edge_stmt.nodes {
|
||||
self.ensure_node(id);
|
||||
if let Some(cls) = subgraph_class {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,15 @@ use std::sync::{Arc, LazyLock};
|
|||
use std::time::Instant;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use fabro_agent::tool_registry::ToolContext;
|
||||
use fabro_agent::Sandbox;
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_llm::generate::{generate_object, GenerateParams};
|
||||
use fabro_llm::types::{Message, Request, ToolResult};
|
||||
use fabro_util::env::{Env, SystemEnv};
|
||||
use tokio::process::Command as TokioCommand;
|
||||
use tokio::time::timeout as tokio_timeout;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::config::{HookDefinition, HookType, TlsMode};
|
||||
use crate::types::{HookContext, HookDecision, HookResult, PromptHookResponse};
|
||||
|
|
@ -40,11 +47,7 @@ pub trait HookExecutor: Send + Sync {
|
|||
/// Interpolate `$VAR` and `${VAR}` references in `value` using environment
|
||||
/// variables, but only when the variable name appears in `allowed_vars`.
|
||||
/// Unlisted or missing vars are replaced with the empty string.
|
||||
pub fn interpolate_env_vars(
|
||||
value: &str,
|
||||
allowed_vars: &[String],
|
||||
env: &dyn fabro_util::env::Env,
|
||||
) -> String {
|
||||
pub fn interpolate_env_vars(value: &str, allowed_vars: &[String], env: &dyn Env) -> String {
|
||||
let mut result = String::with_capacity(value.len());
|
||||
let mut chars = value.chars().peekable();
|
||||
|
||||
|
|
@ -149,7 +152,7 @@ impl HookExecutorImpl {
|
|||
},
|
||||
}
|
||||
} else {
|
||||
let mut cmd = tokio::process::Command::new("sh");
|
||||
let mut cmd = TokioCommand::new("sh");
|
||||
cmd.arg("-c").arg(command);
|
||||
if let Some(wd) = work_dir {
|
||||
cmd.current_dir(wd);
|
||||
|
|
@ -238,7 +241,7 @@ impl HookExecutorImpl {
|
|||
F: FnOnce() -> Fut,
|
||||
Fut: std::future::Future<Output = HookDecision>,
|
||||
{
|
||||
match tokio::time::timeout(timeout, f()).await {
|
||||
match tokio_timeout(timeout, f()).await {
|
||||
Ok(decision) => decision,
|
||||
Err(_) => {
|
||||
tracing::warn!("{hook_kind} hook timed out, proceeding");
|
||||
|
|
@ -258,12 +261,12 @@ impl HookExecutorImpl {
|
|||
let user_msg = Self::build_hook_user_message(prompt, context);
|
||||
|
||||
Self::execute_llm_with_timeout(timeout, "prompt", || async move {
|
||||
let params = fabro_llm::generate::GenerateParams::new(&resolved_model)
|
||||
let params = GenerateParams::new(&resolved_model)
|
||||
.system(HOOK_EVALUATOR_SYSTEM_PROMPT)
|
||||
.prompt(user_msg)
|
||||
.max_tokens(1024);
|
||||
|
||||
match fabro_llm::generate::generate_object(params, HOOK_RESPONSE_SCHEMA.clone()).await {
|
||||
match generate_object(params, HOOK_RESPONSE_SCHEMA.clone()).await {
|
||||
Ok(result) => match result.output {
|
||||
Some(obj) => match serde_json::from_value::<PromptHookResponse>(obj) {
|
||||
Ok(resp) if resp.ok => HookDecision::Proceed,
|
||||
|
|
@ -306,7 +309,7 @@ impl HookExecutorImpl {
|
|||
let user_msg = Self::build_hook_user_message(prompt, context);
|
||||
|
||||
Self::execute_llm_with_timeout(timeout, "agent", || async move {
|
||||
let client = match fabro_llm::client::Client::from_env().await {
|
||||
let client = match LlmClient::from_env().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "agent hook client creation failed, proceeding");
|
||||
|
|
@ -320,15 +323,15 @@ impl HookExecutorImpl {
|
|||
let tool_defs = registry.definitions();
|
||||
|
||||
let mut messages = vec![
|
||||
fabro_llm::types::Message::system(HOOK_EVALUATOR_SYSTEM_PROMPT),
|
||||
fabro_llm::types::Message::user(user_msg),
|
||||
Message::system(HOOK_EVALUATOR_SYSTEM_PROMPT),
|
||||
Message::user(user_msg),
|
||||
];
|
||||
|
||||
let rounds = max_tool_rounds.unwrap_or(50);
|
||||
let cancel = tokio_util::sync::CancellationToken::new();
|
||||
let cancel = CancellationToken::new();
|
||||
|
||||
for _ in 0..rounds {
|
||||
let request = fabro_llm::types::Request {
|
||||
let request = Request {
|
||||
model: resolved_model.clone(),
|
||||
messages: messages.clone(),
|
||||
provider: None,
|
||||
|
|
@ -362,25 +365,23 @@ impl HookExecutorImpl {
|
|||
|
||||
for tc in &tool_calls {
|
||||
let tool = registry.get(&tc.name).cloned();
|
||||
let ctx = fabro_agent::tool_registry::ToolContext {
|
||||
let ctx = ToolContext {
|
||||
env: sandbox.clone(),
|
||||
cancel: cancel.child_token(),
|
||||
tool_env: None,
|
||||
};
|
||||
let result = match tool {
|
||||
Some(t) => match (t.executor)(tc.arguments.clone(), ctx).await {
|
||||
Ok(output) => fabro_llm::types::ToolResult::success(
|
||||
tc.id.clone(),
|
||||
serde_json::json!(output),
|
||||
),
|
||||
Err(err) => fabro_llm::types::ToolResult::error(tc.id.clone(), err),
|
||||
Ok(output) => {
|
||||
ToolResult::success(tc.id.clone(), serde_json::json!(output))
|
||||
}
|
||||
Err(err) => ToolResult::error(tc.id.clone(), err),
|
||||
},
|
||||
None => fabro_llm::types::ToolResult::error(
|
||||
tc.id.clone(),
|
||||
format!("Unknown tool: {}", tc.name),
|
||||
),
|
||||
None => {
|
||||
ToolResult::error(tc.id.clone(), format!("Unknown tool: {}", tc.name))
|
||||
}
|
||||
};
|
||||
messages.push(fabro_llm::types::Message::tool_result(
|
||||
messages.push(Message::tool_result(
|
||||
result.tool_call_id,
|
||||
result.content,
|
||||
result.is_error,
|
||||
|
|
@ -414,7 +415,7 @@ impl HookExecutorImpl {
|
|||
tls: &TlsMode,
|
||||
context: &HookContext,
|
||||
timeout: std::time::Duration,
|
||||
env: &dyn fabro_util::env::Env,
|
||||
env: &dyn Env,
|
||||
) -> HookDecision {
|
||||
// Enforce URL scheme based on TLS mode
|
||||
match tls {
|
||||
|
|
@ -551,7 +552,7 @@ impl HookExecutor for HookExecutorImpl {
|
|||
tls,
|
||||
context,
|
||||
definition.timeout(),
|
||||
&fabro_util::env::SystemEnv,
|
||||
&SystemEnv,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ use async_trait::async_trait;
|
|||
use dialoguer::console::Term;
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use fabro_util::terminal::Styles;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use tokio::task;
|
||||
|
||||
use crate::{Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType};
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ fn find_matching_option(response: &str, options: &[QuestionOption]) -> Option<An
|
|||
async fn read_line(prompt: &str) -> PromptRead {
|
||||
// Print the prompt to stderr so it doesn't interfere with piped stdout
|
||||
eprint!("{prompt}");
|
||||
let stdin = tokio::io::stdin();
|
||||
let stdin = io::stdin();
|
||||
let mut reader = BufReader::new(stdin);
|
||||
let mut line = String::new();
|
||||
match reader.read_line(&mut line).await {
|
||||
|
|
@ -219,7 +220,7 @@ impl Interviewer for ConsoleInterviewer {
|
|||
eprint!("{rendered}");
|
||||
}
|
||||
let q = question;
|
||||
return tokio::task::spawn_blocking(move || match q.question_type {
|
||||
return task::spawn_blocking(move || match q.question_type {
|
||||
QuestionType::MultipleChoice => ask_select_interactive(&q),
|
||||
QuestionType::MultiSelect => ask_multi_select_interactive(&q),
|
||||
QuestionType::YesNo | QuestionType::Confirmation => ask_confirm_interactive(&q),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ use std::path::PathBuf;
|
|||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::fs;
|
||||
use tokio::time;
|
||||
|
||||
use crate::{Answer, Interviewer, Question};
|
||||
|
||||
|
|
@ -49,17 +51,17 @@ impl FileInterviewer {
|
|||
let json = serde_json::to_string_pretty(question).expect("Question serialization failed");
|
||||
let request_path = self.request_path();
|
||||
if let Some(parent) = request_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
fs::create_dir_all(parent).await?;
|
||||
}
|
||||
let temp_path = request_path.with_extension("json.tmp");
|
||||
tokio::fs::write(&temp_path, json).await?;
|
||||
tokio::fs::rename(temp_path, request_path).await
|
||||
fs::write(&temp_path, json).await?;
|
||||
fs::rename(temp_path, request_path).await
|
||||
}
|
||||
|
||||
async fn cleanup_ipc_files(&self) {
|
||||
let _ = tokio::fs::remove_file(self.request_path()).await;
|
||||
let _ = tokio::fs::remove_file(self.response_path()).await;
|
||||
let _ = tokio::fs::remove_file(self.claim_path()).await;
|
||||
let _ = fs::remove_file(self.request_path()).await;
|
||||
let _ = fs::remove_file(self.response_path()).await;
|
||||
let _ = fs::remove_file(self.claim_path()).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,9 +83,9 @@ impl Interviewer for FileInterviewer {
|
|||
let response_path = self.response_path();
|
||||
let claim_path = self.claim_path();
|
||||
let mut claim_was_seen = false;
|
||||
let mut reattach_deadline: Option<tokio::time::Instant> = None;
|
||||
let mut reattach_deadline: Option<time::Instant> = None;
|
||||
loop {
|
||||
match tokio::fs::read_to_string(&response_path).await {
|
||||
match fs::read_to_string(&response_path).await {
|
||||
Ok(data) => match serde_json::from_str::<Answer>(&data) {
|
||||
Ok(answer) => {
|
||||
self.cleanup_ipc_files().await;
|
||||
|
|
@ -107,23 +109,23 @@ impl Interviewer for FileInterviewer {
|
|||
claim_was_seen = true;
|
||||
reattach_deadline = None;
|
||||
} else if claim_was_seen && reattach_deadline.is_none() {
|
||||
reattach_deadline = Some(tokio::time::Instant::now() + REATTACH_WINDOW);
|
||||
reattach_deadline = Some(time::Instant::now() + REATTACH_WINDOW);
|
||||
}
|
||||
|
||||
if let Some(deadline) = reattach_deadline {
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
if time::Instant::now() >= deadline {
|
||||
self.cleanup_ipc_files().await;
|
||||
return default_for_claim_timeout.unwrap_or_else(Answer::timeout);
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(secs) = timeout_secs {
|
||||
let duration = std::time::Duration::from_secs_f64(secs);
|
||||
match tokio::time::timeout(duration, poll).await {
|
||||
match time::timeout(duration, poll).await {
|
||||
Ok(answer) => answer,
|
||||
Err(_) => {
|
||||
self.cleanup_ipc_files().await;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use std::collections::HashMap;
|
|||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time;
|
||||
|
||||
/// The type of question being asked.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -174,7 +175,7 @@ pub async fn ask_with_timeout(interviewer: &dyn Interviewer, question: Question)
|
|||
|
||||
if let Some(secs) = timeout_secs {
|
||||
let duration = std::time::Duration::from_secs_f64(secs);
|
||||
match tokio::time::timeout(duration, interviewer.ask(question)).await {
|
||||
match time::timeout(duration, interviewer.ask(question)).await {
|
||||
Ok(answer) => answer,
|
||||
Err(_elapsed) => default_answer.unwrap_or_else(Answer::timeout),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ use anyhow::{bail, Context, Result};
|
|||
use clap::{Args, Subcommand};
|
||||
use cli_table::format::{Border, Justify, Separator};
|
||||
use cli_table::{print_stdout, Cell, CellStruct, Color, Style, Table};
|
||||
use futures::StreamExt;
|
||||
use futures::{stream, StreamExt};
|
||||
use serde::Deserialize;
|
||||
use tokio::task;
|
||||
use tokio::time;
|
||||
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
|
|
@ -17,7 +19,7 @@ use fabro_model::{Catalog, Model, Provider};
|
|||
|
||||
use crate::generate::{self, GenerateParams};
|
||||
use crate::tools::Tool;
|
||||
use crate::types::{ContentPart, Message};
|
||||
use crate::types::{ContentPart, GenerateResult, Message, ReasoningEffort, StreamEvent, Usage};
|
||||
|
||||
pub struct ServerConnection {
|
||||
pub client: reqwest::Client,
|
||||
|
|
@ -161,7 +163,7 @@ fn models_title() -> Vec<CellStruct> {
|
|||
]
|
||||
}
|
||||
|
||||
fn print_models_table(models: &[crate::types::Model], s: &Styles) {
|
||||
fn print_models_table(models: &[Model], s: &Styles) {
|
||||
let use_color = s.use_color;
|
||||
let rows: Vec<Vec<CellStruct>> = models.iter().map(|m| model_row(m, use_color)).collect();
|
||||
let table = rows
|
||||
|
|
@ -251,7 +253,7 @@ fn apply_options(
|
|||
Ok(params)
|
||||
}
|
||||
|
||||
fn print_usage(usage: &crate::types::Usage) {
|
||||
fn print_usage(usage: &Usage) {
|
||||
eprintln!(
|
||||
"Tokens: {} input, {} output, {} total",
|
||||
usage.input_tokens, usage.output_tokens, usage.total_tokens
|
||||
|
|
@ -278,7 +280,7 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> {
|
|||
|
||||
loop {
|
||||
let line = if is_tty {
|
||||
let result = tokio::task::spawn_blocking(|| {
|
||||
let result = task::spawn_blocking(|| {
|
||||
dialoguer::Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(">")
|
||||
.interact_on(&Term::stderr())
|
||||
|
|
@ -318,7 +320,7 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> {
|
|||
let mut stream_result = generate::stream(params).await?;
|
||||
let mut full_text = String::new();
|
||||
while let Some(event) = stream_result.next().await {
|
||||
if let crate::types::StreamEvent::TextDelta { delta, .. } = event? {
|
||||
if let StreamEvent::TextDelta { delta, .. } = event? {
|
||||
print!("{delta}");
|
||||
full_text.push_str(&delta);
|
||||
}
|
||||
|
|
@ -380,7 +382,7 @@ pub async fn run_prompt(args: PromptArgs) -> Result<()> {
|
|||
(false, None) => {
|
||||
let mut stream_result = generate::stream(params).await?;
|
||||
while let Some(event) = stream_result.next().await {
|
||||
if let crate::types::StreamEvent::TextDelta { delta, .. } = event? {
|
||||
if let StreamEvent::TextDelta { delta, .. } = event? {
|
||||
print!("{delta}");
|
||||
}
|
||||
}
|
||||
|
|
@ -480,22 +482,22 @@ pub async fn run_prompt_via_server(args: PromptArgs, server: &ServerConnection)
|
|||
}
|
||||
|
||||
let show_usage = args.usage;
|
||||
let mut output_usage: Option<crate::types::Usage> = None;
|
||||
let mut output_usage: Option<Usage> = None;
|
||||
|
||||
parse_sse_frames(response, |event_type, data| {
|
||||
if event_type == "stream_event" {
|
||||
if let Ok(event) = serde_json::from_str::<crate::types::StreamEvent>(data) {
|
||||
if let Ok(event) = serde_json::from_str::<StreamEvent>(data) {
|
||||
match event {
|
||||
crate::types::StreamEvent::TextDelta { delta, .. } => {
|
||||
StreamEvent::TextDelta { delta, .. } => {
|
||||
print!("{delta}");
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
crate::types::StreamEvent::Finish { usage, .. } => {
|
||||
StreamEvent::Finish { usage, .. } => {
|
||||
if show_usage {
|
||||
output_usage = Some(usage);
|
||||
}
|
||||
}
|
||||
crate::types::StreamEvent::Error { error, .. } => {
|
||||
StreamEvent::Error { error, .. } => {
|
||||
bail!("Server error: {error}");
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -646,7 +648,7 @@ pub async fn run_chat_via_server(args: ChatArgs, server: &ServerConnection) -> R
|
|||
|
||||
loop {
|
||||
let line = if is_tty {
|
||||
let result = tokio::task::spawn_blocking(|| {
|
||||
let result = task::spawn_blocking(|| {
|
||||
dialoguer::Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(">")
|
||||
.interact_on(&Term::stderr())
|
||||
|
|
@ -869,16 +871,13 @@ fn build_deep_test_params(info: &Model) -> Option<GenerateParams> {
|
|||
.max_tokens(1024);
|
||||
|
||||
if info.features.reasoning {
|
||||
params = params.reasoning_effort(crate::types::ReasoningEffort::High);
|
||||
params = params.reasoning_effort(ReasoningEffort::High);
|
||||
}
|
||||
|
||||
Some(params)
|
||||
}
|
||||
|
||||
fn validate_deep_result(
|
||||
result: &crate::types::GenerateResult,
|
||||
info: &Model,
|
||||
) -> (cli_table::Color, String) {
|
||||
fn validate_deep_result(result: &GenerateResult, info: &Model) -> (cli_table::Color, String) {
|
||||
// Check tool use: need at least 2 steps (tool call + follow-up)
|
||||
if result.steps.len() < 2 {
|
||||
return (
|
||||
|
|
@ -1048,7 +1047,7 @@ async fn test_one_model(info: &Model, deep: bool) -> (Color, String) {
|
|||
None => (Color::Yellow, "deep: skipped (no tool support)".to_string()),
|
||||
Some(params) => {
|
||||
let result =
|
||||
tokio::time::timeout(Duration::from_secs(90), generate::generate(params)).await;
|
||||
time::timeout(Duration::from_secs(90), generate::generate(params)).await;
|
||||
match result {
|
||||
Ok(Ok(ref gen_result)) => validate_deep_result(gen_result, info),
|
||||
Ok(Err(e)) => (Color::Red, format!("deep: error: {e}")),
|
||||
|
|
@ -1062,8 +1061,7 @@ async fn test_one_model(info: &Model, deep: bool) -> (Color, String) {
|
|||
.prompt("Say OK")
|
||||
.max_tokens(16);
|
||||
|
||||
let result =
|
||||
tokio::time::timeout(Duration::from_secs(30), generate::generate(params)).await;
|
||||
let result = time::timeout(Duration::from_secs(30), generate::generate(params)).await;
|
||||
match result {
|
||||
Ok(Ok(_)) => (Color::Green, "ok".to_string()),
|
||||
Ok(Err(e)) => (Color::Red, format!("error: {e}")),
|
||||
|
|
@ -1109,7 +1107,7 @@ async fn test_models(
|
|||
indexed.shuffle(&mut rand::thread_rng());
|
||||
|
||||
// Run tests concurrently, 6 at a time
|
||||
let results: Vec<(usize, Color, String)> = futures::stream::iter(indexed)
|
||||
let results: Vec<(usize, Color, String)> = stream::iter(indexed)
|
||||
.map(|(idx, info)| {
|
||||
let pb = pb.clone();
|
||||
async move {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,14 @@ use crate::types::{
|
|||
ResponseFormat, ResponseFormatType, RetryPolicy, StepResult, StreamEvent, TimeoutConfig,
|
||||
ToolCall, ToolChoice, ToolDefinition, Usage,
|
||||
};
|
||||
use futures::{Stream, StreamExt};
|
||||
use fabro_util::backoff::BackoffPolicy;
|
||||
use futures::{future, stream, Stream, StreamExt};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::sync::{mpsc, OnceCell};
|
||||
use tokio::time;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
|
|
@ -109,7 +112,7 @@ pub async fn generate(params: GenerateParams) -> Result<GenerateResult, SdkError
|
|||
};
|
||||
let retry_policy = RetryPolicy {
|
||||
max_retries: params.max_retries,
|
||||
backoff: fabro_util::backoff::BackoffPolicy {
|
||||
backoff: BackoffPolicy {
|
||||
initial_delay: std::time::Duration::from_micros(1),
|
||||
jitter: false,
|
||||
..Default::default()
|
||||
|
|
@ -156,7 +159,7 @@ pub async fn generate(params: GenerateParams) -> Result<GenerateResult, SdkError
|
|||
let response = if let Some(per_step) = params.timeout.as_ref().and_then(|t| t.per_step)
|
||||
{
|
||||
let duration = std::time::Duration::from_secs_f64(per_step);
|
||||
tokio::time::timeout(
|
||||
time::timeout(
|
||||
duration,
|
||||
retry(&retry_policy, || {
|
||||
let c = client_ref.clone();
|
||||
|
|
@ -262,7 +265,7 @@ pub async fn generate(params: GenerateParams) -> Result<GenerateResult, SdkError
|
|||
|
||||
if let Some(total) = params.timeout.as_ref().and_then(|t| t.total) {
|
||||
let duration = std::time::Duration::from_secs_f64(total);
|
||||
tokio::time::timeout(duration, generate_future)
|
||||
time::timeout(duration, generate_future)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
warn!(timeout_secs = total, "Total generation timeout exceeded");
|
||||
|
|
@ -583,7 +586,7 @@ impl StreamResult {
|
|||
#[must_use]
|
||||
pub fn text_stream(self) -> Pin<Box<dyn Stream<Item = Result<String, SdkError>> + Send>> {
|
||||
Box::pin(self.filter_map(|result| {
|
||||
futures::future::ready(match result {
|
||||
future::ready(match result {
|
||||
Ok(StreamEvent::TextDelta { delta, .. }) => Some(Ok(delta)),
|
||||
Err(e) => Some(Err(e)),
|
||||
_ => None,
|
||||
|
|
@ -661,12 +664,12 @@ async fn stream_with_tool_loop(params: GenerateParams) -> Result<StreamEventStre
|
|||
}
|
||||
|
||||
// Tool loop: collect events from each round, execute tools, continue
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<Result<StreamEvent, SdkError>>(64);
|
||||
let (tx, rx) = mpsc::channel::<Result<StreamEvent, SdkError>>(64);
|
||||
|
||||
let tools = params.tools.clone();
|
||||
let retry_policy = RetryPolicy {
|
||||
max_retries: params.max_retries,
|
||||
backoff: fabro_util::backoff::BackoffPolicy {
|
||||
backoff: BackoffPolicy {
|
||||
initial_delay: std::time::Duration::from_micros(1),
|
||||
jitter: false,
|
||||
..Default::default()
|
||||
|
|
@ -703,7 +706,7 @@ async fn stream_with_tool_loop(params: GenerateParams) -> Result<StreamEventStre
|
|||
let stream_result =
|
||||
if let Some(per_step) = params.timeout.as_ref().and_then(|t| t.per_step) {
|
||||
let duration = std::time::Duration::from_secs_f64(per_step);
|
||||
tokio::time::timeout(duration, stream_connect)
|
||||
time::timeout(duration, stream_connect)
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
Err(SdkError::RequestTimeout {
|
||||
|
|
@ -832,10 +835,7 @@ async fn stream_with_tool_loop(params: GenerateParams) -> Result<StreamEventStre
|
|||
// Apply total timeout if configured (Section 4.7)
|
||||
if let Some(total) = params.timeout.as_ref().and_then(|t| t.total) {
|
||||
let duration = std::time::Duration::from_secs_f64(total);
|
||||
if tokio::time::timeout(duration, tool_loop_future)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
if time::timeout(duration, tool_loop_future).await.is_err() {
|
||||
let _ = tx
|
||||
.send(Err(SdkError::RequestTimeout {
|
||||
message: format!("Total timeout of {total}s exceeded"),
|
||||
|
|
@ -848,7 +848,7 @@ async fn stream_with_tool_loop(params: GenerateParams) -> Result<StreamEventStre
|
|||
}
|
||||
});
|
||||
|
||||
Ok(Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx)))
|
||||
Ok(Box::pin(ReceiverStream::new(rx)))
|
||||
}
|
||||
|
||||
/// Internal single-round streaming (no tool loop). Used by `stream_object()`.
|
||||
|
|
@ -863,7 +863,7 @@ async fn stream_generate_raw(
|
|||
// Apply per_step timeout to the initial connection (Section 4.7)
|
||||
let inner_stream = if let Some(per_step) = params.timeout.as_ref().and_then(|t| t.per_step) {
|
||||
let duration = std::time::Duration::from_secs_f64(per_step);
|
||||
tokio::time::timeout(duration, client.stream(&request))
|
||||
time::timeout(duration, client.stream(&request))
|
||||
.await
|
||||
.map_err(|_| SdkError::RequestTimeout {
|
||||
message: format!("Per-step timeout of {per_step}s exceeded"),
|
||||
|
|
@ -892,25 +892,24 @@ async fn stream_generate_raw(
|
|||
// Apply total timeout to the stream (Section 4.7)
|
||||
if let Some(total) = params.timeout.as_ref().and_then(|t| t.total) {
|
||||
let duration = std::time::Duration::from_secs_f64(total);
|
||||
let deadline = tokio::time::Instant::now() + duration;
|
||||
let deadline = time::Instant::now() + duration;
|
||||
let total_copy = total;
|
||||
let timed_stream =
|
||||
futures::stream::unfold((stream, false), move |(mut stream, done)| async move {
|
||||
if done {
|
||||
return None;
|
||||
}
|
||||
match tokio::time::timeout_at(deadline, stream.next()).await {
|
||||
Ok(Some(item)) => Some((item, (stream, false))),
|
||||
Ok(None) => None, // stream completed naturally
|
||||
Err(_) => Some((
|
||||
Err(SdkError::RequestTimeout {
|
||||
message: format!("Total timeout of {total_copy}s exceeded"),
|
||||
source: None,
|
||||
}),
|
||||
(stream, true),
|
||||
)),
|
||||
}
|
||||
});
|
||||
let timed_stream = stream::unfold((stream, false), move |(mut stream, done)| async move {
|
||||
if done {
|
||||
return None;
|
||||
}
|
||||
match time::timeout_at(deadline, stream.next()).await {
|
||||
Ok(Some(item)) => Some((item, (stream, false))),
|
||||
Ok(None) => None, // stream completed naturally
|
||||
Err(_) => Some((
|
||||
Err(SdkError::RequestTimeout {
|
||||
message: format!("Total timeout of {total_copy}s exceeded"),
|
||||
source: None,
|
||||
}),
|
||||
(stream, true),
|
||||
)),
|
||||
}
|
||||
});
|
||||
Ok(Box::pin(timed_stream))
|
||||
} else {
|
||||
Ok(stream)
|
||||
|
|
@ -1099,7 +1098,7 @@ pub async fn stream_object(
|
|||
}
|
||||
}
|
||||
|
||||
futures::future::ready(Some(futures::stream::iter(events)))
|
||||
future::ready(Some(stream::iter(events)))
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1672,7 +1671,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
let events: Vec<ObjectStreamEvent> = obj_stream
|
||||
.filter_map(|r| futures::future::ready(r.ok()))
|
||||
.filter_map(|r| future::ready(r.ok()))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
|
|
@ -1710,7 +1709,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
let events: Vec<ObjectStreamEvent> = obj_stream
|
||||
.filter_map(|r| futures::future::ready(r.ok()))
|
||||
.filter_map(|r| future::ready(r.ok()))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
|
|
@ -1967,7 +1966,7 @@ mod tests {
|
|||
|
||||
let texts: Vec<String> = result
|
||||
.text_stream()
|
||||
.filter_map(|r| futures::future::ready(r.ok()))
|
||||
.filter_map(|r| future::ready(r.ok()))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
|
||||
use futures::stream;
|
||||
|
||||
use crate::error::SdkError;
|
||||
use crate::provider::{ProviderAdapter, StreamEventStream};
|
||||
use crate::error::{error_from_status_code, SdkError};
|
||||
use crate::provider::{validate_tool_choice, ProviderAdapter, StreamEventStream};
|
||||
use crate::providers::common::{
|
||||
extract_system_prompt, parse_error_body, parse_rate_limit_headers, parse_retry_after,
|
||||
send_and_read_response,
|
||||
self as common, extract_system_prompt, parse_error_body, parse_rate_limit_headers,
|
||||
parse_retry_after, send_and_read_response,
|
||||
};
|
||||
use crate::types::{
|
||||
ContentPart, FinishReason, Message, Request, Response, ResponseFormatType, Role, StreamEvent,
|
||||
ThinkingData, ToolCall, ToolChoice, ToolDefinition, Usage,
|
||||
AdapterTimeout, ContentPart, FinishReason, Message, RateLimitInfo, Request, Response,
|
||||
ResponseFormatType, Role, StreamEvent, ThinkingData, ToolCall, ToolChoice, ToolDefinition,
|
||||
Usage,
|
||||
};
|
||||
|
||||
/// Provider adapter for the Anthropic Messages API.
|
||||
|
|
@ -47,7 +49,7 @@ impl Adapter {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_timeout(self, timeout: crate::types::AdapterTimeout) -> Self {
|
||||
pub fn with_timeout(self, timeout: AdapterTimeout) -> Self {
|
||||
Self {
|
||||
http: self.http.with_timeout(timeout),
|
||||
..self
|
||||
|
|
@ -266,8 +268,8 @@ fn content_part_to_api(part: &ContentPart) -> Option<serde_json::Value> {
|
|||
}
|
||||
ContentPart::Image(img) => {
|
||||
if let Some(url) = &img.url {
|
||||
if crate::providers::common::is_file_path(url) {
|
||||
return match crate::providers::common::load_file_as_base64(url) {
|
||||
if common::is_file_path(url) {
|
||||
return match common::load_file_as_base64(url) {
|
||||
Ok((b64, mime)) => Some(serde_json::json!({
|
||||
"type": "image",
|
||||
"source": {"type": "base64", "media_type": mime, "data": b64}
|
||||
|
|
@ -286,8 +288,8 @@ fn content_part_to_api(part: &ContentPart) -> Option<serde_json::Value> {
|
|||
}
|
||||
ContentPart::Document(doc) => {
|
||||
if let Some(url) = &doc.url {
|
||||
if crate::providers::common::is_file_path(url) {
|
||||
return match crate::providers::common::load_file_as_base64(url) {
|
||||
if common::is_file_path(url) {
|
||||
return match common::load_file_as_base64(url) {
|
||||
Ok((b64, mime)) => Some(serde_json::json!({
|
||||
"type": "document",
|
||||
"source": {"type": "base64", "media_type": mime, "data": b64}
|
||||
|
|
@ -669,11 +671,11 @@ struct StreamAccumulator {
|
|||
/// Accumulated raw JSON arguments for the current `tool_use` block.
|
||||
current_tool_args: String,
|
||||
/// Rate limit info parsed from the initial HTTP response headers.
|
||||
rate_limit: Option<crate::types::RateLimitInfo>,
|
||||
rate_limit: Option<RateLimitInfo>,
|
||||
}
|
||||
|
||||
impl StreamAccumulator {
|
||||
fn new(rate_limit: Option<crate::types::RateLimitInfo>) -> Self {
|
||||
fn new(rate_limit: Option<RateLimitInfo>) -> Self {
|
||||
Self {
|
||||
id: String::new(),
|
||||
model: String::new(),
|
||||
|
|
@ -974,7 +976,7 @@ struct SseReaderState {
|
|||
impl SseReaderState {
|
||||
fn new(
|
||||
http_resp: reqwest::Response,
|
||||
rate_limit: Option<crate::types::RateLimitInfo>,
|
||||
rate_limit: Option<RateLimitInfo>,
|
||||
json_schema_mode: bool,
|
||||
stream_read_timeout: Option<std::time::Duration>,
|
||||
) -> Self {
|
||||
|
|
@ -1214,7 +1216,7 @@ impl ProviderAdapter for Adapter {
|
|||
|
||||
async fn complete(&self, request: &Request) -> Result<Response, SdkError> {
|
||||
if let Some(tc) = &request.tool_choice {
|
||||
crate::provider::validate_tool_choice(self, tc)?;
|
||||
validate_tool_choice(self, tc)?;
|
||||
}
|
||||
|
||||
// Non-Anthropic providers (e.g. Kimi) require stream=true even for
|
||||
|
|
@ -1290,7 +1292,7 @@ impl ProviderAdapter for Adapter {
|
|||
|
||||
async fn stream(&self, request: &Request) -> Result<StreamEventStream, SdkError> {
|
||||
if let Some(tc) = &request.tool_choice {
|
||||
crate::provider::validate_tool_choice(self, tc)?;
|
||||
validate_tool_choice(self, tc)?;
|
||||
}
|
||||
let (_api_request, req_builder) = build_api_request(self, request, true);
|
||||
|
||||
|
|
@ -1307,7 +1309,7 @@ impl ProviderAdapter for Adapter {
|
|||
.await
|
||||
.map_err(|e| SdkError::network(e.to_string(), e))?;
|
||||
let (msg, code, raw) = parse_error_body(&body, "type");
|
||||
return Err(crate::error::error_from_status_code(
|
||||
return Err(error_from_status_code(
|
||||
status.as_u16(),
|
||||
msg,
|
||||
self.provider_name.clone(),
|
||||
|
|
@ -1321,7 +1323,7 @@ impl ProviderAdapter for Adapter {
|
|||
let json_schema_mode = uses_json_schema_format(request);
|
||||
let stream_read_timeout = self.http.stream_read_timeout;
|
||||
|
||||
let stream = futures::stream::unfold(
|
||||
let stream = stream::unfold(
|
||||
SseReaderState::new(http_resp, rate_limit, json_schema_mode, stream_read_timeout),
|
||||
|mut state| async move {
|
||||
loop {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
|
|||
|
||||
use crate::error::{error_from_status_code, SdkError};
|
||||
use crate::types::{Message, RateLimitInfo, Role};
|
||||
use reqwest::header::HeaderMap;
|
||||
use tokio::time;
|
||||
use tracing::warn;
|
||||
|
||||
/// Parse an error response body, extracting the message and error code.
|
||||
|
|
@ -104,7 +106,7 @@ pub fn load_file_as_base64(path: &str) -> Result<(String, String), std::io::Erro
|
|||
|
||||
/// Extract the `Retry-After` header value from an HTTP response as seconds.
|
||||
#[must_use]
|
||||
pub fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<f64> {
|
||||
pub fn parse_retry_after(headers: &HeaderMap) -> Option<f64> {
|
||||
headers
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
|
|
@ -115,15 +117,15 @@ pub fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<f64> {
|
|||
///
|
||||
/// Returns `None` if no rate limit headers are present.
|
||||
#[must_use]
|
||||
pub fn parse_rate_limit_headers(headers: &reqwest::header::HeaderMap) -> Option<RateLimitInfo> {
|
||||
fn header_i64(headers: &reqwest::header::HeaderMap, name: &str) -> Option<i64> {
|
||||
pub fn parse_rate_limit_headers(headers: &HeaderMap) -> Option<RateLimitInfo> {
|
||||
fn header_i64(headers: &HeaderMap, name: &str) -> Option<i64> {
|
||||
headers
|
||||
.get(name)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse::<i64>().ok())
|
||||
}
|
||||
|
||||
fn header_str(headers: &reqwest::header::HeaderMap, name: &str) -> Option<String> {
|
||||
fn header_str(headers: &HeaderMap, name: &str) -> Option<String> {
|
||||
headers
|
||||
.get(name)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
|
|
@ -166,7 +168,7 @@ pub async fn send_and_read_response(
|
|||
request: reqwest::RequestBuilder,
|
||||
provider: &str,
|
||||
error_code_field: &str,
|
||||
) -> Result<(String, reqwest::header::HeaderMap), SdkError> {
|
||||
) -> Result<(String, HeaderMap), SdkError> {
|
||||
let http_resp = request.send().await.map_err(|e| {
|
||||
if e.is_timeout() {
|
||||
warn!(provider = %provider, error = %e, "Provider request timed out");
|
||||
|
|
@ -239,7 +241,7 @@ impl LineReader {
|
|||
}
|
||||
|
||||
let chunk_result = match self.stream_read_timeout {
|
||||
Some(timeout) => tokio::time::timeout(timeout, self.response.chunk()).await,
|
||||
Some(timeout) => time::timeout(timeout, self.response.chunk()).await,
|
||||
None => Ok(self.response.chunk().await),
|
||||
};
|
||||
match chunk_result {
|
||||
|
|
@ -317,7 +319,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parse_rate_limit_headers_all_present() {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-ratelimit-remaining-requests", "99".parse().unwrap());
|
||||
headers.insert("x-ratelimit-limit-requests", "100".parse().unwrap());
|
||||
headers.insert("x-ratelimit-remaining-tokens", "9000".parse().unwrap());
|
||||
|
|
@ -337,13 +339,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parse_rate_limit_headers_none_present() {
|
||||
let headers = reqwest::header::HeaderMap::new();
|
||||
let headers = HeaderMap::new();
|
||||
assert!(parse_rate_limit_headers(&headers).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rate_limit_headers_partial() {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-ratelimit-remaining-requests", "50".parse().unwrap());
|
||||
|
||||
let info = parse_rate_limit_headers(&headers).unwrap();
|
||||
|
|
@ -356,7 +358,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parse_rate_limit_headers_reset_tokens_fallback() {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-ratelimit-limit-tokens", "5000".parse().unwrap());
|
||||
headers.insert(
|
||||
"x-ratelimit-reset-tokens",
|
||||
|
|
@ -370,7 +372,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parse_rate_limit_headers_invalid_values_ignored() {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"x-ratelimit-remaining-requests",
|
||||
"not-a-number".parse().unwrap(),
|
||||
|
|
@ -499,27 +501,27 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parse_retry_after_valid() {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("retry-after", "2.5".parse().unwrap());
|
||||
assert_eq!(parse_retry_after(&headers), Some(2.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_retry_after_missing() {
|
||||
let headers = reqwest::header::HeaderMap::new();
|
||||
let headers = HeaderMap::new();
|
||||
assert_eq!(parse_retry_after(&headers), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_retry_after_invalid() {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("retry-after", "not-a-number".parse().unwrap());
|
||||
assert_eq!(parse_retry_after(&headers), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_retry_after_integer() {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("retry-after", "5".parse().unwrap());
|
||||
assert_eq!(parse_retry_after(&headers), Some(5.0));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use crate::error::{error_from_status_code, SdkError};
|
|||
use crate::provider::{ProviderAdapter, StreamEventStream};
|
||||
use crate::providers::common::LineReader;
|
||||
use crate::types::{FinishReason, Message, Request, Response, StreamEvent, Usage};
|
||||
use futures::stream;
|
||||
use tracing::{debug, error};
|
||||
|
||||
/// Provider adapter that routes LLM requests through an fabro server's
|
||||
|
|
@ -161,35 +162,34 @@ impl ProviderAdapter for Adapter {
|
|||
let body = build_body(request, true)?;
|
||||
let http_resp = send_request(&self.client, &url, &body, &self.provider_name).await?;
|
||||
|
||||
let stream =
|
||||
futures::stream::unfold(LineReader::new(http_resp, None), |mut reader| async move {
|
||||
loop {
|
||||
match reader.read_next_chunk("\n\n").await {
|
||||
Ok(Some(block)) => {
|
||||
if let Some((event_type, data)) = parse_sse_block(&block) {
|
||||
if event_type == "stream_event" {
|
||||
match serde_json::from_str::<StreamEvent>(&data) {
|
||||
Ok(event) => return Some((Ok(event), reader)),
|
||||
Err(e) => {
|
||||
return Some((
|
||||
Err(SdkError::stream_error(
|
||||
format!("failed to parse stream event: {e}"),
|
||||
e,
|
||||
)),
|
||||
reader,
|
||||
));
|
||||
}
|
||||
let stream = stream::unfold(LineReader::new(http_resp, None), |mut reader| async move {
|
||||
loop {
|
||||
match reader.read_next_chunk("\n\n").await {
|
||||
Ok(Some(block)) => {
|
||||
if let Some((event_type, data)) = parse_sse_block(&block) {
|
||||
if event_type == "stream_event" {
|
||||
match serde_json::from_str::<StreamEvent>(&data) {
|
||||
Ok(event) => return Some((Ok(event), reader)),
|
||||
Err(e) => {
|
||||
return Some((
|
||||
Err(SdkError::stream_error(
|
||||
format!("failed to parse stream event: {e}"),
|
||||
e,
|
||||
)),
|
||||
reader,
|
||||
));
|
||||
}
|
||||
}
|
||||
// Skip non-stream_event SSE events
|
||||
}
|
||||
// Empty or unparsable block — keep reading.
|
||||
// Skip non-stream_event SSE events
|
||||
}
|
||||
Ok(None) => return None,
|
||||
Err(e) => return Some((Err(e), reader)),
|
||||
// Empty or unparsable block — keep reading.
|
||||
}
|
||||
Ok(None) => return None,
|
||||
Err(e) => return Some((Err(e), reader)),
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Box::pin(stream))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@ use crate::error::{
|
|||
error_from_grpc_status, error_from_status_code, ProviderErrorDetail, ProviderErrorKind,
|
||||
SdkError,
|
||||
};
|
||||
use crate::provider::{ProviderAdapter, StreamEventStream};
|
||||
use crate::provider::{validate_tool_choice, ProviderAdapter, StreamEventStream};
|
||||
use crate::providers::common::{
|
||||
extract_system_prompt, parse_error_body, parse_rate_limit_headers, parse_retry_after,
|
||||
self as common, extract_system_prompt, parse_error_body, parse_rate_limit_headers,
|
||||
parse_retry_after,
|
||||
};
|
||||
use crate::types::{
|
||||
ContentPart, FinishReason, Message, Request, Response, ResponseFormat, ResponseFormatType,
|
||||
Role, StreamEvent, ThinkingData, ToolCall, ToolChoice, ToolDefinition, Usage,
|
||||
AdapterTimeout, ContentPart, FinishReason, Message, RateLimitInfo, Request, Response,
|
||||
ResponseFormat, ResponseFormatType, Role, StreamEvent, ThinkingData, ToolCall, ToolChoice,
|
||||
ToolDefinition, Usage,
|
||||
};
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
const DEFAULT_BASE_URL: &str = "https://generativelanguage.googleapis.com/v1beta";
|
||||
|
||||
|
|
@ -43,7 +46,7 @@ impl Adapter {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_timeout(self, timeout: crate::types::AdapterTimeout) -> Self {
|
||||
pub fn with_timeout(self, timeout: AdapterTimeout) -> Self {
|
||||
Self {
|
||||
http: self.http.with_timeout(timeout),
|
||||
}
|
||||
|
|
@ -251,8 +254,8 @@ fn translate_messages(messages: &[&Message]) -> Vec<Content> {
|
|||
})
|
||||
},
|
||||
|url| {
|
||||
if crate::providers::common::is_file_path(url) {
|
||||
match crate::providers::common::load_file_as_base64(url) {
|
||||
if common::is_file_path(url) {
|
||||
match common::load_file_as_base64(url) {
|
||||
Ok((b64, mime)) => Some(serde_json::json!({"inlineData": {"mimeType": mime, "data": b64}})),
|
||||
Err(_) => None,
|
||||
}
|
||||
|
|
@ -273,8 +276,8 @@ fn translate_messages(messages: &[&Message]) -> Vec<Content> {
|
|||
})
|
||||
},
|
||||
|url| {
|
||||
if crate::providers::common::is_file_path(url) {
|
||||
match crate::providers::common::load_file_as_base64(url) {
|
||||
if common::is_file_path(url) {
|
||||
match common::load_file_as_base64(url) {
|
||||
Ok((b64, mime)) => Some(serde_json::json!({"inlineData": {"mimeType": mime, "data": b64}})),
|
||||
Err(_) => None,
|
||||
}
|
||||
|
|
@ -295,8 +298,8 @@ fn translate_messages(messages: &[&Message]) -> Vec<Content> {
|
|||
})
|
||||
},
|
||||
|url| {
|
||||
if crate::providers::common::is_file_path(url) {
|
||||
match crate::providers::common::load_file_as_base64(url) {
|
||||
if common::is_file_path(url) {
|
||||
match common::load_file_as_base64(url) {
|
||||
Ok((b64, mime)) => Some(serde_json::json!({"inlineData": {"mimeType": mime, "data": b64}})),
|
||||
Err(_) => None,
|
||||
}
|
||||
|
|
@ -505,7 +508,7 @@ fn parse_usage(metadata: Option<&UsageMetadata>) -> Usage {
|
|||
/// Like `send_and_read_response` but uses gRPC status code mapping when available.
|
||||
async fn send_gemini_response(
|
||||
request: reqwest::RequestBuilder,
|
||||
) -> Result<(String, reqwest::header::HeaderMap), SdkError> {
|
||||
) -> Result<(String, HeaderMap), SdkError> {
|
||||
let http_resp = request.send().await.map_err(|e| {
|
||||
if e.is_timeout() {
|
||||
SdkError::request_timeout(format!("gemini: {e}"), e)
|
||||
|
|
@ -589,7 +592,7 @@ async fn send_streaming_request(
|
|||
fn process_sse_stream(
|
||||
http_resp: reqwest::Response,
|
||||
model: String,
|
||||
rate_limit: Option<crate::types::RateLimitInfo>,
|
||||
rate_limit: Option<RateLimitInfo>,
|
||||
stream_read_timeout: Option<std::time::Duration>,
|
||||
) -> StreamEventStream {
|
||||
Box::pin(stream::unfold(
|
||||
|
|
@ -698,14 +701,14 @@ struct SseStreamState {
|
|||
/// Whether we have emitted the `Finish` event.
|
||||
finished: bool,
|
||||
/// Rate limit info parsed from HTTP response headers.
|
||||
rate_limit: Option<crate::types::RateLimitInfo>,
|
||||
rate_limit: Option<RateLimitInfo>,
|
||||
}
|
||||
|
||||
impl SseStreamState {
|
||||
fn new(
|
||||
http_resp: reqwest::Response,
|
||||
model: String,
|
||||
rate_limit: Option<crate::types::RateLimitInfo>,
|
||||
rate_limit: Option<RateLimitInfo>,
|
||||
stream_read_timeout: Option<std::time::Duration>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -884,7 +887,7 @@ impl ProviderAdapter for Adapter {
|
|||
|
||||
async fn complete(&self, request: &Request) -> Result<Response, SdkError> {
|
||||
if let Some(tc) = &request.tool_choice {
|
||||
crate::provider::validate_tool_choice(self, tc)?;
|
||||
validate_tool_choice(self, tc)?;
|
||||
}
|
||||
let api_body = build_api_request(request);
|
||||
|
||||
|
|
@ -950,7 +953,7 @@ impl ProviderAdapter for Adapter {
|
|||
|
||||
async fn stream(&self, request: &Request) -> Result<StreamEventStream, SdkError> {
|
||||
if let Some(tc) = &request.tool_choice {
|
||||
crate::provider::validate_tool_choice(self, tc)?;
|
||||
validate_tool_choice(self, tc)?;
|
||||
}
|
||||
let api_body = build_api_request(request);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
|
||||
use futures::StreamExt;
|
||||
use futures::{stream, StreamExt};
|
||||
|
||||
use crate::error::SdkError;
|
||||
use crate::provider::{ProviderAdapter, StreamEventStream};
|
||||
use crate::error::{error_from_status_code, SdkError};
|
||||
use crate::provider::{validate_tool_choice, ProviderAdapter, StreamEventStream};
|
||||
use crate::providers::common::{
|
||||
parse_error_body, parse_rate_limit_headers, parse_retry_after, send_and_read_response,
|
||||
self as common, parse_error_body, parse_rate_limit_headers, parse_retry_after,
|
||||
send_and_read_response,
|
||||
};
|
||||
use crate::types::{
|
||||
ContentPart, FinishReason, Message, Request, Response, ResponseFormat, ResponseFormatType,
|
||||
Role, StreamEvent, ToolCall, ToolChoice, ToolDefinition, Usage,
|
||||
AdapterTimeout, ContentPart, FinishReason, Message, RateLimitInfo, Request, Response,
|
||||
ResponseFormat, ResponseFormatType, Role, StreamEvent, ToolCall, ToolChoice, ToolDefinition,
|
||||
Usage,
|
||||
};
|
||||
|
||||
const DEFAULT_BASE_URL: &str = "https://api.openai.com/v1";
|
||||
|
|
@ -69,7 +71,7 @@ impl Adapter {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_timeout(self, timeout: crate::types::AdapterTimeout) -> Self {
|
||||
pub fn with_timeout(self, timeout: AdapterTimeout) -> Self {
|
||||
Self {
|
||||
http: self.http.with_timeout(timeout),
|
||||
..self
|
||||
|
|
@ -216,8 +218,8 @@ fn translate_input(messages: &[Message]) -> (Option<String>, Vec<serde_json::Val
|
|||
})
|
||||
},
|
||||
|url| {
|
||||
if crate::providers::common::is_file_path(url) {
|
||||
match crate::providers::common::load_file_as_base64(url) {
|
||||
if common::is_file_path(url) {
|
||||
match common::load_file_as_base64(url) {
|
||||
Ok((b64, mime)) => Some(serde_json::json!({"type": "input_image", "image_url": format!("data:{mime};base64,{b64}")})),
|
||||
Err(_) => None,
|
||||
}
|
||||
|
|
@ -550,7 +552,7 @@ struct SseStreamState {
|
|||
emitted_text_start: bool,
|
||||
emitted_reasoning_start: bool,
|
||||
raw_response: Option<serde_json::Value>,
|
||||
rate_limit: Option<crate::types::RateLimitInfo>,
|
||||
rate_limit: Option<RateLimitInfo>,
|
||||
}
|
||||
|
||||
/// Parse a single SSE message block into an (`event_type`, `data`) pair.
|
||||
|
|
@ -944,7 +946,7 @@ impl ProviderAdapter for Adapter {
|
|||
}
|
||||
|
||||
if let Some(tc) = &request.tool_choice {
|
||||
crate::provider::validate_tool_choice(self, tc)?;
|
||||
validate_tool_choice(self, tc)?;
|
||||
}
|
||||
let request_body = build_request_body(request, false, false);
|
||||
let url = format!("{}/responses", self.http.base_url);
|
||||
|
|
@ -999,7 +1001,7 @@ impl ProviderAdapter for Adapter {
|
|||
|
||||
async fn stream(&self, request: &Request) -> Result<StreamEventStream, SdkError> {
|
||||
if let Some(tc) = &request.tool_choice {
|
||||
crate::provider::validate_tool_choice(self, tc)?;
|
||||
validate_tool_choice(self, tc)?;
|
||||
}
|
||||
let request_body = build_request_body(request, true, self.codex_mode);
|
||||
let url = format!("{}/responses", self.http.base_url);
|
||||
|
|
@ -1019,7 +1021,7 @@ impl ProviderAdapter for Adapter {
|
|||
.await
|
||||
.map_err(|e| SdkError::network(e.to_string(), e))?;
|
||||
let (msg, code, raw) = parse_error_body(&body, "type");
|
||||
return Err(crate::error::error_from_status_code(
|
||||
return Err(error_from_status_code(
|
||||
status.as_u16(),
|
||||
msg,
|
||||
"openai".to_string(),
|
||||
|
|
@ -1051,14 +1053,14 @@ impl ProviderAdapter for Adapter {
|
|||
rate_limit,
|
||||
};
|
||||
|
||||
let stream = futures::stream::unfold(state, |mut state| async move {
|
||||
let stream = stream::unfold(state, |mut state| async move {
|
||||
let events = process_next_sse_events(&mut state).await;
|
||||
let items: Vec<Result<StreamEvent, SdkError>> = match events {
|
||||
Ok(events) if events.is_empty() => return None,
|
||||
Ok(events) => events.into_iter().map(Ok).collect(),
|
||||
Err(e) => vec![Err(e)],
|
||||
};
|
||||
Some((futures::stream::iter(items), state))
|
||||
Some((stream::iter(items), state))
|
||||
})
|
||||
.flatten();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
use futures::StreamExt;
|
||||
use futures::{stream, StreamExt};
|
||||
|
||||
use crate::error::{error_from_status_code, ProviderErrorDetail, ProviderErrorKind, SdkError};
|
||||
use crate::provider::{ProviderAdapter, StreamEventStream};
|
||||
use crate::provider::{validate_tool_choice, ProviderAdapter, StreamEventStream};
|
||||
use crate::providers::common::{
|
||||
parse_error_body, parse_rate_limit_headers, parse_retry_after, send_and_read_response,
|
||||
};
|
||||
use crate::types::{
|
||||
ContentPart, FinishReason, Message, Request, Response, ResponseFormat, ResponseFormatType,
|
||||
Role, StreamEvent, ThinkingData, ToolCall, ToolChoice, ToolDefinition, Usage,
|
||||
AdapterTimeout, ContentPart, FinishReason, Message, RateLimitInfo, Request, Response,
|
||||
ResponseFormat, ResponseFormatType, Role, StreamEvent, ThinkingData, ToolCall, ToolChoice,
|
||||
ToolDefinition, Usage,
|
||||
};
|
||||
|
||||
/// `OpenAI`-compatible Chat Completions adapter (Section 7.10).
|
||||
|
|
@ -46,7 +47,7 @@ impl Adapter {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_timeout(self, timeout: crate::types::AdapterTimeout) -> Self {
|
||||
pub fn with_timeout(self, timeout: AdapterTimeout) -> Self {
|
||||
Self {
|
||||
http: self.http.with_timeout(timeout),
|
||||
..self
|
||||
|
|
@ -451,7 +452,7 @@ impl ProviderAdapter for Adapter {
|
|||
|
||||
async fn complete(&self, request: &Request) -> Result<Response, SdkError> {
|
||||
if let Some(tc) = &request.tool_choice {
|
||||
crate::provider::validate_tool_choice(self, tc)?;
|
||||
validate_tool_choice(self, tc)?;
|
||||
}
|
||||
let api_body = build_api_request(request, None, &self.provider_name);
|
||||
let url = format!("{}/chat/completions", self.http.base_url);
|
||||
|
|
@ -530,7 +531,7 @@ impl ProviderAdapter for Adapter {
|
|||
|
||||
async fn stream(&self, request: &Request) -> Result<StreamEventStream, SdkError> {
|
||||
if let Some(tc) = &request.tool_choice {
|
||||
crate::provider::validate_tool_choice(self, tc)?;
|
||||
validate_tool_choice(self, tc)?;
|
||||
}
|
||||
let api_body = build_api_request(request, Some(true), &self.provider_name);
|
||||
let url = format!("{}/chat/completions", self.http.base_url);
|
||||
|
|
@ -565,7 +566,7 @@ impl ProviderAdapter for Adapter {
|
|||
let rate_limit = parse_rate_limit_headers(http_resp.headers());
|
||||
let stream_read_timeout = self.http.stream_read_timeout;
|
||||
|
||||
let stream = futures::stream::unfold(
|
||||
let stream = stream::unfold(
|
||||
StreamState::new(
|
||||
http_resp,
|
||||
provider_name,
|
||||
|
|
@ -629,7 +630,7 @@ impl ProviderAdapter for Adapter {
|
|||
);
|
||||
|
||||
// Flatten batched events into individual stream events.
|
||||
let flat_stream = futures::stream::unfold(
|
||||
let flat_stream = stream::unfold(
|
||||
FlattenState {
|
||||
inner: Box::pin(stream),
|
||||
pending: Vec::new(),
|
||||
|
|
@ -680,7 +681,7 @@ struct StreamState {
|
|||
done: bool,
|
||||
/// True after `finish_events()` has been called (guards against duplicates).
|
||||
finished: bool,
|
||||
rate_limit: Option<crate::types::RateLimitInfo>,
|
||||
rate_limit: Option<RateLimitInfo>,
|
||||
}
|
||||
|
||||
impl StreamState {
|
||||
|
|
@ -688,7 +689,7 @@ impl StreamState {
|
|||
response: reqwest::Response,
|
||||
provider_name: String,
|
||||
model: String,
|
||||
rate_limit: Option<crate::types::RateLimitInfo>,
|
||||
rate_limit: Option<RateLimitInfo>,
|
||||
stream_read_timeout: Option<std::time::Duration>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue