Build a research swarm
Turn signed observations into retained assignments, run bounded investigations, and compare adaptive with fixed scheduling on the same workload.
On this page
A research swarm can choose its next task from evidence its workers have already produced. This application investigates a corpus with discovery, numerical-claim inspection, and reproduction tasks; workers send signed observations to a receiving host, which admits and stores them and ranks governed assignments by their concentration.
Run the investigationObservations steer the next assignment and capabilities decide each call
Research that follows the evidence
Signed observations influence a real assignment queue. Chio still admits every selected tool call independently.
Change the research question, the receiver’s trust weight, the observation age, or the work allowance. The concentration view reads the native signal store (the pheromone layer) at the selected query time. Compare adaptive and fixed assignment on the same sources and allowance: the application reports completed work, refused work, duplicate claims, and work units for each.
Signals rank the work and the kernel admits each call
The observation path never reaches the kernel’s authority: a stronger signal moves a task up the ranking and grants nothing. The assigned worker presents its own capability when it calls a research tool.
Selection proposes; the capability decides
A signal ranks work and grants nothing. The assigned worker presents its own capability at the tool call, and removing that authority stops the selected task before it reaches the research handler. Signals between agents.
The receiver admits observations before it stores them
The sender transmits a signed GossipBatch over HTTP to a separately configured receiver. The receiving endpoint checks the sender and treaty, the signature, replay state, and scarcity policy before it stores a deposit. The starter runs both host configurations in one process; the personal network application carries observations over iroh between independent processes.
async fn receive(
State(state): State<Arc<ReceiverState>>,
headers: HeaderMap,
Json(batch): Json<PheromoneGossipBatch>,
) -> std::result::Result<Json<Value>, (StatusCode, String)> {
if headers.get("authorization").and_then(|v| v.to_str().ok())
!= Some(&format!("Bearer {}", state.token))
{
return Err((
StatusCode::UNAUTHORIZED,
"Use the configured observer identity".into(),
));
}
let mut context = state.context.clone();
context.now_unix_ms = now_ms();
let config = PheromoneReceiverConfig {
recipient_kernel_id: RECEIVER.into(),
authenticated_sender_kernel_id: SENDER.into(),
validation_context: context,
};
let report = state
.store
.receive_batch(&batch, &state.policy, &config, &NoWorkflowClaims)
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
let value = json!(report);
state
.run
.emit(
"observation.received",
"receiving-host",
if report.accepted {
"Admitted signed observations"
} else {
"Refused an observation batch"
},
value.clone(),
)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(value))
}In the verified run, event 31 refuses a batch with replay_window_exceeded and event 32 refuses one with signature_invalid.
Concentration is read at the application clock
A concentration is the receiver’s current view of admitted evidence. The selected query time sets the native store’s decay calculation, and receiver trust is explicit and adjustable. The browser draws the returned values; decay and admission happen in the store.
pub fn concentrations(
&self,
query_seconds: u64,
weight: f64,
) -> Result<Vec<PheromoneConcentration>> {
let mut context = self.state.context.clone();
context.now_unix_ms = self.timestamp + query_seconds * 1000;
let weights = StaticPeerWeightProvider::new(10, [(SENDER.into(), weight)]);
CLASSES
.iter()
.map(|class| {
self.state
.store
.query_concentration(
class,
NAMESPACE,
context.now_unix_ms,
10,
&context,
&weights,
)
.map_err(Into::into)
})
.collect()
}Event 06 ranks the stored observations: contradiction at strength 0.95, reproduce at 0.8, and discover at 0.45.
One claim record owns each assignment
A unique job record decides who owns an assignment, so competing or restarted workers cannot make a job available twice. Selection proposes work; the assigned worker presents its own capability at the tool call, and removing that authority stops the task before it runs.
pub fn claim(&self, job: &str, worker: &str) -> Result<bool> {
let connection = self
.0
.lock()
.map_err(|_| anyhow::anyhow!("Claim lock failed"))?;
Ok(connection.execute(
"INSERT OR IGNORE INTO claims(job,worker,state) VALUES(?1,?2,'claimed')",
rusqlite::params![job, worker],
)? == 1)
}Events 07 to 18 alternate a retained claim with an allowed call, each with its own receipt, such as 08fb429b…61e89c.
Each investigation stays attached to its request and source
The reproduction tool parses numerical samples from the corpus and computes their mean. Discovery returns versioned passages. The claim inspector surfaces competing numerical statements with their context and leaves the semantic judgement to the reader. Each result stays attached to its governed request and source version.
fn perform(&self, tool: &str, args: Value) -> Result<Value> {
let source = self
.sources
.iter()
.find(|s| s.id == args["source"])
.context("Choose a source from the supplied corpus")?;
let question = args["question"]
.as_str()
.context("Provide a research question")?;
let result = match tool {
"discover" => self.observe(question),
"contradiction" => {
// Surface competing numerical claims and retain the passages. The
// reader or synthesis worker decides whether their contexts conflict.
let mut claims = Vec::new();
for s in &self.sources {
let mut start = 0;
for line in s.text.split_inclusive('\n') {
if line.chars().any(|c| c.is_ascii_digit())
&& !words(question).is_disjoint(&words(line))
{
claims.push(citation(s, start, start + line.len()));
}
start += line.len();
}
}
json!({
"claims":claims,
"assessment":"Competing numerical statements require context review; no semantic contradiction is asserted by this extractor"
})
}
"reproduce" => {
let values = source
.text
.lines()
.find_map(|line| line.strip_prefix("samples:"))
.context("This source has no samples: line to reproduce")?;
let values = values
.split(',')
.map(|s| s.trim().parse::<f64>())
.collect::<std::result::Result<Vec<_>, _>>()?;
anyhow::ensure!(
!values.is_empty()
&& values.len() <= 100
&& values.iter().all(|v| v.is_finite()),
"Use 1 to 100 finite numeric samples"
);
let mean = values.iter().sum::<f64>() / values.len() as f64;
json!({
"source":source.id,
"version_sha256":chio_core::sha256_hex(source.text.as_bytes()),
"values":values,
"mean":mean,
"sample_count":values.len(),
"calculation":"sum(samples) / count(samples)"
})
}
_ => anyhow::bail!("Unknown research responsibility"),
};
self.effects.fetch_add(1, Ordering::SeqCst);
Ok(result)
}
}Replayed, altered, and over-quota observations are refused
After the comparison, the sample input sends a replayed batch, an altered batch, and a fresh one. The receiver’s reports are retained:
./run --run input.json31 observation.received receiving-host
rejected: replay_window_exceeded
32 observation.received receiving-host
rejected: signature_invalid
33 observation.received receiving-host
accepted: accepted- Change or replay a signed observation: the receiving host refuses it.
- Exceed the receiver’s per-class scarcity allowance: extra deposits are not admitted.
- Remove the selected worker’s authority: the assignment is refused before it enters the research handler.
- Reopen the claim store and claim a completed job: it stays unavailable.
The downloadable tests assert each effect or its absence: run cargo test --locked -p chio-research-swarm from the extracted project.
Run the research swarm on your machine
Keep the run directory to connect admitted observations, selected assignments, claims, and research results.
cd chio-research-swarm
./runThe archive holds the application, the shared host code, tests, sample input, pinned dependencies, and a README. The launcher builds and runs the Rust source included in this archive. Install Rust 1.94.1 and the README’s native prerequisites; filesystem isolation and operator workloads require the listed Linux host profile. Cargo resolves immutable public dependencies, so a sibling checkout is unnecessary.
For model workers, set OPENROUTER_API_KEY on the local host and select model mode; credentials stay on that host and never enter the docs form. The qualified model is openai/gpt-4.1-mini. Analysis, retrieval, and deterministic worker modes run without a model key. Ctrl-C stops the local web process and its active child work and keeps the run directory.
Download identity and source revision
Download complete project · View public source
2c2c1fd490e5700839b1ed3b16d7abae60a658cf9346a684ca59b7e1100aa559Replace the ranking policy and measure it
Replace the ranking policy with one that values the information your team needs, and keep the workload and allowance fixed when you compare it with fixed assignment. Add a model researcher through the mission host’s bounded tool loop and retain the observations that informed its next task.