Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion crates/openhuman-core/src/agent/prompts/sections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.<!--route:mcp-->

## Sub-agents
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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]
Expand Down
60 changes: 21 additions & 39 deletions crates/openhuman-core/src/agent/todos/ops.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub cards: Vec<TaskBoardCard>,
pub markdown: String,
}
pub use tinyagents_graph::todos::TodosSnapshot;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TodoScope {
Expand All @@ -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<dyn Store> {
session_todos_store()
}

fn finish(
pub async fn replace(
scope: &TodoScope,
result: tinyagents_harness::error::Result<tinyagents_graph::todos::TodosSnapshot>,
cards: Vec<TaskBoardCard>,
) -> Result<TodosSnapshot, String> {
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<TaskBoardCard>) -> Result<TodosSnapshot, String> {
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<TodosSnapshot, String> {
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<TodosSnapshot, String> {
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())
}

Expand Down
25 changes: 2 additions & 23 deletions crates/openhuman-core/src/agent/todos/types.rs
Original file line number Diff line number Diff line change
@@ -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::<i64>() {
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};
Loading
Loading