diff --git a/Cargo.lock b/Cargo.lock index 0a9f3634b4..f0019d28ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2648,7 +2648,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.58.0", + "windows-core 0.57.0", ] [[package]] @@ -6512,13 +6512,13 @@ dependencies = [ [[package]] name = "tinyflows" -version = "0.6.0" +version = "0.6.1" dependencies = [ "async-trait", "axum", "futures-timer", "futures-util", - "getrandom 0.3.4", + "getrandom 0.4.2", "jaq-core", "jaq-json", "jaq-std", @@ -7878,7 +7878,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index ec3ca83fd8..4117beda5c 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -2984,9 +2984,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -7771,13 +7771,13 @@ dependencies = [ [[package]] name = "tinyflows" -version = "0.6.0" +version = "0.6.1" dependencies = [ "async-trait", "axum", "futures-timer", "futures-util", - "getrandom 0.3.4", + "getrandom 0.4.3", "jaq-core", "jaq-json", "jaq-std", diff --git a/src/bin/rss_bench.rs b/src/bin/rss_bench.rs index 4158ab27c2..4c5230968b 100644 --- a/src/bin/rss_bench.rs +++ b/src/bin/rss_bench.rs @@ -398,15 +398,27 @@ mod tests { assert_eq!(dirs.len(), 8, "workspaces must be isolated per agent"); } - #[tokio::test] - async fn warm_up_turn_completes_without_network() { - let mut roster = build_roster(1).expect("1-agent roster builds"); - warm_up(&mut roster).await.expect("warm-up turn completes"); - // The mock provider reports usage, so last_turn_usage is populated — - // proving the embedding cost-metering contract works on the bare Agent. - assert!( - roster.agents[0].last_turn_usage().is_some(), - "usage should be readable after a turn" - ); + #[test] + fn warm_up_turn_completes_without_network() { + // The agent-turn future is large in debug builds. Run it on a worker + // with explicit stack headroom instead of libtest's smaller default. + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .thread_stack_size(8 * 1024 * 1024) + .build() + .expect("test runtime builds"); + runtime + .block_on(runtime.spawn(async { + let mut roster = build_roster(1).expect("1-agent roster builds"); + warm_up(&mut roster).await.expect("warm-up turn completes"); + // The mock provider reports usage, so last_turn_usage is populated — + // proving the embedding cost-metering contract works on the bare Agent. + assert!( + roster.agents[0].last_turn_usage().is_some(), + "usage should be readable after a turn" + ); + })) + .expect("warm-up task joins"); } } diff --git a/src/openhuman/agent/artifacts/ops.rs b/src/openhuman/agent/artifacts/ops.rs index 5cbff6f9b3..6d77feb58f 100644 --- a/src/openhuman/agent/artifacts/ops.rs +++ b/src/openhuman/agent/artifacts/ops.rs @@ -207,7 +207,8 @@ async fn regenerate_presentation( &config.workspace_dir, &config.action_dir, )); - let tool = PresentationTool::new(config.workspace_dir.clone(), security); + let tool = + PresentationTool::with_config(config.workspace_dir.clone(), security, config.clone()); let chat_ctx = ApprovalChatContext { thread_id: thread_id.to_string(), diff --git a/src/openhuman/agent/artifacts/ops_tests.rs b/src/openhuman/agent/artifacts/ops_tests.rs index 8970740c5e..7539598c90 100644 --- a/src/openhuman/agent/artifacts/ops_tests.rs +++ b/src/openhuman/agent/artifacts/ops_tests.rs @@ -274,7 +274,10 @@ async fn regenerate_reruns_producer_and_reuses_id() { let value = outcome.into_cli_compatible_json().unwrap(); assert_eq!(value["artifact_id"], meta.id); assert_eq!(value["regenerated"], true); - assert_eq!(value["is_error"], false); + assert_eq!( + value["is_error"], false, + "regeneration unexpectedly returned a tool error: {value}" + ); // Same id reused in place; the re-run drove it to Ready. let got = get_artifact(tmp.path(), &meta.id).await.unwrap(); diff --git a/src/openhuman/agent/harness/run_queue/mod.rs b/src/openhuman/agent/harness/run_queue/mod.rs index f742e37bd4..8e01c6b348 100644 --- a/src/openhuman/agent/harness/run_queue/mod.rs +++ b/src/openhuman/agent/harness/run_queue/mod.rs @@ -15,38 +15,31 @@ mod types; use std::sync::Arc; -use tokio::sync::Mutex; +use tinyagents::harness::run_queue::{QueueLane, RunQueue as TinyAgentsRunQueue}; -pub use types::{QueueMode, QueueStatus, QueuedMessage}; +pub use tinyagents::harness::run_queue::QueueStatus; +pub use types::{QueueMode, QueuedMessage}; /// Thread-safe run queue with three lanes. Wrapped in `Arc` for shared /// ownership between the web channel producer and the engine consumer. #[derive(Debug)] pub struct RunQueue { - inner: Mutex, -} - -#[derive(Debug, Default)] -struct RunQueueInner { - steers: Vec, - followups: Vec, - collects: Vec, + inner: TinyAgentsRunQueue, } impl RunQueue { pub fn new() -> Arc { Arc::new(Self { - inner: Mutex::new(RunQueueInner::default()), + inner: TinyAgentsRunQueue::new(), }) } /// Push a message into the appropriate lane based on its mode. pub async fn push(&self, msg: QueuedMessage) { - let mut inner = self.inner.lock().await; match msg.mode { - QueueMode::Steer => inner.steers.push(msg), - QueueMode::Followup => inner.followups.push(msg), - QueueMode::Collect => inner.collects.push(msg), + QueueMode::Steer => self.inner.push(QueueLane::Steer, msg).await, + QueueMode::Followup => self.inner.push(QueueLane::Followup, msg).await, + QueueMode::Collect => self.inner.push(QueueLane::Collect, msg).await, QueueMode::Interrupt => { log::warn!( "[run_queue] interrupt-mode message pushed to queue — should have been handled by caller" @@ -62,44 +55,27 @@ impl RunQueue { /// Drain all pending steer messages (FIFO order). pub async fn drain_steers(&self) -> Vec { - let mut inner = self.inner.lock().await; - std::mem::take(&mut inner.steers) + self.inner.drain(QueueLane::Steer).await } /// Drain all pending collect messages (FIFO order). pub async fn drain_collects(&self) -> Vec { - let mut inner = self.inner.lock().await; - std::mem::take(&mut inner.collects) + self.inner.drain(QueueLane::Collect).await } /// Drain all pending followup messages (FIFO order). pub async fn drain_followups(&self) -> Vec { - let mut inner = self.inner.lock().await; - std::mem::take(&mut inner.followups) + self.inner.drain(QueueLane::Followup).await } /// Snapshot the current queue depth per lane. pub async fn status(&self) -> QueueStatus { - let inner = self.inner.lock().await; - let steers = inner.steers.len(); - let followups = inner.followups.len(); - let collects = inner.collects.len(); - QueueStatus { - steers, - followups, - collects, - total: steers + followups + collects, - } + self.inner.status().await } /// Clear all lanes and return the total number of messages dropped. pub async fn clear(&self) -> usize { - let mut inner = self.inner.lock().await; - let total = inner.steers.len() + inner.followups.len() + inner.collects.len(); - inner.steers.clear(); - inner.followups.clear(); - inner.collects.clear(); - total + self.inner.clear().await } } diff --git a/src/openhuman/agent/harness/run_queue/types.rs b/src/openhuman/agent/harness/run_queue/types.rs index c394342f35..5ccb6618fd 100644 --- a/src/openhuman/agent/harness/run_queue/types.rs +++ b/src/openhuman/agent/harness/run_queue/types.rs @@ -51,12 +51,3 @@ pub struct QueuedMessage { pub profile_id: Option, pub locale: Option, } - -/// Snapshot of the queue state for introspection. -#[derive(Debug, Clone, serde::Serialize)] -pub struct QueueStatus { - pub steers: usize, - pub followups: usize, - pub collects: usize, - pub total: usize, -} diff --git a/src/openhuman/flows/builder_tools.rs b/src/openhuman/flows/builder_tools.rs index a9e6ea1acc..4300d5d761 100644 --- a/src/openhuman/flows/builder_tools.rs +++ b/src/openhuman/flows/builder_tools.rs @@ -2660,7 +2660,7 @@ impl Tool for GetNodeKindContractTool { "properties": { "kind": { "type": "string", - "description": "One of the 15 node kinds, e.g. 'tool_call' (from list_node_kinds).", + "description": "One of the 16 node kinds, e.g. 'tool_call' (from list_node_kinds).", "enum": crate::openhuman::flows::NODE_KINDS, } }, diff --git a/src/openhuman/flows/builder_tools_tests.rs b/src/openhuman/flows/builder_tools_tests.rs index 877e4b9d9d..b6f946b88d 100644 --- a/src/openhuman/flows/builder_tools_tests.rs +++ b/src/openhuman/flows/builder_tools_tests.rs @@ -1880,18 +1880,19 @@ async fn save_workflow_accepts_correctly_schemad_graph() { } #[tokio::test] -async fn list_node_kinds_tool_returns_all_fifteen() { +async fn list_node_kinds_tool_returns_all_sixteen() { let tool = ListNodeKindsTool::new(); let result = tool.execute(json!({})).await.unwrap(); assert!(!result.is_error, "{}", result.output()); let parsed: Value = serde_json::from_str(&result.output()).unwrap(); let kinds = parsed["node_kinds"].as_array().unwrap(); - assert_eq!(kinds.len(), 15); + assert_eq!(kinds.len(), 16); // Each entry carries a kind + summary + the config-field name lists. assert!(kinds.iter().any(|k| k["kind"] == "tool_call")); assert!(kinds.iter().any(|k| k["kind"] == "memory")); assert!(kinds.iter().any(|k| k["kind"] == "dedup")); assert!(kinds.iter().any(|k| k["kind"] == "loop")); + assert!(kinds.iter().any(|k| k["kind"] == "shell")); assert!(kinds.iter().all(|k| k.get("summary").is_some())); } diff --git a/src/openhuman/flows/node_contracts.rs b/src/openhuman/flows/node_contracts.rs index ebd741e509..3d3b6be9e2 100644 --- a/src/openhuman/flows/node_contracts.rs +++ b/src/openhuman/flows/node_contracts.rs @@ -126,7 +126,7 @@ fn apply_host_overlay(contract: NodeKindContract) -> NodeKindContract { } } -/// All 15 node-kind contracts with this host's overlay applied, in +/// All 16 node-kind contracts with this host's overlay applied, in /// [`NODE_KINDS`] order. pub fn all_node_kind_contracts() -> Vec { tinyflows::catalog::all_contracts() @@ -136,7 +136,7 @@ pub fn all_node_kind_contracts() -> Vec { } /// The overlaid contract for one node kind, or `None` if `kind` is not one of -/// the 14. +/// the 16. pub fn node_kind_contract(kind: &str) -> Option { tinyflows::catalog::contract_for(kind).map(apply_host_overlay) } @@ -186,8 +186,8 @@ mod tests { use super::*; #[test] - fn overlay_preserves_all_15_kinds() { - assert_eq!(all_node_kind_contracts().len(), 15); + fn overlay_preserves_all_16_kinds() { + assert_eq!(all_node_kind_contracts().len(), 16); for kind in NODE_KINDS { assert!(node_kind_contract(kind).is_some(), "missing {kind}"); } diff --git a/src/openhuman/flows/tinyflows/caps/ops.rs b/src/openhuman/flows/tinyflows/caps/ops.rs index e0a212a6e8..5a8cc0e826 100644 --- a/src/openhuman/flows/tinyflows/caps/ops.rs +++ b/src/openhuman/flows/tinyflows/caps/ops.rs @@ -635,6 +635,11 @@ pub fn build_capabilities(config: Arc, state_namespace: impl Into Option { .and_then(Value::as_str) .map(str::to_string) .or_else(|| Some("javascript".to_string())), + NodeKind::Shell => cfg + .get("script_path") + .and_then(Value::as_str) + .map(|path| truncate_hint(&format!("script: {path}"))) + .or_else(|| cfg.get("source").and_then(Value::as_str).map(truncate_hint)), NodeKind::Condition => cfg .get("field") .and_then(Value::as_str) diff --git a/src/openhuman/flows/tools_tests.rs b/src/openhuman/flows/tools_tests.rs index 8e71dab14e..2809948bbc 100644 --- a/src/openhuman/flows/tools_tests.rs +++ b/src/openhuman/flows/tools_tests.rs @@ -212,6 +212,45 @@ fn dedup_config_hint_is_truncated_for_a_long_key_expression() { ); } +#[test] +fn shell_config_hint_prefers_the_script_path_and_truncates_inline_source() { + let graph = WorkflowGraph { + nodes: vec![ + Node { + id: "path".to_string(), + kind: NodeKind::Shell, + type_version: 1, + name: "Script file".to_string(), + config: json!({ + "script_path": "scripts/report.sh", + "source": "ignored when a path is present" + }), + ports: Vec::new(), + position: None, + }, + Node { + id: "inline".to_string(), + kind: NodeKind::Shell, + type_version: 1, + name: "Inline script".to_string(), + config: json!({ "source": "x".repeat(200) }), + ports: Vec::new(), + position: None, + }, + ], + ..Default::default() + }; + + let summary = build_summary(&graph); + assert_eq!( + summary["steps"][0]["config_hint"], + "script: scripts/report.sh" + ); + let inline_hint = summary["steps"][1]["config_hint"].as_str().unwrap(); + assert_eq!(inline_hint.chars().count(), MAX_CONFIG_HINT_CHARS); + assert!(inline_hint.ends_with('…')); +} + #[tokio::test] async fn summary_trigger_describes_schedule() { let tmp = TempDir::new().unwrap(); diff --git a/src/openhuman/inference/provider/chat_template.rs b/src/openhuman/inference/provider/chat_template.rs index 05a34b9ac2..bde0938fc7 100644 --- a/src/openhuman/inference/provider/chat_template.rs +++ b/src/openhuman/inference/provider/chat_template.rs @@ -1,133 +1,8 @@ -//! Classifier for **chat-template rejections** from local serving runtimes. +//! Host compatibility path for TinyAgents' provider-neutral chat-template +//! rejection classifier. //! -//! A local runtime (LM Studio, llama.cpp, Ollama) renders every request -//! through the model's own Jinja chat template, baked into the GGUF. When -//! the template cannot render the message list it aborts with a `400` -//! *before* the model is ever called, and the body describes a template -//! failure — not a bad model id, not a bad sampling parameter: -//! -//! ```text -//! lmstudio returned: Engine protocol predict request returned 400: -//! {"error":{"code":400,"message":"Unable to generate parser for this -//! template. Automatic parser generation failed: While executing -//! CallExpression at line 79, column 24 in source: ...multi_step_tool %} -//! {{- raise_exception('No user query found in messages.') }}... -//! Error: Jinja Exception: No user query found in messages.", -//! "type":"invalid_request_error"}} -//! ``` -//! -//! The canonical instance (tinyhumansai/openhuman#5291) is Qwen 3's -//! required-user-query guard firing against the prompt-guided tool loop's -//! message shape, on a model that reports no native tool calling. The -//! harness-side fix is to guarantee a resolvable user turn -//! (`tinyagents::harness::tool::ensure_resolvable_user_turn`); this -//! classifier exists so the class is *named accurately* while it happens -//! — with any template, any model. -//! -//! ## Why it needs its own arm -//! -//! Nothing in the raw body matches -//! [`super::config_rejection::is_provider_config_rejection_message`], but -//! the retry aggregate that wraps two failed attempts does (`"may not be -//! available on your provider"`), so the user was shown *"Your AI provider -//! rejected the request's model or temperature setting. Check your model -//! and routing in Settings → LLM."* That is wrong in a way that costs the -//! user real time: the model is fine, the temperature is fine, and every -//! remediation it suggests is a dead end. Classified ahead of the -//! config-rejection arm, the template failure keeps its own identity even -//! when it reaches the classifier inside an aggregate. -//! -//! Deliberately NOT added to -//! [`crate::core::observability::expected_error_kind`]: unlike a user -//! picking an unavailable model, a template that rejects our own message -//! shape is a defect on our side, and it should stay visible until the -//! harness normalization has shipped everywhere. - -/// Returns true if a provider error body indicates the model's **chat -/// template** rejected the request — a template render/parse failure, as -/// opposed to a rejected model id, sampling parameter, or credential. -/// -/// Case-insensitive substring match, anchored on phrases emitted by the -/// template engine itself (`raise_exception` text, the Jinja exception -/// marker) and by LM Studio's template-parser generation, so it holds -/// across runtimes that embed the same Jinja templates. Keep the list -/// tight: a false positive would relabel an unrelated provider error as a -/// template problem and send the user chasing a template they cannot see. -pub fn is_chat_template_rejection_message(body: &str) -> bool { - const PHRASES: &[&str] = &[ - // Qwen 3-family required-user-query guard — the #5291 repro. Raised - // by the template when it cannot locate a user turn to answer. - "no user query found in messages", - // LM Studio wraps a template it cannot compile into a tool-call - // parser with this prefix; the inner cause is always a template - // render failure. - "unable to generate parser for this template", - "automatic parser generation failed", - // Generic escape hatch for any other template that raises: the - // engine tags every one of these with the Jinja exception marker. - // Covers the sibling guards in Llama-3 / Mistral / ChatML templates - // (alternating-role requirements, "conversation roles must - // alternate", tool-response ordering) without enumerating each. - "jinja exception", - ]; - - let lower = body.to_ascii_lowercase(); - PHRASES.iter().any(|phrase| lower.contains(phrase)) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// The verbatim body from the #5291 user log, wrapper and all. - const LMSTUDIO_5291_BODY: &str = "lmstudio returned: Engine protocol predict request \ - returned 400: {\"error\":{\"code\":400,\"message\":\"Unable to generate parser for \ - this template. Automatic parser generation failed: While executing CallExpression \ - at line 79, column 24 in source: ...multi_step_tool %} {{- raise_exception('No \ - user query found in messages.') }}...Error: Jinja Exception: No user query found \ - in messages.\",\"type\":\"invalid_request_error\"}}"; - - #[test] - fn classifies_the_lmstudio_template_body() { - assert!(is_chat_template_rejection_message(LMSTUDIO_5291_BODY)); - } - - #[test] - fn classifies_inside_a_retry_aggregate() { - // Two attempts fail and the aggregate wraps both; the anchor must - // survive so the template arm still wins over the config-rejection - // arm the aggregate's own wording would otherwise trip. - let aggregate = format!( - "The model `qwen/qwen3.5-9b` may not be available on your provider. \ - Configure a fallback chain via `reliability.model_fallbacks` in your \ - OpenHuman config.\n\nAll providers/models failed. Attempts:\n\ - provider=lmstudio model=qwen/qwen3.5-9b attempt 1/2: {LMSTUDIO_5291_BODY}\n\ - provider=lmstudio model=qwen/qwen3.5-9b attempt 2/2: {LMSTUDIO_5291_BODY}" - ); - assert!(is_chat_template_rejection_message(&aggregate)); - } - - #[test] - fn detection_is_case_insensitive() { - assert!(is_chat_template_rejection_message( - "Error: JINJA EXCEPTION: No User Query Found In Messages." - )); - } +//! OpenHuman retains the user-facing error mapping in `web_chat::web_errors`; +//! reusable recognition of LM Studio, llama.cpp, and Ollama template-engine +//! failures belongs to the default inference driver. - #[test] - fn does_not_classify_unrelated_provider_errors() { - for body in [ - "openai API error (400): invalid temperature: only 1 is allowed for this model", - "The model `gpt-5.5` does not exist or you do not have access to it.", - "lmstudio returned: model 'qwen3.5-9b' does not support tools", - "openrouter API error (429): rate limited", - // A prose mention of templates is not a template rejection. - "Failed to render the prompt template file on disk", - ] { - assert!( - !is_chat_template_rejection_message(body), - "{body:?} must not be classified as a chat-template rejection" - ); - } - } -} +pub use tinyagents::harness::providers::openai::is_chat_template_rejection_message; diff --git a/src/openhuman/tools/impl/presentation/engine.rs b/src/openhuman/tools/impl/presentation/engine.rs index 047ce8ab99..b33e8663cb 100644 --- a/src/openhuman/tools/impl/presentation/engine.rs +++ b/src/openhuman/tools/impl/presentation/engine.rs @@ -39,6 +39,7 @@ pub(super) async fn generate( input: &GeneratePresentationInput, images: &[Vec], deadline: Duration, + config: Option<&crate::openhuman::config::Config>, ) -> Result, PresentationError> { let (deck, payload) = build_request(input, images); let started = std::time::Instant::now(); @@ -55,15 +56,22 @@ pub(super) async fn generate( "[presentation:engine] generate:start" ); - let config = match crate::openhuman::config::Config::load_or_init().await { - Ok(config) => config, - Err(error) => { - return Err(PresentationError::GenerationFailed { - exit_code: -1, - stderr_truncated: PresentationError::truncate_stderr(&format!( - "config unavailable: {error}" - )), - }); + let loaded_config; + let config = match config { + Some(config) => config, + None => { + loaded_config = match crate::openhuman::config::Config::load_or_init().await { + Ok(config) => config, + Err(error) => { + return Err(PresentationError::GenerationFailed { + exit_code: -1, + stderr_truncated: PresentationError::truncate_stderr(&format!( + "config unavailable: {error}" + )), + }); + } + }; + &loaded_config } }; @@ -71,11 +79,11 @@ pub(super) async fn generate( // artifact, and a deadline meant for generation should not be spent on that // — otherwise the first document a user ever asks for is the one that times // out. Cached after the first call, so this is free from then on. - if let Err(error) = documents::ensure_ready(&config).await { + if let Err(error) = documents::ensure_ready(config).await { return Err(PresentationError::from(error)); } - let call = timeout(deadline, documents::generate_pptx(&config, &deck, &payload)).await; + let call = timeout(deadline, documents::generate_pptx(config, &deck, &payload)).await; let elapsed_ms = started.elapsed().as_millis() as u64; match call { diff --git a/src/openhuman/tools/impl/presentation/mod.rs b/src/openhuman/tools/impl/presentation/mod.rs index 689ef869f1..1635821080 100644 --- a/src/openhuman/tools/impl/presentation/mod.rs +++ b/src/openhuman/tools/impl/presentation/mod.rs @@ -67,6 +67,11 @@ pub const TOOL_NAME: &str = "generate_presentation"; /// One-shot `.pptx` generator. See module docs for the request flow. pub struct PresentationTool { workspace_dir: PathBuf, + /// Existing host config when the caller already owns the authoritative + /// runtime snapshot. Keeping this optional preserves the ordinary agent + /// constructor while avoiding a process-global config reload during + /// artifact regeneration. + config: Option, /// Security policy used to validate agent-supplied `File` image paths /// before any filesystem read — an image path must pass the same /// `validate_path` checks (allowed-location, symlink-escape, forbidden @@ -82,6 +87,20 @@ impl PresentationTool { pub fn new(workspace_dir: PathBuf, security: Arc) -> Self { Self { workspace_dir, + config: None, + security, + } + } + + /// Construct the tool with an authoritative host config snapshot. + pub(crate) fn with_config( + workspace_dir: PathBuf, + security: Arc, + config: crate::openhuman::config::Config, + ) -> Self { + Self { + workspace_dir, + config: Some(config), security, } } @@ -263,7 +282,14 @@ impl Tool for PresentationTool { ); } - let bytes = match engine::generate(&input, &resolved_images, GENERATION_TIMEOUT).await { + let bytes = match engine::generate( + &input, + &resolved_images, + GENERATION_TIMEOUT, + self.config.as_ref(), + ) + .await + { Ok(bytes) => bytes, Err(err) => { let _ = fail_artifact(&self.workspace_dir, &meta.id, &err.to_string()).await; diff --git a/vendor/tinyagents b/vendor/tinyagents index 27a3f39dc6..5e026cd8c2 160000 --- a/vendor/tinyagents +++ b/vendor/tinyagents @@ -1 +1 @@ -Subproject commit 27a3f39dc6d7db676efe58d0f7b89752a8ab4746 +Subproject commit 5e026cd8c2c6432390f5c2d9e11add6e384d4a55