Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/agentic-core/benches/executor_throughput.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ fn make_request(input: &str, stream: bool, prev_id: Option<String>) -> RequestPa
conversation_id: None,
tools: None,
tool_choice: ToolChoice::Auto,
tool_choice_explicitly_set: false,
stream,
store: true,
include: None,
Expand Down
52 changes: 52 additions & 0 deletions crates/agentic-core/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
use std::collections::HashMap;
use std::hash::BuildHasher;

#[derive(Debug, Clone)]
pub struct Config {
pub llm_api_base: String,
pub openai_api_key: Option<String>,
pub llm_ready_timeout_s: f64,
pub llm_ready_interval_s: f64,
pub skip_llm_ready_check: bool,
/// Database URL for conversation and response storage.
/// `None` means stateful features are disabled; all requests are proxied.
pub db_url: Option<String>,
pub model_aliases: HashMap<String, String>,
}

#[must_use]
Expand All @@ -19,6 +24,39 @@ pub fn normalize_base_url(url: &str) -> String {
s
}

#[must_use]
pub fn resolve_model_alias<S: BuildHasher>(model: &str, aliases: &HashMap<String, String, S>) -> String {
aliases.get(model).cloned().unwrap_or_else(|| model.to_string())
}

/// Parse `alias=target` entries from CLI/env configuration.
///
/// # Errors
/// Returns an error when an entry is missing `=`, or either side is empty.
pub fn parse_model_aliases<I, S>(entries: I) -> Result<HashMap<String, String>, String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut aliases = HashMap::new();
for entry in entries {
let entry = entry.as_ref().trim();
if entry.is_empty() {
continue;
}
let Some((alias, target)) = entry.split_once('=') else {
return Err(format!("model alias '{entry}' must use alias=target"));
};
let alias = alias.trim();
let target = target.trim();
if alias.is_empty() || target.is_empty() {
return Err(format!("model alias '{entry}' must have non-empty alias and target"));
}
aliases.insert(alias.to_string(), target.to_string());
}
Ok(aliases)
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -34,4 +72,18 @@ mod tests {
assert_eq!(normalize_base_url("http://host:8000"), "http://host:8000");
assert_eq!(normalize_base_url("http://host:8000/"), "http://host:8000");
}

#[test]
fn model_aliases_parse_and_resolve() {
let aliases = parse_model_aliases(["codex-auto-review=Qwen/Qwen3"]).unwrap();
assert_eq!(resolve_model_alias("codex-auto-review", &aliases), "Qwen/Qwen3");
assert_eq!(resolve_model_alias("other", &aliases), "other");
}

#[test]
fn model_aliases_reject_invalid_entries() {
assert!(parse_model_aliases(["missing-separator"]).is_err());
assert!(parse_model_aliases(["=target"]).is_err());
assert!(parse_model_aliases(["alias="]).is_err());
}
}
1 change: 1 addition & 0 deletions crates/agentic-core/src/events/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fn extract_output_item_added(json: &Value) -> EventPayload {
item_type: SSEItemType::from(json_str(item, "type")),
output_index: json_u32(json, "output_index"),
name: json_str_opt(item, "name"),
namespace: json_str_opt(item, "namespace"),
call_id: json_str_opt(item, "call_id"),
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/agentic-core/src/events/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ pub enum EventPayload {
item_type: SSEItemType,
output_index: u32,
name: Option<String>,
namespace: Option<String>,
call_id: Option<String>,
},

Expand Down
11 changes: 11 additions & 0 deletions crates/agentic-core/src/executor/accumulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ mod tests {
item_type: "message".into(),
output_index: 0,
name: None,
namespace: None,
call_id: None,
},
sequence_number: Some(1),
Expand Down Expand Up @@ -625,6 +626,7 @@ mod tests {
item_type: "function_call".into(),
output_index: 0,
name: Some("get_weather".into()),
namespace: Some("mcp__weather".into()),
call_id: Some("call_abc".into()),
},
sequence_number: Some(1),
Expand Down Expand Up @@ -680,6 +682,7 @@ mod tests {
assert_eq!(fc.id, "fc_1");
assert_eq!(fc.call_id, "call_abc");
assert_eq!(fc.name, "get_weather");
assert_eq!(fc.namespace.as_deref(), Some("mcp__weather"));
assert_eq!(fc.arguments, r#"{"location":"Paris"}"#);
assert_eq!(fc.status, MessageStatus::Completed);
} else {
Expand All @@ -698,6 +701,7 @@ mod tests {
item_type: "function_call".into(),
output_index: 0,
name: Some("search".into()),
namespace: None,
call_id: Some("call_1".into()),
},
sequence_number: Some(1),
Expand Down Expand Up @@ -746,6 +750,7 @@ mod tests {
item_type: "function_call".into(),
output_index: 0,
name: Some("get_weather".into()),
namespace: None,
call_id: Some("call_1".into()),
},
sequence_number: Some(1),
Expand All @@ -769,6 +774,7 @@ mod tests {
item_type: "function_call".into(),
output_index: 1,
name: Some("get_time".into()),
namespace: None,
call_id: Some("call_2".into()),
},
sequence_number: Some(3),
Expand Down Expand Up @@ -811,6 +817,7 @@ mod tests {
item_type: "message".into(),
output_index: 0,
name: None,
namespace: None,
call_id: None,
},
sequence_number: Some(1),
Expand All @@ -833,6 +840,7 @@ mod tests {
item_type: "function_call".into(),
output_index: 1,
name: Some("lookup".into()),
namespace: None,
call_id: Some("call_x".into()),
},
sequence_number: Some(3),
Expand Down Expand Up @@ -875,6 +883,7 @@ mod tests {
item_type: "function_call".into(),
output_index: 0,
name: Some("old_name".into()),
namespace: None,
call_id: Some("old_call".into()),
},
sequence_number: Some(1),
Expand Down Expand Up @@ -912,6 +921,7 @@ mod tests {
item_type: "function_call".into(),
output_index: 0,
name: Some("tool".into()),
namespace: None,
call_id: Some("c1".into()),
},
sequence_number: Some(1),
Expand Down Expand Up @@ -968,6 +978,7 @@ mod tests {
item_type: "function_call".into(),
output_index: 0,
name: Some("partial".into()),
namespace: None,
call_id: Some("c1".into()),
},
sequence_number: Some(1),
Expand Down
29 changes: 29 additions & 0 deletions crates/agentic-core/src/executor/dispatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use crate::tool::ToolRegistry;
use crate::types::io::{InputItem, OutputItem};

#[derive(Debug, Clone)]
pub enum LoopDecision {
Continue(Vec<InputItem>),
RequiresClientAction(Vec<OutputItem>),
Done,
Incomplete(String),
}

#[must_use]
pub fn client_action_items(output: &[OutputItem], registry: &ToolRegistry) -> Vec<OutputItem> {
output
.iter()
.filter(|item| item.requires_client_action(registry))
.cloned()
.collect()
}

#[must_use]
pub fn decide_client_action(output: &[OutputItem], registry: &ToolRegistry) -> LoopDecision {
let items = client_action_items(output, registry);
if items.is_empty() {
LoopDecision::Done
} else {
LoopDecision::RequiresClientAction(items)
}
}
Loading