# What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit)

> An agent's class says what it may do alone, what it must propose to a person first, and what it never does — by what a tool DOES (changes data, sends, spends, reaches outside), as the compiler proved it, and tool by tool. Said nothing, an agent looks things up alone and proposes everything else. A proposal is the exact call: the tool, its arguments, why, and what the agent had looked at. The work waits; a yes runs the tool as whoever said yes — and as the agent: only what both may — for the version of things that was shown; a no gives the agent the reason. A person the app trusts may say "from now on, alone", and take it back.

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

## Summary        {#summary}
```osy syntax
[Actor(Roles = [ShopRole.Helper])]
public class Estimator : Agent {
  public override Permits May => new Permits {
    Alone = [Actions.Of(NoteRepair)],          // it writes its note by itself
    Never = [Actions.Of(ScrapRepair)],         // and it never scraps a repair
  };                                           // everything else that changes anything is PROPOSED
}

var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);
if (outcome.Waiting) { … }                     // it stopped on a person; the answer comes as an event
ApproveAgentProposal(proposal);                // …runs it now, as whoever approves
RejectAgentProposal(proposal, "Why not.");     // …the agent is told, and goes on
```

## Signature      {#signature}
```osy syntax
class Agent     { virtual Permits May; }               // the agent's ceiling
class SkillBase { virtual Permits? May;                // stricter for this skill, never looser
                  virtual string VersionOf(string action, string argumentsJson);     // what a yes covers
                  virtual string Names(string action, string argument, string value); }  // an id → the record's name

class Permits {
  Leave Changes, Sends, Spends, ReachesOut;            // by what a tool does — each Propose until you say
  List<ActionInfo> Alone, Propose, ProposeEveryTime, Never;      // tool by tool; the strictest wins
  static Permits Everything();   static Permits ChangesAlone();
}
enum Leave { Alone, Propose, ProposeEveryTime, Never }

entity AgentProposal {
  AgentTask Task; string Agent; string Skill; string Action; string Label; string Does;
  string ArgumentsJson; string ShownJson; string Why; string EvidenceJson;
  string SubjectVersion; string Fingerprint;
  Principal? DecideBy;                                 // who it is put to
  ProposalStatus Status;                               // Waiting, Done, Failed, Rejected, Stale, Withdrawn
  Principal? DecidedBy; string Reason; string? ResultJson; string Problem;
}

ApproveAgentProposal(AgentProposal proposal, bool alwaysLikeThis = false) → ProposalDecision
RejectAgentProposal(AgentProposal proposal, string reason)                → ProposalDecision
ProposalsForMe() → List<AgentProposal>

AllowAlone(string agent, ActionInfo tool, string skill = "", bool forEverybody = false) → AgentStandingRule
RevokeStandingRule(AgentStandingRule rule, string why = "")
virtual policy WidensAgentAutonomy => false;           // who may say "from now on, alone"

// events: AgentProposed · AgentProposalDecided · AgentTaskWaiting
```

## Description    {#description}

### What may an agent do alone?   {#gate}
What its class says — and, said nothing, only what reads:

| A tool that… | Default | Say otherwise with |
|---|---|---|
| only reads (`[Action(…, Query)]`) | runs alone, always | — |
| creates, changes or deletes the app's data | proposed | `Changes = Leave.Alone` |
| sends something to somebody (`[Sends]`) | proposed | `Sends = Leave.Alone` |
| spends (`[Spends]`) | proposed | `Spends = Leave.Alone` |
| reaches outside the app some other way | proposed | `ReachesOut = Leave.Alone` |

What a tool does is what the compiler proved of the [action](https://osysharp.com/reference/function/action/), so nothing is declared safer than
it is. A tool named in `Alone`, `Propose`, `ProposeEveryTime` or `Never` is treated as that list says whatever it
does, and a tool in two lists gets the stricter. `Permits.ChangesAlone()` is the common middle: the app's own data
alone; anything that leaves it proposed. `Permits.Everything()` proposes nothing.

A [skill](https://osysharp.com/reference/agent/skills/)'s `May` can only be stricter for that skill: the stricter of the agent's and the skill's
decides, tool by tool. `Skills.Catalog()` says the result per tool (`SkillToolInfo.Leave`).

### What is a proposal?   {#proposal}
The call the agent wants to make, kept exactly: the tool and its arguments, what it does, **why** — what the model
said as it asked for the tool — and **what it had looked at**: each tool it ran before, with what came back. It is
put to the person the work is for; to the agent's sponsor when the work is for nobody (a process).

The work stops where it stands. `Do` answers an outcome whose `Status` is `Waiting`; the receipt says the same and
what it waits on; nothing runs and no model is asked meanwhile. The wait costs nothing and has no deadline of its
own. See [[agent-people#waiting]] for how the work — and a process that gave it — goes on.

With [`Osysharp.Agents.HumanRequests`](https://osysharp.com/reference/agent/requests/) in the app the proposal is an approval request in that
person's inbox; without it, the app shows `ProposalsForMe()` wherever it likes and calls the two functions below.

### Can a skill send its proposals to somebody else?   {#route}
Yes — "refunds go to Finance". A skill answers `DecidedBy(action)` for a tool, with a queue or with one person:

```osy syntax
public class SettleTerms : Skill<Supplier, Vetting> {
  public override List<ActionInfo> Tools => [Actions.Of(LookUp), Actions.Of(AgreeTerm)];
  // What this skill proposes for a payment term waits in the Finance queue.
  public override ProposalRoute? DecidedBy(string action) =>
    action == Actions.Of(AgreeTerm).Name ? ProposalRoute.ToQueue("Finance") : null;
}
```

- `ProposalRoute.ToQueue("Finance")` — a work queue, by its name: whoever works that queue decides. A queue is how a
  role with several holders is asked, and a *restricted* queue keeps the request to its members
  ([The WorkQueues kit — queues, who holds what, and the service levels promised on any item](https://osysharp.com/reference/workflow/inbox-kit/)). It needs [`Osysharp.Agents.HumanRequests`](https://osysharp.com/reference/agent/requests/), which puts the proposal
  there; without it the proposal waits for whoever `DecidesAgentProposals` admits.
- `ProposalRoute.ToPerson(account)` — one person: the holder of a role the app looks up.
- `null` — the default: the person the work is for; the sponsor when it is for nobody.

It changes who is asked and nothing else. **The person the work is for does not decide a proposal that was sent
elsewhere** — asking an agent for a refund is not approving one — neither on the proposal nor from the queue, if they
work it too. Whoever `DecidesAgentProposals` admits still may. A tool the agent never runs is still never proposed;
a yes still runs the tool as whoever says it, with what they may do; and the receipt says what it waits for: "a
decision from the Finance queue". `AgentProposal.Routed`, `DecideQueue` and `RouteWords(proposal)` say where one
went, and `Skills.Catalog()` says it of each tool (`DecidedByQueue`, `DecidedByOnePerson`).

The last example below sends a price to the manager. A queue is shown end to end by
`kits/agents/tests/routing.test.osy` and `kits/agents-human-requests/tests/routing.test.osy`.

### What does approving do?   {#approve}
`ApproveAgentProposal` runs the tool **now**, as the agent **on behalf of whoever approved**: it does only what
both may, by the app's ordinary `security {}` rules. So a yes from somebody who may not do the thing does not do it —
they are told so, and so is the agent. It is the same intersection as work the agent does alone for a person
([[agent-ask#for]]); the person is simply the one who said yes.

A yes is to **what was shown**. The proposal carries a fingerprint — the agent, the skill and its version, the tool,
its arguments, the version of what it would act on, which the skill says with `VersionOf` (an order's revision,
a document's hash), and the [[agent-teaching#sets|instruction set]] the agent was on as it proposed. When that reads
differently as the person decides, nothing runs: the proposal is `Stale`, the person is told, and the agent is told
to look again — told, when it is the instruction set that changed, what it is told now.

The proposal is for whoever it is put to, the person the work is for — unless its skill [[#route|sent it
elsewhere]] — and whoever `DecidesAgentProposals` admits (whoever supervises, until the app says more). It is decided
once.

`RejectAgentProposal` needs a reason. The agent is given it as what its tool answered, and goes on — to propose
something else, or to finish without.

### What can never be approved?   {#never}
A tool in `Never` is not offered to the model at all. Asked for anyway, it does not run **and is not proposed**, so
there is nothing for anybody to approve; `AllowAlone` refuses it; and a proposal whose tool the declaration has
since ruled out does not run whoever says yes. The same holds for a tool that is not one of the work's tools.

### "Approve all like this"?   {#standing}
`ApproveAgentProposal(proposal, alwaysLikeThis: true)` — or `AllowAlone(agent, tool)` — makes a **standing rule**:
from its next step the agent runs that tool without proposing it, **in the work it does for whoever said so**. It is
for whoever the app's `WidensAgentAutonomy` admits; `forEverybody: true`, for work done for anybody, is for whoever
supervises the agents. It widens nothing past the declaration: a tool the agent never runs, or proposes *every*
time (`ProposeEveryTime`), takes no rule — and the tool still runs as the agent and never past the person the work
is for. `RevokeStandingRule` takes it back; the agent proposes again from its next step. A rule counts its own use
(`Used`, `LastUsedAt`): each time a tool ran alone because of it.

## Examples       {#examples}
```osy title="it notes alone, proposes a price, and a yes runs it as whoever said yes — or as the manager the skill sends it to" run app=agent-autonomy
app Workshop {
  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 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);

policy UsesAgents          => IsMechanic || IsManager;
policy SupervisesAgents    => IsManager;
policy AuditsAgents        => IsManager;
policy WidensAgentAutonomy => IsManager;              // who may say "from now on, alone"
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. The manager sets it — and so may the estimator's own account, which, working for
  /// a person, does it only when that person may too.
  decimal Price;
  /// Counts up with every change to the fault: what an approval of a price is an approval FOR.
  int Revision = 1;
  bool Scrapped = false;
  security {
    allow read, update when IsMechanic || IsManager || IsHelper;
    allow create when IsMechanic || IsManager;
    deny update Price when !IsManager && !IsHelper;
  }
}

public class Estimate {
  public decimal Hours;
  public string Summary = "";
}

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

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

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

[Action("Scrap the repair")]
public string ScrapRepair(Repair repair) { repair.Scrapped = true; return "Scrapped."; }

Repair? RepairIn(string argumentsJson) {
  var values = JsonSerializer.Deserialize<Dictionary<string, Json>>(argumentsJson);
  if (!values.ContainsKey("repair")) { return null; }
  var id = Guid.Parse(values["repair"].ToString().Replace("\"", ""));
  return Repair.FirstOrDefault(r => r.Id == id);
}

public class EstimateRepair : Skill<Repair, Estimate> {
  public override string Description => "Estimate one repair, and price it.";
  public override List<ActionInfo> Tools => [Actions.Of(PartsFor), Actions.Of(NoteRepair), Actions.Of(SetPrice), Actions.Of(ScrapRepair)];
  public override string Brief(Repair r) => $"Ticket {r.Ticket}: {r.Fault}. Its id is {r.Id}.";
  // A yes to a price covers the repair as it was described: described again, it is asked again.
  public override string VersionOf(string action, string argumentsJson) {
    var repair = RepairIn(argumentsJson);
    return repair == null ? "" : "r" + repair!.Revision.ToString();
  }
  // A person reads the ticket, not its id.
  public override string Names(string action, string argument, string value) {
    if (argument != "repair") { return value; }
    var repair = Repair.FirstOrDefault(r => r.Id == Guid.Parse(value));
    return repair == null ? value : "Ticket " + repair!.Ticket;
  }
}

/// It writes its note by itself, never scraps a repair, and proposes the rest — the price.
[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(), new PriceFleetRepair()];
  public override Permits May => new Permits { Alone = [Actions.Of(NoteRepair)], Never = [Actions.Of(ScrapRepair)] };
}

/// ⭐ Prices a fleet customer's repair — and what it proposes for a price is the manager's to decide, whoever asked
/// for the estimate.
public class PriceFleetRepair : Skill<Repair, Estimate> {
  public override string Description => "Price one repair for a fleet customer.";
  public override List<ActionInfo> Tools => [Actions.Of(PartsFor), Actions.Of(SetPrice)];
  public override string Brief(Repair r) => $"Ticket {r.Ticket}: {r.Fault}. Its id is {r.Id}.";
  public override ProposalRoute? DecidedBy(string action) =>
    action == Actions.Of(SetPrice).Name ? ProposalRoute.ToPerson(TheManager()) : null;
}

/// The manager, looked up by the role the workshop gives her.
Principal TheManager() { return RoleGrant.First(g => g.Role == ShopRole.Manager).Member; }

[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 };
}
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 + "}"; }
AgentScript NoteThenPrice(Repair repair) {
  return AgentScript.Of(
    Says.Tool(Actions.Of(NoteRepair), RepairArgs(repair, ",\"note\":\"Pads and a cable.\"")),
    Says.Tool("Pads, a cable and an hour of work.", Actions.Of(SetPrice), RepairArgs(repair, ",\"price\":45")),
    Says.HandBack(new Estimate { Hours = 1, Summary = "Pads and a cable." }));
}
/// Let the work a person has decided go on: its wait's own run is settled.
void GoOn(Guid task) { Workflow.Settle(AgentWait.Where(w => w.Task.Id == task).OrderByDescending(w => w.Since).First()); }

[Test(Shop)]
void It_notes_alone_proposes_the_price_and_a_yes_runs_it_as_whoever_said_yes() {
  Guid task;
  // The mechanic asks for an estimate. The note is written; the price is PROPOSED, and the work waits.
  runas (Mia) {
    var repair = Repair.Single(r => r.Ticket == "T-7");
    var script = NoteThenPrice(repair);
    ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

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

    Assert.True(outcome.Waiting);
    Assert.Equal("Pads and a cable.", Repair.Single(r => r.Ticket == "T-7").Note);
    Assert.Equal(0, Repair.Single(r => r.Ticket == "T-7").Price);
    var proposal = ProposalsForMe().Single();
    Assert.Equal("Set the price", proposal.Label);
    Assert.Equal("Pads, a cable and an hour of work.", proposal.Why);
    Assert.Equal("[{\"Name\":\"repair\",\"Value\":\"Ticket T-7\"},{\"Name\":\"price\",\"Value\":\"45\"}]", proposal.ShownJson);
    Assert.Contains("Write a note on the repair", proposal.EvidenceJson);
    Assert.Equal("r1", proposal.SubjectVersion);

    // Her yes runs it as her — and a mechanic may not set a price. It is not done, and she is told.
    var decision = ApproveAgentProposal(proposal);
    Assert.Equal(ProposalStatus.Failed, decision.Status);
    Assert.Equal(0, Repair.Single(r => r.Ticket == "T-7").Price);
    GoOn(task);
    Assert.Equal(AgentTaskStatus.Done, AgentTask.Single().Status);
  }
  // The same proposal, decided by the manager: it runs as her, and the price is set.
  runas (Mia) {
    var repair = Repair.Single(r => r.Ticket == "T-7");
    task = (await Agents.Of<Estimator>().Do(new EstimateRepair(), repair)).Task;
  }
  runas (Noor) {
    var proposal = AgentProposal.Single(p => p.Status == ProposalStatus.Waiting);
    Assert.Equal(ProposalStatus.Done, ApproveAgentProposal(proposal).Status);
    Assert.Equal(45, Repair.Single(r => r.Ticket == "T-7").Price);
    GoOn(task);
    Assert.Contains("Noor approved SetPrice, and it ran as them.", Trajectory.Said(task));
  }
}

[Test(Shop)]
[runas(Noor)]
void A_no_gives_the_agent_the_reason_and_a_yes_does_not_cover_what_changed() {
  var repair = Repair.Single(r => r.Ticket == "T-7");
  var script = NoteThenPrice(repair);
  ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

  var first = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);
  Assert.Throws<ValidationException>(() => RejectAgentProposal(ProposalsForMe().Single(), ""));
  RejectAgentProposal(ProposalsForMe().Single(), "Quote the cable separately.");
  GoOn(first.Task);
  Assert.Contains("Noor said no to SetPrice: Quote the cable separately.", Trajectory.Said(first.Task));
  Assert.Equal(0, Repair.Single(r => r.Ticket == "T-7").Price);

  // Proposed again — and the fault is described anew before she decides. Her yes was to the repair as it was.
  var second = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);
  repair.Fault = "rear brake does not bite; the rim is bent";
  repair.Revision = 2;
  UnitOfWork.Commit();
  var decision = ApproveAgentProposal(ProposalsForMe().Single());
  Assert.Equal(ProposalStatus.Stale, decision.Status);
  Assert.Equal(0, Repair.Single(r => r.Ticket == "T-7").Price);
}

[Test(Shop)]
[runas(Noor)]
void From_now_on_alone_and_what_it_never_does() {
  var repair = Repair.Single(r => r.Ticket == "T-7");
  var script = NoteThenPrice(repair);
  ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

  // "Yes — and from now on, do this alone."
  var first = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);
  ApproveAgentProposal(ProposalsForMe().Single(), alwaysLikeThis: true);
  GoOn(first.Task);
  var rule = AgentStandingRule.Single();
  Assert.Equal("SetPrice", rule.Action);

  // The next estimate she asks for sets its price without asking.
  var second = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);
  Assert.True(second.Done);
  Assert.Equal(1, AgentProposal.Count());

  // She takes it back: it proposes again.
  RevokeStandingRule(rule, "Prices are mine again.");
  UnitOfWork.Commit();
  Assert.True((await Agents.Of<Estimator>().Do(new EstimateRepair(), repair)).Waiting);

  // What it never does takes no rule, and is not offered to the model at all.
  Assert.Throws<AgentRefused>(() => AllowAlone("Estimator", Actions.Of(ScrapRepair)));
  var tools = Skills.Find("Estimator", "EstimateRepair")!.Tools;
  Assert.Equal(["Alone", "Alone", "Propose", "Never"], tools.Select(t => t.Leave.Name).ToList());
}

[Test(Shop)]
void A_price_the_skill_sends_to_the_manager_is_not_decided_by_who_asked_for_it() {
  Guid task;
  runas (Mia) {
    var repair = Repair.Single(r => r.Ticket == "T-7");
    var script = AgentScript.Of(
      Says.Tool("A fleet rate: pads, a cable and an hour.", Actions.Of(SetPrice), RepairArgs(repair, ",\"price\":38")),
      Says.HandBack(new Estimate { Hours = 1, Summary = "Fleet rate." }));
    ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

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

    // It waits on the manager, not on Mia, and the receipt says so.
    var proposal = AgentProposal.Single();
    Assert.True(proposal.Routed);
    Assert.Equal(Noor.Id, proposal.DecideBy!.Id);
    Assert.Equal("a decision from Noor", AgentTask.Single(t => t.Id == task).WaitingOn);
    // Mia asked for the work; the decision is not hers.
    var refused = Assert.Throws<AgentRefused>(() => ApproveAgentProposal(AgentProposal.Single()));
    Assert.Equal("This proposal is decided by Noor: the skill that proposed it says so. The work is for you; the decision is not yours.", refused.Message);
    Assert.Equal(0, Repair.Single(r => r.Ticket == "T-7").Price);
  }
  runas (Noor) {
    Assert.Equal(ProposalStatus.Done, ApproveAgentProposal(AgentProposal.Single()).Status);
    Assert.Equal(38, Repair.Single(r => r.Ticket == "T-7").Price);
    GoOn(task);
    Assert.Equal(AgentTaskStatus.Done, AgentTask.Single(t => t.Id == task).Status);
  }
}
```

## See also       {#see-also}
- [An agent asks a person, and waits — the person rung of the ladder of help (Agents kit)](https://osysharp.com/reference/agent/people/) — asking a person a question, and how work — and a process — waits
- [An agent's proposals and questions in a person's inbox, and an inbox item in an agent's hands (Agents.HumanRequests kit)](https://osysharp.com/reference/agent/requests/) — proposals and questions as requests in a person's inbox
- [Writing an agent — a class with a purpose, skills and an account of its own (Agents kit)](https://osysharp.com/reference/agent/agents/) — the agent class, its account and who is in charge of it
- [A skill — a typed job an agent can be given, its check and its tools (Agents kit)](https://osysharp.com/reference/agent/skills/) — a typed job and its tools
- [Giving an agent work — Do, Ask, Stream, For and Decide (Agents kit)](https://osysharp.com/reference/agent/ask/) — giving an agent work; whose rights it runs with
- [[Action] — what an app offers to be done, by name](https://osysharp.com/reference/function/action/) — an action, and what the compiler proves it does
- [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
