# Shape 2 — work done for people, with approvals

> The agent may do some things alone and must propose the rest. A proposal lands in a person's inbox as an approval showing the exact call, why, and what the agent had looked at. A yes runs it as the person who said yes; a no goes back to the agent with its reason. It adds `Osysharp.HumanRequests` and the inbox to shape 1; the bridge between the two kits arrives by itself.

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

> **You are here:** [Building with agents](https://osysharp.com/reference/agent/guide/) › [Deciding what to build](https://osysharp.com/reference/agent/guide-deciding/) › Shape 2

## Summary        {#summary}
The translation bureau from [shape 1](https://osysharp.com/reference/agent/guide-assistant/) now lets its agent work out a fee. Writing a note is
harmless, so the agent does that alone. Setting a fee is money, so the agent **proposes** it: the manager finds an
approval in his inbox that says what will happen, to which commission, and why. He says yes, and the fee is set —
as him, with his rights.

This is the second shape: **work done for people, with approvals.**

![A proposal in a person's inbox: what will happen, to what, and what the agent looked at first](https://osysharp.com/media/ref/364e22-agents-requests-proposal-wide-light.png)

*A proposal as a person sees it, from a purchasing office's app. The two buttons are Approve and Reject.*

## Description    {#description}

### What does it add to shape 1?   {#takes}

| You add | It gives you |
|---|---|
| [[agent-kit-human-requests\|Osysharp.HumanRequests]] | a question, an approval or a form put to a person, and the answer brought back |
| [[agent-kit-work-queues\|Osysharp.WorkQueues]] | the inbox those requests wait in |
| [[agent-kit-agents-human-requests\|Osysharp.Agents.HumanRequests]] | nothing to write: it arrives by itself when the two above are both in the app, and turns an agent's proposal into an approval in the inbox |

### What do you decide now?   {#decisions}

1. **What the agent does alone, what it proposes, what it never does.** One line on the agent:
   `new Permits { Alone = [Actions.Of(NoteCommission)] }`. Everything else that changes something is proposed.
   A tool listed in `Never` is not even offered to the model. See [[agent-autonomy#gate]].
2. **Who a proposal goes to.** Said nothing, it goes to the person the work is for. A skill can send it elsewhere,
   to a queue or to one person: "fees go to Finance". See [[agent-autonomy#route]].
3. **Who else may decide.** Whoever the app's `DecidesAgentProposals` admits may decide a proposal that was put to
   somebody else. By default that is whoever supervises the agents. See [[agent-roles#deciders]].
4. **Who may say "from now on, do this alone".** The policy `WidensAgentAutonomy`. Nobody, until the app says who.
   See [[agent-autonomy#standing]].
5. **Where requests wait and how long.** `app.AgentRequests` names the queue and the time a decision is wanted
   within. See [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/).

### What holds, whatever you decide?   {#holds}
- **A yes runs as the person who said it.** If they may not do the thing themselves, it is not done, and they are
  told. The test below shows both cases.
- **A yes covers what was shown.** If the thing changed before the person decided, nothing runs and the agent is
  told to look again. See [[agent-autonomy#approve]].
- **Waiting costs nothing.** While a proposal waits, no model is asked. The work goes on from where it stood,
  however many days later. See [[agent-people#waiting]].
- **A no needs a reason**, and the agent is given it.

### What does the whole app look like?   {#app}
What is new since shape 1 is marked `NEW`.

```osy title="the agent notes alone and proposes a fee; a yes in the inbox runs it as whoever said yes" run app=agent-guide-approvals
app Bureau {
  use Osysharp.Agents;
  use Osysharp.Agents.Testing;
  use Osysharp.Agents.HumanRequests;     // NEW: the bridge — `osy lock` adds it by itself to an app that uses both kits
  use Osysharp.HumanRequests;            // NEW
  use Osysharp.WorkQueues;               // NEW
  use Osysharp.Llm;
  use Osysharp.Identity;
  use Osysharp.Organizations;
  use Osysharp.Organizations.ServicePlans;
  use Osysharp.Presence;
  use Osysharp.Observability;
  use Osysharp.Workflow;
  use Osysharp.ServiceHours;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Organizations.ServicePlans;
using Osysharp.WorkQueues;
using Osysharp.HumanRequests;
using Osysharp.Agents;
using Osysharp.Agents.Testing;
using Osysharp.Agents.HumanRequests;
using Osysharp.Llm;
using Osysharp.Workflow;
using Osysharp.ServiceHours;

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 Preparer()], TaskBudget = 0.05m };
// NEW — decision 5: proposals wait in the Bureau queue, and are wanted within a working day.
app.AgentRequests = new AgentRequestsSetup { Queue = "Bureau", DecideWithin = TimeSpan.FromHours(8) };
app.Inbox = new InboxSetup { DefaultQueue = "Bureau" };

[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 IsOffice     => IsTranslator || IsManager;

policy UsesAgents          => IsOffice;
policy SupervisesAgents    => IsManager;
policy AuditsAgents        => IsManager;
policy WidensAgentAutonomy => IsManager;            // NEW — decision 4: who may say "from now on, alone"
policy ReadsModelUsage     => IsManager;
policy ManagesModelSpend   => IsManager;
// NEW — the inbox's and the requests' own questions.
policy WorksInbox       => IsOffice || IsAssistant;
policy LeadsInbox       => IsManager;
policy AdministersInbox => IsManager;
policy ManagesOrganizations  => IsManager;
policy CollaboratesOnRecords => IsOffice;
policy AsksPeople       => IsOffice;
policy OverseesRequests => IsManager;

entity Person : Principal {
  security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}
partial entity ServiceHours { security { allow read when IsOffice; } }
partial entity ServiceWindow { security { allow read when IsOffice; } }
partial entity ServiceException { security { allow read when IsOffice; } }
partial entity WorkItem { security { allow read when IsOffice || IsAssistant; } }

/// One text a customer has sent in to be translated.
entity Commission {
  [Required, Unique, MaxLength(40)] string Reference;
  [Required, MaxLength(200)] string Title;
  [MaxLength(40)] string Into = "";
  int Words = 0;
  [MaxLength(400)] string Note = "";
  /// NEW: what the customer is charged. The manager sets it — and so may the agent's own account, which, working for
  /// a person, does it only when that person may too.
  decimal Fee;
  security {
    allow read, update when IsOffice || IsAssistant;
    allow create when IsOffice;
    deny update Fee when !IsManager && !IsAssistant;
  }
}

public class Preparation {
  public int Terms;
  public string Summary = "";
}

[Action("Look up the house terms for a language", Query)]
public List<string> HouseTerms(string language) {
  return language == "Finnish" ? ["sopimus: agreement", "toimitus: delivery"] : [];
}

[Action("Write a note on the commission")]
public string NoteCommission(Commission commission, string note) { commission.Note = note; return $"Noted on {commission.Reference}."; }

// NEW: a tool that changes money.
[Action("Set the fee")]
public string SetFee(Commission commission, decimal fee) { commission.Fee = fee; return $"{commission.Reference} is charged {fee}."; }

public class PrepareCommission : Skill<Commission, Preparation> {
  public override string Description => "Prepare one commission for its translator, and work out its fee.";
  public override List<ActionInfo> Tools => [Actions.Of(HouseTerms), Actions.Of(NoteCommission), Actions.Of(SetFee)];
  public override string Brief(Commission c) => $"Commission {c.Reference}: {c.Title}, {c.Words} words into {c.Into}. Its id is {c.Id}.";
  // NEW: a person reads the commission's reference on the proposal, not its id.
  public override string Names(string action, string argument, string value) {
    if (argument != "commission") { return value; }
    var c = Commission.FirstOrDefault(x => x.Id == Guid.Parse(value));
    return c == null ? value : "Commission " + c!.Reference;
  }
}

[Actor(Roles = [BureauRole.Assistant])]
public class Preparer : Agent {
  public static Person Principal => new Person { Email = "preparer@bureau.example", DisplayName = "Preparer" };
  public override string Purpose => "You prepare commissions for the translators of a small bureau.";
  public override List<SkillBase> Skills => [new PrepareCommission()];
  // NEW — decision 1: it writes its note alone. Everything else that changes something is proposed.
  public override Permits May => new Permits { Alone = [Actions.Of(NoteCommission)] };
}

public AgentOutcome<Preparation> Prepare(Commission commission) {
  return await Agents.Of<Preparer>().Do(new PrepareCommission(), commission, about: "Commission " + commission.Reference);
}

[TestFixture]
void Office() {
  var ines = new Person { Email = "ines@bureau.example", DisplayName = "Ines Varga" };
  var otto = new Person { Email = "otto@bureau.example", DisplayName = "Otto Reyes" };
  new RoleGrant { Member = ines, Role = BureauRole.Translator };
  new RoleGrant { Member = otto, Role = BureauRole.Manager };
  new OrganizationTier { Name = "Standard", Rank = 1, IsDefault = true };
  var queue = new Queue { Name = "Bureau", Position = 1 };
  new QueueMember { Queue = queue, Member = ines };
  new QueueMember { Queue = queue, Member = otto };
  new Commission { Reference = "C-31", Title = "Supply agreement", Into = "Finnish", Words = 2400 };
  new AgentSeat { Agent = "Preparer", Sponsor = otto };
}
principal Ines => Person.Single(p => p.Email == "ines@bureau.example");
principal Otto => Person.Single(p => p.Email == "otto@bureau.example");

AgentScript NoteThenFee(Commission c) {
  var id = "{\"commission\":\"" + c.Id.ToString() + "\"";
  return AgentScript.Of(
    Says.Tool(Actions.Of(NoteCommission), id + ",\"note\":\"Two house terms apply.\"}"),
    Says.Tool("2,400 words at the legal rate.", Actions.Of(SetFee), id + ",\"fee\":312}"),
    Says.HandBack(new Preparation { Terms = 2, Summary = "Noted, and a fee of 312." }));
}
/// Let the work a person has answered go on: the request's own handlers, then the wait's run.
void GoOn(Guid task) {
  Background.Settle();
  Workflow.Settle(AgentWait.Where(w => w.Task.Id == task).OrderByDescending(w => w.Since).First());
}

[Test(Office)]
void The_manager_finds_the_proposal_in_his_inbox_and_his_yes_runs_it_as_him() {
  runas (Otto) {
    var commission = Commission.Single();
    var script = NoteThenFee(commission);
    ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

    var outcome = Prepare(commission);
    Background.Settle();

    // The note is written. The fee is not: it is proposed, and the work waits.
    Assert.True(outcome.Waiting);
    Assert.Equal("Two house terms apply.", Commission.Single().Note);
    Assert.Equal(0, Commission.Single().Fee);

    // In his inbox: an approval, from the agent, saying what will happen and why.
    var approval = Requests.Mine().Single();
    Assert.Equal(RequestKind.Approval, approval.Asks);
    Assert.Equal("Preparer proposes: Set the fee", approval.Title);
    Assert.Equal("2,400 words at the legal rate.", approval.Context);
    var card = ProposalCardOf(AgentProposal.Single().Id)!;
    Assert.Equal("Commission C-31", card.Lines[0].Value);

    // His yes runs it at once, as him. The agent finishes.
    AnswerRequest(approval, RequestAnswer.Approve());
    GoOn(outcome.Task);
    Assert.Equal(312, Commission.Single().Fee);
    Assert.Equal(Otto.Id, AgentProposal.Single().DecidedBy!.Id);
    Assert.Equal(AgentTaskStatus.Done, AgentTask.Single().Status);
  }
}

[Test(Office)]
void A_yes_from_somebody_who_may_not_set_a_fee_does_not_set_it() {
  runas (Ines) {
    var commission = Commission.Single();
    var script = NoteThenFee(commission);
    ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
    var task = Prepare(commission).Task;
    Background.Settle();

    // The proposal comes to her, who asked. A translator may not set a fee.
    AnswerRequest(Requests.Mine().Single(), RequestAnswer.Approve());
    GoOn(task);

    Assert.Equal(0, Commission.Single().Fee);
    Assert.Equal(ProposalStatus.Failed, AgentProposal.Single().Status);
    Assert.StartsWith("It was approved, and could not be done:", ProposalCardOf(AgentProposal.Single().Id)!.Outcome);
  }
}
```

### What would make this the wrong shape?   {#not-this}
- The approval belongs to a fixed step of a longer process with several people. Go to
  [shape 3](https://osysharp.com/reference/agent/guide-process/).
- The same kind of thing is approved every time and nobody ever says no. Give a
  [[agent-guide-glossary#standing-rule|standing rule]], or let the agent do it alone: see
  [[agent-guide-choosing#alone-or-propose]].

## What to read next   {#read-next}
- [Shape 3 — agents inside a business process](https://osysharp.com/reference/agent/guide-process/) — the same app, grown: the agent's work becomes a step of a business process
- [What a person sees — the screens of an app with agents in it](https://osysharp.com/reference/agent/overview-what-people-see/) — the inbox, the approval and the question, as screenshots
- [Choosing — I want X, so use Y, not Z, because](https://osysharp.com/reference/agent/guide-choosing/) — alone, propose, or a standing rule?

## Go deeper   {#see-also}
- [What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit)](https://osysharp.com/reference/agent/autonomy/) — the gate, proposals, what a yes covers, standing rules
- [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 the inbox
- [An agent asks a person, and waits — the person rung of the ladder of help (Agents kit)](https://osysharp.com/reference/agent/people/) — an agent asks a person a question, and waits
- [Requests to people — a question, an approval, a form, and the answer coming back (HumanRequests kit)](https://osysharp.com/reference/agent/human-requests/) — the requests kit: questions, approvals, forms
- [The WorkQueues kit — queues, who holds what, and the service levels promised on any item](https://osysharp.com/reference/workflow/inbox-kit/) — the inbox itself
