Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

Writing an agent — a class with a purpose, skills and an account of its own (Agents kit)

[Actor(Roles = [R])] public class X : Agent { static P Principal => …; Purpose; Skills; Limits; Consults } · app.Agents = new AgentsSetup { Agents = [new X()] } · Agents.Of<X>()

An agent is a class of the `Osysharp.Agents` kit: what it is for, the skills it holds, what it may spend. It runs as an account of its own that holds the roles its class names, and when it works for a person it reads and changes only what both of them may. List it in `app.Agents`, have somebody answer for it, and give it work with `Agents.Of<X>()`.

preview1 example compiled by CIagentskit

Summary#

use Osysharp.Agents;

[Actor(Roles = [ShopRole.Helper])]
public class Estimator : Agent {
  public static Person Principal => new Person { Email = "[email protected]" };
  public override string Purpose => "You estimate bicycle repairs for a small workshop.";
  public override List<SkillBase> Skills => [new EstimateRepair()];
  public override Permits May => Permits.ChangesAlone();        // it notes and prices by itself
}

app.Agents = new AgentsSetup { Agents = [new Estimator()] };

var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);

Signature#

public abstract class Agent {
  virtual string Purpose;              // what it is for — the first thing the model is told
  virtual string Instructions;         // standing instructions, after the purpose
  virtual List<SkillBase> Skills;      // what it can be asked to do
  virtual AgentLimits Limits;          // PerTask and Daily, in dollars
  virtual ModelNeeds Needs;            // what the app's model router decides from
  virtual string Consults;             // a model it may ask for help — "" for nobody
  virtual int StuckAfter;              // failed attempts in a row before it consults (2)
  virtual decimal ConfidenceFloor;     // a decision under this is asked again
}

class AgentsSetup { List<Agent> Agents; decimal TaskBudget = 0.50m; int MaxTurns = 12; }

AgentRef Agents.Of<T>()                // the agent, ready for work — T is listed in app.Agents
List<string> Agents.Names()

void SponsorAgent(string agent, P sponsor)     // somebody answers for it — it does not run until somebody does
void PauseAgent(string agent, string why = "")    void ResumeAgent(string agent, string why = "")   // each written down: agent-roles

policy UsesAgents · SupervisesAgents · AuditsAgents · AgentsMayConsult

Description#

What does an agent class say?#

What it is for (Purpose), what it can be asked to do (Skills — see A skill — a typed job an agent can be given, its check and its tools (Agents kit)), and optionally its standing Instructions, its Limits (What an agent may spend — a hard limit on every piece of work (Agents kit)) and whom it Consults (Asking for help — an agent that consults a more capable model (Agents kit)). The class is also a declared actor: [Actor(Roles = […])] names the roles its account holds and the static Principal says what that account is. The platform makes the account when the app is compiled and keeps its roles exactly as declared.

What may it see and change?#

Its tools run as the agent's account, on behalf of whoever asked. A read finds what the agent and that person may both read; a change needs both to be allowed. Here the Estimator holds Helper, and a helper may not set a price — so the agent cannot either, even for a mechanic who may. In a process, where nobody is signed in, it works with its own roles alone.

Who may give it work, and who is in charge of it?#

The app answers three policies, and each is nobody's until it does: UsesAgents gives agents work and reads the work they asked for, SupervisesAgents names sponsors and pauses or stops agents, AuditsAgents reads everything they did. An agent runs only once somebody answers for it — SponsorAgent("Estimator", person) — and a paused agent takes no new work. Naming a sponsor, pausing, reading every receipt and deciding other people's proposals come with supervising and can each be handed to somebody else; every policy is in one table in Who is in charge of an app's agents — every policy in one table, pausing, sponsoring, and trying work again.

What is it refused before it starts?#

Work that never starts leaves no receipt and throws: AgentNotListed (the class is not in app.Agents), AgentNotSponsored, AgentPaused, SkillNotHeld, and AgentRefused when the caller may not use agents.

How do I start an app that has one?#

osy init --starter agents lays out a project with sign-in and roles, one agent (Assistant) with one typed skill, a scripted model so nothing needs a key, the management screens for an admin at /agents, and a test that gives the agent work with the model's answer written out. It is a shell to fill in. osy docs sample agents-desk prints a small finished app of the same pieces: an editor that reads notes alone and proposes each summary to a person before it is saved.

Examples#

app Workshop {
  use Osysharp.Agents;
  use Osysharp.Agents.Testing;
  use Osysharp.Llm;
  use Osysharp.Identity;
  use Osysharp.Observability;
  model "model/**/*.osy";
}

using Osysharp.Agents;
using Osysharp.Agents.Testing;
using Osysharp.Identity;
using Osysharp.Llm;

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 } ],
};
app.Agents = new AgentsSetup { Agents = [new Estimator()], TaskBudget = 0.05m };

[Role] enum ShopRole { Authenticator, Mechanic, Manager, Helper }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsMechanic => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Mechanic);
policy IsManager  => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Manager);
policy IsHelper   => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Helper);

// The kit's questions, answered by this app.
policy UsesAgents       => IsMechanic || IsManager;   // gives agents work, reads the work they asked for
policy SupervisesAgents => IsManager;                 // names sponsors, pauses and stops, reads that work happened
policy AuditsAgents     => IsManager;                 // reads everything, content included
policy ReadsModelUsage   => IsManager;
policy ManagesModelSpend => IsManager;

entity Person : Principal {
  security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}

/// One bike in for repair.
entity Repair {
  [Required, Unique, MaxLength(40)] string Ticket;
  [Required, MaxLength(200)] string Fault;
  [MaxLength(400)] string Note = "";
  /// What the customer is charged. Only a mechanic or the manager sets it.
  decimal Price;
  security {
    allow read, update when IsMechanic || IsManager || IsHelper;
    allow create when IsMechanic || IsManager;
    deny update Price when IsHelper;
  }
}

/// What an estimate says.
public class Estimate {
  public decimal Hours;
  public string Summary = "";
  public bool NeedsParts;
}

/// The parts a fault needs.
[Action("Look up the parts a fault needs", Query)]
public List<string> PartsFor(string fault) { return fault.Contains("brake") ? ["brake pads", "cable"] : []; }

/// Write a note on the repair's ticket.
[Action("Write a note on the repair")]
public string NoteRepair(Repair repair, string note) { repair.Note = note; return $"Noted on {repair.Ticket}."; }

/// Set what the customer is charged.
[Action("Set the price")]
public string SetPrice(Repair repair, decimal price) { repair.Price = price; return $"{repair.Ticket} costs {price}."; }

/// Works out how long one repair takes.
public class EstimateRepair : Skill<Repair, Estimate> {
  public override string Description => "Estimate one repair.";
  public override string Instructions => "Look up the parts first. Write the estimate as a note on the repair.";
  public override List<ActionInfo> Tools => [Actions.Of(PartsFor), Actions.Of(NoteRepair), Actions.Of(SetPrice)];
  public override string Brief(Repair r) => $"Ticket {r.Ticket}: {r.Fault}. Its id is {r.Id}.";
  public override string? Check(Estimate e) => e.Hours <= 0 ? "An estimate takes some time." : null;
}

/// Estimates the repairs that come in. It holds the Helper role and nothing more.
[Actor(Roles = [ShopRole.Helper])]
public class Estimator : Agent {
  public static Person Principal => new Person { Email = "[email protected]", DisplayName = "Estimator" };
  public override string Purpose => "You estimate bicycle repairs for a small workshop.";
  public override List<SkillBase> Skills => [new EstimateRepair()];
  public override Permits May => Permits.ChangesAlone();        // it notes and prices by itself
}

[TestFixture]
void Shop() {
  var mia = new Person { Email = "[email protected]", DisplayName = "Mia" };
  var noor = new Person { Email = "[email protected]", DisplayName = "Noor" };
  new RoleGrant { Member = mia, Role = ShopRole.Mechanic };
  new RoleGrant { Member = noor, Role = ShopRole.Manager };
  new Repair { Ticket = "T-7", Fault = "rear brake does not bite" };
  new AgentSeat { Agent = "Estimator", Sponsor = noor };          // the manager answers for the agent
}
principal Mia  => Person.Single(p => p.Email == "[email protected]");
principal Noor => Person.Single(p => p.Email == "[email protected]");

string RepairArgs(Repair r, string rest) { return "{\"repair\":\"" + r.Id.ToString() + "\"" + rest + "}"; }

[Test(Shop)]
[runas(Mia)]
void The_agent_does_a_typed_job_with_the_apps_own_functions() {
  var repair = Repair.Single(r => r.Ticket == "T-7");
  var script = AgentScript.Of(
    Says.Tool(Actions.Of(PartsFor), "{\"fault\":\"rear brake does not bite\"}"),
    Says.Tool(Actions.Of(NoteRepair), RepairArgs(repair, ",\"note\":\"New pads and a cable: one hour.\"")),
    Says.HandBack(new Estimate { Hours = 1, Summary = "New pads and a cable.", NeedsParts = true }));
  ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

  var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);

  Assert.True(outcome.Done);
  Assert.Equal(1, outcome.Result.Hours);
  Assert.True(outcome.Result.NeedsParts);
  Assert.Equal("New pads and a cable: one hour.", Repair.Single(r => r.Ticket == "T-7").Note);
  Assert.Equal(["PartsFor", "NoteRepair"], Trajectory.Of(outcome.Task));
}

[Test(Shop)]
[runas(Mia)]
void What_the_agents_own_roles_do_not_allow_it_cannot_do_for_anybody() {
  var repair = Repair.Single(r => r.Ticket == "T-7");
  // Mia may set a price. The agent is a Helper, and a helper may not — so the tool is refused, and the model is told.
  var script = AgentScript.Of(
    Says.Tool(Actions.Of(SetPrice), RepairArgs(repair, ",\"price\":40")),
    Says.HandBack(new Estimate { Hours = 1, Summary = "Could not price it." }));
  ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

  var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);

  Assert.True(outcome.Done);
  Assert.Equal(0, Repair.Single(r => r.Ticket == "T-7").Price);
}

[Test(Shop)]
void A_paused_agent_takes_no_work_and_leaves_no_receipt() {
  runas (Noor) { PauseAgent("Estimator"); }
  runas (Mia) {
    var repair = Repair.Single(r => r.Ticket == "T-7");
    Assert.Throws<AgentPaused>(() => await Agents.Of<Estimator>().Do(new EstimateRepair(), repair));
  }
  runas (Noor) {
    ResumeAgent("Estimator");
    Assert.Equal(0, AgentTask.Count());                  // work that never started left no receipt
  }
}

See also#

Related

Agents (a class with a job, its own account, a budget and a record)

An agent is a class of the `Osysharp.Agents` kit: a purpose, the skills it holds and what it may spend. A skill is a…

A skill — a typed job an agent can be given, its check and its tools (Agents kit)

A skill is a class: what goes in, what comes back, and the app's own functions the agent may use to get there. The…

Giving an agent work — Do, Ask, Stream, For and Decide (Agents kit)

`Do` gives an agent a typed job and answers an `AgentOutcome<TOut>`: the checked result, how it ended, what it cost and…

The receipt and the record — what every piece of an agent's work leaves (Agents kit)

Every piece of work leaves one receipt, `AgentTask` — who asked, the account it ran as, what it cost against its limit…

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…

Asking for help — an agent that consults a more capable model (Agents kit)

An agent can name one of the app's models to consult. The model doing the work may then ask it one focused question…

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…

[Actor] — a principal that code may run as

A class marked `[Actor]` declares a principal that code may run as: a robot, a nightly job, an integration, a digital…

The Llm kit — call a model, and know what each call cost

`Osysharp.Llm` is the road every model call takes, written in Osy#: the operator's AI switch, the app's router, a…