You are here: Building with agents › Deciding what to build › Shape 4
Summary#
A large commission arrives at the translation bureau. Planning it is two different jobs: somebody has to check its terms against the house glossary, and somebody has to price it. The bureau has an agent for each. A third agent, the coordinator, is given the commission, hands both jobs on in one go, and writes the plan from what comes back.
This is the fourth shape: a team of agents with a coordinator.

The tree of a piece of work, from a purchasing office's app: the agent in charge, its three specialists, where each one stands and what each one cost.
Description#
What does it add?#
Nothing new to install. Osysharp.Agents does it:
| You write | It gives you |
|---|---|
Delegates on the coordinator, naming each specialist and the skill it is handed | the coordinator's model is offered each as a tool; pieces asked for in one turn run side by side |
| a skill on each specialist | typed in, typed out, checked — exactly as in [[agent-guide-assistant|shape 1]] |
optionally budget: on a specialist | that piece may spend at most this share of what the coordinator has left |
What do you decide now?#
- Who is in charge, and of what. The coordinator holds the skill people ask for. It needs no tools of its own.
- What each specialist is handed.
Specialist.Of<Pricer>(new PriceWork()): an agent and one of its skills. A coordinator hands on only what its class declares. See [[agent-delegation#declared]]. - What each piece may spend. A piece spends out of what the work above it has left, and
budget:caps it further. A piece that runs out is stopped; the piece beside it is not touched. See [[agent-delegation#money]]. - How deep it goes.
MaxDepthsays whether a specialist may hand work on in turn. - What a specialist may do alone. Its own class says, as in shape 2 — and it is never more than the work above it may do alone. See [[agent-delegation#people]].
What holds, whatever you decide?#
- Rights never grow down the tree. A specialist acts with its own agent's roles and, like the coordinator, never past the person the whole job is for. See [[agent-delegation#rights]].
- Money never grows down the tree. Every piece is charged to its own receipt and to every piece above it. The whole costs what its pieces cost.
- An approval from deep in the tree reaches the right person. When a specialist must propose something, the proposal goes to the person the work at the top is for, and says through whom it comes. The whole chain waits.
- One record for the whole tree, and it can be checked for changes. See [[agent-task-log#chain]].
What does the whole app look like?#
What is new since shape 1 is marked NEW.
app Bureau {
use Osysharp.Agents;
use Osysharp.Agents.Testing;
use Osysharp.Llm;
use Osysharp.Identity;
use Osysharp.Observability;
use Osysharp.Workflow;
model "model/**/*.osy";
}
using Osysharp.Agents;
using Osysharp.Agents.Testing;
using Osysharp.Identity;
using Osysharp.Llm;
using Osysharp.Workflow;
app.Llm = new LlmSetup {
Models = [ new LlmModel { Name = "Everyday", Client = new ScriptedModel { Name = "Everyday" }, Model = "scripted-everyday",
Price = new LlmPrice { Input = 1, Output = 5 }, MaxOutputTokens = 1000 } ],
Default = "Everyday",
};
app.Agents = new AgentsSetup { Agents = [new Coordinator(), new Terminologist(), new Pricer()], TaskBudget = 0.05m };
[Role] enum BureauRole { Authenticator, Translator, Manager, Assistant }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsTranslator => RoleGrant.Any(g => g.Member == user && g.Role == BureauRole.Translator);
policy IsManager => RoleGrant.Any(g => g.Member == user && g.Role == BureauRole.Manager);
policy IsAssistant => RoleGrant.Any(g => g.Member == user && g.Role == BureauRole.Assistant);
policy UsesAgents => IsTranslator || IsManager;
policy SupervisesAgents => IsManager;
policy AuditsAgents => IsManager;
policy ReadsModelUsage => IsManager;
policy ManagesModelSpend => IsManager;
entity Person : Principal {
security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}
entity Commission {
[Required, Unique, MaxLength(40)] string Reference;
[Required, MaxLength(200)] string Title;
[MaxLength(40)] string Into = "";
int Words = 0;
[MaxLength(400)] string Note = "";
security { allow read, create, update when IsTranslator || IsManager || IsAssistant; }
}
/// What the coordinator hands back.
public class Plan {
public int Terms;
/// The fee, in euros — 0 when it could not be priced.
public decimal Fee;
public string Summary = "";
}
/// NEW: what each specialist hands back, and what the pricer is given.
public class TermReport { public int Terms; public string Finding = ""; }
public class PriceAsk { public int Words; public string Into = ""; }
public class Price { public decimal Euros; }
[Action("Look up the house terms for a language", Query)]
public List<string> HouseTerms(string language) {
return language == "Finnish" ? ["sopimus: agreement", "toimitus: delivery"] : [];
}
[Action("Look up the rate per word", Query)]
public decimal RatePerWord(string language) { return language == "Finnish" ? 0.13m : 0.10m; }
/// NEW: a specialist's skill that takes a ROW. Handed on by another agent, the row is named by its id.
public class CheckTerms : Skill<Commission, TermReport> {
public override string Description => "Check one commission against the house terms for its language.";
public override List<ActionInfo> Tools => [Actions.Of(HouseTerms)];
public override string Brief(Commission c) => $"Commission {c.Reference}, into {c.Into}. Its id is {c.Id}.";
public override string HandedSchema() => HandOn.ById("The commission to check.");
public override Commission Handed(string inputJson) {
var id = HandOn.IdIn(inputJson);
return Commission.Single(c => c.Id == id);
}
}
/// NEW: a specialist's skill that takes plain values. Nothing more to say.
public class PriceWork : Skill<PriceAsk, Price> {
public override string Description => "Price one piece of translation work.";
public override List<ActionInfo> Tools => [Actions.Of(RatePerWord)];
}
public class PlanCommission : Skill<Commission, Plan> {
public override string Description => "Plan one commission: have its terms checked and its work priced.";
public override string Brief(Commission c) => $"Commission {c.Reference}: {c.Title}, {c.Words} words into {c.Into}. Its id is {c.Id}.";
}
[Actor(Roles = [BureauRole.Assistant])]
public class Terminologist : Agent {
public static Person Principal => new Person { Email = "[email protected]", DisplayName = "Terminologist" };
public override string Purpose => "You check texts against the bureau's house terms.";
public override List<SkillBase> Skills => [new CheckTerms()];
}
[Actor(Roles = [BureauRole.Assistant])]
public class Pricer : Agent {
public static Person Principal => new Person { Email = "[email protected]", DisplayName = "Pricer" };
public override string Purpose => "You price translation work.";
public override List<SkillBase> Skills => [new PriceWork()];
}
/// NEW — the coordinator. Decision 2: what it hands on, and to whom. Decision 3: the pricing gets a share.
[Actor(Roles = [BureauRole.Assistant])]
public class Coordinator : Agent {
public static Person Principal => new Person { Email = "[email protected]", DisplayName = "Coordinator" };
public override string Purpose => "You plan the commissions of a small translation bureau.";
public override List<SkillBase> Skills => [new PlanCommission()];
public override Delegation Delegates => new Delegation {
To = [Specialist.Of<Terminologist>(new CheckTerms()), Specialist.Of<Pricer>(new PriceWork(), budget: 0.006m)],
MaxDepth = 1, // decision 4
};
}
[TestFixture]
void Office() {
var ines = new Person { Email = "[email protected]", DisplayName = "Ines Varga" };
var otto = new Person { Email = "[email protected]", DisplayName = "Otto Reyes" };
new RoleGrant { Member = ines, Role = BureauRole.Translator };
new RoleGrant { Member = otto, Role = BureauRole.Manager };
new Commission { Reference = "C-31", Title = "Supply agreement", Into = "Finnish", Words = 2400 };
new AgentSeat { Agent = "Coordinator", Sponsor = otto };
new AgentSeat { Agent = "Terminologist", Sponsor = otto };
new AgentSeat { Agent = "Pricer", Sponsor = otto };
}
principal Ines => Person.Single(p => p.Email == "[email protected]");
/// What the coordinator's model says: both pieces in ONE turn — so they are done side by side — then the plan.
AgentScript Plans(Commission c, Plan plan) {
return AgentScript.Of(
Says.Together(
Says.HandTo<Terminologist>(new CheckTerms(), Says.Row(c.Id)),
Says.HandTo<Pricer>(new PriceWork(), JsonSerializer.Serialize(new PriceAsk { Words = 2400, Into = "Finnish" }))),
Says.HandBack(plan));
}
[Test(Office)]
[runas(Ines)]
void The_coordinator_hands_two_pieces_on_at_once_and_plans_from_what_comes_back() {
var commission = Commission.Single();
var cast = AgentCast.Of()
.Plays<Coordinator>(Plans(commission, new Plan { Terms = 2, Fee = 312, Summary = "Two house terms; 312 euros." }))
.Plays<Terminologist>(AgentScript.Of(
Says.Tool(Actions.Of(HouseTerms), "{\"language\":\"Finnish\"}"),
Says.HandBack(new TermReport { Terms = 2, Finding = "Both apply." })))
.Plays<Pricer>(AgentScript.Of(
Says.Tool(Actions.Of(RatePerWord), "{\"language\":\"Finnish\"}"),
Says.HandBack(new Price { Euros = 312 })));
ScriptedAnswer.Stub((model, request) => cast.Answer(model, request));
var outcome = await Agents.Of<Coordinator>().Do(new PlanCommission(), commission, about: "Planning C-31");
// Typed out of each piece, typed out of the whole.
Assert.True(outcome.Done);
Assert.Equal(312m, outcome.Result.Fee);
// One receipt for each piece, under the coordinator's — for the same person, about the same thing.
Assert.Equal(["Coordinator · Plan commission: Done", " Terminologist · Check terms: Done", " Pricer · Price work: Done"], Trajectory.Tree(outcome.Task));
var pieces = WorkTree.HandedOnBy(AgentTask.Single(t => t.Id == outcome.Task));
Assert.Equal(Ines.Id, pieces[0].Requester!.Id);
Assert.Equal("Planning C-31", pieces[1].About);
// The pricer's piece was opened with its share; and the whole costs what its pieces cost.
Assert.Equal(6000, pieces[1].LimitMicros);
Assert.Equal(AgentTask.ToList().Sum(t => t.SpentMicros), Trajectory.TreeCost(outcome.Task));
// One record for the tree, and it checks.
Assert.True(VerifyTrace(pieces[0]).Intact);
}
[Test(Office)]
[runas(Ines)]
void A_piece_that_runs_out_of_money_is_stopped_and_the_coordinator_carries_on() {
var commission = Commission.Single();
var cast = AgentCast.Of()
.Plays<Coordinator>(Plans(commission, new Plan { Terms = 2, Fee = 0, Summary = "Two house terms. Not priced: the pricing ran out of money." }))
.Plays<Terminologist>(AgentScript.Of(Says.HandBack(new TermReport { Terms = 2 })))
// The pricer's first answer is a long one — 1000 tokens in, 600 out: 4000 of its 6000. A second call does not fit.
.Plays<Pricer>(AgentScript.Of(
Says.Tool(Actions.Of(RatePerWord), "{\"language\":\"Finnish\"}", 1000, 600),
Says.HandBack(new Price { Euros = 312 })));
ScriptedAnswer.Stub((model, request) => cast.Answer(model, request));
var outcome = await Agents.Of<Coordinator>().Do(new PlanCommission(), commission);
// The piece was stopped at its own share; the piece beside it was not touched; the coordinator was told, and
// handed back what it had.
Assert.Equal(["Coordinator · Plan commission: Done", " Terminologist · Check terms: Done", " Pricer · Price work: Stopped"], Trajectory.Tree(outcome.Task));
Assert.True(Trajectory.Said(outcome.Task).Any(s => s.StartsWith("Pricer was stopped: Its money ran out:")));
Assert.True(outcome.Done);
Assert.Equal(0m, outcome.Result.Fee);
}What would make this the wrong shape?#
- The pieces must happen in a fixed order, with people in between. That is a process: see shape 3. A process step can itself be a coordinator.
- One agent with two more tools would do. Then do that: see [[agent-guide-choosing#tools-or-delegation]].
What to read next#
- One piece of work, followed end to end — one piece of work followed through every kit, delegation included
- Choosing — I want X, so use Y, not Z, because — one agent with more tools, or delegation?
- A supplier is qualified — five days, four agents, one that failed, and a three-day wait — a coordinator and three specialists qualifying a supplier
Go deeper#
- An agent hands work on to other agents — at once, typed, and never with more than it holds (Agents kit) — handing work on: rights, money, waiting, failing
- What an agent may spend — a hard limit on every piece of work (Agents kit) — what a piece of work may spend
- [[agent-requests#tree]] — the tree of a piece of work on a page
- [[agent-testing#cast]] — scripting several agents at once