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
586 changes: 464 additions & 122 deletions Cargo.lock

Large diffs are not rendered by default.

22 changes: 16 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,17 @@ tinyhivemind-tools = { path = "crates/tinyhivemind-tools" }
# host writes the same four lines against its own. `tinytools` is pinned at
# the commit OpenHuman's own tree vendors, which is what makes the unification
# exact; `.github/scripts/assert-openhuman-pin.sh` checks all three revs.
openhuman-embed = { git = "https://github.com/tinyhumansai/openhuman", rev = "9aebda6e5746d6a0030b5239b1e022b4a9ab45ec", default-features = false }
openhuman = { git = "https://github.com/tinyhumansai/openhuman", rev = "9aebda6e5746d6a0030b5239b1e022b4a9ab45ec", default-features = false }
tinytools = { git = "https://github.com/tinyhumansai/tinytools", rev = "8e5008c375f9e717e6cbb81fa9532599b98ff0a8" }
tinytools-agent = { git = "https://github.com/tinyhumansai/tinytools", rev = "8e5008c375f9e717e6cbb81fa9532599b98ff0a8" }
openhuman-embed = { git = "https://github.com/tinyhumansai/openhuman", rev = "bc59324d2ef7aaac2d7ead81097f1f9c264dc3bd", default-features = false }
openhuman = { git = "https://github.com/tinyhumansai/openhuman", rev = "bc59324d2ef7aaac2d7ead81097f1f9c264dc3bd", default-features = false }
tinytools = { git = "https://github.com/tinyhumansai/tinytools", rev = "8a87a26293341c51afa11bccfdbe920def7ca9d6" }
tinytools-agent = { git = "https://github.com/tinyhumansai/tinytools", rev = "8a87a26293341c51afa11bccfdbe920def7ca9d6" }
# The memory engine contract and the per-agent lifecycle OpenHuman runs its
# seats' memory through, for the adapter's `Recall`/`Remember` over the same
# engine and layout. Git sources pinned at the commit OpenHuman's tree vendors
# and patched onto that copy below, for the same one-copy reason as the tool
# crates; `.github/scripts/assert-openhuman-pin.sh` checks the rev.
tinymemory-api = { git = "https://github.com/tinyhumansai/tinymemory", rev = "581a2bc853467d77f4b76debb3066524a4a46142" }
tinymemory-tools = { git = "https://github.com/tinyhumansai/tinymemory", rev = "581a2bc853467d77f4b76debb3066524a4a46142" }
tinymemory-api = { git = "https://github.com/tinyhumansai/tinymemory", rev = "f30b9bbb7319a6a7c2fb1aaa3670281f1dc4a9ba" }
tinymemory-tools = { git = "https://github.com/tinyhumansai/tinymemory", rev = "f30b9bbb7319a6a7c2fb1aaa3670281f1dc4a9ba" }
# Derive macros for the crate-wide error type in `crates/tinyhivemind-core/src/error/`.
# Every dependency entry should carry a comment like this one saying why it is
# here.
Expand All @@ -77,6 +77,10 @@ serde = { version = "1", features = ["derive"] }
# Pins the exact JSON object shape and required fields of host-facing DTOs in
# unit tests without adding JSON handling to the runtime dependency graph.
serde_json = "1"
# Typed Markdown profile frontmatter in the OpenHuman configuration adapter.
serde_yaml_ng = "0.10"
# Fixed-time validation through OpenHuman's native cron schedule parser.
chrono = { version = "0.4", default-features = false, features = ["std"] }
# Drives coordinator scheduling, adapter turn deadlines/progress, and async
# tests. Pure core remains executor-neutral and exposes boxed host futures.
tokio = { version = "1", features = ["macros", "rt", "sync"] }
Expand Down Expand Up @@ -158,7 +162,13 @@ tinymemory-api = { path = "vendor/openhuman/vendor/tinymemory/crates/tinymemory-
tinymemory-tools = { path = "vendor/openhuman/vendor/tinymemory/crates/tinymemory-tools" }
tinymemory-integrations = { path = "vendor/openhuman/vendor/tinymemory/crates/tinymemory-integrations" }

# Storage contracts must resolve to the harness copy across git and path users.
[patch."https://github.com/tinyhumansai/tinystoragedrivers"]
tinystoragedrivers-core = { path = "vendor/openhuman/vendor/tinyagents/vendor/tinystoragedrivers/crates/tinystoragedrivers-core" }
tinystoragedrivers-sqlite = { path = "vendor/openhuman/vendor/tinyagents/vendor/tinystoragedrivers/crates/tinystoragedrivers-sqlite" }

[patch."https://github.com/tinyhumansai/tinyinference"]
tinyinference-decisions = { path = "vendor/openhuman/vendor/tinyagents/vendor/tinyinference/crates/tinyinference-decisions" }
tinyinference-core = { path = "vendor/openhuman/vendor/tinyagents/vendor/tinyinference/crates/tinyinference-core" }
tinyinference-embeddings = { path = "vendor/openhuman/vendor/tinyagents/vendor/tinyinference/crates/tinyinference-embeddings" }
tinyinference-llm = { path = "vendor/openhuman/vendor/tinyagents/vendor/tinyinference/crates/tinyinference-llm" }
Expand Down
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ consumer builds.

| Phase | What lands | State |
| --- | --- | --- |
| OpenHuman configuration | One runtime, reusable Markdown profiles, persistent seats, narrowed hive permissions and native workflows; [accepted contract](docs/specs/openhuman-hive-config.md), [example manifests](examples/openhuman/hives/README.md) | Implemented for review in #113 |
| P0 | Reshape the TinyBus module template into a plain library workspace | **done** |
| P1 | Chat identity: `MAIN_THREAD_ID`, `GENERAL_DESK`, `is_general_chat`, `same_conversation` | **done** |
| P2 | Desk types, then the membership algebra behind `DeskSet<'a>` | **done** |
Expand Down
10 changes: 10 additions & 0 deletions crates/tinyhivemind-core/src/driver/conduct/test/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,13 @@ fn conduct_policy_wire_preserves_conversation_and_episode_walls() {
policy
);
}

#[test]
fn conduct_policy_wire_form_and_omitted_defaults_are_stable() {
let value: crate::driver::ConductPolicy = serde_json::from_str("{}").unwrap();
assert_eq!(value, crate::driver::ConductPolicy::default());
assert_eq!(
serde_json::to_value(value).unwrap(),
serde_json::json!({"child_turn_wall":6,"turn_wall":60})
);
}
2 changes: 1 addition & 1 deletion crates/tinyhivemind-core/src/driver/conduct/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::collections::BTreeMap;

/// The walls a conducted episode runs inside.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
#[serde(default, deny_unknown_fields)]
pub struct ConductPolicy {
/// Turns a conversation may take before it concludes without an answer.
pub child_turn_wall: u64,
Expand Down
10 changes: 10 additions & 0 deletions crates/tinyhivemind-core/src/hive/division/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,13 @@ fn division_policy_wire_preserves_width_and_directory_choice() {
policy
);
}

#[test]
fn division_policy_wire_form_and_omitted_defaults_are_stable() {
let value: DivisionPolicy = serde_json::from_str("{}").unwrap();
assert_eq!(value, DivisionPolicy::default());
assert_eq!(
serde_json::to_value(value).unwrap(),
serde_json::json!({"round_width":4,"follow_directory":true})
);
}
2 changes: 1 addition & 1 deletion crates/tinyhivemind-core/src/hive/division/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ impl Division {
/// default that made a caller opt into the thing that wins would be the wrong
/// way round. See `docs/experiments/2026-09-09-variety-and-roles.md`.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
#[serde(default, deny_unknown_fields)]
pub struct DivisionPolicy {
/// Assignments one round may authorize concurrently.
///
Expand Down
3 changes: 3 additions & 0 deletions crates/tinyhivemind-hives/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@
| `storage/` | Serializable snapshots and transactional storage implementations |

OpenHuman types belong in the adapter crate. Core algebra remains pure.

The coordinator freezes per-hive `HiveSettings` and scheduled job provenance at
acceptance; its settings module owns the typed configuration boundary.
30 changes: 30 additions & 0 deletions crates/tinyhivemind-hives/src/coordinator/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# Dynamic coordinator

`release_parked` matches the unique reservation, native session, episode,
accepted message identities and scheduled provenance against a completed
parked turn in the transaction that releases it. Approval adapters use this
port to reject early answers and stale records, including a later park on the
same input. `release_with` remains the host's manual release operation.

| File | Responsibility |
| --- | --- |
| `mod.rs` | Shared handle, dynamic APIs, and authorization snapshots |
| `transaction.rs` | Writer gate, incremental commits outside the live lock, conflict reload |
| `settings.rs` | Per-hive native policy and retained roles, frozen at acceptance |
| `types.rs` | Host runner port and public payloads |
| `messaging.rs` | Atomic acceptance, retry IDs, starters, attribution, and private reads |
| `observe.rs` | Host transcript, committed-revision watch, episode status |
Expand Down Expand Up @@ -68,3 +75,26 @@ the same transaction as interruption. Its input is not acknowledged, and its
reply and staged episode actions are discarded. Later inputs continue that
conversation, including after SQLite reopen. Empty or changed session IDs fail
validation without replacing a prior binding.

`configure_hive(id, HiveSettings)` configures future episodes. Acceptance freezes
native completion options, routing thresholds, and roles; reconfiguration never
changes already accepted work. Roles reach native routing candidates, rendered
briefs, and `TurnRequest::teammates`. Retained role entries survive leave/rejoin.
Both per-hive widths must fit the global concurrent-turn cap; routing width is
intersected with completion width. Retention remains global: differing per-hive
retention settings are refused rather than ignored.

`send_scheduled_as_host(job_id, request)` commits scheduled authority with the
accepted transcript row and episode. Every descendant assignment and claimed
turn carries `scheduled_job_id`, including queued or recovered work. Retrying
with a changed job identity or interactive origin fails with `MessageConflict`.
Sends attributed to an actively scheduled agent inherit the same durable authority;
forwarding to another agent or hive cannot strip it. The host must scope automation
authority from each captured request explicitly.

The current completion coordinator attaches no semantic routing provider
(`BroadcastRouting.primary` and `.reasoning` are absent). Broadcasts therefore
use the driver's deterministic single-owner fallback. Native routing policy and
roles reach the routing request, and widths remain bounded, but confidence,
clarification and high-impact thresholds cannot change fallback selection.
Configuring these thresholds does not activate a model client.
100 changes: 89 additions & 11 deletions crates/tinyhivemind-hives/src/coordinator/conduct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ struct Environment {
routing: RoutingPolicy,
}
impl Environment {
fn new(hive: &HiveInfo, options: &CoordinatorOptions) -> Result<Self> {
fn new(
hive: &HiveInfo,
options: &CoordinatorOptions,
settings: Option<&crate::HiveSettings>,
) -> Result<Self> {
let graph = HiveGraph::new(
Desk {
id: hive.hive_id.clone(),
Expand All @@ -44,7 +48,7 @@ impl Environment {
.map(|id| RouteCandidate {
id: id.clone(),
label: id.clone(),
role: None,
role: settings.and_then(|settings| settings.roles.get(id).cloned()),
description: None,
capabilities: Vec::new(),
learned_topics: Vec::new(),
Expand All @@ -59,16 +63,21 @@ impl Environment {
.map(|id| AgentBinding::new(id, Seat(id.clone())))
.collect(),
)?;
Ok(Self {
hive: bound,
routing: RoutingPolicy {
let mut routing = settings.map_or(
RoutingPolicy {
minimum_confidence: Probability::ZERO,
high_impact_minimum_confidence: Probability::ONE,
clarification_threshold: Probability::ONE,
high_impact_threshold: Probability::ONE,
round_width: options.round_width,
choice_option_limit: 8,
},
|settings| settings.routing.clone(),
);
routing.round_width = routing.round_width.min(options.round_width);
Ok(Self {
hive: bound,
routing,
})
}
fn driver(&self, options: &CoordinatorOptions) -> Result<CompletionDriver<'_, Seat>> {
Expand Down Expand Up @@ -133,6 +142,7 @@ fn append(
let only_for = narrow_readers(state, &destination, thread, only_for)?;
let sequence = next_sequence(state)?;
state.messages.push(Message {
scheduled_job_id: episode.scheduled_job_id.clone(),
message_id: format!("hivemind:event:{sequence}"),
sequence,
sender,
Expand Down Expand Up @@ -203,7 +213,12 @@ pub(super) async fn prepare(state: &mut StoredState, options: &CoordinatorOption
if running || !episode.pending.is_empty() || episode.waiting {
continue;
}
let environment = Environment::new(&episode.hive, options)?;
let effective = episode
.settings
.as_ref()
.map(|settings| settings.options.clone());
let options = effective.as_ref().unwrap_or(options);
let environment = Environment::new(&episode.hive, options, episode.settings.as_ref())?;
let driver = environment.driver(options)?;
let mut conductor = environment.conductor(&driver, &episode, options)?;
if episode.wave_open {
Expand Down Expand Up @@ -278,7 +293,12 @@ pub(super) fn open(
options: &CoordinatorOptions,
) -> Result<(Vec<Message>, String)> {
let mut episode = state.episodes[index].clone();
let environment = Environment::new(&episode.hive, options)?;
let effective = episode
.settings
.as_ref()
.map(|settings| settings.options.clone());
let options = effective.as_ref().unwrap_or(options);
let environment = Environment::new(&episode.hive, options, episode.settings.as_ref())?;
let driver = environment.driver(options)?;
let mut conductor = environment.conductor(&driver, &episode, options)?;
let messages: Vec<_> = state
Expand Down Expand Up @@ -331,7 +351,16 @@ pub(super) fn open(
opened_it: false,
};
}
let brief = brief.render();
let mut brief = brief.render();
let teammates = teammates(&episode);
for teammate in &teammates {
if let Some(role) = &teammate.role {
brief.push_str("\n@");
brief.push_str(&teammate.id);
brief.push_str(": ");
brief.push_str(role);
}
}
checkpoint(&conductor, &mut episode)?;
state.episodes[index] = episode;
Ok((messages, brief))
Expand All @@ -348,7 +377,12 @@ pub(super) fn record(
if episode.finished {
return Ok(());
}
let environment = Environment::new(&episode.hive, options)?;
let effective = episode
.settings
.as_ref()
.map(|settings| settings.options.clone());
let options = effective.as_ref().unwrap_or(options);
let environment = Environment::new(&episode.hive, options, episode.settings.as_ref())?;
let driver = environment.driver(options)?;
let mut conductor = environment.conductor(&driver, &episode, options)?;
let mut calls: Vec<_> = actions
Expand Down Expand Up @@ -392,7 +426,12 @@ pub(super) fn release(
{
continue;
}
let environment = Environment::new(&episode.hive, options)?;
let effective = episode
.settings
.as_ref()
.map(|settings| settings.options.clone());
let options = effective.as_ref().unwrap_or(options);
let environment = Environment::new(&episode.hive, options, episode.settings.as_ref())?;
let driver = environment.driver(options)?;
let mut conductor = environment.conductor(&driver, &episode, options)?;
conductor.resume_seat(agent_id);
Expand Down Expand Up @@ -420,7 +459,12 @@ fn prune_removed(
.cloned()
.collect();
if !removed.is_empty() {
let environment = Environment::new(&episode.hive, options)?;
let effective = episode
.settings
.as_ref()
.map(|settings| settings.options.clone());
let options = effective.as_ref().unwrap_or(options);
let environment = Environment::new(&episode.hive, options, episode.settings.as_ref())?;
let driver = environment.driver(options)?;
let mut conductor = environment.conductor(&driver, episode, options)?;
for turn in &removed {
Expand Down Expand Up @@ -457,3 +501,37 @@ pub(super) fn prune_pending(state: &mut StoredState, options: &CoordinatorOption
}
Ok(changed)
}

/// Explicit host-neutral teammate payload, using the frozen episode roles.
pub(super) fn teammates(
episode: &EpisodeRecord,
) -> Vec<tinyhivemind_core::runtime::BriefedTeammate> {
episode
.hive
.members
.iter()
.map(|id| tinyhivemind_core::runtime::BriefedTeammate {
id: id.clone(),
label: id.clone(),
role: episode
.settings
.as_ref()
.and_then(|settings| settings.roles.get(id).cloned()),
description: None,
})
.collect()
}

#[cfg(test)]
mod test;

/// Visible teammates exclude the viewing agent.
pub(super) fn teammates_for(
episode: &EpisodeRecord,
agent_id: &str,
) -> Vec<tinyhivemind_core::runtime::BriefedTeammate> {
teammates(episode)
.into_iter()
.filter(|teammate| teammate.id != agent_id)
.collect()
}
8 changes: 8 additions & 0 deletions crates/tinyhivemind-hives/src/coordinator/conduct/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Conductor test support

| File | Responsibility |
| --- | --- |
| `test.rs` | Native routing thresholds, width intersection, and role propagation |

The conductor implementation remains in the parent `conduct.rs`; this directory
holds its private behavior tests.
27 changes: 27 additions & 0 deletions crates/tinyhivemind-hives/src/coordinator/conduct/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Native routing configuration and membership role propagation.
use super::*;

#[test]
fn configured_roles_and_routing_thresholds_reach_the_native_routing_request() -> Result<()> {
let hive = HiveInfo {
hive_id: "one".into(),
name: "One".into(),
description: None,
members: vec!["agent".into()],
};
let mut settings = crate::HiveSettings::default();
settings.roles.insert("agent".into(), "reviewer".into());
settings.routing.minimum_confidence = Probability::ONE;
settings.routing.choice_option_limit = 3;
settings.routing.round_width = 4;
let environment = Environment::new(&hive, &settings.options, Some(&settings))?;
let request =
environment
.hive
.desk_request("work", Vec::new(), None, 0, environment.routing.clone());
assert_eq!(request.candidates[0].role.as_deref(), Some("reviewer"));
assert_eq!(request.policy.minimum_confidence, Probability::ONE);
assert_eq!(request.policy.choice_option_limit, 3);
assert_eq!(request.policy.round_width, 1);
Ok(())
}
Loading
Loading