# 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 approval request in the inbox of the person it is put to — showing the exact call, why, and what the agent had looked at — and what it asks is a question there. The person's answer is carried back to the waiting work: a yes runs the proposal as whoever answered. A person can hand an inbox item to an agent and take it back, and work an agent gave up on is put to its sponsor.

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

## Summary        {#summary}
```osy syntax
app Purchasing {
  use Osysharp.Agents;
  use Osysharp.HumanRequests;
  use Osysharp.Agents.HumanRequests;       // proposals and questions are requests in a person's inbox
}
app.AgentRequests = new AgentRequestsSetup { Queue = "Purchasing", DecideWithin = TimeSpan.FromHours(8) };

var outcome = await AgentDesk.Hand(ticket, Agents.Of<Vetter>(), new VetSupplier(), ticket.Supplier);
AgentDesk.TakeBack(ticket, "I will call them myself.");
```

## Signature      {#signature}
```osy syntax
[AppSetting("AgentRequests")]
class AgentRequestsSetup {
  string Queue;                    // the work queue they wait in. "": the requests kit's default
  TimeSpan? DecideWithin;          // how long a proposal may wait before it is late
  TimeSpan? AnswerWithin;          // …and a question
  TimeSpan? GiveUpAfter;           // how long either may wait at all
  string Hours;                    // the ServiceHours their time is counted in, by name
  bool TellSponsorOfFailures = true;
}

ProposalCardOf(Guid proposal) → ProposalCard?         // what the inbox draws: Label, Lines, Looked, Outcome, MayAllowAlone
ApproveAndAllowAlone(Guid proposal, Guid request)     // "yes — and from now on, alone"

class AgentDesk {
  static async Task<AgentOutcome<TOut>> Hand<TIn, TOut>(WorkItem item, AgentRef agent, Skill<TIn, TOut> skill, TIn input);
  static void TakeBack(WorkItem item, string why = "");
  static AgentHandover? Of(WorkItem item);            // the handover an item is under now
  static List<AgentHandover> TrailOf(WorkItem item);
}
entity AgentHandover { WorkItem Item; string Agent; AgentTask Task; Principal By; AgentHandoverState State; string Note; }
enum AgentHandoverState { WithAgent, Back, TakenBack }

component AgentWaitingOnPeople(string agent, Func<Guid, string>? open = null)
component WorkWaitingOnPeople(Guid task, Func<Guid, string>? open = null)   // …and everything the piece handed on
component AgentWorkTree(Guid task, Func<Guid, string>? open = null)         // the tree of a piece of work, on its page
component WorkTreeBlock(WorkTreeCard card, Func<Guid, string>? open = null, Action? refresh = null)
WorkTreeCardOf(Guid task) → WorkTreeCard?                                   // the same, as values
class WorkTreeCard { string About; AgentTaskStatus Status; string Stands, Waits; int Pieces; string Cost, Limit; List<WorkTreeLine> Lines; }
class WorkTreeLine { Guid Task; int Depth; string Agent, Skill; AgentTaskStatus Status; string Stands, Waits, Ended, Own, WithUnder; int Attempt; }
RequestsBehindWork(Guid task) → List<Guid>                                  // the open requests behind a piece and all it handed on
// events: WorkHandedToAgent · WorkBackFromAgent
```

## Description    {#description}

### What lands in the inbox?   {#inbox}
| The agent… | The person it is put to finds | Their answer |
|---|---|---|
| [[agent-autonomy#proposal|proposes]] a tool it may not run alone | an **approval**: "Vetter proposes: Clear the supplier to trade" | a yes runs it, as them; a no — which asks why — is given to the agent with the reason |
| [asks](https://osysharp.com/reference/agent/people/) a question | a **question**, with the agent's options as buttons, or a box to write in | given to the agent as what its question answered |
| [[agent-guards#retry|gave up]] on a piece of work | a **question** to its sponsor: what should become of it | recorded; the app hears `HumanRequestAnswered` |

Each is an ordinary [request](https://osysharp.com/reference/agent/human-requests/): it waits in the queue `AgentRequestsSetup` names, has the
date and reminders the app's `RequestsSetup` gives it, says which piece of work is waiting on it, and can be
answered by whoever the inbox lets hold it. Nothing is wired: the bridge hears the agents' events.

A proposal its skill sends to a queue ([[agent-autonomy#route]]) waits in **that** queue, put to nobody by name.
Whoever works the queue reads the request, not the proposal's own record — so what would be done is written on the
request as its facts: what it does, who the work is for, each argument as the skill names it, and what the agent had
looked at. The person the work is for may not answer it, though they work that queue (`NotBy`).

### What does the person see of a proposal?   {#sheet}
On the request's sheet, under the agent's own words for why: **what will happen** — the action's label and each
argument, with a record's name where the skill gives one ([[agent-autonomy#proposal]], `Names`) — whether it changes
data, sends or spends; **what the agent looked at first**; that a yes is done at once, as them; and Approve and
Reject. Somebody the app's `WidensAgentAutonomy` admits is also offered "Approve, and let it do this alone from now
on" ([[agent-autonomy#standing]]), which says what that means before it is said. Once decided, the sheet says what
came of it — including that a yes could not be done because the person who gave it may not do the thing.

Approving several at once is the inbox's own "Approve all" ([Requests to people — a question, an approval, a form, and the answer coming back (HumanRequests kit)](https://osysharp.com/reference/agent/human-requests/)): each is approved as the
person, one by one.

### What happens to the request when the proposal is decided somewhere else?   {#elsewhere}
A proposal approved or rejected with `ApproveAgentProposal` / `RejectAgentProposal` directly, or withdrawn because
its work was called off, calls its request off: nothing is left to answer. The other way round, a request that is
called off or runs out of time is a **no** to its proposal, with that as the reason — and a question is told nobody
answered — so the agent's work goes on rather than waiting for ever.

### How do I hand an inbox item to an agent?   {#handoff}
`AgentDesk.Hand(item, agent, skill, input)` gives the item to the agent's own account — the inbox shows the agent
holding it — and gives the agent the work, **for whoever handed it over**: it does nothing that person may not. The
agent's account must be able to hold items of that queue. When the work ends, however it ends, the item comes back
to that person with a sentence of what came of it; the agent's receipt and record say the rest
(`AgentDesk.TrailOf(item)`). `AgentDesk.TakeBack(item, why)` calls the work off where it stands and takes the item:
for whoever handed it over, and whoever leads the inbox.

### Who is an agent waiting on?   {#waiting}
`AgentWaitingOnPeople(agent)` lists every open request the agent has put — of whom, since when, late or not — and
says so when there is none; `WorkWaitingOnPeople(task)` does the same for one piece of work, and for every piece it
[handed on](https://osysharp.com/reference/agent/delegation/): a request three pieces down is what the piece at the top is waiting on too.

### How do I show a piece of work with everything it handed on?   {#tree}
`AgentWorkTree(task, open: t => "/work/" + t)` draws the tree of the work `task` is part of: what it is about, how
many pieces, what the whole has cost of what it may; what it waits for; and each piece under the one that handed it
on — the agent, the job, where it stands, what it waits for or how it ended, and what it cost. It is drawn for
whoever may read the work; anybody else is told it is not theirs to see. `WorkTreeCardOf(task)` answers the same as
values, for a screen of your own.

A request that comes from a piece of handed-on work says through whom it comes: its `AskedByName` is "Checker,
working for Coordinator", in the inbox and on the sheet.

## Examples       {#examples}
```osy title="the vetter asks, proposes, and the lead's yes in her inbox clears the supplier as her" run app=agent-requests
app Purchasing {
  use Osysharp.Agents;
  use Osysharp.Agents.Testing;
  use Osysharp.Agents.HumanRequests;
  use Osysharp.HumanRequests;
  use Osysharp.WorkQueues;
  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 = "Small", Client = new ScriptedModel { Name = "Small" }, Model = "scripted-small",
                            Price = new LlmPrice { Input = 1, Output = 5 }, MaxOutputTokens = 1000 } ],
  Default = "Small",
};
app.Agents = new AgentsSetup { Agents = [new Vetter()], TaskBudget = 0.05m };
app.AgentRequests = new AgentRequestsSetup { Queue = "Purchasing", DecideWithin = TimeSpan.FromHours(8) };
app.Inbox = new InboxSetup { DefaultQueue = "Purchasing" };

[Role] enum OfficeRole { Authenticator, Buyer, Lead, Vetting }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsBuyer   => RoleGrant.Any(g => g.Member == user && g.Role == OfficeRole.Buyer);
policy IsLead    => RoleGrant.Any(g => g.Member == user && g.Role == OfficeRole.Lead);
policy IsVetting => RoleGrant.Any(g => g.Member == user && g.Role == OfficeRole.Vetting);
policy IsOffice  => IsBuyer || IsLead;

// The inbox, the requests and the agents — each kit's questions, answered.
policy WorksInbox       => IsOffice || IsVetting;
policy LeadsInbox       => IsLead;
policy AdministersInbox => IsLead;
policy ManagesOrganizations  => IsLead;
policy CollaboratesOnRecords => IsOffice;
policy AsksPeople       => IsOffice;
policy OverseesRequests => IsLead;
policy UsesAgents          => IsOffice;
policy SupervisesAgents    => IsLead;
policy AuditsAgents        => IsLead;
policy WidensAgentAutonomy => IsLead;
policy ReadsModelUsage     => IsLead;
policy ManagesModelSpend   => IsLead;

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 || IsVetting; } }

/// A company that wants to sell to the bakeries.
entity Supplier {
  [Required, Unique, MaxLength(120)] string Name;
  int PaymentDays = 0;
  [MaxLength(600)] string Risk = "";
  /// Cleared to trade. The lead clears a supplier, and so may the vetter's own account — which, working for a
  /// person, does it only when that person may too.
  bool Cleared = false;
  security {
    allow read, create, update when IsOffice || IsVetting;
    deny update Cleared when !IsLead && !IsVetting;
  }
}

public class Vetting {
  public bool Recommend;
  public string Finding = "";
}

[Action("Note a risk")]
public string NoteRisk(Supplier supplier, string risk) { supplier.Risk = risk; return "Noted."; }

[Action("Clear the supplier to trade")]
public string ClearSupplier(Supplier supplier) { supplier.Cleared = true; return $"{supplier.Name} is cleared to trade."; }

public class VetSupplier : Skill<Supplier, Vetting> {
  public override string Description => "Vet one supplier, and clear it to trade if it is sound.";
  public override List<ActionInfo> Tools => [Actions.Of(NoteRisk), Actions.Of(ClearSupplier)];
  public override string Brief(Supplier s) => $"{s.Name} asks to be paid in {s.PaymentDays} days. Its id is {s.Id}.";
  public override string Names(string action, string argument, string value) {
    if (argument != "supplier") { return value; }
    var s = Supplier.FirstOrDefault(x => x.Id == Guid.Parse(value));
    return s == null ? value : s!.Name;
  }
}

/// It notes what it finds by itself, may ask a person, and PROPOSES clearing a supplier.
[Actor(Roles = [OfficeRole.Vetting])]
public class Vetter : Agent {
  public static Person Principal => new Person { Email = "vetter@crumbandcrust.example", DisplayName = "Vetter" };
  public override string Purpose => "You vet the companies that apply to supply the bakeries.";
  public override List<SkillBase> Skills => [new VetSupplier()];
  public override Permits May => new Permits { Alone = [Actions.Of(NoteRisk)] };
  public override AsksPeople People => new AsksPeople { MayChoose = true };
}

public AgentOutcome<Vetting> Vet(Supplier supplier) {
  return await Agents.Of<Vetter>().Do(new VetSupplier(), supplier, about: supplier.Name);
}

[TestFixture]
void Office() {
  var noor  = new Person { Email = "noor@crumbandcrust.example",  DisplayName = "Noor Haddad" };
  var tariq = new Person { Email = "tariq@crumbandcrust.example", DisplayName = "Tariq Aziz" };
  new RoleGrant { Member = noor,  Role = OfficeRole.Buyer };
  new RoleGrant { Member = tariq, Role = OfficeRole.Lead };
  new OrganizationTier { Name = "Standard", Rank = 1, IsDefault = true };
  var queue = new Queue { Name = "Purchasing", Position = 1 };
  new QueueMember { Queue = queue, Member = noor };
  new QueueMember { Queue = queue, Member = tariq };
  new Supplier { Name = "Hollin Mill", PaymentDays = 45 };
  new AgentSeat { Agent = "Vetter", Sponsor = tariq };
}
principal Noor  => Person.Single(p => p.Email == "noor@crumbandcrust.example");
principal Tariq => Person.Single(p => p.Email == "tariq@crumbandcrust.example");

string SupplierArg(Supplier s) { return "{\"supplier\":\"" + s.Id.ToString() + "\"}"; }
/// 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_lead_answers_the_question_and_approves_the_proposal_in_her_inbox() {
  runas (Tariq) {
    var mill = Supplier.Single();
    var script = AgentScript.Of(
      Says.AskPerson("Hollin Mill asks to be paid in 45 days. Is that acceptable?", "Our standard term is 30 days."),
      Says.Tool("The lead accepted 45 days.", Actions.Of(ClearSupplier), SupplierArg(mill)),
      Says.HandBack(new Vetting { Recommend = true, Finding = "Cleared on 45 days." }));
    ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

    var task = Vet(mill).Task;
    Background.Settle();

    // The question is a request in his inbox, from the agent, saying what work waits on it.
    var question = Requests.Mine().Single();
    Assert.Equal("Hollin Mill asks to be paid in 45 days. Is that acceptable?", question.Title);
    Assert.Equal(AskerKind.Agent, question.AskedByKind);
    Assert.Equal("Vetter", question.AskedByName);
    Assert.Equal("Hollin Mill", question.WaitingLabel);
    AnswerRequest(question, RequestAnswer.Say("Yes, for a mill."));
    GoOn(task);

    // The agent went on with the answer — and stopped again, on a proposal: an approval in the same inbox.
    Background.Settle();
    var approval = Requests.Mine().Single();
    Assert.Equal(RequestKind.Approval, approval.Asks);
    Assert.Equal("Vetter proposes: Clear the supplier to trade", approval.Title);
    Assert.Equal("The lead accepted 45 days.", approval.Context);
    var card = ProposalCardOf(AgentProposal.Single().Id)!;
    Assert.Equal("Hollin Mill", card.Lines[0].Value);
    Assert.True(card.MayAllowAlone);
    Assert.False(Supplier.Single().Cleared);

    // His yes runs it at once, as him; the agent finishes.
    AnswerRequest(approval, RequestAnswer.Approve());
    GoOn(task);
    Assert.True(Supplier.Single().Cleared);
    Assert.Equal(Tariq.Id, AgentProposal.Single().DecidedBy!.Id);
    Assert.Equal(AgentTaskStatus.Done, AgentTask.Single().Status);
    Assert.Equal(0, Requests.Mine().Count);
  }
}

[Test(Office)]
void A_yes_from_somebody_who_may_not_do_it_does_not_do_it() {
  runas (Noor) {
    var mill = Supplier.Single();
    var script = AgentScript.Of(
      Says.Tool("It looks sound.", Actions.Of(ClearSupplier), SupplierArg(mill)),
      Says.HandBack(new Vetting { Recommend = true, Finding = "Proposed; not cleared." }));
    ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
    var task = Vet(mill).Task;
    Background.Settle();

    // The proposal comes to her, who asked. A buyer may not clear a supplier — and is not offered "from now on".
    Assert.False(ProposalCardOf(AgentProposal.Single().Id)!.MayAllowAlone);
    AnswerRequest(Requests.Mine().Single(), RequestAnswer.Approve());
    GoOn(task);

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

## Notes          {#notes}
- **The bridge decides nothing.** It carries a person's answer to `ApproveAgentProposal`'s own checks: who is
  deciding, whether what was shown still stands, whether the agent may do the thing at all
  ([[agent-autonomy#approve]]).
- **The yes is the answerer's.** When somebody else takes the item and approves it — a lead covering for a
  colleague — the tool runs as that person.
- The walk from a supplier's application to its first order — a link sent to somebody with no account, a question,
  a proposal, and the process that waits through all three — is the kit's own test app (`kits/agents-human-requests/tests`).

## See also       {#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 and standing rules
- [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's questions, and how its work 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, time
- [The WorkQueues kit — queues, who holds what, and the service levels promised on any item](https://osysharp.com/reference/workflow/inbox-kit/) — the work queues a request waits in
- [What an assignment asks of people — requests as an assignment's work and as inbox items (Assignments.WorkQueues kit)](https://osysharp.com/reference/agent/assignment-requests/) — what an assignment asks of people
