Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

Agents in business processes — an agent's work as a step of a workflow, told on the run (Agents.Processes kit)

workflow P { state S { enter { var o = await Agents.Of<X>().For(person).Do(new Skill(), this.Item); … } } } · AgentTask.Run / RunWorkflow / RunState / RunItem / RunStarter · SetProcessBudget(workflow, perRun) · AgentWorkInRun(run) · ProcessRunOfWork(task) · ProcessTrail.Skills/States/Ended/Proposed/Cost(item)

An agent's work is a step of a business process by being called from the process's workflow: an awaited call in a state's body that answers the skill's typed result, however long the work waited on a person, and whose outcome — done, failed, stopped, given up — the process handles like any branch. The receipt of the work says which run of which workflow gave it, and in which state; every model call of the work is charged to that run, so a limit for each run holds it; a run that ends while a step still waits calls the work off. With Osysharp.Processes in the app, the Agents.Processes bridge tells each piece of work on the run, opens it there as the whole inspector, and links the work's own page back to the run.

preview1 example compiled by CIagentsworkflowkit

Summary#

workflow Qualification {
  Tracks = Supplier.Stage; Initial = Screening;
  state Screening {
    enter {
      var check = await Agents.Of<Screener>().For(this.Item.RequestedBy).Do(new CheckDocuments(), this.Item);
      if (check.Done && !check.Result.Complete) { goto Incomplete; }      // the typed result decides
      goto Assessing;
    }
  }
  state Assessing {
    enter {
      var risk = await Agents.Of<Assessor>().For(this.Item.RequestedBy).Do(new AssessRisk(), this.Item);
      if (!risk.Done) {                                                   // it gave up: the process does not
        Processes.Problem("The assessor could not finish, so the risk officer assesses this supplier by hand.");
        goto ByHand;
      }
      goto Deciding;
    }
  }
}

Signature#

await Agents.Of<X>().Do(skill, input)                 // in a state's body: answers AgentOutcome<TOut>, never `Waiting`
await Agents.Of<X>().For(person).Do(skill, input)     // …for a person: never past what they may do themselves

entity AgentTask { Guid? Run; string RunWorkflow; string RunState; Guid? RunItem; Guid? RunStarter; int RunStep; … }   // the receipt says its run
[Event] class AgentWorkNoLongerAwaited { AgentTask Task; }                // the run that waited is gone; the kit calls the work off

// with Osysharp.Processes in the app (the Agents.Processes bridge):
AgentWorkInRun(Guid run) → AgentStepsOfRun            // a run's agent work, piece by piece, with what each cost
ProcessRunOfWork(Guid task) → RunOfWork?              // the run a piece of work is a step of
SetProcessBudget("Qualification", 2.00m)              // each run of the process: two dollars on models

// in a test (Osysharp.Agents.Testing):
ProcessTrail.Skills(item) · .States(item) · .Ended(item) · .Proposed(item) · .Cost(item)

Description#

How is an agent's work a step of a process?#

By being called from the process's workflow. Do in a state's body is the same call it is anywhere (Giving an agent work — Do, Ask, Stream, For and Decide (Agents kit)): a skill and its typed input in, an AgentOutcome<TOut> out. What the workflow adds is that it waits. Work that stops on a person — a proposal somebody must decide (What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit)), a question somebody must answer (An agent asks a person, and waits — the person rung of the ladder of help (Agents kit)) — parks the run with it, for a minute or a month, and the step answers the finished work. Nothing runs and no model is asked while it waits.

Who does a step work for?#

Nobody is signed in while a process runs. A step that names nobody runs with the agent's own rights alone, and what it proposes goes to whoever answers for the agent. For(person) — the buyer who asked, the customer whose order it is — makes the work that person's: it is never done past what they may do themselves, they read its receipt, and its proposals and questions go to them.

What does a process do when a step does not finish?#

It reads the outcome. A step never throws for work that ran and ended badly: outcome.Done is false, Status says how — Failed (the model failed, the result never fitted, it went in circles; after as many attempts as the skill's Retry allows), Stopped (its turns or its money ran out), Cancelled (somebody called it off) — and Problem says why in a sentence. The process goes on from there like from any branch: another state, a person's step, a line on the run.

What happens to the work when the process is stopped?#

A run that ends while one of its steps is still waiting on a person — an event sent it to a terminal, somebody stopped it — calls that work off (What happens to work a run stops waiting for): the work ends as Cancelled, its proposal is withdrawn so nobody can approve it afterwards, and nothing more is asked of a model. The kit raises AgentWorkNoLongerAwaited for it, and hears that itself.

How does the work know its process?#

The receipt says it. A piece of work given in a workflow's run records the run's id, the workflow's name, the state whose body gave it, the row the run tracks and the run that started that run (AgentTask.Run, RunWorkflow, RunState, RunItem, RunStarter), and its place among the pieces of work that run gave (RunStep) — read from the engine where the work is started (Workflow.Current (which run is this code in?)), never taken from the caller. Work an agent hands on (An agent hands work on to other agents — at once, typed, and never with more than it holds (Agents kit)) belongs to the step that handed it on, and is charged where that step is.

What does a run cost, and how is it held to a limit?#

Every model call of a step's work is charged to the workflow and to that run ([[agent-llm-spend#runs]]) — also when the work goes on days later, after a person decided. So a budget for each run of the process holds all of it:

SetProcessBudget("Qualification", 2.00m);     // or: SetBudget(RunKind("Qualification"), "", LlmBudgetWindow.Total, 2.00m)

A step whose next call would pass it stops where it stands and answers Stopped, with what it had done.

What does the run's page show of its agents?#

With Osysharp.Processes in the app, the Osysharp.Agents.Processes bridge is there by itself, and:

  • a run is listed from its first agent step, with nothing written in the workflow;
  • each piece of work the run gave is a line of the run, in the state that gave it: given (and which attempt), waiting for a person, finished, failed, given up, called off. A line names the agent and the skill — never what the work was about or said, which stays on the receipt under its own rules;
  • a line opens in place as the whole inspector (The inspector — one piece of an agent's work, readable by a person who was not there (Agents.Inspector kit));
  • Agent work in this run lists each piece with how it ended and what it cost, with the work it handed on;
  • on the work's own page, Open the run leads back to the run it is a step of.

Who reads what is decided where it always was: the run by FollowsProcesses and the app's rules for the row it is about, each piece of work by the Agents kit's rules for its receipt. Somebody who follows the run and may not read a piece of its work sees the line, and is told the work is not theirs when they open it.

How is a whole process tested?#

As any agent is (Testing an agent without a model — Says, AgentScript and Trajectory (Agents.Testing kit)): the models are scripted, a person is simulated by doing what she would do (runas (Mara) { ApproveAgentProposal(…); }), TestClock.Advance moves the days, and Workflow.Settle lets the run go on. ProcessTrail reads back what the agents did in the run about one row:

Assert.Equal(["CheckDocuments", "AssessRisk", "SettleTerms"], ProcessTrail.Skills(supplier.Id));
Assert.Equal(["Done", "Done", "Done"], ProcessTrail.Ended(supplier.Id));
Assert.Equal(["ApproveSupplier"], ProcessTrail.Proposed(supplier.Id));
Assert.True(ProcessTrail.Cost(supplier.Id) <= 20000);            // two cents, in millionths of a dollar

Examples#

app Returns {
  use Osysharp.Agents;
  use Osysharp.Agents.Testing;
  use Osysharp.Agents.Inspector;
  use Osysharp.Agents.Processes;
  use Osysharp.Processes;
  use Osysharp.Llm;
  use Osysharp.Identity;
  use Osysharp.Observability;
  use Osysharp.Workflow;
  use Osysharp.Diagrams;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Llm;
using Osysharp.Workflow;
using Osysharp.Agents;
using Osysharp.Agents.Testing;
using Osysharp.Agents.Inspector;
using Osysharp.Processes;
using Osysharp.Agents.Processes;

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 Checker()] };
app.Processes = new ProcessesSetup { Processes = [new ReturnOfGoods()] };

[Role] enum ShopRole { Authenticator, Desk, Checking }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsDesk     => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Desk);
policy IsChecking => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Checking);

policy UsesAgents        => IsDesk;
policy SupervisesAgents  => IsDesk;
policy FollowsProcesses  => IsDesk;
policy ReadsModelUsage   => IsDesk;
policy ManagesModelSpend => IsDesk;

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

enum ReturnStage { Checking, Refunded, Refused, [Label("For the desk")] ForTheDesk }

entity GoodsReturn {
  [Required, MaxLength(80)] string Item;
  int DaysSincePurchase = 0;
  ReturnStage Stage = ReturnStage.Checking;
  security { allow read, create, update when IsDesk || IsChecking; }
}

/// How many days ago the item was bought.
[Action("Look up the purchase", Query)]
public int DaysSince(GoodsReturn back) { return back.DaysSincePurchase; }

public class Verdict { public bool Refund; public string Why = ""; }

public class CheckReturn : Skill<GoodsReturn, Verdict> {
  public override string Description => "Decide whether a returned item is refunded.";
  public override string Instructions => "Look up the purchase. Within 30 days it is refunded.";
  public override List<ActionInfo> Tools => [Actions.Of(DaysSince)];
  public override string Brief(GoodsReturn r) => $"{r.Item}. Its id is {r.Id}.";
}

[Actor(Roles = [ShopRole.Checking])]
public class Checker : Agent {
  public static Person Principal => new Person { Email = "[email protected]", DisplayName = "Checker" };
  public override string Purpose => "You check the goods customers send back.";
  public override List<SkillBase> Skills => [new CheckReturn()];
}

/// ⭐ The process. Its one agent step is an ordinary call: typed in, typed out, and the result decides.
workflow ReturnFlow {
  Tracks = GoodsReturn.Stage; Autostart = false; Initial = Checking;
  state Checking {
    enter {
      var checkedBack = await Agents.Of<Checker>().Do(new CheckReturn(), this.Item, about: "Return of " + this.Item.Item);
      if (!checkedBack.Done) {
        // The step did not finish — it failed, was stopped, or ran out of money. The process handles it.
        Processes.Problem("The checker could not decide, so the desk does.");
        goto ForTheDesk;
      }
      if (checkedBack.Result.Refund) { goto Refunded; }
      goto Refused;
    }
  }
  terminal cancel  ForTheDesk { Message = "handed to the desk"; }
  terminal success Refunded { }
  terminal cancel  Refused { Message = "outside the return period"; }
}

public class ReturnOfGoods : Process<GoodsReturn> {
  public override string Name => "Return of goods";
  public override string Workflow => "ReturnFlow";
  public override WorkflowGraph Graph => ReturnFlow.Graph;
  public override string TitleOf(GoodsReturn r) => r.Item;
  public override List<WorkflowAuditEntry> HistoryOf(GoodsReturn r) => ReturnFlow.For(r).Audit;
}

GoodsReturn TakeBack(string item, int daysSincePurchase) {
  var back = new GoodsReturn { Item = item, DaysSincePurchase = daysSincePurchase };
  UnitOfWork.Commit();
  Workflow.Run(back);
  return back;
}

[TestFixture]
void Shop() {
  var desk = new Person { Email = "[email protected]", DisplayName = "Noa Lindahl" };
  new RoleGrant { Member = desk, Role = ShopRole.Desk };
  new AgentSeat { Agent = "Checker", Sponsor = desk };
}
principal Desk => Person.Single(p => p.Email == "[email protected]");

[Test(Shop)]
void an_agents_step_in_a_process_is_told_on_the_run_and_charged_to_it() {
  // The checker, scripted: it looks the purchase up and refunds it.
  ScriptedAnswer.Stub((model, request) => {
    var brief = request.Messages[0].Content[0].Text ?? "";
    var id = brief.Substring(brief.IndexOf("Its id is ") + 10, 36);
    if (request.Messages.Count(m => m.Role == "assistant") == 0) { return Says.Tool(Actions.Of(DaysSince), "{\"back\":\"" + id + "\"}", 300, 20); }
    return Says.HandBack(new Verdict { Refund = true, Why = "Bought 12 days ago." }, 400, 40);
  });
  runas (Desk) {
    var back = TakeBack("Rain jacket", 12);
    Workflow.Settle(back);
    Assert.Equal(ReturnStage.Refunded, GoodsReturn.Single().Stage);      // the typed result decided

    // The receipt says which run of which workflow gave it the work, and in which state.
    var step = AgentTask.Single();
    Assert.Equal("ReturnFlow", step.RunWorkflow);
    Assert.Equal("Checking", step.RunState);
    Assert.Equal(back.Id, step.RunItem);

    // The run was recorded by its first agent step, and tells the work in the state that gave it.
    var run = ProcessRunOf(ProcessRunAbout("ReturnFlow", back.Id)!.Id)!;
    Assert.Equal("Rain jacket", run.Card.Title);
    Assert.Equal(ProcessStatus.Finished, run.Card.Status);
    Assert.Equal(["Checker was given work: check return.", "Checker finished: check return."], run.Stays[0].Moments.Select(m => m.Said).ToList());
    Assert.Equal(step.Id, run.Stays[0].Moments[0].Subject);

    // What the run cost is what its agent work spent: every call was charged to the run.
    Assert.Equal(step.SpentMicros / 1000000m, run.Cost.Spent);
    Assert.Equal(step.SpentMicros / 1000000m, SpentOn(RunScope("ReturnFlow", step.Run!)));

    // …and from the work, the run.
    Assert.Equal(run.Card.Id, ProcessRunOfWork(step.Id)!.Run);
    Assert.Equal(["CheckReturn"], ProcessTrail.Skills(back.Id));
    Assert.Equal(["Done"], ProcessTrail.Ended(back.Id));
  }
}

[Test(Shop)]
void a_limit_for_each_run_stops_the_step_and_the_process_handles_it() {
  // The checker, scripted: it looks the purchase up and refunds it.
  ScriptedAnswer.Stub((model, request) => {
    var brief = request.Messages[0].Content[0].Text ?? "";
    var id = brief.Substring(brief.IndexOf("Its id is ") + 10, 36);
    if (request.Messages.Count(m => m.Role == "assistant") == 0) { return Says.Tool(Actions.Of(DaysSince), "{\"back\":\"" + id + "\"}", 300, 20); }
    return Says.HandBack(new Verdict { Refund = true, Why = "Bought 12 days ago." }, 400, 40);
  });
  runas (Desk) {
    // Each return may spend a tenth of a cent. The checker's first call would hold half a cent: nothing is sent.
    SetProcessBudget("ReturnFlow", 0.001m);
    UnitOfWork.Commit();
    var back = TakeBack("Rain jacket", 12);
    Workflow.Settle(back);
    Assert.Equal(ReturnStage.ForTheDesk, GoodsReturn.Single().Stage);
    Assert.Equal(AgentTaskStatus.Stopped, AgentTask.Single().Status);
    var run = ProcessRunOf(ProcessRunAbout("ReturnFlow", back.Id)!.Id)!;
    Assert.Equal("The checker could not decide, so the desk does.", run.Stays[0].Moments.Last().Said);
    Assert.Equal(0.001m, run.Cost.Limit);
  }
}

Notes#

  • From a page or a plain function Do answers at once with Waiting when the work stops on a person; only in a workflow does it wait (Giving an agent work — Do, Ask, Stream, For and Decide (Agents kit)).
  • A step that is tried again (Retry) is one line per attempt on the run, and one receipt per attempt.
  • A run started by another run's body is a run of its own: its agent work is charged to it, and the run that started it shows what the two cost together.

See also#

Related

Processes — every run of a business process, listed and read from start to now (Processes kit)

Osysharp.Processes makes the runs of an app's workflows followable by the people who answer for them. The app names…

The inspector — one piece of an agent's work, readable by a person who was not there (Agents.Inspector kit)

Osysharp.Agents.Inspector draws one page for one piece of an agent's work: what it was and how it ended; who gave it…

Workflow.Current (which run is this code in?)

`Workflow.Current` is the workflow run this code is executing in — one value saying its id, the workflow's declared…

An agent hands work on to other agents — at once, typed, and never with more than it holds (Agents kit)

An agent's class may name work it hands on: skills of other agents, or of its own done apart. Its model is offered each…

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…

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

What an agent may spend — a hard limit on every piece of work (Agents kit)

Every piece of work has a limit in dollars — the skill's `Budget`, else the agent's `Limits.PerTask`, else the app's…

Model spend — budgets, the meter and the price book (Llm kit)

The Llm kit keeps three things about what model calls cost. The METER: one `ModelCall` row for every attempt, with the…

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…

What happens to work a run stops waiting for

A run that is waiting on child runs — one it awaits, several joined, tasks that wait — and is ended from somewhere else…

The receipt and the record — what every piece of an agent's work leaves (Agents kit)

Every piece of work leaves one receipt, `AgentTask` — who asked, the account it ran as, what it cost against its limit…

Agents (a class with a job, its own account, a budget and a record)

An agent is a class of the `Osysharp.Agents` kit: a purpose, the skills it holds and what it may spend. A skill is a…