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

> 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>()`.

<!-- id: agent-agents · area: agent · stability: preview · html: https://osysharp.com/reference/agent/agents/ -->

## Summary        {#summary}
```osy syntax
use Osysharp.Agents;

[Actor(Roles = [ShopRole.Helper])]
public class Estimator : Agent {
  public static Person Principal => new Person { Email = "estimator@workshop.example" };
  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      {#signature}
```osy syntax
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    {#description}

### What does an agent class say?   {#class}
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)](https://osysharp.com/reference/agent/skills/)), and optionally its standing
`Instructions`, its `Limits` ([What an agent may spend — a hard limit on every piece of work (Agents kit)](https://osysharp.com/reference/agent/budgets/)) and whom it `Consults` ([Asking for help — an agent that consults a more capable model (Agents kit)](https://osysharp.com/reference/agent/help/)). The class is also a
[declared actor](https://osysharp.com/reference/security/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?   {#rights}
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?   {#who}
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](https://osysharp.com/reference/agent/roles/).

### What is it refused before it starts?   {#refused}
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?   {#start}
`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       {#examples}
```osy title="a workshop with one agent: its account, its skill, its tools, and what it may not do" run app=agent-agents
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 = "estimator@workshop.example", 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 = "mia@workshop.example", DisplayName = "Mia" };
  var noor = new Person { Email = "noor@workshop.example", 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 == "mia@workshop.example");
principal Noor => Person.Single(p => p.Email == "noor@workshop.example");

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       {#see-also}
- [Starting a project](https://osysharp.com/reference/local/starting-a-project/) — `osy init --starter agents`, the shell this page's agent starts from
- [What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit)](https://osysharp.com/reference/agent/autonomy/) — `May`: what it does alone, what it proposes to a person, what it never does
- [An agent asks a person, and waits — the person rung of the ladder of help (Agents kit)](https://osysharp.com/reference/agent/people/) — `People`: when it turns to a person, and how its work waits
- [Guards and trying again — the app's own checks on an agent's work, and what happens when work fails (Agents kit)](https://osysharp.com/reference/agent/guards/) — the app's own checks on its work
- [A skill — a typed job an agent can be given, its check and its tools (Agents kit)](https://osysharp.com/reference/agent/skills/) — what an agent can be asked to do: a typed job, its check and its tools
- [Giving an agent work — Do, Ask, Stream, For and Decide (Agents kit)](https://osysharp.com/reference/agent/ask/) — giving it work: `Do`, `Ask`, `Stream`, `For`, `Decide`
- [The receipt and the record — what every piece of an agent's work leaves (Agents kit)](https://osysharp.com/reference/agent/task-log/) — the receipt and the record every piece of work leaves
- [What an agent may spend — a hard limit on every piece of work (Agents kit)](https://osysharp.com/reference/agent/budgets/) — what a piece of work may spend
- [Asking for help — an agent that consults a more capable model (Agents kit)](https://osysharp.com/reference/agent/help/) — asking a more capable model
- [Testing an agent without a model — Says, AgentScript and Trajectory (Agents.Testing kit)](https://osysharp.com/reference/agent/testing/) — scripting the model in a test
- [[Actor] — a principal that code may run as](https://osysharp.com/reference/security/declared-actor/) — the account an agent runs as
