An agent run can delegate a task to another agent, a peer. The peer runs as a sub-agent inside the same workflow. A mandate bounds what each agent of the tree may do. Flow rules stop a dangerous sequence of tool calls across the whole tree.

These primitives are in autumn-harvest-agent, modules mesh and flow. They work on Postgres and on SQLite. The research behind them is in rnd/2026-10-13-agent-mesh-primitives.md. The plan is in docs/design/DESIGN-agent-mesh.md.

Each Rust example compiles as written, and CI checks this. An example takes each value from your app as a function parameter. The type of the parameter shows what to pass.

1. The primitives

PrimitiveTypeWhat it does
RosterPeer, AgentHarness::peerNames each agent, its prompt, its tools and its bounds.
Discoveryactivity agent_discover_peersReads the peers that a mandate names. History records the answer.
Delegationworkflow tool delegateRuns a peer as a sub-agent. Only its final answer returns.
MandateMandateBounds depth, delegations, peers and tools. A child gets the meet.
RefusalRefusalNames why a delegation did not run.
Budget roll-upAgentReport::delegationsCuts the child cap from the parent remainder. Adds the child usage to the parent.
Flow rulesFlowRules, Tool::labels, RecheckGate the call that completes a forbidden label set in the tree. By default, also gate each later call that touches the set.
Label carryAgentReport::flow_labels, AgentTask::flow_labelsKeeps the labels of a conversation from one run to the next.
Wire formMANDATE_META_KEY, Mandate::outbound, Mandate::acceptCarries a mandate to another process. The receiver can only narrow it.

2. Delegate to a peer

Rust
use std::sync::Arc;
use autumn_harvest_agent::{AgentHarness, AgentModel, AgentTask, Mandate, Peer, Tool};

fn planner(
    model: Arc<dyn AgentModel>,
    web_search: Arc<dyn Tool>,
    write_file: Arc<dyn Tool>,
) -> (AgentHarness, AgentTask) {
    let harness = AgentHarness::new(model)
        .tool(web_search)
        .tool(write_file)
        .peer(
            Peer::new("researcher", "Finds facts on the web.")
                .system("You research. Cite each source.")
                .tools(["web_search"])
                .max_steps(6)
                .max_total_tokens(40_000),
        );

    let task = AgentTask::new("Plan a trip to Lisbon.")
        .max_total_tokens(100_000)
        .mandate(Mandate::new("planner").peers(["researcher"]));
    (harness, task)
}

A harness takes each tool as an Arc<dyn Tool>. FnTool::new wraps an async function, and shared returns it as an Arc<dyn Tool>. Section 7 shows both.

The planner sees one more tool, delegate. Its input is agent, one of the peer names, and task, the text for the peer. The tool description lists each peer with its description. A run with a mandate reserves the name delegate, so an app tool with that name is hidden. The name stays reserved when the lookup finds no peer. The model then sees no delegate tool. A call to it still gets a named verdict and a record, as in section 5, also under a tool allowlist or flow rules.

The result of a delegation is JSON:

Json
{"agent": "researcher", "answer": "The ferry leaves at 9.", "stop": "completed", "truncated": false}

The stop field tells the parent model how the child ended. The truncated field is true when the harness cuts the answer (section 3). The parent model reads both and decides what to do. A durable peer result also has error.

3. What the child gets

The child sees nothing of the parent transcript. Put every fact it needs in the task.

Child settingValue
System promptPeer::system
InputThe task argument
TranscriptFresh
Run id{root run id}:d{n}. n counts the delegations that ran in the tree, from 1. A refusal takes no number.
ToolsThe meet of the parent mandate and Peer::tools
StepsPeer::max_steps (default DEFAULT_MAX_STEPS)
ModelPeer::model, else the model of the parent (section 14)
Token capThe smaller of Peer::max_total_tokens and the parent remainder
Read-onlyOn when the parent, the peer or the mandate is read-only
Memory, follow-ups, deliveryOff
Session, tenant, output cap, unattended flagAs the parent
Loop guard, approval timeout, request capAs the parent
AnswerCut to Peer::answer_limit, else to AgentHarness::tool_output_limit (default 8 000 characters).

The same limit applies to an inline child, a fan-out child (section 15) and a durable peer (section 13). The answer is also cut to a sixth of the request cap of the parent, as a tool result is under its result cap. So a large limit cannot drop a whole answer. The request cap bounds bytes, not tokens. Keep the limit inside the context window of your model.

A cut answer ends with …[truncated]. The result also has "truncated": true, the report sets DelegationRecord::answer_truncated, and the worker logs a warning. The peer lookup records the limit, so replay cuts each answer as the live run did. A run recorded before 0.10.0 keeps its old cut of 8 000 characters and has no truncated field.

Tools key idempotency on (run_id, step, call_id). A child has its own run id, so its keys never collide with the parent keys. A child of a run whose run id is the workflow id skips the response cache, as its parent does.

4. Mandates

A mandate holds the authority of one agent:

FieldMeaningDefault
chainThe agents from the root to the holder. The holder is last.[holder]
max_depthThe bound on depth. The root agent has depth 0.3
peersThe peers that the holder may delegate to.none
toolsThe harness tools that the holder may call. None means every tool.None
max_delegationsThe bound on delegations in the whole tree.16
read_onlyRefuse every write and external call.off
flow_labelsThe labels that a caller in another process sends.none
modelsThe harness models that the subtree may use. None means every model.None

A mandate bounds its holder too. A root mandate with tools(["lookup"]) gives the root agent only lookup.

Mandate::delegate computes the child mandate. Authority only narrows:

  • chain gets the peer name. Depth goes up by one.
  • peers is the meet of the parent peers and Peer::peers.
  • tools is the meet of the parent tools and Peer::tools.
  • max_depth and max_delegations stay the same. They never grow.
  • models stays the same. Mandate::accept takes the meet of both lists.
  • read_only stays on once it is on.

Property tests prove each rule (a_delegation_never_widens_authority).

The model does not see a tool outside the mandate. The decision denies a call to it, and the tool-call activity refuses it again. The mandate never bounds a workflow tool, such as delegate or schedule_followup. The memory tool writes state that later runs read, so an allowlist must name memory.

An agent name has 1 to 64 characters from a-z, 0-9, _ and -. A tool name or a label has 1 to 128 bytes and no control character. Each set holds at most 256 entries. A run whose task mandate is not valid fails at its start, before any model call.

5. Refusals

A refused delegation does not run. The model reads an error result with the verdict, and the report lists it:

Json
{"error": "\"researcher\" is already in the delegation chain", "verdict": "cycle"}
VerdictCause
invalid_argumentsThe delegate call does not have exactly the arguments agent and task.
not_permittedThe mandate does not name the peer.
unknown_peerThe mandate names the peer, but the roster has no valid peer of that name.
cycleThe peer is already in the chain.
depth_exceededThe delegation passes max_depth.
fan_out_exhaustedThe tree has used max_delegations.
model_forbiddenThe mandate does not allow the model of the peer.
budget_exhaustedThe child would get a token cap of zero.
invalid_mandateMandate::accept got a malformed mandate.
deadlockThe durable peer waits on this agent, so it would never reply (section 13).
peer_unavailableThe durable peer has no running run, or the roster names no valid agent id for it.

A delegation runs the first eight checks in the order of the table. Mandate::accept returns invalid_mandate, cycle or depth_exceeded. A durable receiver returns model_forbidden when the accepted mandate does not allow its own model. Only a send to a durable peer returns deadlock or peer_unavailable. Refusal implements std::error::Error.

The report keeps a peer name only when it is a valid agent name. Any other name shows as _invalid, because the model wrote it.

6. Budgets

The tree shares one token budget, the max_total_tokens of the root task:

  1. A child cap is the smaller of the peer cap and the parent remainder. A cap of zero is budget_exhausted.
  2. A child stops with tokens_exhausted when it passes its cap, as a root run does.
  3. The parent adds the child usage. A parent over its cap makes no further model call. It stops with tokens_exhausted.

Inline children run one after another, inside the workflow of the parent. Each one reads the remainder that the one before it left. The siblings of a fan-out (section 15) run at the same time, as child runs. bounds::sibling_cap gives each sibling a share of the remainder, and the shares sum to the remainder. So two children never spend the same tokens, and they cannot overspend together.

AgentReport::delegations lists each delegation, refused ones too, in the order each ended:

FieldMeaning
chainThe agents from the root to the delegate
run_idThe child run id. None for a refusal.
stopHow the child ended. None for a refusal.
refusalThe verdict of a refused delegation
usageTokens of the child and of every agent under it
steps_used, tool_callsThe rounds and calls of the child and of every agent under it

AgentReport::usage, cost_usd_micros, steps_used and tool_calls cover the whole tree. Each agent still has its own step bound, so a child round never counts against the max_steps of its parent.

7. Flow rules

Some calls are safe alone, but dangerous together. Read an inbox with untrusted mail, read private data, then send mail: a prompt injection in the inbox can exfiltrate the data. Flow rules stop that sequence.

Give each tool its labels:

Rust
use std::sync::Arc;
use autumn_harvest_agent::flow::labels;
use autumn_harvest_agent::{AgentError, AgentHarness, AgentModel, FlowRules, FnTool, Tool, ToolEffect};
use serde_json::{Value, json};

fn mail_agent(model: Arc<dyn AgentModel>, send_mail: Arc<dyn Tool>) -> AgentHarness {
    let inbox = FnTool::new("inbox", "Read mail.", json!({"type": "object"}), read_mail)
        .effect(ToolEffect::ReadOnly)
        .label(labels::UNTRUSTED_INPUT)
        .label(labels::PRIVATE_DATA)
        .shared();

    AgentHarness::new(model)
        .tool(inbox)
        .tool(send_mail)
        .flow_rules(FlowRules::rule_of_two())
}

async fn read_mail(_input: Value) -> Result<Value, AgentError> {
    Ok(json!({"messages": []}))
}

Each call also carries the label of its effect: effect:read_only, effect:internal, effect:write or effect:external.

FlowRules::rule_of_two() follows Meta's Agents Rule of Two. A run tree needs a person before it holds all three of untrusted input, private data, and a write or external effect. The call that completes the three waits for a person. Each later call with one of the labels waits too. One approval releases one call, so a second send needs a second approval.

Write your own rule with FlowRules::new().forbid(labels, rule). Rule::Ask waits for a person. Rule::Deny(reason) refuses the call. Rule::Allow and an empty set add nothing.

Recheck sets what one approval releases:

RecheckAfter the set is complete
EachCall (default)Each call with a label of the set trips the rule again. One approval releases one call.
UntilApprovedEach call with a label of the set trips the rule until a person approves one of them. That approval releases the rule for the rest of the run tree.

Set it on one rule with forbid_with(labels, rule, recheck), or on every rule with with_recheck(recheck):

Rust
use autumn_harvest_agent::{FlowRules, Recheck};

let rules = FlowRules::rule_of_two().with_recheck(Recheck::UntilApproved);

Use UntilApproved when a person approves a flow, not each send. With EachCall, each later read of untrusted input waits for a person too, because the read carries a label of the set.

UntilApproved is weaker. After the approval, each send runs with no person, and a send can carry data from a later prompt injection. Keep EachCall when a send can leak data.

Only a person's approval, or an edit, releases a rule. Each of these releases nothing, so the rule still gates:

  • A rejection or a timeout.
  • A call that waits in the same turn. A second send in the turn of the first send waits for its own decision.
  • Labels from another run: AgentTask::flow_labels, or an inbound mandate. A release lasts for one run tree, so a continued run or a remote peer asks again.
  • A Rule::Deny rule. It never asks, so nothing releases it.

The workflow holds the released rules, by label set. Two rules with the same label set share one release.

How the check works:

  • A rule trips when the call carries a label of the set, and the set is complete with the call. The label can already be in the tree. So after the set is complete, each call that touches it trips the rule again. A released UntilApproved rule does not trip. A call with no label of the set passes.
  • The strictest tripped rule wins, and the policy answer still counts.
  • A label counts once the decision lets its call run: an allow, or an approval. It counts even when the tool then fails. A denied, rejected or timed-out call adds nothing.
  • In one turn, a call counts for the calls after it before it runs, so the check fails closed.
  • A delegation can add the labels of its child. A later call in the same turn then holds a decision that did not see them. That call does not run, and the model reads that it must call again. The next turn decides it with the new labels.
  • With flow rules, the decision denies a call to an unknown tool, because the worker cannot label it.
  • The tool-call activity gets the recorded labels of the call. It refuses a tool that gained a label after the decision, for example in a rolling deploy. The model reads that it must call again.
  • One label set spans the tree. A child answer enters the parent context, and a parent task text enters the child. So labels flow both ways.

The workflow holds the label set and the released rules. The model-turn activity records each decision, the labels of each call, and the rules that an approval of each call releases. Model output cannot remove a label or release a rule, and replay does not check the rules again. A turn records labels only when the harness has flow rules. Install the same flow rules on every worker.

Carry the labels to the next run

A run that continues a conversation must continue its labels too:

Rust
use autumn_harvest_agent::{AgentReport, AgentTask};

fn reply(report: &AgentReport) -> AgentTask {
    AgentTask::new("Now reply to the sender.")
        .history(report.messages.clone())
        .flow_labels(report.flow_labels.clone())
}

HeartbeatTask::flow_labels does the same for a heartbeat tick. The next run gets no release. An UntilApproved rule asks again there.

8. Approvals in a child

A gated call in a child waits on a signal, as in a root run. The name is tool_approval:<round>:<position>:<call_id>. round counts the tool rounds of the whole tree, so a parent and a child never share a name. A run with no delegation keeps the names it had before.

Read the name from the wait. On SQLite, RunState::WaitingSignal holds it. On Postgres, the deadline timer of the wait is __signal_timeout:<seq>:<signal name>.

9. Across processes

A mandate can travel with a call to another process. Put it in the _meta of an MCP call or in the metadata of an A2A message:

Rust
use std::collections::BTreeSet;
use serde_json::{Map, Value};
use autumn_harvest_agent::{AgentTask, Mandate, Peer};

fn send(
    mandate: &Mandate,
    remote: &Peer,
    used: u32,
    labels: &BTreeSet<String>,
) -> Result<Map<String, Value>, Box<dyn std::error::Error>> {
    let mut meta = Map::new();
    mandate.outbound(remote, used, labels)?.insert_into(&mut meta);
    Ok(meta)
}

fn receive(meta: &Map<String, Value>, text: &str)
    -> Result<AgentTask, Box<dyn std::error::Error>>
{
    let local = Mandate::new("researcher").peers(["critic"]).tools(["web_search"]);
    let effective = match Mandate::from_meta(meta)? {
        Some(inbound) => local.accept(&inbound)?,
        None => local,
    };
    Ok(AgentTask::new(text).mandate(effective))
}
  • Mandate::outbound is delegate for the wire. It sends the remainder of the fan-out, because the receiver starts its own count at zero. It sends the labels of the tree, because the receiver must keep them.
  • Mandate::from_meta is strict. It refuses an unknown key, so a field that a newer sender adds to narrow the run is never dropped without a sign.
  • Mandate::accept returns the meet of both mandates and the union of their labels. The result is never wider than either side, so an inbound mandate can only narrow a run. The local mandate must be a root mandate.
  • The chain keeps the caller agents, so a cycle across processes is refused.
  • The mandate carries no signature. Authenticate the caller as you do for any request.

10. Crash and replay

In flight at the crashOn resume
A peer lookupThat lookup runs again.
A child model callThe worker sends that one call again.
A child tool callThat one call runs again, with the same idempotency key.
A child approval waitThe wait resumes. Its deadline is durable.

Replay reads every finished child step from history and sends none of them again. A roster change after the lookup does not change a recorded run. a_restart_inside_a_child_resends_nothing_and_looks_up_nothing proves this.

eval::evaluate re-drives a delegation run. The recorded lookups answer, so the candidate needs no roster. A lookup that failed the source fails the candidate at the same lookup. A recorded tool outcome answers only an equal call of the same agent. A candidate call takes a recorded call id only from a turn of the same agent, so an approval for a child never releases a root call. An agent is its run id suffix :d{n} together with its name. The suffix alone counts delegations, so another peer can take the same n.

11. Observability

A run with a mandate records the acting agent in each model-turn request, as agent. The model-turn span agent_model_turn carries gen_ai.agent.name. On both backends, the cost ledger counts the turns of an inline child in the run of the root agent. A fan-out child (section 15) writes its rows under its own child run. A DelegationRecord gives the tokens, rounds and calls per subtree.

12. Limits

  • A child runs inline, on the worker of the parent, unless it is part of a fan-out (section 15). Inline children run one after another.
  • Each child turn adds to the history of the parent. Keep max_delegations and the peer step bounds low.
  • A durable peer (section 13) needs no manual step. A peer in another deployment still does: use ctx.call_remote_task and the wire form above.
  • Flow rules are only as good as the tool labels. A tool with no label adds only its effect label.
  • A run whose run id is a workflow id that ends in :d<number> can share tool idempotency keys with a child run id. New tasks use the execution id, so they are not affected.
  • The mesh has no handoff. A peer cannot take over the rest of the run.

13. Durable peers

A peer can be a durable Agent with its own identity, inbox, history, budget and memory. A delegate call to it sends a message and waits for the reply. Inline delegation stays for short, bounded sub-tasks.

Rust
use std::sync::Arc;
use autumn_harvest_agent::{
    AgentHarness, AgentId, AgentIdError, AgentModel, AgentProfile, Mandate, Peer,
};

fn mesh(model: Arc<dyn AgentModel>) -> Result<AgentHarness, AgentIdError> {
    let researcher = AgentId::new("research", "r1")?;
    let triage = Mandate::new("triage").peers(["researcher"]);
    Ok(AgentHarness::new(model)
        .peer(Peer::agent("researcher", "Finds facts.", &researcher))
        .agent(AgentProfile::new("triage").mandate(triage))
        .agent(AgentProfile::new("research").mandate(Mandate::new("researcher"))))
}
StepWhat happens
SendThe sender signals the agent by workflow id with the key <from>:<seq>. A durable sender sends from as <agent id>@<incarnation>, so a new life never matches the keys of the old one. seq counts from 1 for each receiver, so a receiver sees no gaps. On one shard the send commits with the decision. Across shards it goes through the outbox. Replay does not send it again.
DedupeThe engine dedupes the key across the run chain of the receiver. The receiver also keeps the applied numbers of each sender, as a floor and a set above it.
MandateThe message carries Mandate::outbound. The receiver runs under local.accept(inbound), so it can only narrow its own mandate. A read-only inbound mandate makes the receiver read-only. A read-only segment, such as a tick, a follow-up or a read-only child, sends a read-only mandate. The flag stops the write, not the delivery. The receiver still adds the message to its transcript, spends its own tokens and advances its inbound sequence. A peer segment books no follow-up, because a follow-up runs under the profile mandate.
LabelsThe message carries the labels of the sender. They taint the receiver. The reply carries the labels of the receiver back.
ReplyA durable promise. The sender races it against the reply deadline, and a durable agent also against an interrupt. Only the segment of the message answers. A failed segment sends an error and no text. A full held queue refuses the message and answers at once.
DeadlockEach message carries the agents that wait on it. A send to one of them is refused at once as deadlock. A fan-out child run gets the chain of its parent, so the rule holds there too.
  • The receiver never drops a number for being at or under the highest one it saw. The outbox can deliver message 2 before message 1. The TLA+ model formal/tla/AgentMesh.tla finds that loss with AgentMeshLastSeq.cfg.
  • A send needs a running target. A send to an agent with no run is refused as peer_unavailable. Start each agent of a mesh first.
  • The reply deadline is Peer::reply_timeout, ten minutes by default. A reply after the deadline is not read. The receiver still applies the message once. When the sender asks again, the task runs twice, so give a slow peer a longer deadline.
  • A durable peer runs on its own worker, in its own history, under its own profile budget. Its tokens are not added to the budget of the sender.

14. A model per agent

The harness holds named models. A task, a peer, a profile or a heartbeat names its model. A cheap triage model can front a strong planner.

Rust
use std::sync::Arc;
use autumn_harvest_agent::{AgentHarness, AgentModel, AgentTask, DEFAULT_MODEL, Mandate, Peer};

fn triage(cheap: Arc<dyn AgentModel>, strong: Arc<dyn AgentModel>) -> (AgentHarness, AgentTask) {
    let harness = AgentHarness::new(cheap)
        .model_named("strong", strong)
        .peer(Peer::new("planner", "Plans.").model("strong"));
    let mandate = Mandate::new("triage").peers(["planner"]).models([DEFAULT_MODEL, "strong"]);
    let task = AgentTask::new("triage this").mandate(mandate);
    (harness, task)
}
SetterScope
AgentTask::modelThe root agent of a run.
Peer::modelAn inline child. A child with no model keeps the model of its parent.
AgentProfile::modelEach durable agent of the kind.
AgentHeartbeat::model, HeartbeatTask::modelEach tick. None takes the profile model.
  • The model-turn input records the name. Replay asks the same model.
  • DEFAULT_MODEL names the model of AgentHarness::new. A task with no name uses it.
  • The response cache key holds the model id. One model never reads the answer of another.
  • The cost ledger records the id of the model that answered each turn.
  • An unknown name fails the turn, with no retry and no fallback.
  • A mandate checks the model at three points: a delegation to a peer, the start of a root run, and a durable receiver. A sender cannot see the model of a durable peer, so the receiver checks it.
  • eval::compare runs two candidates on one recorded run. Candidate::agent_model sets the model of one agent of the tree. eval::compare_anonymised runs a pool under opaque ids.

15. Concurrent children

AgentTask::parallel_children(n) lets one model turn run up to n delegations at the same time. AgentProfile::parallel_children does the same for a durable agent. ADR 0013 records the design.

Rust
use autumn_harvest_agent::{AgentTask, Mandate};

let task = AgentTask::new("plan the trip")
    .mandate(Mandate::new("planner").peers(["flights", "hotels", "visas"]))
    .parallel_children(3);

The default is 0, so the fan-out is off until you set n. A call joins a batch only when each of these is true. Otherwise it runs inline, one after another, as with no bound.

  1. n is 2 or more. MAX_PARALLEL_CHILDREN caps it.
  2. The call is a delegate call.
  3. The policy allows the call. A gated call waits for a person.
  4. The peer is inline. A durable peer runs on its own worker.
  5. The call is next to another call that meets 2 to 4.

A run of such calls splits into batches of at most n, in call order. A call left over at the end runs alone.

A delegation that runs alone names why. Its DelegationRecord::serial holds the reason. A debug event logs it once, with the message a delegation ran alone and the fields call_id, chain and reason. A bound under 2 names no reason. A refused delegation names its refusal instead. A call that the policy denies, or that a person rejects, does not run, so it has no record.

serialWhy the call ran alone
single_callNo other call of the turn meets 2 to 4.
not_consecutiveAnother call meets 2 to 4, but no neighbour does.
gatedThe policy gated the call. A person can edit its target, so the peer does not count.
durable_peerThe policy allowed a call to a durable peer.
bound_reachedThe call is the last of a run. The batch before it is full.
RuleWhat happens
BatchSee the list above.
ChildEach call of a batch runs as a child agent_loop run, with its own history.
TokensThe siblings split the token remainder of the tree with sibling_cap. The caps of a batch sum to the remainder.
DelegationsThe siblings split the delegation remainder. Each child run counts its own delegations from zero.
LabelsEach child starts with the labels of the tree. The labels of every child join the tree when the batch ends.
RecordsThe parent adds the usage, the steps and the records of each child, in call order.
  • One history cannot hold two children that each take many steps. Replay pairs a command with the next recorded event, so two children could swap their results. A child run keeps each child in its own history.
  • A sibling does not see the labels of another sibling. They share no context, so no data flows between them.
  • Each child stops after the call that passes its cap, as any run does.
  • A gated call in a child run waits on the child run. Send its approval to that run. The record of the delegation names its run id.
  • A child run starts with no released flow rules.
  • A durable agent takes an interrupt only after the batch ends.
  • A single delegation, a gated one, or one to a durable peer runs inline, as before. Its record names why.
  • eval::evaluate answers each child run with the recorded report of the child with the same delegation chain and task text. With no match, the candidate fails.

16. Mesh-wide controls

A mesh is a set of durable agents that share an id. A long-lived mesh needs controls that span time and agents.

Rust
use std::path::Path;
use std::sync::Arc;
use autumn_harvest_agent::{AgentHarness, AgentModel, AgentProfile, MeshBudget, sqlite};

fn support_mesh(model: Arc<dyn AgentModel>, db: &Path) -> AgentHarness {
    AgentHarness::new(model)
        .agent(AgentProfile::new("triage").mesh("support"))
        .agent(AgentProfile::new("research").mesh("support"))
        .mesh_budget(MeshBudget::daily("support").max_tokens(2_000_000))
        .mesh_spend(Arc::new(sqlite::LedgerSpend::open(db)))
}

On Postgres, use postgres::LedgerSpend::new(pg) and postgres::pause_mesh(&pg, mesh, reason). They do the same, over every database of pg.

ControlWhat happens
Mesh idAgentProfile::mesh names it. Each run, model turn and peer message carries it. A run publishes it as the search attribute agent_mesh.
Period budgetMeshBudget caps tokens or cost for each day, month or fixed window, in UTC. The model-turn activity sums the spend of the mesh with a MeshSpend source before the call. A spent budget refuses the turn with MeshBudgetExceeded.
ParkingA refused durable agent parks until the period ends. Its work waits, first the refused message, and its ticks are skipped. A wake at the period end opens it.
Kill switchsqlite::pause_mesh(rt, mesh, reason) or postgres::pause_mesh(&pg, mesh, reason) sends an interrupt and Control::Pause to every running agent of the mesh. It returns a MeshPause audit record and logs it to the target harvest.audit.
Labelled memoryA memory entry keeps the flow labels of the run that wrote it. A run that reads the entry takes the labels.
  • sqlite::LedgerSpend sums the cost ledger of every run with the mesh attribute, child runs included. postgres::LedgerSpend sums harvest_llm_ledger the same way, with one read for each database. The GIN index on search_attrs and the recorded_at index serve the read.
  • The budget is a soft cap, as the run budgets are. Turns that start before any of them records can all pass the check.
  • A refused message runs again only when no tool ran in its op. Any other op ends as budget_exceeded.
  • A refused first turn leaves the transcript as it was, because no model read the input. A message that runs again then holds its input once.
  • An interrupt stops an op in flight, such as an approval wait. A paused agent takes the pause before its held work.
  • A store that cannot keep labels refuses a write from a run with labels, so no note loses its taint. The default MemoryStore::apply_labelled does this. InMemoryMemoryStore keeps labels.
  • Control::ClearTranscript drops the taint of the transcript. The labels of the memory stay, and the next op reads them again.