# An agent asks a person, and waits — the person rung of the ladder of help (Agents kit)

> An agent that says so may put one focused question to a person: the model chooses to (a tool it is offered), or work that is stuck does before it gives up — after the model it consults, when it names one. Who is asked is the person the work is for; the agent's sponsor when it is for nobody. The work stops where it stands — its receipt says `Waiting` and on what — and goes on from the same point when the person has answered, a minute or a month later; nothing runs and nothing is spent meanwhile. Inside a workflow the run waits with the work, and `Do` answers the finished result.

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

## Summary        {#summary}
```osy syntax
public class Estimator : Agent {
  public override AsksPeople People => new AsksPeople {
    MayChoose = true,                                   // the model may ask when it judges it should
    WhenStuck = true,                                   // …and stuck work asks once before it gives up
    About = "Any repair over 200 EUR.",                 // …and these decisions are a person's
  };
}

var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);
if (outcome.Waiting) { … }                              // it asked somebody; the receipt says whom
AnswerAgentQuestion(question, "Replace it.");           // …and it goes on from where it stood
```

## Signature      {#signature}
```osy syntax
class Agent { virtual AsksPeople People; }
class AsksPeople {
  bool MayChoose;        // the model is offered `ask_person`
  bool WhenStuck;        // stuck work asks a person once — after the model it consults, when it names one
  string About;          // kinds of decision that are a person's: said to the model, which asks first
  int Most = 3;          // the most questions one piece of work may put
}

entity AgentHelp {       // the same row a consulted model answers — `From` says who was asked
  HelpFrom From;         // Model, Person
  HelpReason Why;        // Chosen, Stuck
  Principal? AskedPerson; string Question; string Context; string OptionsJson;
  HelpStatus Status;     // Waiting, Answered, Unavailable
  string Answer; Principal? AnsweredBy; DateTime? AnsweredAt;
}
AnswerAgentQuestion(AgentHelp question, string answer)
QuestionsForMe() → List<AgentHelp>

enum AgentTaskStatus { Working, Done, Failed, Stopped, Cancelled, Waiting }
entity AgentTask { …; string WaitingOn; }              // "an answer from Noor", "a decision from Noor"
class AgentOutcome<T> { …; bool Waiting; }   class AgentReply { …; bool Waiting; }

await WorkEnds(Guid task) → AgentTaskStatus            // in a workflow: wait for work that waits on a person
// events: AgentAskedForHelp · AgentQuestionClosed · AgentTaskWaiting · AgentTaskFinished / Failed / Stopped
```

## Description    {#description}

### When does an agent ask a person?   {#when}
Only when its class says it may (`People`). Then in three ways, each a row of `AgentHelp` with `From = Person` and
a line of the [record](https://osysharp.com/reference/agent/task-log/):

| | When | The person's answer reaches the model as |
|---|---|---|
| `MayChoose` | the model calls the `ask_person` tool it is offered, with a `Question`, the `Context` the person needs and, when the answer is one of a few, `Options` | what `ask_person` answered |
| `About` | the same tool — the model is told these decisions are a person's, and asks before it decides one | what `ask_person` answered |
| `WhenStuck` | a step failed `StuckAfter` times in a row, and — when the agent [consults a model](https://osysharp.com/reference/agent/help/) — that model has been asked | advice before its next turn |

One piece of work puts at most `Most` questions; past that the model is told so and finishes without.

### Who is asked, and where?   {#who}
The person the work is for ([[agent-ask#for]]); the agent's sponsor when it is for nobody — a step of a process.
With [`Osysharp.Agents.HumanRequests`](https://osysharp.com/reference/agent/requests/) in the app the question is a request in that person's
inbox, with its options as buttons. Without it the app shows `QuestionsForMe()` wherever it likes and calls
`AnswerAgentQuestion`. The person asked answers; whoever `SupervisesAgents` may answer for them. A question is
answered once.

### What happens to the work while it waits?   {#waiting}
It stops where it stands — on a question, or on a [[agent-autonomy#proposal|proposal]]:

- its receipt is `Waiting`, and `WaitingOn` says on what; the record has a line saying so;
- everything it needs to go on is kept with the receipt; nothing runs, no model is asked, nothing is spent;
- when the person has answered or decided, it goes on **from the same point**, with their answer as what its tool
  answered — as the app, in a run of its own, so nobody needs to be signed in or looking;
- `StopAgentTask` calls waiting work off: it ends `Cancelled`, a proposal still open is withdrawn, and a late yes
  is refused.

There is no deadline on the wait itself. A request in the inbox has one, with reminders
([Requests to people — a question, an approval, a form, and the answer coming back (HumanRequests kit)](https://osysharp.com/reference/agent/human-requests/)); when it runs out, the agent is told nobody answered and goes on.

### What does the caller get?   {#caller}
- **A page's action or a plain function**: `Do` answers at once with `Status == Waiting` and the receipt's id. The
  end comes as an event — `[On] void Done(AgentTaskFinished e) { … }` — or is read from the receipt later.
- **A workflow** — its body, or anything it calls: `await agent.Do(skill, input)` **waits with the work**. The run
  parks, for as long as the person takes, and `Do` answers the finished outcome. `await WorkEnds(task)` is the same
  wait for work opened in two steps ([[agent-ask#open]]).

`Ask` and `Stream` answer a reply whose `Status` is `Waiting`; a stream says so with a `Waiting` signal before its
`Done`.

## Examples       {#examples}
```osy title="it asks the person it works for, waits a month, and a process waits with it" run app=agent-people
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 ReadsModelUsage   => IsManager;
policy ManagesModelSpend => IsManager;

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

public enum RepairStage { In, Quoted }

/// One bike in for repair.
entity Repair {
  [Required, Unique, MaxLength(40)] string Ticket;
  [Required, MaxLength(200)] string Fault;
  [MaxLength(400)] string Note = "";
  RepairStage Stage = RepairStage.In;
  security { allow read, create, update when IsMechanic || IsManager || IsHelper; }
}

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

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

public class EstimateRepair : Skill<Repair, Estimate> {
  public override string Description => "Estimate one repair.";
  public override List<ActionInfo> Tools => [Actions.Of(NoteRepair)];
  public override string Brief(Repair r) => $"Ticket {r.Ticket}: {r.Fault}. Its id is {r.Id}.";
}

/// It may ask a person — when the model judges it should, and when it is stuck.
[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();
  public override AsksPeople People => new AsksPeople { MayChoose = true, WhenStuck = true };
}

/// A repair's quote, as a process: the estimator estimates, and the process goes on with what it found — however
/// long the estimator had to wait for a person.
workflow Quoting {
  Tracks = Repair.Stage; Autostart = false; Initial = In;
  state In {
    enter {
      var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), this.Item, about: "Ticket " + this.Item.Ticket);
      this.Item.Note = outcome.Done ? outcome.Result.Summary : "Not estimated: " + outcome.Problem;
      goto Quoted;
    }
  }
  terminal success Quoted { }
}

[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 wheel is buckled" };
  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");

/// Let the work a person has answered 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_asks_the_person_it_works_for_and_goes_on_a_month_later() {
  TestClock.Set(new DateTime(2031, 3, 3, 9, 0, 0, DateTimeKind.Utc));
  Guid task;
  runas (Mia) {
    var repair = Repair.Single(r => r.Ticket == "T-7");
    var script = AgentScript.Of(
      Says.AskPerson("True the wheel, or replace it?", "The rim is buckled by about a centimetre."),
      Says.HandBack(new Estimate { Hours = 1, Summary = "Replace the wheel." }));
    ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

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

    // It stopped on her: the receipt says so, and the question is hers to answer.
    Assert.True(outcome.Waiting);
    Assert.Equal("an answer from Mia", AgentTask.Single().WaitingOn);
    var question = QuestionsForMe().Single();
    Assert.Equal("True the wheel, or replace it?", question.Question);
    Assert.Equal("The rim is buckled by about a centimetre.", question.Context);
    Assert.Equal(HelpFrom.Person, question.From);
  }
  // A month goes by. Nothing runs, and the model is not asked again.
  TestClock.Advance(TimeSpan.FromDays(30));
  Background.Settle();
  runas (Mia) {
    Assert.Equal(1, AgentTask.Single().ModelCalls);
    AnswerAgentQuestion(QuestionsForMe().Single(), "Replace it. The rim is cracked too.");
    GoOn(task);
    // It went on from where it stood, with her answer, and finished.
    var receipt = AgentTask.Single();
    Assert.Equal(AgentTaskStatus.Done, receipt.Status);
    Assert.Contains("Replace the wheel.", receipt.Result!);
    Assert.Contains("Mia answered after 30 days.", Trajectory.Said(task));
    // Answered once.
    Assert.Throws<AgentRefused>(() => AnswerAgentQuestion(AgentHelp.Single(), "On second thoughts, true it."));
  }
}

[Test(Shop)]
void A_process_waits_with_the_work_and_goes_on_with_its_result() {
  Guid id;
  runas (Mia) {
    var repair = Repair.Single(r => r.Ticket == "T-7");
    id = repair.Id;
    var script = AgentScript.Of(
      Says.AskPerson("True the wheel, or replace it?"),
      Says.HandBack(new Estimate { Hours = 1, Summary = "True the wheel." }));
    ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
    Workflow.Run(repair);
    Workflow.Settle(repair);
    // The process has not moved: its step is waiting with the estimator's work.
    Assert.Equal(RepairStage.In, Repair.Single(r => r.Id == id).Stage);
  }
  // Nobody is signed in while a process runs, so the estimator asks its sponsor.
  runas (Noor) {
    var question = QuestionsForMe().Single();
    AnswerAgentQuestion(question, "True it.");
    GoOn(question.Task!.Id);
    Workflow.Settle(AgentWorkWait.Single());
  }
  runas (Mia) {
    var repair = Repair.Single(r => r.Id == id);
    Workflow.Settle(repair);
    Assert.Equal(RepairStage.Quoted, repair.Stage);
    Assert.Equal("True the wheel.", repair.Note);
  }
}

[Test(Shop)]
[runas(Mia)]
void Stuck_work_asks_a_person_once_and_waiting_work_can_be_called_off() {
  var repair = Repair.Single(r => r.Ticket == "T-7");
  // Twice it answers in words where a result must be handed back: it is stuck, and has no model to consult.
  var script = AgentScript.Of(Says.Words("An hour, I think."), Says.Words("About an hour."),
                              Says.HandBack(new Estimate { Hours = 1, Summary = "An hour." }));
  ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

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

  Assert.True(outcome.Waiting);
  Assert.Equal(HelpReason.Stuck, QuestionsForMe().Single().Why);

  // She calls the work off instead of answering: it ends where it stood.
  StopAgentTask(AgentTask.Single(), "The customer took the bike back.");
  UnitOfWork.Commit();
  GoOn(outcome.Task);
  Assert.Equal(AgentTaskStatus.Cancelled, AgentTask.Single().Status);
  Assert.Equal(2, AgentTask.Single().ModelCalls);
}
```

## 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/) — what an agent proposes to a person before it does it
- [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/) — questions and proposals as requests in a person's inbox
- [Asking for help — an agent that consults a more capable model (Agents kit)](https://osysharp.com/reference/agent/help/) — the rung before this one: a more capable model
- [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
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — a process that waits
- [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
