Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

Shape 4 — a team of agents with a coordinator

One agent is in charge of a job and hands typed pieces of it to specialists, several at once. Each piece is a piece of work of its own, with its own receipt under the coordinator's. A specialist never holds more rights than the work above it, never spends more than is left above it, and never does alone what the work above it must propose. It takes no new kit: delegation is part of `Osysharp.Agents`.

preview1 example compiled by CIagentsguide

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.

A piece of work with everything it handed on, and what each piece waits for

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 writeIt gives you
Delegates on the coordinator, naming each specialist and the skill it is handedthe coordinator's model is offered each as a tool; pieces asked for in one turn run side by side
a skill on each specialisttyped in, typed out, checked — exactly as in [[agent-guide-assistant|shape 1]]
optionally budget: on a specialistthat piece may spend at most this share of what the coordinator has left

What do you decide now?#

  1. Who is in charge, and of what. The coordinator holds the skill people ask for. It needs no tools of its own.
  2. 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]].
  3. 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]].
  4. How deep it goes. MaxDepth says whether a specialist may hand work on in turn.
  5. 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]].

Go deeper#

Related

Building with agents — the developer's guide

Where a developer starts with agents in an Osy# app. Decide what you are building, read the smallest working app for…

Deciding what to build — four shapes of app with agents in it

Start from what the app is for, not from the kits. There are four shapes: an assistant for one person, work done for…

Shape 3 — agents inside a business process

A business process is a workflow, and an agent's work is one of its steps: an ordinary awaited call that answers the…

An agent hands work on to other agents — at once, typed, and never with more than it holds (Agents kit)

An agent's class may name work it hands on: skills of other agents, or of its own done apart. Its model is offered each…

What an agent may spend — a hard limit on every piece of work (Agents kit)

Every piece of work has a limit in dollars — the skill's `Budget`, else the agent's `Limits.PerTask`, else the app's…

An agent's proposals and questions in a person's inbox, and an inbox item in an agent's hands (Agents.HumanRequests kit)

With the Agents kit and the HumanRequests kit both in an app, this bridge is there: what an agent proposes is an…

Testing an agent without a model — Says, AgentScript and Trajectory (Agents.Testing kit)

`Osysharp.Agents.Testing` scripts what the model answers, turn by turn, and reads back what the agent then did. Only…