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
6 changes: 3 additions & 3 deletions crates/sprout-cli/src/commands/pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ pub fn cmd_inspect(path: &str) -> Result<(), CliError> {
println!(" Display: {}", persona.display_name);
println!(" Description: {}", persona.description);

if let Some(ref provider) = persona.provider {
if let Some(ref llm_provider) = persona.llm_provider {
if let Some(ref model) = persona.model {
println!(" Model: {provider}:{model}");
println!(" Model: {llm_provider}:{model}");
} else {
println!(" Provider: {provider}");
println!(" Provider: {llm_provider}");
}
} else if let Some(ref model) = persona.model {
println!(" Model: {model}");
Expand Down
4 changes: 4 additions & 0 deletions crates/sprout-persona/src/pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ pub struct LoadedPersona {
pub description: String,
pub avatar: Option<String>,
pub model: Option<String>,
/// Preferred ACP runtime ID from the persona config (e.g., 'goose', 'claude').
pub runtime: Option<String>,
pub temperature: Option<f64>,
pub max_context_tokens: Option<u64>,
pub subscribe: Vec<String>,
Expand Down Expand Up @@ -440,6 +442,7 @@ fn parse_persona_file(
description: pc.description,
avatar: pc.avatar,
model: resolved.model,
runtime: pc.runtime.clone(),
temperature: resolved.temperature,
max_context_tokens: resolved.max_context_tokens,
subscribe: resolved.subscribe.unwrap_or_default(),
Expand Down Expand Up @@ -634,6 +637,7 @@ You are Berry, a fast and direct worker.
description: String::new(),
avatar: None,
model: None,
runtime: None,
temperature: None,
max_context_tokens: None,
subscribe: vec![],
Expand Down
7 changes: 7 additions & 0 deletions crates/sprout-persona/src/persona.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ pub struct PersonaConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,

/// Preferred ACP runtime ID (e.g., 'goose', 'claude'). Maps to PersonaRecord.runtime during
/// pack import.
#[serde(skip_serializing_if = "Option::is_none")]
pub runtime: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,

Expand Down Expand Up @@ -200,6 +205,7 @@ struct Frontmatter {
#[serde(alias = "respond_to")]
triggers: Option<RespondTo>,
model: Option<String>,
runtime: Option<String>,
temperature: Option<f64>,
max_context_tokens: Option<u64>,
thread_replies: Option<bool>,
Expand Down Expand Up @@ -260,6 +266,7 @@ pub fn parse_persona_md(content: &str) -> Result<PersonaConfig, PersonaError> {
subscribe: fm.subscribe,
triggers: fm.triggers,
model: fm.model,
runtime: fm.runtime,
temperature: fm.temperature,
max_context_tokens: fm.max_context_tokens,
thread_replies: fm.thread_replies,
Expand Down
26 changes: 16 additions & 10 deletions crates/sprout-persona/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ pub struct ResolvedPersona {

// → Config.model (plain model ID, post-split)
pub model: Option<String>,
// → PersonaRecord.provider (for desktop display / GOOSE_PROVIDER env)
pub provider: Option<String>,
/// LLM inference provider extracted from the model string colon prefix (e.g., 'databricks'
/// from 'databricks:model-id'). Flows into harness-specific env vars (GOOSE_PROVIDER) only.
pub llm_provider: Option<String>,
/// Preferred ACP runtime ID from the persona config (e.g., 'goose', 'claude'). Maps to
/// PersonaRecord.runtime during pack import.
pub runtime: Option<String>,
pub temperature: Option<f64>,
pub max_context_tokens: Option<u64>,

Expand Down Expand Up @@ -200,7 +204,7 @@ fn resolve_one_persona(
let system_prompt = compose_prompt(&lp.prompt, pack_instructions);

// Split "provider:model-id" into separate fields (V3 contract).
let (provider, model) = match lp.model.as_deref() {
let (llm_provider, model) = match lp.model.as_deref() {
Some(s) if !s.trim().is_empty() => {
let (prov, id) = split_model(s);
(
Expand Down Expand Up @@ -231,7 +235,8 @@ fn resolve_one_persona(
version,
system_prompt,
model,
provider,
llm_provider,
runtime: lp.runtime.clone(),
temperature: lp.temperature,
max_context_tokens: lp.max_context_tokens,
subscribe: lp.subscribe.clone(),
Expand Down Expand Up @@ -642,7 +647,7 @@ mod tests {
assert_eq!(p.name, "bot");
assert_eq!(p.system_prompt, "You are Bot.\n");
assert!(p.model.is_none());
assert!(p.provider.is_none());
assert!(p.llm_provider.is_none());
assert!(p.triggers.mentions); // built-in default
assert!(p.mcp_servers.is_empty());
assert!(p.goose_env_vars.is_empty());
Expand Down Expand Up @@ -685,7 +690,7 @@ mod tests {

// Model split into separate fields (V3 contract)
assert_eq!(p.model.as_deref(), Some("claude-sonnet-4-20250514"));
assert_eq!(p.provider.as_deref(), Some("anthropic"));
assert_eq!(p.llm_provider.as_deref(), Some("anthropic"));

// Env vars projected
let env_map: HashMap<&str, &str> = p
Expand Down Expand Up @@ -740,10 +745,10 @@ mod tests {

// pip overrides model
assert_eq!(pip.model.as_deref(), Some("claude-4-opus-20250514"));
assert_eq!(pip.provider.as_deref(), Some("anthropic"));
assert_eq!(pip.llm_provider.as_deref(), Some("anthropic"));
// lep inherits model from defaults
assert_eq!(lep.model.as_deref(), Some("claude-sonnet-4-20250514"));
assert_eq!(lep.provider.as_deref(), Some("anthropic"));
assert_eq!(lep.llm_provider.as_deref(), Some("anthropic"));

// pip inherits temperature from defaults
assert_eq!(pip.temperature, Some(0.7));
Expand Down Expand Up @@ -835,7 +840,7 @@ mod tests {
let pack = resolve_pack(dir).unwrap();
let p = &pack.personas[0];
assert_eq!(p.model.as_deref(), Some("gpt-4o"));
assert_eq!(p.provider.as_deref(), Some("openai"));
assert_eq!(p.llm_provider.as_deref(), Some("openai"));
}

#[test]
Expand All @@ -859,7 +864,7 @@ mod tests {
let pack = resolve_pack(dir).unwrap();
let p = &pack.personas[0];
assert_eq!(p.model.as_deref(), Some("gpt-4o"));
assert!(p.provider.is_none());
assert!(p.llm_provider.is_none());
}

// ── Test helpers ──────────────────────────────────────────────────────
Expand All @@ -876,6 +881,7 @@ mod tests {
description: "A test persona.".into(),
avatar: None,
model: model.map(str::to_owned),
runtime: None,
temperature,
max_context_tokens,
subscribe: vec![],
Expand Down
10 changes: 5 additions & 5 deletions crates/sprout-persona/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,12 +426,12 @@ fn resolve_full_pipeline() {
assert_eq!(pip.description, "Orchestration agent");
assert_eq!(pip.version, "1.0.0"); // defaults to pack version

// Model split: "anthropic:claude-4-opus-20250514" → provider + model
assert_eq!(pip.provider.as_deref(), Some("anthropic"));
// Model split: "anthropic:claude-4-opus-20250514" → llm_provider + model
assert_eq!(pip.llm_provider.as_deref(), Some("anthropic"));
assert_eq!(pip.model.as_deref(), Some("claude-4-opus-20250514"));

// Lep inherits pack default model
assert_eq!(lep.provider.as_deref(), Some("anthropic"));
assert_eq!(lep.llm_provider.as_deref(), Some("anthropic"));
assert_eq!(lep.model.as_deref(), Some("claude-sonnet-4-20250514"));

// System prompt composed: persona body + pack instructions
Expand Down Expand Up @@ -544,12 +544,12 @@ fn resolve_multi_persona_pack() {
.unwrap();

// Alpha overrides model and temperature
assert_eq!(alpha.provider.as_deref(), Some("anthropic"));
assert_eq!(alpha.llm_provider.as_deref(), Some("anthropic"));
assert_eq!(alpha.model.as_deref(), Some("claude-sonnet-4-20250514"));
assert_eq!(alpha.temperature, Some(0.9));

// Beta inherits all defaults
assert_eq!(beta.provider.as_deref(), Some("openai"));
assert_eq!(beta.llm_provider.as_deref(), Some("openai"));
assert_eq!(beta.model.as_deref(), Some("gpt-4o"));
assert_eq!(beta.temperature, Some(0.5));
assert!(beta.thread_replies);
Expand Down
Loading
Loading