diff --git a/AGENTS.md b/AGENTS.md index 7d92468763..8d9251c130 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -277,6 +277,16 @@ progress events. - Use the `tinytools` copy vendored through `vendor/tinyagents/`; a second path creates incompatible Rust types. - Keep conversions mechanical. Policy decisions belong in OpenHuman. +- **Put a change in the repo that owns it, not where it is easiest to land.** + Tool-call parsing, grammars, the `Tool` trait and generic tool types go to + `vendor/tinyagents/vendor/tinytools`; the agent loop, dialects, prompt + cache layout, run policy, progress events and generic harness tools (the + session todo list, goals, delegation graph) go to `vendor/tinyagents` + (`tinyagents-harness` / `tinyagents-graph`); OpenHuman keeps only the host + adapters (scope, dispatch, approvals, progress projection). Open the + upstream PR in that repo first, then move the gitlink here. A host-side + workaround for a harness or parser bug is a stopgap, not a fix: file or + fix it upstream in the same PR. - `openhuman_embed::Runtime` → `Agent` is the public library API: one runtime per process (features, services, backend URL, TinyHumans API key), then any number of independently configured agents on it (`AgentSpec`: provider, diff --git a/crates/openhuman-core/src/agent/prompts/mod_tests_builder_sections_tests.rs b/crates/openhuman-core/src/agent/prompts/mod_tests_builder_sections_tests.rs index 92e4eecb0a..980fd90f01 100644 --- a/crates/openhuman-core/src/agent/prompts/mod_tests_builder_sections_tests.rs +++ b/crates/openhuman-core/src/agent/prompts/mod_tests_builder_sections_tests.rs @@ -57,7 +57,7 @@ fn grounding_contract_appended_to_every_build_path() { // A distinctive clause from GROUNDING_BODY — present regardless of which // builder produced the prompt (single source of truth, central append). - let marker = "Your tools are exactly the ones listed in this prompt"; + let marker = "Your tools are exactly the ones you have been given for this turn"; // 1. Static default chain. let defaults = SystemPromptBuilder::with_defaults().build(&ctx).unwrap(); diff --git a/crates/openhuman-core/src/agent/prompts/mod_tests_subagent_render_tests.rs b/crates/openhuman-core/src/agent/prompts/mod_tests_subagent_render_tests.rs index 9249fec5a9..dffffb9336 100644 --- a/crates/openhuman-core/src/agent/prompts/mod_tests_subagent_render_tests.rs +++ b/crates/openhuman-core/src/agent/prompts/mod_tests_subagent_render_tests.rs @@ -58,7 +58,7 @@ fn render_subagent_system_prompt_renders_workspace_tail() { // sub-agent renderer — same source const, so it can never drift from // `GroundingSection` / the central `build()` append. assert!(rendered.contains("## Grounding and tool use")); - assert!(rendered.contains("Your tools are exactly the ones listed in this prompt")); + assert!(rendered.contains("Your tools are exactly the ones you have been given for this turn")); assert!(rendered.contains("Preserve numeric evidence exactly")); let _ = std::fs::remove_dir_all(workspace); diff --git a/crates/openhuman-core/src/agent/prompts/sections.rs b/crates/openhuman-core/src/agent/prompts/sections.rs index 9cdf145ec3..30617f221a 100644 --- a/crates/openhuman-core/src/agent/prompts/sections.rs +++ b/crates/openhuman-core/src/agent/prompts/sections.rs @@ -460,7 +460,7 @@ impl PromptSection for SafetySection { pub const GROUNDING_HEADING: &str = "Grounding and tool use"; pub const GROUNDING_BODY: &str = "## Grounding and tool use\n\n\ - - Your tools are exactly the ones listed in this prompt. You can only act through them. If a capability is not one of your tools, say so plainly rather than pretending it exists.\n\ + - Your tools are exactly the ones you have been given for this turn, whether they arrive as a tool list or are described in this prompt. You can only act through them. Check that list before saying you lack a capability, and if it is not there, say so plainly rather than pretending it exists.\n\ - Never invent tool names, arguments, ids, slugs, file paths, URLs, chain ids, addresses, quotes, metrics, or any other value. If you do not have it from a tool result or the user, ask for it or look it up with a tool.\n\ - Preserve numeric evidence exactly. For numbers, counts, sizes, dates, timestamps, durations, currencies, percentages, quotas, and ids, copy the exact value from the observed tool result, user message, or cited memory into your answer.\n\ - Do not round, convert units, rewrite relative times, or recalculate numeric values unless the user asks and you show the calculation from observed values. If sources disagree, name the discrepancy instead of choosing a plausible value.\n\ diff --git a/crates/openhuman-core/src/agent/registry/agents/orchestrator/prompt.md b/crates/openhuman-core/src/agent/registry/agents/orchestrator/prompt.md index 981b61fe38..b3d4a41fe2 100644 --- a/crates/openhuman-core/src/agent/registry/agents/orchestrator/prompt.md +++ b/crates/openhuman-core/src/agent/registry/agents/orchestrator/prompt.md @@ -4,12 +4,12 @@ Take the first branch that applies: 1. **Answerable without tools**: reply. Small talk, simple Q&A, general knowledge. 1b. **Needs a capability you do not see listed**: call `tool_search` with the intent in plain words before delegating or declining. Your list is a core set; one clear action on a connected service (send this message, create that issue) is a search-then-call, not a delegation. -2. **Needs a connected service's own data or actions** (inbox, messages, calendar, docs, tickets, "send/check X"): call `delegate_to_integrations_agent` with the `toolkit` from **Connected Integrations**. Use the live service even when memory could plausibly answer. A service being connected is not a reason to touch it: general knowledge, web/news lookups, headlines, date/time and math never delegate here. Not connected? Raise a connect card with `composio_connect`: the list shows what is connected, not what is connectable, so never refuse from it or send the user to settings, and never paste OAuth URLs. If the connect call reports the toolkit unavailable, relay its message; that is the only honest refusal. +2. **Needs a connected service's own data or actions** (inbox, messages, calendar, docs, tickets, "send/check X"): call `delegate_to_integrations_agent` with the `toolkit` from **Connected Integrations**. Use the live service even when memory could plausibly answer. A service being connected is not a reason to touch it: general knowledge, web/news lookups, headlines, date/time, math, and anything public on the web (a public repository, a product page, docs) never delegate here; those are `web_fetch` / `web_search_tool` / `research` work. Delegate to a toolkit only for the user's own account data or actions on it. Not connected? Raise a connect card with `composio_connect`: the list shows what is connected, not what is connectable, so never refuse from it or send the user to settings, and never paste OAuth URLs. If the connect call reports the toolkit unavailable, relay its message; that is the only honest refusal. 3. **Solvable with a direct tool**: do it yourself. `web_search_tool` and `web_fetch` for a fact or a page, `memory_recall` and `memory_store` for the user's own facts, `shell` plus `apply_patch` for repository work. Keep code work end-to-end: edit and verify in the same turn; never delegate merely because a task touches a repository. 4. **Needs a specialist**: the specialists you can call are in your tool list with their own descriptions. **Capabilities not in your tool list** names the ones a skill holds; reach those through `use_skill`. Workers return only their result; carry out any `## Handoff Plan` they return yourself, under the approval gate. 5. **Distill every delegated reply**: keep what answers the question, drop the worker's notes. Never paste a sub-agent's response verbatim. -Live or time-sensitive asks (weather, forecasts, prices, recent news, "use live data") get answered now: one quick fact direct, anything broader via `research`. Don't stop at a lead-in; make the tool call in the same message. +Live or time-sensitive asks (weather, forecasts, prices, recent news, "use live data") get answered now: one quick fact direct, anything broader via `research`. Don't stop at a lead-in; make the tool call in the same message. A `todo` write is bookkeeping, not progress: the response that updates the list also carries the call that does the next item, and an item is `completed` only once its result is in the conversation. Before searching, check **Connected MCP Servers**: if one can answer, hand it to `use_mcp_server`. ## Sub-agents @@ -26,7 +26,7 @@ Three or more steps? Track them on `todo` cards. Don't stop with a plan: execute ## Grounding and tool use -- Your tools are the ones listed in this prompt plus whatever `tool_search` returns. Before saying a capability does not exist, search once; if nothing comes back, say so. +- Your tools are the ones you have been given for this turn (the tool list, however it reaches you) plus whatever `tool_search` returns. Read that list before claiming a capability is missing: `web_search_tool` and `web_fetch` are usually in it. If it is not there, search once; if nothing comes back, say so. - Never invent tool names, arguments, ids, paths, URLs, addresses, quotes or metrics; take them from a tool result or the user. - Preserve numeric evidence exactly: copy numbers, dates, durations, currencies and ids as observed; don't round or recompute unless asked, and then show the working. - A sub-agent's summary is claims: check it against its `Evidence used`, `Actions taken` and `Failed tool calls`. Do not introduce facts its evidence does not support. Output marked truncated, oversized, partial or unavailable is not complete: fetch more or say so. diff --git a/crates/openhuman-core/src/agent/registry/agents/orchestrator/prompt_tests.rs b/crates/openhuman-core/src/agent/registry/agents/orchestrator/prompt_tests.rs index f04ae523eb..9859eeea87 100644 --- a/crates/openhuman-core/src/agent/registry/agents/orchestrator/prompt_tests.rs +++ b/crates/openhuman-core/src/agent/registry/agents/orchestrator/prompt_tests.rs @@ -348,7 +348,7 @@ fn build_scope_gates_integrations_delegation() { // delegation-guide clause. let no_integrations = build(&ctx_with(&[])).unwrap(); assert!( - no_integrations.contains("general knowledge, web/news lookups, headlines, date/time and math never delegate here"), + no_integrations.contains("general knowledge, web/news lookups, headlines, date/time, math, and anything public on the web (a public repository, a product page, docs) never delegate here"), "Step-2 scope gate must keep general/web/date asks off integrations delegation" ); assert!( @@ -555,6 +555,12 @@ fn build_includes_evidence_aware_synthesis_contract() { assert!(body.contains("Preserve numeric evidence exactly")); assert!(body.contains("plus whatever `tool_search` returns")); assert!(body.contains("call `tool_search` with the intent in plain words")); + // Under the native dialect no tool is "listed in this prompt"; a model told + // that its tools are the listed ones concluded it had no web search while + // `web_search_tool` sat in its tool list (thread-7e52b, 2026-09-22). + assert!(!body.contains("listed in this prompt"), "{body}"); + assert!(body.contains("`web_search_tool` and `web_fetch` are usually in it")); + assert!(body.contains("anything public on the web (a public repository, a product page, docs) never delegate here")); } #[test] diff --git a/crates/openhuman-core/src/agent/todos/ops.rs b/crates/openhuman-core/src/agent/todos/ops.rs index 22859aa3bd..229865d335 100644 --- a/crates/openhuman-core/src/agent/todos/ops.rs +++ b/crates/openhuman-core/src/agent/todos/ops.rs @@ -1,27 +1,19 @@ -//! OpenHuman host adapter over [`tinyagents_graph::todos`]. +//! OpenHuman host adapter over TinyAgents' `todos::session_list`. //! //! A todo list is scoped to one agent session ([`TodoScope::Session`]) or, //! when a tool runs with no session at all, to a scratch list -//! ([`TodoScope::Scratch`]). Both live in the one in-process store; the -//! normalisation and rendering are TinyAgents'. The whole-list `replace` is -//! the only write the `todo` tool needs; `clear` is for tests and cleanup. +//! ([`TodoScope::Scratch`]). Both live in the one in-process [`store`]; +//! validation, the whole-list write and rendering are TinyAgents'. This file +//! only maps a scope onto a store key. `clear` is for tests and cleanup. -use serde::{Deserialize, Serialize}; -use tinyagents_graph::todos::store as todos; +use std::sync::Arc; + +use tinyagents_graph::todos::session_list; +use tinyagents_harness::store::Store; use crate::agent::tinyagents::todos::{session_todos_store, SCRATCH_SESSION_ID}; -use crate::agent::todos::types::normalize_cards_for_wire; pub use crate::agent::todos::types::{TaskBoardCard, TaskCardStatus}; - -pub use tinyagents_graph::todos::{parse_status, render_markdown}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TodosSnapshot { - pub session_id: Option, - pub cards: Vec, - pub markdown: String, -} +pub use tinyagents_graph::todos::TodosSnapshot; #[derive(Debug, Clone, PartialEq, Eq)] pub enum TodoScope { @@ -37,43 +29,33 @@ impl TodoScope { } } - fn key(&self) -> &str { + /// The store key this scope's list lives under. + pub fn key(&self) -> &str { self.session_id().unwrap_or(SCRATCH_SESSION_ID) } } -fn snapshot(scope: &TodoScope, value: tinyagents_graph::todos::TodosSnapshot) -> TodosSnapshot { - TodosSnapshot { - session_id: scope.session_id().map(str::to_owned), - cards: value.cards, - markdown: value.markdown, - } +/// The process-wide store every session's list lives in. +pub fn store() -> Arc { + session_todos_store() } -fn finish( +pub async fn replace( scope: &TodoScope, - result: tinyagents_harness::error::Result, + cards: Vec, ) -> Result { - let mut value = result.map_err(|error| error.to_string())?; - normalize_cards_for_wire(&mut value.cards); - Ok(snapshot(scope, value)) -} - -pub async fn replace(scope: &TodoScope, cards: Vec) -> Result { - let store = session_todos_store(); - finish(scope, todos::replace(&store, scope.key(), cards).await) + session_list::write(&store(), scope.key(), cards) + .await + .map_err(|error| error.to_string()) } pub async fn clear(scope: &TodoScope) -> Result { - let store = session_todos_store(); - finish(scope, todos::clear(&store, scope.key()).await) + replace(scope, Vec::new()).await } pub async fn list(scope: &TodoScope) -> Result { - let store = session_todos_store(); - todos::list(&store, scope.key()) + session_list::read(&store(), scope.key()) .await - .map(|value| snapshot(scope, value)) .map_err(|error| error.to_string()) } diff --git a/crates/openhuman-core/src/agent/todos/types.rs b/crates/openhuman-core/src/agent/todos/types.rs index 42284a5250..5a68fee050 100644 --- a/crates/openhuman-core/src/agent/todos/types.rs +++ b/crates/openhuman-core/src/agent/todos/types.rs @@ -1,24 +1,3 @@ -//! TinyAgents todo types and OpenHuman wire-format normalization. +//! TinyAgents todo types, re-exported for OpenHuman callers. -use chrono::{TimeZone, Utc}; - -pub use tinyagents_graph::todos::{TaskApprovalMode, TaskBoard, TaskBoardCard, TaskCardStatus}; - -pub(crate) fn normalize_timestamp_for_wire(value: &str) -> String { - if chrono::DateTime::parse_from_rfc3339(value).is_ok() { - return value.to_owned(); - } - if let Ok(updated_at_ms) = value.parse::() { - if let Some(updated_at) = Utc.timestamp_millis_opt(updated_at_ms).single() { - return updated_at.to_rfc3339(); - } - } - tracing::warn!(updated_at = %value, "invalid todo timestamp; using current time"); - Utc::now().to_rfc3339() -} - -pub(crate) fn normalize_cards_for_wire(cards: &mut [TaskBoardCard]) { - for card in cards { - card.updated_at = normalize_timestamp_for_wire(&card.updated_at); - } -} +pub use tinyagents_graph::todos::{TaskBoardCard, TaskCardStatus}; diff --git a/crates/openhuman-core/src/agent/tools/todo.rs b/crates/openhuman-core/src/agent/tools/todo.rs index aef5846d35..b1fa6bb0ef 100644 --- a/crates/openhuman-core/src/agent/tools/todo.rs +++ b/crates/openhuman-core/src/agent/tools/todo.rs @@ -1,25 +1,25 @@ //! `todo` — the session's todo list, the way Claude Code and Codex have it. //! -//! One call writes the whole list: `{"todos": [{"content", "status"}]}`. -//! There is no per-card CRUD, no approval gate, no evidence, no plan; the -//! list is a progress checklist the model rewrites as it works. It is scoped -//! to the agent session the turn runs in (in memory, for the life of the -//! process) via [`crate::agent::todos::ops`]; without a session (a bare -//! `execute` in a test) it falls back to a scratch list. Calling with no -//! `todos` returns the current list. +//! The tool itself is TinyAgents' `todos::session_list` (schema, argument +//! validation, the whole-list write, markdown). This file is only the host +//! adapter: it decides **which** list a call is about — the agent session the +//! turn runs in, in memory for the life of the process — and registers the +//! harness dispatch. Nothing here may turn a bad argument into an `Err`: a +//! dispatch `Err` is fatal to the run, and a turn died that way when a model +//! sent the retired `{"cards": …}` shape to the previous host-side copy. use crate::agent::harness::fork_context::ParentExecutionContext; use crate::agent::todos::ops::{self, TodoScope}; -use crate::agent::todos::types::{TaskBoardCard, TaskCardStatus}; use async_trait::async_trait; -use serde::Deserialize; -use serde_json::json; use std::sync::Arc; +use tinyagents_graph::todos::session_list; use tinyagents_harness::context::RunContext; use tinyagents_harness::tool::{ToolDispatch, ToolExecutionContext}; use tinytools::{PermissionLevel, Tool, ToolCallOptions, ToolResult, ToolRunContext}; -pub struct TodoTool; +pub struct TodoTool { + inner: session_list::SessionTodoTool, +} pub(crate) struct TodoToolDispatch { tool: Arc, @@ -43,15 +43,24 @@ impl ToolDispatch<(), crate::agent::tinyagents::host::OpenHumanRunContext> for T parent: &RunContext, ) -> anyhow::Result { let context = ToolExecutionContext::from_run_context(parent, _call_id.clone()); - TodoTool::new() + match TodoTool::new() .execute_with_parent_context(arguments, parent.data.parent.clone(), Some(&context)) .await + { + Ok(result) => Ok(result), + Err(error) => { + tracing::warn!(%error, "[tool][todo] rejected call"); + Ok(ToolResult::error(format!("todo failed: {error}"))) + } + } } } impl TodoTool { pub fn new() -> Self { - Self + Self { + inner: session_list::SessionTodoTool::new(ops::store()), + } } } @@ -61,50 +70,18 @@ impl Default for TodoTool { } } -/// One item as the model writes it. `status` accepts the Claude-style -/// `pending` / `in_progress` / `completed` plus the older `todo` / `done` -/// spellings the store already parses. -#[derive(Deserialize)] -struct TodoItem { - content: String, - #[serde(default)] - status: Option, -} - #[async_trait] impl Tool for TodoTool { fn name(&self) -> &str { - "todo" + self.inner.name() } fn description(&self) -> &str { - "Your todo list for this conversation. Pass the complete list every time; it \ - replaces what was there. Use it for work with 3+ steps: write the steps up front, \ - keep exactly one `in_progress`, mark each `completed` the moment it is done. Omit \ - `todos` to read the current list." + self.inner.description() } fn parameters_schema(&self) -> serde_json::Value { - json!({ - "type": "object", - "properties": { - "todos": { - "type": "array", - "description": "The full list, in order.", - "items": { - "type": "object", - "properties": { - "content": { "type": "string" }, - "status": { - "type": "string", - "enum": ["pending", "in_progress", "completed"] - } - }, - "required": ["content", "status"] - } - } - } - }) + self.inner.parameters_schema() } fn permission_level(&self) -> PermissionLevel { @@ -136,64 +113,7 @@ impl TodoTool { ) -> anyhow::Result { let scope = current_scope(parent.as_ref(), tool_context); tracing::debug!(session_id = ?scope.session_id(), "[tool][todo] dispatch"); - - let result = match args.get("todos") { - None | Some(serde_json::Value::Null) => ops::list(&scope).await, - Some(raw) => { - let items: Vec = serde_json::from_value(raw.clone()) - .map_err(|e| anyhow::anyhow!("invalid `todos`: {e}"))?; - let mut cards = Vec::with_capacity(items.len()); - for item in items { - let content = item.content.trim(); - if content.is_empty() { - anyhow::bail!("every todo needs non-empty `content`"); - } - let mut card = TaskBoardCard::new(content); - card.status = match item.status.as_deref() { - None => TaskCardStatus::Todo, - Some(raw) => ops::parse_status(raw).map_err(anyhow::Error::msg)?, - }; - cards.push(card); - } - ops::replace(&scope, cards).await - } - }; - - match result { - Ok(snap) => { - let todos: Vec = snap - .cards - .iter() - .map(|card| { - json!({ - "content": card.title, - "status": wire_status(card.status), - }) - }) - .collect(); - let payload = json!({ - "sessionId": snap.session_id, - "todos": todos, - "markdown": snap.markdown, - }); - Ok(ToolResult::success(payload.to_string())) - } - Err(err) => Ok(ToolResult::error(err)), - } - } -} - -/// The three states the model is told about. Store states the list can no -/// longer produce (`ready`, `awaiting_approval`, `rejected`, `blocked`) fold -/// into the nearest one so an old thread still reads sensibly. -fn wire_status(status: TaskCardStatus) -> &'static str { - match status { - TaskCardStatus::InProgress => "in_progress", - TaskCardStatus::Done | TaskCardStatus::Rejected => "completed", - TaskCardStatus::Todo - | TaskCardStatus::Ready - | TaskCardStatus::AwaitingApproval - | TaskCardStatus::Blocked => "pending", + Ok(session_list::call(&ops::store(), scope.key(), &args).await?) } } diff --git a/crates/openhuman-core/src/agent/tools/todo_tests.rs b/crates/openhuman-core/src/agent/tools/todo_tests.rs index 7c926b9ae2..a23249e654 100644 --- a/crates/openhuman-core/src/agent/tools/todo_tests.rs +++ b/crates/openhuman-core/src/agent/tools/todo_tests.rs @@ -1,5 +1,6 @@ use super::*; -use serde_json::Value; +use crate::agent::todos::ops::{TaskBoardCard, TaskCardStatus}; +use serde_json::{json, Value}; /// Serialize tests that share the process-global scratch store. Same lock /// as `todos::ops` — otherwise the two test modules race under `cargo test`'s @@ -77,20 +78,38 @@ async fn two_in_progress_items_are_rejected() { reset_scratch().await; } +/// Bad input is a tool error the model can correct, never an `Err`: a +/// dispatch `Err` is fatal to the whole run in the harness, and a turn died +/// exactly that way when a model sent the retired `{"cards": …}` shape. #[tokio::test] -async fn empty_content_and_unknown_status_are_errors() { +async fn bad_input_is_a_tool_error_not_a_harness_error() { let tool = TodoTool::new(); - let err = tool - .execute(json!({ "todos": [{ "content": " ", "status": "pending" }] })) - .await - .unwrap_err(); - assert!(err.to_string().contains("content"), "{err}"); - - let err = tool - .execute(json!({ "todos": [{ "content": "x", "status": "someday" }] })) - .await - .unwrap_err(); - assert!(err.to_string().contains("invalid status"), "{err}"); + for (args, expect) in [ + ( + json!({ "todos": [{ "content": " ", "status": "pending" }] }), + "content", + ), + ( + json!({ "todos": [{ "content": "x", "status": "someday" }] }), + "status must be", + ), + (json!({ "todos": "not a list" }), "invalid `todos`"), + ( + json!({ "cards": [{ "content": "x", "status": "todo" }] }), + "pass `todos`", + ), + ] { + let result = tool + .execute(args.clone()) + .await + .expect("never an Err: {args}"); + assert!(result.is_error, "{args}"); + assert!( + result.output().contains(expect), + "{args}: {}", + result.output() + ); + } } #[test] @@ -99,14 +118,21 @@ fn schema_is_the_claude_shape() { let schema = tool.parameters_schema(); let props = &schema["properties"]; assert!(props.get("todos").is_some()); - assert_eq!(props.as_object().unwrap().len(), 1, "no per-card ops: {props}"); + assert_eq!( + props.as_object().unwrap().len(), + 1, + "no per-card ops: {props}" + ); assert_eq!( props["todos"]["items"]["properties"]["status"]["enum"], json!(["pending", "in_progress", "completed"]) ); let desc = tool.description(); assert!(desc.contains("3+ steps"), "missing when-to-use guidance"); - assert!(desc.contains("one `in_progress`"), "missing single-in_progress rule"); + assert!( + desc.contains("one `in_progress`"), + "missing single-in_progress rule" + ); assert!( !desc.contains("board"), "the tool must not describe itself as a board" @@ -168,17 +194,30 @@ fn every_agent_binds_to_its_own_session() { #[tokio::test] async fn sessions_do_not_see_each_other_and_a_list_survives_across_turns() { - let a = TodoScope::Session { id: "sess-a".into() }; - let b = TodoScope::Session { id: "sess-b".into() }; + let a = TodoScope::Session { + id: "sess-a".into(), + }; + let b = TodoScope::Session { + id: "sess-b".into(), + }; crate::agent::todos::ops::clear(&a).await.unwrap(); crate::agent::todos::ops::clear(&b).await.unwrap(); let mut card = TaskBoardCard::new("only in a"); card.status = TaskCardStatus::InProgress; - crate::agent::todos::ops::replace(&a, vec![card]).await.unwrap(); + crate::agent::todos::ops::replace(&a, vec![card]) + .await + .unwrap(); let a_again = crate::agent::todos::ops::list(&a).await.unwrap(); - assert_eq!(a_again.cards.len(), 1, "a later turn of the same session reads it back"); - assert_eq!(a_again.session_id.as_deref(), Some("sess-a")); - assert!(crate::agent::todos::ops::list(&b).await.unwrap().cards.is_empty()); + assert_eq!( + a_again.cards.len(), + 1, + "a later turn of the same session reads it back" + ); + assert!(crate::agent::todos::ops::list(&b) + .await + .unwrap() + .cards + .is_empty()); } diff --git a/crates/openhuman-core/src/integrations/task_sources/store.rs b/crates/openhuman-core/src/integrations/task_sources/store.rs index 8c2ebfc499..ce8fd614e7 100644 --- a/crates/openhuman-core/src/integrations/task_sources/store.rs +++ b/crates/openhuman-core/src/integrations/task_sources/store.rs @@ -295,9 +295,8 @@ pub fn mark_ingested(config: &Config, source_id: &str, task: &NormalizedTask) -> /// brand-new one in its logs. pub fn was_ingested(config: &Config, source_id: &str, external_id: &str) -> Result { with_connection(config, |conn| { - let mut stmt = conn.prepare( - "SELECT 1 FROM ingested_tasks WHERE source_id = ?1 AND external_id = ?2", - )?; + let mut stmt = + conn.prepare("SELECT 1 FROM ingested_tasks WHERE source_id = ?1 AND external_id = ?2")?; let mut rows = stmt.query(params![source_id, external_id])?; Ok(rows.next()?.is_some()) }) diff --git a/crates/openhuman-core/src/integrations/task_sources/store_tests.rs b/crates/openhuman-core/src/integrations/task_sources/store_tests.rs index 18a9ad0b61..c11b3ebf36 100644 --- a/crates/openhuman-core/src/integrations/task_sources/store_tests.rs +++ b/crates/openhuman-core/src/integrations/task_sources/store_tests.rs @@ -163,8 +163,7 @@ fn remove_deletes_and_cascades_ingested() { 25, ) .unwrap(); - mark_ingested(&config, &src.id, &sample_task("1", "A", "2025-01-01")) - .unwrap(); + mark_ingested(&config, &src.id, &sample_task("1", "A", "2025-01-01")).unwrap(); remove_source(&config, &src.id).unwrap(); assert!(get_source(&config, &src.id).is_err()); @@ -268,10 +267,8 @@ fn list_ingested_orders_newest_first() { ) .unwrap(); - mark_ingested(&config, &src.id, &sample_task("1", "first", "2025-01-01")) - .unwrap(); - mark_ingested(&config, &src.id, &sample_task("2", "second", "2025-01-02")) - .unwrap(); + mark_ingested(&config, &src.id, &sample_task("1", "first", "2025-01-01")).unwrap(); + mark_ingested(&config, &src.id, &sample_task("2", "second", "2025-01-02")).unwrap(); let listed = list_ingested(&config, &src.id, 10).unwrap(); assert_eq!(listed.len(), 2); // Newest ingested_at first; "2" was inserted last. diff --git a/scripts/prompt-budget.limits b/scripts/prompt-budget.limits index 04c77eead0..4ede93c5ad 100644 --- a/scripts/prompt-budget.limits +++ b/scripts/prompt-budget.limits @@ -222,38 +222,38 @@ # without their generated budget update. The morning briefing's # fixed prefix is 3 B larger; all other recorded ceilings stay put. -morning_briefing:10654:59064 -trigger_triage:7422:0 -workflow_builder:76386:28987 -summarizer:7236:0 -tools_agent:5114:59064 -orchestrator:9169:20455 -code_executor:11340:13028 -crypto_agent:10877:10454 -task_manager_agent:4416:7602 -planner:7714:5306 -skill_creator:5349:11280 -flow_discovery:8407:8228 -profile_memory_agent:5400:11010 -settings_agent:4606:9652 -context_scout:8737:5438 -skill_executor:7674:5469 -scheduler_agent:7758:5144 -agent_memory:8116:5423 -skill_setup:5252:5693 -trigger_reactor:6446:5606 -mcp_agent:7032:2569 -flow_memory_agent:7411:2534 -tool_maker:4414:4543 -presentation_agent:4676:4265 -video_agent:5144:1106 -help:6627:952 -image_agent:5188:1106 -goals_agent:5111:1191 -vision_agent:5056:1106 -archivist:4311:1686 -researcher:5485:816 -critic:4405:695 +morning_briefing:10769:59334 +trigger_triage:7537:0 +workflow_builder:76501:28987 +summarizer:7351:0 +tools_agent:5229:59334 +orchestrator:9717:20704 +code_executor:11455:13298 +crypto_agent:10992:10454 +task_manager_agent:4531:7602 +planner:7829:5576 +skill_creator:5464:11550 +flow_discovery:8522:8228 +profile_memory_agent:5515:11010 +settings_agent:4721:9652 +context_scout:8852:5438 +skill_executor:7789:5469 +scheduler_agent:7873:5144 +agent_memory:8231:5423 +skill_setup:5367:5693 +trigger_reactor:6561:5606 +mcp_agent:7147:2569 +flow_memory_agent:7526:2534 +tool_maker:4529:4543 +presentation_agent:4791:4265 +video_agent:5259:1106 +help:6742:952 +image_agent:5303:1106 +goals_agent:5226:1191 +vision_agent:5171:1106 +archivist:4426:1686 +researcher:5600:816 +critic:4520:695 # ── Per-tool schema ratchet ────────────────────────────────────────────── # @@ -331,9 +331,9 @@ tool:suggest_workflows:2445 tool:spawn_async_subagent:1556 tool:save_workflow:1957 tool:spawn_parallel_agents:1839 -tool:todo:590 +tool:todo:860 tool:search_tool_catalog:1695 -tool:use_skill:1732 +tool:use_skill:1711 # One action-dispatched memory surface replaces the separately registered # memory operations while keeping read/write/forget routing explicit. tool:memory:3937 diff --git a/vendor/tinyagents b/vendor/tinyagents index 0bc4ec443b..3c9ba00cea 160000 --- a/vendor/tinyagents +++ b/vendor/tinyagents @@ -1 +1 @@ -Subproject commit 0bc4ec443bdbd87170ea014b0a7e79991348394b +Subproject commit 3c9ba00cea1581352977f8c5b7167c1747fa7c8d