diff --git a/Cargo.lock b/Cargo.lock index 88111003..8652fc3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3866,7 +3866,7 @@ dependencies = [ [[package]] name = "openfang-api" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "axum", @@ -3903,7 +3903,7 @@ dependencies = [ [[package]] name = "openfang-channels" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "axum", @@ -3934,7 +3934,7 @@ dependencies = [ [[package]] name = "openfang-cli" -version = "0.3.19" +version = "0.3.20" dependencies = [ "clap", "clap_complete", @@ -3961,7 +3961,7 @@ dependencies = [ [[package]] name = "openfang-desktop" -version = "0.3.19" +version = "0.3.20" dependencies = [ "axum", "open", @@ -3987,7 +3987,7 @@ dependencies = [ [[package]] name = "openfang-extensions" -version = "0.3.19" +version = "0.3.20" dependencies = [ "aes-gcm", "argon2", @@ -4015,7 +4015,7 @@ dependencies = [ [[package]] name = "openfang-hands" -version = "0.3.19" +version = "0.3.20" dependencies = [ "chrono", "dashmap", @@ -4032,7 +4032,7 @@ dependencies = [ [[package]] name = "openfang-kernel" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "chrono", @@ -4068,7 +4068,7 @@ dependencies = [ [[package]] name = "openfang-memory" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "chrono", @@ -4087,7 +4087,7 @@ dependencies = [ [[package]] name = "openfang-migrate" -version = "0.3.19" +version = "0.3.20" dependencies = [ "chrono", "dirs 6.0.0", @@ -4106,7 +4106,7 @@ dependencies = [ [[package]] name = "openfang-runtime" -version = "0.3.19" +version = "0.3.20" dependencies = [ "anyhow", "async-trait", @@ -4138,7 +4138,7 @@ dependencies = [ [[package]] name = "openfang-skills" -version = "0.3.19" +version = "0.3.20" dependencies = [ "chrono", "hex", @@ -4161,7 +4161,7 @@ dependencies = [ [[package]] name = "openfang-types" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "chrono", @@ -4180,7 +4180,7 @@ dependencies = [ [[package]] name = "openfang-wire" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "chrono", @@ -8802,7 +8802,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xtask" -version = "0.3.19" +version = "0.3.20" [[package]] name = "yoke" diff --git a/Cargo.toml b/Cargo.toml index eb6cc39f..612a64f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.3.20" +version = "0.3.21" edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/RightNow-AI/openfang" diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 29198ace..57b28842 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -7575,10 +7575,16 @@ pub async fn update_agent_identity( }; match state.kernel.registry.update_identity(agent_id, identity) { - Ok(()) => ( - StatusCode::OK, - Json(serde_json::json!({"status": "ok", "agent_id": id})), - ), + Ok(()) => { + // Persist identity to SQLite + if let Some(entry) = state.kernel.registry.get(agent_id) { + let _ = state.kernel.memory.save_agent(&entry); + } + ( + StatusCode::OK, + Json(serde_json::json!({"status": "ok", "agent_id": id})), + ) + } Err(_) => ( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Agent not found"})), diff --git a/crates/openfang-memory/src/structured.rs b/crates/openfang-memory/src/structured.rs index 0288c835..d71b6bc9 100644 --- a/crates/openfang-memory/src/structured.rs +++ b/crates/openfang-memory/src/structured.rs @@ -130,11 +130,19 @@ impl StructuredStore { "ALTER TABLE agents ADD COLUMN session_id TEXT DEFAULT ''", [], ); + // Add identity column (migration compat) + let _ = conn.execute( + "ALTER TABLE agents ADD COLUMN identity TEXT DEFAULT '{}'", + [], + ); + + let identity_json = serde_json::to_string(&entry.identity) + .map_err(|e| OpenFangError::Serialization(e.to_string()))?; conn.execute( - "INSERT INTO agents (id, name, manifest, state, created_at, updated_at, session_id) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) - ON CONFLICT(id) DO UPDATE SET name = ?2, manifest = ?3, state = ?4, updated_at = ?6, session_id = ?7", + "INSERT INTO agents (id, name, manifest, state, created_at, updated_at, session_id, identity) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) + ON CONFLICT(id) DO UPDATE SET name = ?2, manifest = ?3, state = ?4, updated_at = ?6, session_id = ?7, identity = ?8", rusqlite::params![ entry.id.0.to_string(), entry.name, @@ -143,6 +151,7 @@ impl StructuredStore { entry.created_at.to_rfc3339(), now, entry.session_id.0.to_string(), + identity_json, ], ) .map_err(|e| OpenFangError::Memory(e.to_string()))?; @@ -157,10 +166,13 @@ impl StructuredStore { .map_err(|e| OpenFangError::Internal(e.to_string()))?; let mut stmt = conn - .prepare("SELECT id, name, manifest, state, created_at, updated_at, session_id FROM agents WHERE id = ?1") + .prepare("SELECT id, name, manifest, state, created_at, updated_at, session_id, identity FROM agents WHERE id = ?1") .or_else(|_| { - // Fallback without session_id column for old DBs - conn.prepare("SELECT id, name, manifest, state, created_at, updated_at FROM agents WHERE id = ?1") + conn.prepare("SELECT id, name, manifest, state, created_at, updated_at, session_id FROM agents WHERE id = ?1") + .or_else(|_| { + // Fallback without session_id column for old DBs + conn.prepare("SELECT id, name, manifest, state, created_at, updated_at FROM agents WHERE id = ?1") + }) }) .map_err(|e| OpenFangError::Memory(e.to_string()))?; @@ -175,11 +187,16 @@ impl StructuredStore { } else { None }; - Ok((name, manifest_blob, state_str, created_str, session_id_str)) + let identity_str: Option = if col_count >= 8 { + row.get(7).ok() + } else { + None + }; + Ok((name, manifest_blob, state_str, created_str, session_id_str, identity_str)) }); match result { - Ok((name, manifest_blob, state_str, created_str, session_id_str)) => { + Ok((name, manifest_blob, state_str, created_str, session_id_str, identity_str)) => { let manifest = rmp_serde::from_slice(&manifest_blob) .map_err(|e| OpenFangError::Serialization(e.to_string()))?; let state = serde_json::from_str(&state_str) @@ -191,6 +208,9 @@ impl StructuredStore { .and_then(|s| uuid::Uuid::parse_str(&s).ok()) .map(openfang_types::agent::SessionId) .unwrap_or_else(openfang_types::agent::SessionId::new); + let identity = identity_str + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); Ok(Some(AgentEntry { id: agent_id, name, @@ -203,7 +223,7 @@ impl StructuredStore { children: vec![], session_id, tags: vec![], - identity: Default::default(), + identity, onboarding_completed: false, onboarding_completed_at: None, })) @@ -239,11 +259,14 @@ impl StructuredStore { .lock() .map_err(|e| OpenFangError::Internal(e.to_string()))?; - // Try with session_id column first, fall back without + // Try with identity+session_id columns first, fall back gracefully let mut stmt = conn .prepare( - "SELECT id, name, manifest, state, created_at, updated_at, session_id FROM agents", + "SELECT id, name, manifest, state, created_at, updated_at, session_id, identity FROM agents", ) + .or_else(|_| { + conn.prepare("SELECT id, name, manifest, state, created_at, updated_at, session_id FROM agents") + }) .or_else(|_| { conn.prepare("SELECT id, name, manifest, state, created_at, updated_at FROM agents") }) @@ -262,6 +285,11 @@ impl StructuredStore { } else { None }; + let identity_str: Option = if col_count >= 8 { + row.get(7).ok() + } else { + None + }; Ok(( id_str, name, @@ -269,6 +297,7 @@ impl StructuredStore { state_str, created_str, session_id_str, + identity_str, )) }) .map_err(|e| OpenFangError::Memory(e.to_string()))?; @@ -278,7 +307,7 @@ impl StructuredStore { let mut repair_queue: Vec<(String, Vec, String)> = Vec::new(); for row in rows { - let (id_str, name, manifest_blob, state_str, created_str, session_id_str) = match row { + let (id_str, name, manifest_blob, state_str, created_str, session_id_str, identity_str) = match row { Ok(r) => r, Err(e) => { tracing::warn!("Skipping agent row with read error: {e}"); @@ -342,6 +371,10 @@ impl StructuredStore { .map(openfang_types::agent::SessionId) .unwrap_or_else(openfang_types::agent::SessionId::new); + let identity = identity_str + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + agents.push(AgentEntry { id: agent_id, name, @@ -354,7 +387,7 @@ impl StructuredStore { children: vec![], session_id, tags: vec![], - identity: Default::default(), + identity, onboarding_completed: false, onboarding_completed_at: None, }); diff --git a/crates/openfang-runtime/src/drivers/openai.rs b/crates/openfang-runtime/src/drivers/openai.rs index 2c3d5c43..212122f0 100644 --- a/crates/openfang-runtime/src/drivers/openai.rs +++ b/crates/openfang-runtime/src/drivers/openai.rs @@ -47,7 +47,8 @@ struct OaiRequest { /// New token limit field required by GPT-5 and o-series reasoning models. #[serde(skip_serializing_if = "Option::is_none")] max_completion_tokens: Option, - temperature: f32, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, #[serde(skip_serializing_if = "Vec::is_empty")] tools: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -309,7 +310,7 @@ impl LlmDriver for OpenAIDriver { messages: oai_messages, max_tokens: mt, max_completion_tokens: mct, - temperature: request.temperature, + temperature: Some(request.temperature), tools: oai_tools, tool_choice, stream: false, @@ -399,6 +400,28 @@ impl LlmDriver for OpenAIDriver { continue; } + // Model doesn't support function calling — retry without tools + // (e.g. GLM-5 on DashScope returns 500 "internal error" when tools are sent) + let body_lower = body.to_lowercase(); + if !oai_request.tools.is_empty() + && attempt < max_retries + && (status == 500 + || body_lower.contains("internal error") + || (status == 400 + && (body_lower.contains("does not support tools") + || body_lower.contains("tool") + && body_lower.contains("not supported")))) + { + warn!( + model = %oai_request.model, + status, + "Model may not support tools, retrying without tools" + ); + oai_request.tools.clear(); + oai_request.tool_choice = None; + continue; + } + return Err(LlmError::Api { status, message: body, @@ -612,7 +635,7 @@ impl LlmDriver for OpenAIDriver { messages: oai_messages, max_tokens: mt, max_completion_tokens: mct, - temperature: request.temperature, + temperature: Some(request.temperature), tools: oai_tools, tool_choice, stream: true, @@ -704,6 +727,27 @@ impl LlmDriver for OpenAIDriver { continue; } + // Model doesn't support function calling — retry without tools + let body_lower = body.to_lowercase(); + if !oai_request.tools.is_empty() + && attempt < max_retries + && (status == 500 + || body_lower.contains("internal error") + || (status == 400 + && (body_lower.contains("does not support tools") + || body_lower.contains("tool") + && body_lower.contains("not supported")))) + { + warn!( + model = %oai_request.model, + status, + "Model may not support tools (stream), retrying without tools" + ); + oai_request.tools.clear(); + oai_request.tool_choice = None; + continue; + } + return Err(LlmError::Api { status, message: body,