Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

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.

preview1 example compiled by CIagentsguide

You are here: Building with agents › Deciding what to build › Shape 2

Summary#

The translation bureau from shape 1 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

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

Description#

What does it add to shape 1?#

You addIt 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?#

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

What holds, whatever you decide?#

  • 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?#

What is new since shape 1 is marked NEW.

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 = "[email protected]", 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 = "[email protected]", DisplayName = "Ines Varga" };
  var otto = new Person { Email = "[email protected]", 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 == "[email protected]");
principal Otto => Person.Single(p => p.Email == "[email protected]");

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?#

  • The approval belongs to a fixed step of a longer process with several people. Go to shape 3.
  • 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]].

Go deeper#

Related

Building with agents — the developer's guide

Where a developer starts with agents in an Osy# app. Decide what you are building, read the smallest working app for…

Deciding what to build — four shapes of app with agents in it

Start from what the app is for, not from the kits. There are four shapes: an assistant for one person, work done for…

Shape 1 — an assistant for one person

The smallest app with an agent in it: one agent, one typed job, two tools, working for whoever is signed in. It takes…

Shape 3 — agents inside a business process

A business process is a workflow, and an agent's work is one of its steps: an ordinary awaited call that answers the…

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…

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…

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…

Requests to people — a question, an approval, a form, and the answer coming back (HumanRequests kit)

The `Osysharp.HumanRequests` kit puts something to a person — a question with the options it may be answered with, an…