From 531119d825edc04ab7baf12f4757d1622e1dbd27 Mon Sep 17 00:00:00 2001 From: YellowSnnowmann <167776381+YellowSnnowmann@users.noreply.github.com> Date: Thu, 9 Jul 2026 07:45:12 +0530 Subject: [PATCH] fix(memory): classify cloud-embedding "No backend session" as AuthMissing (#4699) Co-authored-by: Steven Enamakel --- src/openhuman/credentials/ops.rs | 7 ++ src/openhuman/credentials/ops_tests.rs | 89 +++++++++++++++++++ src/openhuman/memory_queue/handlers/mod.rs | 25 +++++- .../memory_queue/handlers/mod_tests.rs | 62 +++++++++++++ src/openhuman/memory_tree/health/mod.rs | 52 +++++++++++ tests/composio_raw_coverage_e2e.rs | 1 + ...ry_tree_memory_round23_raw_coverage_e2e.rs | 42 ++++----- tests/memory_tree_summarizer_e2e.rs | 47 +++++----- tests/memory_tree_sync_raw_coverage_e2e.rs | 34 +++---- 9 files changed, 290 insertions(+), 69 deletions(-) diff --git a/src/openhuman/credentials/ops.rs b/src/openhuman/credentials/ops.rs index 6c25b58e8..4fff72fc7 100644 --- a/src/openhuman/credentials/ops.rs +++ b/src/openhuman/credentials/ops.rs @@ -473,6 +473,13 @@ async fn store_session_inner( // in place. Workers that were sleeping in the paused poll loop will // pick this up at their next iteration and resume LLM-bound work. crate::openhuman::scheduler_gate::set_signed_out(false); + tracing::debug!( + domain = "credentials", + operation = "store_session", + "[credentials][auth-store] scheduler gate cleared; ensuring re-embed backfill after login" + ); + crate::openhuman::memory_queue::ensure_reembed_backfill(&effective_config); + logs.push("memory re-embed backfill checked after login".to_string()); // Bind the Sentry scope to this user so background events that fire // before the frontend's `app_state_snapshot` warms the user cache still diff --git a/src/openhuman/credentials/ops_tests.rs b/src/openhuman/credentials/ops_tests.rs index 0e36932dd..78c2d2a31 100644 --- a/src/openhuman/credentials/ops_tests.rs +++ b/src/openhuman/credentials/ops_tests.rs @@ -47,6 +47,17 @@ fn jwt_with_payload(payload: serde_json::Value) -> String { format!("eyJhbGciOiJIUzI1NiJ9.{payload}.sig") } +fn count_reembed_backfill_jobs(config: &Config) -> i64 { + crate::openhuman::memory_store::chunks::store::with_connection(config, |conn| { + Ok(conn.query_row( + "SELECT COUNT(*) FROM mem_tree_jobs WHERE kind = 'reembed_backfill'", + [], + |row| row.get(0), + )?) + }) + .unwrap() +} + async fn spawn_auth_me_status(status: StatusCode) -> String { let app = Router::new().route("/auth/me", get(move || async move { status })); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -275,6 +286,84 @@ async fn store_session_defers_minimal_live_jwt_when_auth_me_transient_and_allowe ); } +#[tokio::test] +async fn store_session_requeues_reembed_backfill_after_login() { + use crate::openhuman::memory_store::chunks::store::{upsert_chunks, upsert_staged_chunks_tx}; + use crate::openhuman::memory_store::chunks::types::{ + chunk_id, Chunk, Metadata, SourceKind, SourceRef, + }; + use crate::openhuman::memory_store::content as content_store; + use chrono::TimeZone; + + let _env_guard = crate::openhuman::config::TEST_ENV_LOCK + .lock() + .unwrap_or_else(|e| e.into_inner()); + let tmp = TempDir::new().unwrap(); + std::fs::create_dir_all(tmp.path().join("workspace")).unwrap(); + let _home = EnvVarGuard::set_to_path("HOME", tmp.path()); + let mut config = test_config(&tmp); + config.api_url = Some(spawn_auth_me_status(StatusCode::SERVICE_UNAVAILABLE).await); + + let ts = chrono::Utc.timestamp_millis_opt(1_700_000_000_000).unwrap(); + let chunk = Chunk { + id: chunk_id(SourceKind::Chat, "slack:#eng", 0, "login-reembed-seed"), + content: "memory content that needs embedding after the user logs in".into(), + metadata: Metadata { + source_kind: SourceKind::Chat, + source_id: "slack:#eng".into(), + owner: "alice".into(), + timestamp: ts, + time_range: (ts, ts), + tags: vec![], + source_ref: Some(SourceRef::new("slack://x")), + path_scope: None, + }, + token_count: 12, + seq_in_source: 0, + created_at: ts, + partial_message: false, + }; + upsert_chunks(&config, &[chunk.clone()]).unwrap(); + let content_root = config.memory_tree_content_root(); + std::fs::create_dir_all(&content_root).unwrap(); + let staged = content_store::stage_chunks(&content_root, &[chunk]).unwrap(); + crate::openhuman::memory_store::chunks::store::with_connection(&config, |conn| { + let tx = conn.unchecked_transaction()?; + upsert_staged_chunks_tx(&tx, &staged)?; + tx.commit()?; + Ok(()) + }) + .unwrap(); + + assert_eq!( + count_reembed_backfill_jobs(&config), + 0, + "precondition: no active reembed backfill job exists before login" + ); + + let token = jwt_with_payload(json!({ + "sub": "unverified-jwt-user", + "email": "jwt@example.test", + "name": "Unverified JWT User", + "exp": (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp() + })); + + let result = store_session_with_deferred_validation(&config, &token, None, Some(json!({}))) + .await + .unwrap(); + + let log_text = result.logs.join(" "); + assert!( + log_text.contains("memory re-embed backfill checked after login"), + "store_session should report the post-login backfill probe, got: {log_text}" + ); + assert_eq!( + count_reembed_backfill_jobs(&config), + 1, + "login must enqueue exactly one reembed_backfill chain for uncovered rows" + ); +} + #[tokio::test] async fn deferred_session_without_user_id_does_not_replace_active_user_profile() { let _env_guard = crate::openhuman::config::TEST_ENV_LOCK diff --git a/src/openhuman/memory_queue/handlers/mod.rs b/src/openhuman/memory_queue/handlers/mod.rs index e6be8595b..aa4a561f6 100644 --- a/src/openhuman/memory_queue/handlers/mod.rs +++ b/src/openhuman/memory_queue/handlers/mod.rs @@ -738,7 +738,9 @@ fn try_mark_summary_reembed_skipped( /// Failure semantics are preserved verbatim from #1574 §6: /// - body read failure → log + persistent tombstone (`"body read failed: {e}"`) /// - embed wrong dim → log + tombstone (`"embed wrong dim"`) -/// - embed error → log + tombstone (`"embed failed: {e}"`) +/// - per-row unrecoverable embed error → log + tombstone (`"embed failed: {e}"`) +/// - global cloud-auth absence (`AuthMissing`, #4359) → fail the backfill +/// **without** tombstoning, so the same rows re-embed once the user logs in. /// /// The single difference vs the old code is the embed call shape: one /// `embed_batch` (which collapses N provider round-trips into one for batch- @@ -809,6 +811,27 @@ async fn reembed_collect( } Err(e) => { let failure = crate::openhuman::memory_tree::health::classify_embed_error(&e); + // #4359: a missing cloud session is a GLOBAL, recoverable-by- + // login condition, not a per-row defect — the row is perfectly + // embeddable once the user signs in. Tombstoning it (the + // unrecoverable branch below) would exclude it from the + // worklist forever, so it would never re-embed even after login. + // Fail the backfill fast with the typed `AuthMissing` failure + // instead: the health banner surfaces the "log in" remediation + // and every row stays re-embeddable. + if matches!( + failure.code, + crate::openhuman::memory_tree::health::FailureCode::AuthMissing + ) { + log::warn!( + "[memory::jobs] reembed_backfill: {label} {id} embed failed — no cloud \ + session; failing backfill without tombstoning so rows stay re-embeddable \ + after login (sig={active_sig}): {e:#}" + ); + return Err(anyhow::Error::new(failure).context(format!( + "reembed_backfill: {label} {id} cloud auth missing (sig={active_sig}): {e:#}" + ))); + } if !failure.is_unrecoverable() { return Err(anyhow::Error::new(failure).context(format!( "reembed_backfill: {label} {id} transient embed failed (sig={active_sig}): {e:#}" diff --git a/src/openhuman/memory_queue/handlers/mod_tests.rs b/src/openhuman/memory_queue/handlers/mod_tests.rs index 3fd878471..a852fa286 100644 --- a/src/openhuman/memory_queue/handlers/mod_tests.rs +++ b/src/openhuman/memory_queue/handlers/mod_tests.rs @@ -680,3 +680,65 @@ async fn prepare_extract_scores_full_body_but_returns_preview() { "returned chunk must NOT retain the full on-disk body" ); } + +/// #4359: a cloud-embedding "No backend session" bail is a global, login- +/// recoverable condition — `reembed_collect` must fail the backfill fast with a +/// typed `AuthMissing` failure and must **not** tombstone any row, so the same +/// rows re-embed once the user signs in. (A per-row tombstone here would +/// exclude the rows from the worklist forever — the regression this guards.) +struct MissingSessionEmbedder; + +#[async_trait::async_trait] +impl crate::openhuman::memory_tree::score::embed::Embedder for MissingSessionEmbedder { + fn name(&self) -> &'static str { + "missing-session" + } + + async fn embed(&self, _text: &str) -> anyhow::Result> { + anyhow::bail!( + "No backend session for cloud embeddings: log in to OpenHuman, or set \ + memory.embedding_provider to \"ollama\" / \"none\" in config.toml" + ) + } +} + +#[tokio::test] +async fn reembed_backfill_auth_missing_fails_fast_without_tombstone() { + use crate::openhuman::memory_tree::health::{FailureCode, PipelineFailure}; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Arc; + + let (_tmp, cfg) = test_config(); + let ids = vec![ + "chunk-a".to_string(), + "chunk-b".to_string(), + "chunk-c".to_string(), + ]; + let tombstoned = Arc::new(AtomicUsize::new(0)); + let tombstoned_for_mark = Arc::clone(&tombstoned); + + let err = reembed_collect( + &cfg, + &MissingSessionEmbedder, + "provider=cloud;model=embedding-v1;dims=1024", + &ids, + "chunk", + |_cfg, id| Ok(format!("body for {id}")), + move |_cfg, _id, _sig, _reason| { + tombstoned_for_mark.fetch_add(1, Ordering::SeqCst); + }, + ) + .await + .expect_err("missing cloud session must fail the backfill, not return Ok"); + + let failure = err + .downcast_ref::() + .expect("auth-missing backfill error must carry a typed PipelineFailure"); + assert_eq!(failure.code, FailureCode::AuthMissing); + assert!(failure.is_unrecoverable()); + assert_eq!( + tombstoned.load(Ordering::SeqCst), + 0, + "auth-missing must NOT tombstone any row — they must stay re-embeddable after login" + ); +} diff --git a/src/openhuman/memory_tree/health/mod.rs b/src/openhuman/memory_tree/health/mod.rs index 838704e3b..5a5451f40 100644 --- a/src/openhuman/memory_tree/health/mod.rs +++ b/src/openhuman/memory_tree/health/mod.rs @@ -242,6 +242,20 @@ pub fn classify_embed_error_str(msg: &str) -> PipelineFailure { .with_detail(truncate_detail(msg)); } + // Sibling of the #13021 case above: `OpenHumanCloudEmbedding::resolve_bearer` + // bails *before any HTTP round-trip* when the desktop/backend session + // bearer is absent (user signed out), with the literal phrase + // "No backend session for cloud embeddings ..." (see + // `src/openhuman/embeddings/cloud.rs`). Being a client-side bail it carries + // no `Embedding API error ()` shape, so without this match it falls + // through to `Transient` — the Memory Tree then shows "temporary error… + // will retry automatically" and the worker retries an auth failure that a + // retry can never fix. Classify as `AuthMissing` so the health banner + // surfaces the "log in to OpenHuman" remediation and the job fails fast. + if lower.contains("no backend session") { + return PipelineFailure::new(FailureCode::AuthMissing).with_detail(truncate_detail(msg)); + } + // Dimension mismatch — the trait validator / CloudEmbedder rejects a // vector whose length isn't EMBEDDING_DIM. Check before status parsing: // it's a 2xx-but-wrong-shape case with no HTTP status to match. @@ -723,6 +737,44 @@ mod tests { assert!(f.is_unrecoverable()); } + /// #4359: `OpenHumanCloudEmbedding::resolve_bearer` bails with "No backend + /// session for cloud embeddings ..." *before any HTTP call* when the user + /// is signed out. This must classify as `AuthMissing` (unrecoverable, "log + /// in to OpenHuman" remediation) rather than falling through to `Transient` + /// ("will retry automatically" — a loop that an auth failure can never win). + #[test] + fn classify_no_backend_session_as_auth_missing() { + let msg = "No backend session for cloud embeddings: log in to OpenHuman, or set \ + memory.embedding_provider to \"ollama\" / \"none\" in config.toml"; + let f = classify_embed_error_str(msg); + assert_eq!( + f.code, + FailureCode::AuthMissing, + "expected AuthMissing for {msg:?}" + ); + assert_eq!(f.class, FailureClass::Unrecoverable); + assert_eq!(f.remediation_key, "memory.health.remediation.auth_missing"); + assert!(f.is_unrecoverable()); + } + + /// The match must be case-insensitive and survive `anyhow` context wrapping: + /// the bail is `.context()`-wrapped on its way up through the embed pipeline + /// (e.g. `embed_each_via_provider` adds "cloud embeddings failed"), and + /// `classify_embed_error` flattens the chain via `{err:#}`. + #[test] + fn classify_no_backend_session_through_anyhow_context_chain() { + let base = anyhow::anyhow!( + "No backend session for cloud embeddings: log in to OpenHuman, or set \ + memory.embedding_provider to \"ollama\" / \"none\" in config.toml" + ); + let wrapped = base + .context("cloud embeddings failed") + .context("reembed_backfill chunk_id=c"); + let f = classify_embed_error(&wrapped); + assert_eq!(f.code, FailureCode::AuthMissing); + assert!(f.is_unrecoverable()); + } + #[test] fn classify_5xx_is_transient() { let f = classify_embed_error_str("Embedding API error (503 Service Unavailable): retry"); diff --git a/tests/composio_raw_coverage_e2e.rs b/tests/composio_raw_coverage_e2e.rs index 251a71259..0820dd436 100644 --- a/tests/composio_raw_coverage_e2e.rs +++ b/tests/composio_raw_coverage_e2e.rs @@ -1333,6 +1333,7 @@ fn composio_types_roundtrip_connection_tool_trigger_and_history_shapes() { name: "GMAIL_SEND_EMAIL".into(), description: Some("Send mail".into()), parameters: Some(json!({ "type": "object" })), + output_parameters: None, }, }], }; diff --git a/tests/memory_tree_memory_round23_raw_coverage_e2e.rs b/tests/memory_tree_memory_round23_raw_coverage_e2e.rs index 3da625f01..554f30ef7 100644 --- a/tests/memory_tree_memory_round23_raw_coverage_e2e.rs +++ b/tests/memory_tree_memory_round23_raw_coverage_e2e.rs @@ -7,7 +7,6 @@ use std::ffi::OsString; use std::path::Path; use std::sync::{Arc, Mutex, OnceLock}; -use anyhow::Result; use async_trait::async_trait; use chrono::{TimeZone, Utc}; use serde_json::{json, Map, Value}; @@ -15,7 +14,6 @@ use tempfile::TempDir; use openhuman_core::openhuman::config::Config; use openhuman_core::openhuman::embeddings::NoopEmbedding; -use openhuman_core::openhuman::inference::provider::traits::Provider; use openhuman_core::openhuman::memory::{ ExtractionMode, MemoryIngestionConfig, MemoryIngestionRequest, }; @@ -24,6 +22,7 @@ use openhuman_core::openhuman::memory_tree::tree_runtime::{ all_tree_summarizer_registered_controllers, engine, rpc as tree_runtime_rpc, store as tree_runtime_store, }; +use tinyagents::harness::model::{ChatModel, ModelRequest, ModelResponse}; struct EnvVarGuard { key: &'static str, @@ -72,21 +71,18 @@ struct ScriptedProvider { } #[async_trait] -impl Provider for ScriptedProvider { - async fn chat_with_system( +impl ChatModel<()> for ScriptedProvider { + async fn invoke( &self, - system_prompt: Option<&str>, - message: &str, - model: &str, - temperature: f64, - ) -> Result { - let system_prompt = system_prompt.expect("tree runtime should pass a system prompt"); - assert!(system_prompt.contains("hierarchical summarizer")); - assert!(system_prompt.contains("under")); - assert!(!message.trim().is_empty()); - assert!(!model.trim().is_empty()); - assert!(temperature > 0.0); - Ok(self.response.clone()) + _state: &(), + request: ModelRequest, + ) -> tinyagents::Result { + let prompt = format!("{:?}", request.messages); + assert!(prompt.contains("hierarchical summarizer")); + assert!(prompt.contains("under")); + assert!(!request.messages.is_empty()); + assert!(request.temperature.unwrap_or_default() > 0.0); + Ok(ModelResponse::assistant(self.response.clone())) } } @@ -222,7 +218,7 @@ async fn tree_runtime_engine_summarizes_preserves_buffer_and_rebuilds() { ) .expect("buffer second entry"); - let hour = engine::run_summarization(&config, &provider, "test-model", namespace, ts) + let hour = engine::run_summarization(&config, &provider, namespace, ts) .await .expect("run summarization") .expect("hour node"); @@ -240,12 +236,10 @@ async fn tree_runtime_engine_summarizes_preserves_buffer_and_rebuilds() { assert!(node.summary.contains("round23 summary") || node.summary.contains("##")); } - assert!( - engine::run_summarization(&config, &provider, "test-model", namespace, ts) - .await - .expect("empty run") - .is_none() - ); + assert!(engine::run_summarization(&config, &provider, namespace, ts) + .await + .expect("empty run") + .is_none()); tree_runtime_store::buffer_write( &config, @@ -256,7 +250,7 @@ async fn tree_runtime_engine_summarizes_preserves_buffer_and_rebuilds() { ) .expect("buffer pending entry"); - let status = engine::rebuild_tree(&config, &provider, "test-model", namespace) + let status = engine::rebuild_tree(&config, &provider, namespace) .await .expect("rebuild tree"); assert!(status.total_nodes >= 5); diff --git a/tests/memory_tree_summarizer_e2e.rs b/tests/memory_tree_summarizer_e2e.rs index 640aaee34..8b0d38db3 100644 --- a/tests/memory_tree_summarizer_e2e.rs +++ b/tests/memory_tree_summarizer_e2e.rs @@ -31,8 +31,9 @@ use chrono::{DateTime, TimeZone, Utc}; use tempfile::tempdir; use openhuman_core::openhuman::config::Config; -use openhuman_core::openhuman::inference::provider::traits::Provider; use openhuman_core::openhuman::memory_tree::tree_runtime::{engine, store}; +use tinyagents::harness::model::{ChatModel, ModelRequest, ModelResponse}; +use tinyagents::TinyAgentsError; // ── Env isolation ───────────────────────────────────────────────────────── @@ -73,7 +74,7 @@ fn env_lock() -> std::sync::MutexGuard<'static, ()> { // ── Mock provider helpers ───────────────────────────────────────────────── -/// A provider whose `chat_with_system` returns scripted responses in order. +/// A provider whose `invoke` returns scripted responses in order. /// Thread-safe via a `Mutex`. Each pop returns the next scripted /// response; once the queue is exhausted, every subsequent call returns an /// error so missing a setup step is caught immediately. @@ -100,24 +101,28 @@ impl ScriptedProvider { } #[async_trait] -impl Provider for ScriptedProvider { - async fn chat_with_system( +impl ChatModel<()> for ScriptedProvider { + async fn invoke( &self, - system_prompt: Option<&str>, - message: &str, - model: &str, - _temperature: f64, - ) -> anyhow::Result { + _state: &(), + request: ModelRequest, + ) -> tinyagents::Result { let mut count = self.call_count.lock().expect("call_count lock"); *count += 1; let call_n = *count; drop(count); + let message_len: usize = request + .messages + .iter() + .map(|message| format!("{message:?}").len()) + .sum(); log::debug!( - "[memory_tree_summarizer_e2e] ScriptedProvider.chat_with_system call #{call_n}: \ - model={model} system_prompt_len={} msg_len={}", - system_prompt.map(|s| s.len()).unwrap_or(0), - message.len() + "[memory_tree_summarizer_e2e] ScriptedProvider.invoke call #{call_n}: \ + model={:?} message_count={} msg_len={}", + request.model, + request.messages.len(), + message_len ); let mut q = self.responses.lock().expect("responses lock"); @@ -127,19 +132,19 @@ impl Provider for ScriptedProvider { "[memory_tree_summarizer_e2e] call #{call_n} → scripted Ok ({} chars)", text.len() ); - Ok(text) + Ok(ModelResponse::assistant(text)) } Some(Err(msg)) => { log::debug!("[memory_tree_summarizer_e2e] call #{call_n} → scripted Err: {msg}"); - Err(anyhow::anyhow!("{msg}")) + Err(TinyAgentsError::Model(msg)) } None => { log::debug!( "[memory_tree_summarizer_e2e] call #{call_n} → queue exhausted (fallback error)" ); - Err(anyhow::anyhow!( + Err(TinyAgentsError::Model(format!( "ScriptedProvider queue exhausted at call #{call_n}" - )) + ))) } } } @@ -233,7 +238,7 @@ async fn builds_hour_day_month_year_chain() { ]); log::debug!("[memory_tree_summarizer_e2e] running summarization"); - let result = engine::run_summarization(&config, &provider, "test-model", NS, Utc::now()).await; + let result = engine::run_summarization(&config, &provider, NS, Utc::now()).await; log::debug!( "[memory_tree_summarizer_e2e] run_summarization returned: {:?}", @@ -379,7 +384,7 @@ async fn merges_into_existing_hour_node() { )]); log::debug!("[memory_tree_summarizer_e2e] first run"); - let r1 = engine::run_summarization(&config, &provider1, "test-model", NS, Utc::now()) + let r1 = engine::run_summarization(&config, &provider1, NS, Utc::now()) .await .expect("first run_summarization"); assert!(r1.is_some(), "first run should yield a node"); @@ -415,7 +420,7 @@ async fn merges_into_existing_hour_node() { )]); log::debug!("[memory_tree_summarizer_e2e] second run (same hour)"); - let r2 = engine::run_summarization(&config, &provider2, "test-model", NS, Utc::now()) + let r2 = engine::run_summarization(&config, &provider2, NS, Utc::now()) .await .expect("second run_summarization"); assert!(r2.is_some(), "second run should yield a node"); @@ -504,7 +509,7 @@ async fn survives_llm_error_with_partial_progress() { ]); log::debug!("[memory_tree_summarizer_e2e] running summarization expecting partial failure"); - let result = engine::run_summarization(&config, &provider, "test-model", NS, Utc::now()).await; + let result = engine::run_summarization(&config, &provider, NS, Utc::now()).await; log::debug!( "[memory_tree_summarizer_e2e] run_summarization result: is_ok={}", diff --git a/tests/memory_tree_sync_raw_coverage_e2e.rs b/tests/memory_tree_sync_raw_coverage_e2e.rs index 6a91c82c3..1f6ca3c49 100644 --- a/tests/memory_tree_sync_raw_coverage_e2e.rs +++ b/tests/memory_tree_sync_raw_coverage_e2e.rs @@ -15,7 +15,6 @@ use tempfile::TempDir; use openhuman_core::core::event_bus::{DomainEvent, EventHandler}; use openhuman_core::openhuman::config::Config; -use openhuman_core::openhuman::inference::provider::traits::{ChatMessage, Provider}; use openhuman_core::openhuman::memory_store::chunks::store::upsert_chunks; use openhuman_core::openhuman::memory_store::chunks::types::{ approx_token_count, chunk_id, Chunk, Metadata, SourceKind as ChunkSourceKind, SourceRef, @@ -44,6 +43,7 @@ use openhuman_core::openhuman::memory_tree::tree::{ use openhuman_core::openhuman::memory_tree::tree_runtime::{ engine, rpc as tree_runtime_rpc, store as runtime_store, }; +use tinyagents::harness::model::{ChatModel, ModelRequest, ModelResponse}; struct EnvVarGuard { key: &'static str, @@ -159,31 +159,19 @@ impl ScriptedProvider { } #[async_trait] -impl Provider for ScriptedProvider { - async fn chat_with_system( +impl ChatModel<()> for ScriptedProvider { + async fn invoke( &self, - system_prompt: Option<&str>, - message: &str, - model: &str, - temperature: f64, - ) -> anyhow::Result { - let _ = (system_prompt, message, model, temperature); - Ok(self + _state: &(), + _request: ModelRequest, + ) -> tinyagents::Result { + let response = self .responses .lock() .unwrap() .pop() - .unwrap_or_else(|| "fallback scripted summary".to_string())) - } - - async fn chat_with_history( - &self, - messages: &[ChatMessage], - model: &str, - temperature: f64, - ) -> anyhow::Result { - let _ = (messages, model, temperature); - self.chat_with_system(None, "", "", 0.0).await + .unwrap_or_else(|| "fallback scripted summary".to_string()); + Ok(ModelResponse::assistant(response)) } } @@ -220,7 +208,7 @@ async fn tree_runtime_engine_rpc_and_walk_cover_success_and_edge_paths() { "rebuilt hour 10", "rebuilt hour 11", ]); - let last = engine::run_summarization(&cfg, &provider, "test-model", ns, Utc::now()) + let last = engine::run_summarization(&cfg, &provider, ns, Utc::now()) .await .expect("run summarization") .expect("last hour node"); @@ -254,7 +242,7 @@ async fn tree_runtime_engine_rpc_and_walk_cover_success_and_edge_paths() { "rebuilt year summary", "rebuilt root summary", ]); - let rebuilt = engine::rebuild_tree(&cfg, &rebuild_provider, "test-model", ns) + let rebuilt = engine::rebuild_tree(&cfg, &rebuild_provider, ns) .await .expect("rebuild tree"); assert_eq!(rebuilt.total_nodes, 6);