Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

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 two kits, `Osysharp.Llm` and `Osysharp.Agents`, and five decisions. The example on this page is a whole app and its tests, compiled and run.

preview1 example compiled by CIagentsguide

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

Summary#

A translator at a small translation bureau opens a commission and asks for it to be prepared. An agent looks up the bureau's house terms for the language, writes them on the commission as a note, and hands back how many it found. It works for her: it can read and change only what she may, and it leaves a receipt saying what it did and what it cost.

That is the first shape: an assistant for one person. Nothing waits, nobody approves, no other agent is involved.

Description#

What does it take?#

You addIt gives you
[[agent-kit-llm|Osysharp.Llm]]every call to a model: which model, what it may cost, what it did cost
one model provider — [[agent-kit-anthropic|Anthropic]], [[agent-kit-openai|OpenAI]] or [[agent-kit-gemini|Gemini]]the class that talks to that provider
[[agent-kit-agents|Osysharp.Agents]]the agent, its job, its tools, the receipt and the record
[[agent-kit-agents-testing|Osysharp.Agents.Testing]], in testsa script in place of the model, so tests cost nothing

What do you decide, in order?#

  1. Who the agent acts as. [Actor(Roles = [BureauRole.Assistant])] gives it an account of its own with exactly that role. See [Actor] — a principal that code may run as.
  2. Who it works for. Whoever is signed in when the work is given. It then does only what both its account and that person may do. See [[agent-ask#for]].
  3. What it may do alone. Here Permits.ChangesAlone(): it may change the bureau's own data without asking. Leave that line out and it only looks things up. See [[agent-autonomy#gate]].
  4. What it may spend. TaskBudget = 0.05m holds each piece of work to five cents. See What an agent may spend — a hard limit on every piece of work (Agents kit).
  5. Who answers for it, and who may use it. A person is named as its [[agent-guide-glossary#sponsor|sponsor]], and the app answers three [[agent-guide-glossary#policy|policies]]: UsesAgents, SupervisesAgents, AuditsAgents. See Who is in charge of an app's agents — every policy in one table, pausing, sponsoring, and trying work again.

What do you get without writing it?#

  • A [[agent-guide-glossary#receipt|receipt]] for every piece of work: who asked, as which account it ran, what it cost, how it ended. See [[agent-task-log#receipt]].
  • A [[agent-guide-glossary#record|record]] of every step, chained so that a changed line is found.
  • A hard stop when the money for a piece of work runs out.
  • A result that is typed and checked before your code sees it. See [[agent-skills#check]].

What does the whole app look like?#

The model here is a scripted model, so the example runs anywhere and costs nothing. In a real app that one line names a provider instead: Client = new AnthropicClient { ApiKey = Secret.Anthropic }.

app Bureau {
  use Osysharp.Agents;
  use Osysharp.Agents.Testing;
  use Osysharp.Llm;
  use Osysharp.Identity;
  use Osysharp.Observability;
  model "model/**/*.osy";
}

using Osysharp.Agents;
using Osysharp.Agents.Testing;
using Osysharp.Identity;
using Osysharp.Llm;

// The models this app has, and the agents it has.
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 } ],
};
app.Agents = new AgentsSetup { Agents = [new Preparer()], TaskBudget = 0.05m };      // decision 4

[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);

// Decision 5: who gives agents work, who is in charge of them, who reads everything they did.
policy UsesAgents       => IsTranslator || 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; }
}

/// 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 = "";
  [MaxLength(400)] string Note = "";
  security {
    allow read, update when IsTranslator || IsManager || IsAssistant;
    allow create when IsTranslator || IsManager;
  }
}

/// What preparing a commission hands back.
public class Preparation {
  public int Terms;
  public string Summary = "";
}

// The agent's tools are the app's own functions. `Query` says this one only reads.
[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}."; }

/// The job: a commission in, a preparation out.
public class PrepareCommission : Skill<Commission, Preparation> {
  public override string Description => "Prepare one commission for its translator.";
  public override string Instructions => "Look up the house terms for the language. Write them on the commission as a note.";
  public override List<ActionInfo> Tools => [Actions.Of(HouseTerms), Actions.Of(NoteCommission)];
  public override string Brief(Commission c) => $"Commission {c.Reference}: {c.Title}, into {c.Into}. Its id is {c.Id}.";
  public override string? Check(Preparation p) => p.Terms < 0 ? "A count cannot be negative." : null;
}

/// The agent. Decision 1: the account it acts as. Decision 3: what it may do alone.
[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()];
  public override Permits May => Permits.ChangesAlone();
}

[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 Commission { Reference = "C-31", Title = "Supply agreement", Into = "Finnish" };
  new AgentSeat { Agent = "Preparer", Sponsor = otto };              // decision 5: the manager answers for it
}
principal Ines => Person.Single(p => p.Email == "[email protected]");
principal Otto => Person.Single(p => p.Email == "[email protected]");

[Test(Office)]
[runas(Ines)]
void The_assistant_prepares_a_commission_for_the_person_who_asked() {
  var commission = Commission.Single(c => c.Reference == "C-31");
  // What the model answers, turn by turn. Everything else is real.
  var script = AgentScript.Of(
    Says.Tool(Actions.Of(HouseTerms), "{\"language\":\"Finnish\"}"),
    Says.Tool(Actions.Of(NoteCommission), "{\"commission\":\"" + commission.Id.ToString() + "\",\"note\":\"sopimus: agreement; toimitus: delivery\"}"),
    Says.HandBack(new Preparation { Terms = 2, Summary = "Two house terms noted." }));
  ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

  // Decision 2: it works for whoever is signed in — here, Ines.
  var outcome = await Agents.Of<Preparer>().Do(new PrepareCommission(), commission, about: "Commission C-31");

  Assert.True(outcome.Done);
  Assert.Equal(2, outcome.Result.Terms);                                             // typed, and checked
  Assert.Equal("sopimus: agreement; toimitus: delivery", Commission.Single().Note);
  Assert.Equal(["HouseTerms", "NoteCommission"], Trajectory.Of(outcome.Task));       // what it did, in order

  // The receipt: who asked, how it ended, what it cost against what it may.
  var receipt = AgentTask.Single();
  Assert.Equal(Ines.Id, receipt.Requester!.Id);
  Assert.Equal(AgentTaskStatus.Done, receipt.Status);
  Assert.True(receipt.SpentMicros > 0);
  Assert.Equal(50000, receipt.LimitMicros);                                          // five cents, in millionths
  Assert.True(VerifyTrace(receipt).Intact);                                          // the record is as it was written
}

[Test(Office)]
void An_agent_nobody_answers_for_does_not_run() {
  runas (Otto) { PauseAgent("Preparer", "The glossary is being rewritten."); }
  runas (Ines) {
    var commission = Commission.Single(c => c.Reference == "C-31");
    Assert.Throws<AgentPaused>(() => await Agents.Of<Preparer>().Do(new PrepareCommission(), commission));
  }
}

What would make this the wrong shape?#

  • The agent must do something a person has to agree to first. Go to shape 2.
  • The work is one step of a longer business process. Go to shape 3.
  • One job is really several jobs for different specialists. Go to shape 4.
  • You only need one answer from a model, with no tools. You do not need an agent: see [[agent-guide-choosing#agent-or-call]].

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 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…

Writing an agent — a class with a purpose, skills and an account of its own (Agents kit)

An agent is a class of the `Osysharp.Agents` kit: what it is for, the skills it holds, what it may spend. It runs as an…

A skill — a typed job an agent can be given, its check and its tools (Agents kit)

A skill is a class: what goes in, what comes back, and the app's own functions the agent may use to get there. The…

Giving an agent work — Do, Ask, Stream, For and Decide (Agents kit)

`Do` gives an agent a typed job and answers an `AgentOutcome<TOut>`: the checked result, how it ended, what it cost and…

Testing an agent without a model — Says, AgentScript and Trajectory (Agents.Testing kit)

`Osysharp.Agents.Testing` scripts what the model answers, turn by turn, and reads back what the agent then did. Only…