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 add | It 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 tests | a script in place of the model, so tests cost nothing |
What do you decide, in order?#
- 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. - 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]].
- 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]]. - What it may spend.
TaskBudget = 0.05mholds each piece of work to five cents. See What an agent may spend — a hard limit on every piece of work (Agents kit). - 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]].
What to read next#
- Shape 2 — work done for people, with approvals — the same app, grown: the agent proposes a fee and a person approves it in their inbox
- One piece of work, followed end to end — one piece of work followed through every kit
- The map of the kits — which sits on which, and what each one is for — the map of the kits
Go deeper#
- Writing an agent — a class with a purpose, skills and an account of its own (Agents kit) — the agent class, its account, who is in charge
- A skill — a typed job an agent can be given, its check and its tools (Agents kit) — a typed job, its check and its tools
- Giving an agent work — Do, Ask, Stream, For and Decide (Agents kit) —
Do,Ask,Stream,For,Decide - The receipt and the record — what every piece of an agent's work leaves (Agents kit) — the receipt and the record
- Testing an agent without a model — Says, AgentScript and Trajectory (Agents.Testing kit) — scripting the model in a test