Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion EMBED.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- [Observability](gitbooks/developing/embed/concepts/observability.md): Subscribe with `Runtime::events` before starting work you want to observe. Each event has a sequence number and runtime identity, with an agent ID and per-call turn ID where applicable. Repeated calls on the same durable session receive different turn IDs; the session ID is not the observation ID.
- [Profiles and SaaS](gitbooks/developing/embed/concepts/profiles-saas.md): Ordinary runtime agents share an operator's configuration path and credentials. Use `ProfileRuntime` when one server serves different authenticated users who must not share those resources. Provision a profile, install its credential, and retain a `ProfileHandle` while serving that user's requests.
- [Providers](gitbooks/developing/embed/concepts/providers.md): A runtime provider is the default inference choice for its agents. An agent can override it with an OpenAI-compatible route or a native `ChatModel<()>` through `Provider::custom`. The `providers` facade exposes the request/response contracts so custom adapters do not need to implement an HTTP server.
- [Runtime defaults](gitbooks/developing/embed/concepts/runtime-defaults.md): The runtime's default provider, access policy and `ModelDefaults` reduce repeated setup. `AgentSpec` can override those defaults for a reviewer, a coding agent or another workload without changing its siblings. Temperature and token limits are forwarded to the selected native model request, rather than being merely descriptive builder values.
- [Runtime defaults](gitbooks/developing/embed/concepts/runtime-defaults.md): The runtime's default provider, access policy and `ModelDefaults` reduce repeated setup. `AgentSpec` can override those defaults for an analyst, a writing agent or another workload without changing its siblings. Temperature and token limits are forwarded to the selected native model request, rather than being merely descriptive builder values.
- [Runtime](gitbooks/developing/embed/concepts/runtime.md): A `Runtime` boots the in-process core once. It owns the selected domains, background services, module host and runtime defaults. Its agents share that core while using separately derived contexts. Build one runtime and pass an `Arc<Runtime>` to the parts of your application that register agents.
- [Skills](gitbooks/developing/embed/concepts/skills.md): A skill bundle contains `SKILL.md` plus any referenced resources. `AgentSpec::skills_dir` copies bundles into the agent's `agents/<id>/skills/` tree. Copying is intentional: discovery rejects symlinked bundles, so linking a shared directory does not install it.
- [Testing](gitbooks/developing/embed/concepts/testing.md): The runnable examples default to loopback fixtures. They assert request bodies, tool results, file effects, transcript scope and event ordering; a successful Rust compilation alone does not prove those behaviors. Live example execution is explicitly opt-in through the documented environment variables.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ Most agent harnesses run one heavy process per agent and resend a big prompt on

<h3>Built for developers</h3>

<p><a href="https://tinyhumans.gitbook.io/openhuman/developing/quickstart">Rust quickstart</a> · <a href="https://tinyhumans.gitbook.io/openhuman/developing/embedding">Embedding guide</a> · <a href="./crates/openhuman-embed/examples">Examples</a></p>
<p><a href="https://tinyhumans.gitbook.io/openhuman/developing/quickstart">Rust quickstart</a> · <a href="./gitbooks/developing/embed/README.md">Embedding guide</a> · <a href="./crates/openhuman-embed/examples">Examples</a></p>

<p>Use it as a Rust library: call an agent like any other function, or run a whole fleet from one small server.</p>

Expand Down Expand Up @@ -288,7 +288,7 @@ let reply = agent.run("Summarize what you can see in this directory.").await?;
println!("{}", reply.reply);
```

Next: the [Rust quickstart](https://tinyhumans.gitbook.io/openhuman/developing/quickstart), the [embedding guide](https://tinyhumans.gitbook.io/openhuman/developing/embedding) and the [developer docs](https://tinyhumans.gitbook.io/openhuman/developing).
Next: the [Rust quickstart](https://tinyhumans.gitbook.io/openhuman/developing/quickstart), the [embedding guide](./gitbooks/developing/embed/README.md) and the [developer docs](https://tinyhumans.gitbook.io/openhuman/developing).

---

Expand Down
2 changes: 1 addition & 1 deletion crates/openhuman-embed/CONSUMERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ need to import core constants or hand-assemble the builder.
Create one `Runtime` during server startup and share it with `Arc<Runtime>`.
Create or reuse independently configured `Agent` handles on that runtime;
clone an agent handle for concurrent requests and use distinct session IDs
for unrelated reviews. Do not call `Runtime::builder().build()` per HTTP
for unrelated requests. Do not call `Runtime::builder().build()` per HTTP
request: process-wide keyring, event bus and subscriber ownership deliberately
refuse a second live runtime with `RuntimeError::AlreadyRunning`.

Expand Down
9 changes: 9 additions & 0 deletions crates/openhuman-embed/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,12 @@ required-features = ["channels"]
[[example]]
name = "storage"
required-features = ["storage-sqlite"]

# SaaS profiles are exposed only when the channel facade is compiled.
[[test]]
name = "saas_profiles"
required-features = ["channels"]

[[example]]
name = "profiles"
required-features = ["channels"]
4 changes: 2 additions & 2 deletions crates/openhuman-embed/ROUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ let ladder = CompletionLadder::new(CompletionRung::new(completer.clone(), "opena
.fallback(CompletionRung::new(completer, "minimax/minimax-m3").unpinned())
.truncation_retry(TruncationRetry::new(2, 4096));
let outcome = ladder.complete(
CompletionRequest::new("overridden-by-rung", vec![ChatMessage::user("Review the attached image.")
.with_image("https://example.org/review.png")])
CompletionRequest::new("overridden-by-rung", vec![ChatMessage::user("Describe the attached image.")
.with_image("https://example.org/image.png")])
.max_tokens(1024)
.provider_options(serde_json::json!({"provider": {"only": ["preferred"]}, "usage": {"include": true}})),
).await?;
Expand Down
4 changes: 2 additions & 2 deletions crates/openhuman-embed/STRUCTURED-OUTPUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ successful read. Before execution the provider gets `tool_choice: required` and
no final response format; afterwards it receives the requested answer schema.
The host also enforces this requirement when a provider ignores the wire hint.

Use a `HostOnly`, read-only agent and the [repository tools](src/repository/README.md)
for untrusted review input. Validation does not grant tool or write permissions.
Use a `HostOnly`, read-only agent with host-owned application tools
for untrusted application input. Validation does not grant tool or write permissions.

Tests: `structured_validation`, `structured_turns`, `tool_required_routing`, and
`completion_routing` use loopback provider fixtures and require no model key.
3 changes: 2 additions & 1 deletion crates/openhuman-embed/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
- [A per-agent post-turn hook observes completed turns](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/hooks.rs): A per-agent post-turn hook observes completed turns. (offline; optional live via OPENHUMAN_EXAMPLE_LIVE=1 and BASE_URL/API_KEY/MODEL.)
- [Host tools and HostOnly keep the advertised tool catalog exact](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/host_tools.rs): Host tools and HostOnly keep the advertised tool catalog exact. (offline with loopback stubs; no live path.)
- [Lean runtime without background services](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/lean_headless.rs): Lean runtime without background services. (offline with loopback stubs; optional live via OPENHUMAN_EXAMPLE_LIVE.)
- [Linux agent fleet memory and latency](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/linux_fleet.rs): Measure retained runtime-owned agents using loopback inference and two worker threads. (offline on Linux; use a fresh constrained cgroup for release measurements.)
- [Connect an actual MCP protocol stub over loopback](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/mcp.rs): Connect an actual MCP protocol stub over loopback. (offline with loopback stubs; no live path.; feature: mcp)
- [Tenant scoped memory facade with an in-memory engine](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/memory.rs): Tenant scoped memory facade with an in-memory engine. (offline with loopback stubs; no live path.)
- [SaaS profiles isolate conversation history](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/profiles.rs): SaaS profiles isolate conversation history. (offline with loopback stubs; no live path.)
- [SaaS profiles isolate conversation history](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/profiles.rs): SaaS profiles isolate conversation history. (offline with loopback stubs; no live path.; feature: channels)
- [Hello agent: a prompt in and a reply out](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/run_turn.rs): Hello agent: a prompt in and a reply out. (offline; optional live via OPENHUMAN_EXAMPLE_LIVE=1 and BASE_URL/API_KEY/MODEL.)
- [Runtime lifecycle events and live hook registration](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/runtime_events.rs): Observe ordered metadata and add/remove a runtime-wide hook while agents keep running. (offline with a loopback provider; no live path.)
- [Access tiers and explicit sandbox choices](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-embed/examples/sandbox_access.rs): Access tiers and explicit sandbox choices. (offline with loopback stubs; no live path.)
Expand Down
3 changes: 3 additions & 0 deletions crates/openhuman-embed/examples/host_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ impl openhuman_embed::Tool for Ping {
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({"type":"object","properties":{}})
}
fn policy(&self) -> openhuman_embed::ToolPolicy {
openhuman_embed::ToolPolicy::read_only()
}
async fn execute(&self, _: serde_json::Value) -> anyhow::Result<openhuman_embed::ToolResult> {
self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(openhuman_embed::ToolResult::success("pong"))
Expand Down
2 changes: 1 addition & 1 deletion crates/openhuman-embed/examples/profiles.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Title: SaaS profiles isolate conversation history
//! Summary: SaaS profiles isolate conversation history.
//! Run: offline with loopback stubs; no live path.
//! Feature: default
//! Feature: channels

mod support;
fn main() -> anyhow::Result<()> {
Expand Down
10 changes: 5 additions & 5 deletions crates/openhuman-embed/examples/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ async fn run() -> anyhow::Result<()> {
.await?;
// ANCHOR: skills
let skills = tempfile::tempdir()?;
std::fs::create_dir(skills.path().join("review"))?;
std::fs::write(skills.path().join("review/SKILL.md"),
"---\nname: review\ndescription: Review Rust functions carefully.\n---\nCheck error paths.\n")?;
std::fs::create_dir(skills.path().join("summarize"))?;
std::fs::write(skills.path().join("summarize/SKILL.md"),
"---\nname: summarize\ndescription: Summarize documents clearly.\n---\nInclude the main points.\n")?;
let agent = runtime.agent(AgentSpec::new("skilled").skills_dir(skills.path()))?;
let copied = agent
.workspace_dir()
.join("agents/skilled/skills/review/SKILL.md");
.join("agents/skilled/skills/summarize/SKILL.md");
assert_eq!(
std::fs::read_to_string(&copied)?,
std::fs::read_to_string(skills.path().join("review/SKILL.md"))?
std::fs::read_to_string(skills.path().join("summarize/SKILL.md"))?
);
assert!(!std::fs::symlink_metadata(copied)?.file_type().is_symlink());
assert!(!agent.run("Hello").await?.reply.is_empty());
Expand Down
12 changes: 6 additions & 6 deletions crates/openhuman-embed/examples/structured_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async fn run() -> anyhow::Result<()> {
.build()
.await?;
// ANCHOR: structured_output
let json_provider = support::provider(r#"{"verdict":"approve"}"#).await;
let json_provider = support::provider(r#"{"summary":"complete"}"#).await;
let agent = runtime.agent(
AgentSpec::new("structured")
.provider(support::route(&json_provider, "fixture"))
Expand All @@ -31,19 +31,19 @@ async fn run() -> anyhow::Result<()> {
.tools(ToolScopeSpec::HostOnly),
),
)?;
let outcome = agent.turn("Review").response_format(openhuman_embed::complete::ResponseFormat::JsonSchema {
name: "review".into(),
schema: serde_json::json!({"type":"object","properties":{"verdict":{"type":"string"}},"required":["verdict"]}),
let outcome = agent.turn("Analyze").response_format(openhuman_embed::complete::ResponseFormat::JsonSchema {
name: "analysis".into(),
schema: serde_json::json!({"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}),
}).max_tokens(128).send().await?;
assert_eq!(
outcome.structured,
Some(serde_json::json!({"verdict":"approve"}))
Some(serde_json::json!({"summary":"complete"}))
);
let requests = support::chat_requests(&json_provider).await;
let body: serde_json::Value = serde_json::from_slice(&requests[0].body)?;
assert_eq!(body["response_format"]["type"], "json_schema");
assert_eq!(body["max_tokens"], 128);
println!("validated verdict: approve");
println!("validated summary: complete");
// ANCHOR_END: structured_output
support::passed("structured_output");
Ok(())
Expand Down
36 changes: 18 additions & 18 deletions crates/openhuman-embed/examples/two_agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,31 @@ async fn run() -> anyhow::Result<()> {
.build()
.await?;
// ANCHOR: two_agents
let reviewer_dir = tempfile::tempdir()?;
let fixer_dir = tempfile::tempdir()?;
let reviewer = runtime.agent(
AgentSpec::new("reviewer")
.system_prompt("REVIEWER_PROMPT: review code")
.action_dir(reviewer_dir.path())
let analyst_dir = tempfile::tempdir()?;
let writer_dir = tempfile::tempdir()?;
let analyst = runtime.agent(
AgentSpec::new("analyst")
.system_prompt("ANALYST_PROMPT: summarize documents")
.action_dir(analyst_dir.path())
.access(openhuman_embed::Access::readonly()),
)?;
let fixer = runtime.agent(
AgentSpec::new("fixer")
.system_prompt("FIXER_PROMPT: explain fixes")
.action_dir(fixer_dir.path())
let writer = runtime.agent(
AgentSpec::new("writer")
.system_prompt("WRITER_PROMPT: compose explanations")
.action_dir(writer_dir.path())
.access(openhuman_embed::Access::full()),
)?;
assert_ne!(reviewer.action_dir(), fixer.action_dir());
assert_ne!(reviewer.home_dir(), fixer.home_dir());
assert_ne!(reviewer.workspace_dir(), reviewer.action_dir());
assert!(!reviewer.run("Review").await?.reply.is_empty());
assert!(!fixer.run("Explain").await?.reply.is_empty());
assert_ne!(analyst.action_dir(), writer.action_dir());
assert_ne!(analyst.home_dir(), writer.home_dir());
assert_ne!(analyst.workspace_dir(), analyst.action_dir());
assert!(!analyst.run("Analyze").await?.reply.is_empty());
assert!(!writer.run("Explain").await?.reply.is_empty());
if support::offline() {
let requests = support::chat_requests(&provider).await;
assert_eq!(requests.len(), 2);
assert!(String::from_utf8_lossy(&requests[0].body).contains("REVIEWER_PROMPT"));
assert!(!String::from_utf8_lossy(&requests[0].body).contains("FIXER_PROMPT"));
assert!(String::from_utf8_lossy(&requests[1].body).contains("FIXER_PROMPT"));
assert!(String::from_utf8_lossy(&requests[0].body).contains("ANALYST_PROMPT"));
assert!(!String::from_utf8_lossy(&requests[0].body).contains("WRITER_PROMPT"));
assert!(String::from_utf8_lossy(&requests[1].body).contains("WRITER_PROMPT"));
}
println!("two distinct prompts and action workspaces verified");
// ANCHOR_END: two_agents
Expand Down
5 changes: 4 additions & 1 deletion crates/openhuman-embed/src/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ directories; transcripts and memory persist with the workspace.
## Tool factories

`AgentSpec::tools` supplies a host's own in-process tools when an agent is

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique likely

Describe the policy contract used by the core

The statement that this is the same contract as the core is not supported by the visible APIs: openhuman_embed::ToolPolicy is re-exported from tinytools, while the core's agent policy code and attachment integration use openhuman_core::agent::tool_policy::ToolPolicy and ToolPolicyDecision. These are distinct paths and may not be interchangeable, so hosts could implement the documented policy type while the agent policy machinery never consumes it. Clarify the intended relationship and document the actual trait required by Tool::policy.

[RULE] incorrect-contract-description ·

created. Each tool is a real tool with its own schema on the wire, unlike a
created. Import `Tool`, `ToolResult` and `ToolPolicy` from `openhuman_embed`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Document imports that are actually exported

The visible openhuman_embed exports include ToolPolicy from tinytools, but no corresponding Tool or ToolResult re-exports were found. A host following this README therefore cannot compile use openhuman_embed::{Tool, ToolResult, ToolPolicy}. Either document the module paths that are publicly available or add the missing root re-exports before directing users to import these names from openhuman_embed.

[RULE] invalid-public-api ·

`Tool::policy` declares execution requirements using the same vendored contract
as the core. Application-specific executors belong to the embedding host.
Each tool is a real tool with its own schema on the wire, unlike a
tool reached through an MCP server's `mcp_call_tool` envelope. The factory
runs once per session build, which in practice is once per turn: an `Agent`
is `Clone` and `Box<dyn Tool>` is not, so a stored belt could not survive the
Expand Down
4 changes: 2 additions & 2 deletions crates/openhuman-embed/src/agent/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ pub enum ToolScopeSpec {
Named(Vec<String>),
/// The host's tools and nothing else.
///
/// For an agent that must never act — a reviewer reading an untrusted
/// diff. The registry the model sees is built from the tools the host
/// For an agent that must never act — an analyst reading an untrusted
/// document. The registry the model sees is built from the tools the host
/// supplies ([`AgentSpec::tools`](super::AgentSpec::tools),
/// [`Agent::attach_tools`](super::Agent::attach_tools)) alone: no
/// config-derived, delegation, memory, skill or MCP tool, and a
Expand Down
8 changes: 4 additions & 4 deletions crates/openhuman-embed/src/agent/definition_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ fn setters_override_one_aspect_each() {
#[test]
fn bare_prompt_is_verbatim_with_nothing_composed_around_it() {
let def = AgentDefinitionSpec::new()
.bare_prompt("Review.")
.bare_prompt("Analyze.")
.into_core("alpha")
.expect("definition");
assert!(matches!(def.system_prompt, PromptSource::Verbatim(ref p) if p == "Review."));
assert!(matches!(def.system_prompt, PromptSource::Verbatim(ref p) if p == "Analyze."));
assert!(def.omit_identity && def.omit_safety_preamble && def.omit_memory_context);
}

#[test]
fn system_prompt_after_bare_prompt_is_wrapped_again() {
let def = AgentDefinitionSpec::new()
.bare_prompt("Review.")
.bare_prompt("Analyze.")
.system_prompt("Be terse.")
.into_core("alpha")
.expect("definition");
Expand All @@ -58,7 +58,7 @@ fn system_prompt_after_bare_prompt_is_wrapped_again() {
#[test]
fn host_only_is_an_empty_read_only_belt_that_cannot_delegate() {
let def = AgentDefinitionSpec::new()
.bare_prompt("Review.")
.bare_prompt("Analyze.")
.tools(ToolScopeSpec::HostOnly)
.sandbox(SandboxModeSpec::None)
.into_core("alpha")
Expand Down
2 changes: 1 addition & 1 deletion crates/openhuman-embed/src/agent/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ impl AgentSpec {
/// use openhuman_embed::{AgentSpec, HostTurnTools, Tool};
///
/// # fn belt_for(_chat: Option<&str>) -> Vec<Box<dyn Tool>> { Vec::new() }
/// let spec = AgentSpec::new("reviewer")
/// let spec = AgentSpec::new("assistant")
/// .tools(|turn| HostTurnTools::advertised(belt_for(turn.session_id())));
/// ```
#[must_use]
Expand Down
2 changes: 1 addition & 1 deletion crates/openhuman-embed/src/cancellation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct State {
}

/// Cancellation shared by attached completers. Cancellation is permanent;
/// create a fresh handle for a new operation or review.
/// create a fresh handle for a new operation or request.
#[derive(Clone)]
pub struct Cancellation(Arc<State>);
impl Default for Cancellation {
Expand Down
Loading
Loading