Summary#
An agent is something you declare, so it is something you can call:
var reply = Triager.Ask(turns);
if (reply.Parked) { /* it asked a person */ } else { /* reply.Text */ }The receiver is the agent's own name. That is not decoration — it means the compiler checks the agent exists, the same way it checks any other name you write, instead of finding out at run time that a string was misspelled.
Signature#
List<Turn> turns = new List<Turn>();
turns.Add(Turn.Context("Acme Ltd, plan Enterprise, customer since 2019"));
turns.Add(Turn.User("Is the Lisbon dinner within policy?"));
AgentReply reply = Triager.Ask(turns);
string answer = reply.Text;| you write | it means |
|---|---|
Turn.User(text) | somebody is asking |
Turn.Assistant(text) | the agent said this earlier — replayed history |
Turn.Context(text) | facts you supplied. Not dialogue |
<agent>.Ask(turns) | run that agent over those turns; answers an AgentReply |
<agent>.StartTask<T>(trigger) | hand the work to the agent's [[agent-loop|loop]] instead of running it here |
Description#
You assemble the turns, and that is deliberate. Which history to replay and which facts to include are product decisions — a support screen sends the customer and their recent orders, a nightly audit sends one line. A platform that guessed for you would guess wrong in both directions: too much costs money on every call, too little produces a confident answer to a question the agent could not actually see.
⚑ Say which kind each turn is. Turn.Context is the one people skip, and it is the one that matters most: an
agent that cannot tell the facts it was handed from the question it was asked will answer the facts. Reading back a
customer record is a very convincing wrong answer.
The agent brings itself. Its instructions, its tools, its model and its memory are on its declaration, so they are not repeated at the call site and cannot drift between two places that call the same agent.
Everything the run did is recorded. The call returns the answer; the rest is a task — what it cost, what it called, how long it waited, and what it produced. See the agent task log (AgentTask) and what an agent hands back (AgentDeliverable). That is why the return is a plain string rather than a result object: the interesting detail is queryable, and putting it in the return type would make every call site hold something it did not ask for.
A repeated run does not repeat the call. If the work is resumed after an interruption, the answer already given is the answer used — the agent is not asked twice. That matters for cost, and because the same question need not produce the same answer twice.
⚠ Asking a person suspends the work. If the agent decides it needs a human, the run stops and waits rather than
inventing an answer, and Ask says so instead of returning an empty string. The waiting run is recorded and can be
picked up once somebody answers.
Examples#
Checking something with the context the app already has:
using Osyrin.Agents;
[Principal]
entity User {
[Required, MaxLength(100)] string DisplayName;
security { allow read when IsAuthenticated; allow create when IsAuthenticated || IsAnonymous; }
}
agent Triager {
Purpose = "Triage expense reports.";
Prompt = "You triage expenses against the travel policy.";
Principal = new User { DisplayName = "Triager" };
}
string Check(string question, string customer) {
var turns = new List<Turn>();
turns.Add(Turn.Context(customer));
turns.Add(Turn.User(question));
return Triager.Ask(turns).Text;
}The agent may stop to ask a PERSON#
An agent that needs something only a human can supply does not fail and does not guess — it stops and asks. Ask
answers that as an outcome you branch on rather than an error thrown out from under you:
var reply = Triager.Ask(turns);
if (reply.Parked) {
// The run is recorded and resumable. `reply.Question` is what it wants to know —
// put it in front of a person, and the run continues when they answer.
return reply.Question;
}
return reply.Text;| member | |
|---|---|
Text | what the agent said. Empty when parked — never null, so emptiness never has to mean two things |
Parked | it stopped to ask a person, and is waiting |
Question | what it asked, when parked |
⚠ Branch on Parked, not on an empty Text. An agent may legitimately reply with nothing, so the two are
deliberately different answers.
Handing the work to the loop instead#
Ask runs the agent now and waits. When the work is big enough to deserve durability, a human step or a retry,
hand it to the agent's loop instead:
Guid task = Triager.StartTask("Review the Lisbon contract.");That opens the agent's task and returns; the declared loop starts on it and runs the agent inside a workflow that
can park, escalate and be resumed. The usual shape is to decide which you want first — a cheap classifying Ask,
then StartTask only for the work that warrants it.
Name a task type to choose which loop runs, when your app declares more than one:
Guid task = Triager.StartTask<PersonalTask>("Review the Lisbon contract.");The loop that tracks that type is the one that starts. The type must be one of yours deriving from AgentTask.
See also#
- the agent task log (AgentTask) — what the run cost, what caused it, and how long it took.
- what an agent hands back (AgentDeliverable) — what the agent hands back beyond a sentence.
- agent conversation memory (using Osyrin.Agents) — turns the platform records for you.
- the agent loop (app.Agent, Loop) — the workflow a started task runs inside.