Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

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

AgentTask { Agent, Skill, Requester, Actor, Status, SpentMicros, LimitMicros, ModelCalls, ToolCalls, Turns, Outcome, Result } · TraceEvent { Seq, Kind, Summary, Payload, Model, RouteRule, CostMicros, Hash } · VerifyTrace(task) · EraseAgentWorkContent(task, why) · AnonymiseAgentWork(task, why) · ForgetAgentWork(task)

Every piece of work leaves one receipt, `AgentTask` — who asked, the account it ran as, what it cost against its limit, how it ended — and a record, `TraceEvent`, of each step in a plain sentence: each model call with the model that served it and the rule that chose it, each tool with what it was given and answered. The record is chained with SHA-256, so `VerifyTrace` finds a line that was changed, removed or slipped in. Only the kit writes either. What a record said can be erased, and the person it was for taken off it, and the chain still checks.

preview1 example compiled by CIagentskit

Summary#

var receipt = AgentTask.Single(t => t.Id == outcome.Task);     // who asked, as whom it ran, what it cost, how it ended
var steps   = TraceEvent.Where(e => e.Task == receipt).OrderBy(e => e.Seq).ToList();
var check   = VerifyTrace(receipt);                             // check.Intact, check.BrokenAt, check.Problem

Signature#

entity AgentTask {
  string Agent; string Skill; string SkillVersion; AgentTaskCause Cause; string About; string Given;
  int InstructionSet; string InstructionsSent; string InstructionsHash;   // what people had told it — see agent-teaching
  P? Requester; P Actor;                         // for whom, and the account it ran as
  AgentTaskStatus Status; string Outcome; string? Result;
  long SpentMicros; long LimitMicros; int ModelCalls; int ToolCalls; int Turns; int InputTokens; int OutputTokens;
  DateTime StartedAt; DateTime? EndedAt;
}

entity TraceEvent {
  AgentTask Task; int Seq; TraceKind Kind; DateTime At;
  string Summary; string SummaryHash;            // the step, in a sentence — `Erased.` once it is erased
  string? Payload; string PayloadHash;           // its content — read by fewer people than the sentence; "" once erased
  DateTime? ErasedAt;
  string Model; string RouteRule; int Attempts; long CostMicros; Guid? Call;     // a model call
  string Tool;                                   // a tool call or its result
  string PrevHash; string Hash;
}
enum TraceKind { Started, ModelCall, ToolCall, ToolResult, AskedForHelp, SentBack, Ended }

List<TraceLine> TraceOf(AgentTask task)
TraceCheck      VerifyTrace(AgentTask task)      // { bool Intact; int Lines; int BrokenAt; string Problem; int Erased; }
TraceCheck      CheckChain(List<TraceLine> lines)

policy ErasesAgentRecords                        // nobody, until the app says
int  EraseAgentWorkContent(AgentTask task, string why)   // what it said: every line's content, help asked, the result
void AnonymiseAgentWork(AgentTask task, string why)      // whose it was, what it was about, every line's sentence
void EraseAgentHelp(AgentHelp help, string why)          // one request for help asked outside any piece of work
int  ForgetAgentWork(AgentTask task)                     // the whole of a piece of work, deleted

[Event] AgentTaskStarted · AgentTaskFinished · AgentTaskFailed · AgentTaskStopped · AgentWorkErased

Description#

What is on the receipt?#

One AgentTask per piece of work, whatever became of it: the agent, the skill and its version, what it was about, who asked (Requester) and the account it ran as (Actor), its status and how it ended in a sentence (Outcome), what it handed back (Result, as JSON), and the counts — model calls, tool calls, turns, tokens, and SpentMicros against LimitMicros in millionths of a dollar. What a receipt says was spent is what the model kit's meter says, call for call. Add your own fields with partial entity AgentTask { … }.

Work given in a workflow's run also says which: Run, RunWorkflow, RunState, RunItem, RunStarter and RunStep — the step of the process it is (Agents in business processes — an agent's work as a step of a workflow, told on the run (Agents.Processes kit)). One page lays a receipt and its record out for a person who was not there: The inspector — one piece of an agent's work, readable by a person who was not there (Agents.Inspector kit).

A piece of work that was handed on by another has its own receipt, which names the piece above it (Parent), the top (Root), how deep it is (Depth), its place among the pieces handed on beside it (Sequence, from 1) and every piece above it (Lineage). The whole tree shares one record — one chain — and WorkTree.Of(task) reads it as a tree, each piece with what it and everything under it cost, siblings in the order they were handed on — by Sequence, so two pieces handed on in the same instant read the same way every time.

What does the record say?#

A TraceEvent per step, in order. Summary is a sentence anybody can read — "Everyday (chosen by llm:default) asked for a tool; it cost $0.0002.", "It asked for NoteRepair (changes data); it is one of this work's tools, so it runs as the agent, for whoever asked." — and Payload is the content: what the model was shown, what a tool was given and what it answered. A model call's line also names the model, the router's rule, the attempts, the cost and the id of its row on the meter.

Who reads them?#

Whoever asked for a piece of work reads its receipt and its record, content included. A person the app's SupervisesAgents policy admits reads every receipt and every sentence, but not the content. A person AuditsAgents admits reads everything, and so does a person TeachesAgents admits — whoever corrects an agent reads what it acted on ([[agent-roles#words]]). ReadsAllAgentWork opens the receipts' figures and none of the words. Nobody else reads any of it.

How do I know the record was not changed?#

Each line carries the SHA-256 of the line before it and of its own fields, and of its content's hash rather than the content. VerifyTrace(task) walks the chain and names the first line that does not fit. Because the chain holds the content's hash, a supervisor who cannot read the content can still verify, and content that was erased — it can be personal data — leaves the record standing.

Can what a record says be erased?#

Yes, and the record of what happened survives it. A line's hash is made from the hash of its sentence and the hash of its content, never from the words — so the words can go and the chain still checks.

  • EraseAgentWorkContent(task, why) empties the content of every line (what the agent was shown, what each tool was given and answered, what the model said), what was asked and answered when it asked for help, and the result on the receipt — for the whole of the work the task is part of. The sentences, models, tools and costs stay.
  • AnonymiseAgentWork(task, why) takes the person off it: Requester and whoever gave the work become nobody, what it was about and how it ended in words are blanked, and every line's sentence becomes Erased. with the time. What stays is that the work happened: which agent and skill, when, each step's kind, model, tool and cost.
  • ForgetAgentWork(task) deletes the work whole — receipts, lines, requests for help — when its time is up.

VerifyTrace answers Intact after either erasure and says how many lines were erased (Erased). A line that was changed afterwards is still found: a cost or a tool that differs breaks the line's hash; a sentence written into an erased line is refused ("Line 4 was erased, and something was written in its place."); so is a line marked erased that still says something. All three are refused while any part of the work is still running.

Who may: a person the app's ErasesAgentRecords policy admits — nobody, until the app says — and they read receipts and sentences but never content. Whoever asked for the work, a supervisor and an auditor may not. An app that uses Osysharp.Privacy writes none of this: Privacy for the agent family — agents, the model meter, assignments and work repositories under a person's request erases for a person who asks, and when a period ends.

Can my app write or correct them?#

No. Receipts and record lines are written only by the kit's own functions; app code that creates, changes or deletes one is denied. Hear what happens instead: AgentTaskStarted, AgentTaskFinished, AgentTaskFailed and AgentTaskStopped are events an app handles with [[function-on|[On]]].

Examples#

app Workshop {
  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;

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 Estimator()], TaskBudget = 0.05m };

[Role] enum ShopRole { Authenticator, Mechanic, Manager, Helper }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsMechanic => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Mechanic);
policy IsManager  => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Manager);
policy IsHelper   => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Helper);

// The kit's questions, answered by this app.
policy UsesAgents       => IsMechanic || IsManager;   // gives agents work, reads the work they asked for
policy SupervisesAgents => IsManager;                 // names sponsors, pauses and stops, reads that work happened
policy AuditsAgents     => IsManager;                 // reads everything, content included
policy ReadsModelUsage   => IsManager;
policy ManagesModelSpend => IsManager;

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

/// One bike in for repair.
entity Repair {
  [Required, Unique, MaxLength(40)] string Ticket;
  [Required, MaxLength(200)] string Fault;
  [MaxLength(400)] string Note = "";
  /// What the customer is charged. Only a mechanic or the manager sets it.
  decimal Price;
  security {
    allow read, update when IsMechanic || IsManager || IsHelper;
    allow create when IsMechanic || IsManager;
    deny update Price when IsHelper;
  }
}

/// What an estimate says.
public class Estimate {
  public decimal Hours;
  public string Summary = "";
  public bool NeedsParts;
}

/// The parts a fault needs.
[Action("Look up the parts a fault needs", Query)]
public List<string> PartsFor(string fault) { return fault.Contains("brake") ? ["brake pads", "cable"] : []; }

/// Write a note on the repair's ticket.
[Action("Write a note on the repair")]
public string NoteRepair(Repair repair, string note) { repair.Note = note; return $"Noted on {repair.Ticket}."; }

/// Set what the customer is charged.
[Action("Set the price")]
public string SetPrice(Repair repair, decimal price) { repair.Price = price; return $"{repair.Ticket} costs {price}."; }

/// Works out how long one repair takes.
public class EstimateRepair : Skill<Repair, Estimate> {
  public override string Description => "Estimate one repair.";
  public override string Instructions => "Look up the parts first. Write the estimate as a note on the repair.";
  public override List<ActionInfo> Tools => [Actions.Of(PartsFor), Actions.Of(NoteRepair), Actions.Of(SetPrice)];
  public override string Brief(Repair r) => $"Ticket {r.Ticket}: {r.Fault}. Its id is {r.Id}.";
  public override string? Check(Estimate e) => e.Hours <= 0 ? "An estimate takes some time." : null;
}

/// Estimates the repairs that come in. It holds the Helper role and nothing more.
[Actor(Roles = [ShopRole.Helper])]
public class Estimator : Agent {
  public static Person Principal => new Person { Email = "[email protected]", DisplayName = "Estimator" };
  public override string Purpose => "You estimate bicycle repairs for a small workshop.";
  public override List<SkillBase> Skills => [new EstimateRepair()];
  public override Permits May => Permits.ChangesAlone();        // it notes and prices by itself
}

/// What the app is not able to do.
public void RewriteReceipt(AgentTask task) { task.SpentMicros = 0; }

[TestFixture]
void Shop() {
  var mia = new Person { Email = "[email protected]", DisplayName = "Mia" };
  var noor = new Person { Email = "[email protected]", DisplayName = "Noor" };
  new RoleGrant { Member = mia, Role = ShopRole.Mechanic };
  new RoleGrant { Member = noor, Role = ShopRole.Manager };
  new Repair { Ticket = "T-7", Fault = "rear brake does not bite" };
  new AgentSeat { Agent = "Estimator", Sponsor = noor };          // the manager answers for the agent
}
principal Mia  => Person.Single(p => p.Email == "[email protected]");
principal Noor => Person.Single(p => p.Email == "[email protected]");

string RepairArgs(Repair r, string rest) { return "{\"repair\":\"" + r.Id.ToString() + "\"" + rest + "}"; }

/// What the model is scripted to answer for one estimate: two tools, then the result.
AgentScript EstimateScript(Repair repair) {
  return AgentScript.Of(
    Says.Tool(Actions.Of(PartsFor), "{\"fault\":\"rear brake does not bite\"}"),
    Says.Tool(Actions.Of(NoteRepair), RepairArgs(repair, ",\"note\":\"New pads and a cable: one hour.\"")),
    Says.HandBack(new Estimate { Hours = 1, Summary = "New pads and a cable.", NeedsParts = true }));
}

[Test(Shop)]
[runas(Mia)]
void The_receipt_says_who_asked_as_whom_it_ran_and_what_it_cost() {
  var repair = Repair.Single(r => r.Ticket == "T-7");
  var script = EstimateScript(repair);
  ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
  var task = (await Agents.Of<Estimator>().Do(new EstimateRepair(), repair, about: "Ticket T-7")).Task;

  var receipt = AgentTask.Single(t => t.Id == task);
  Assert.Equal("Estimator", receipt.Agent);
  Assert.Equal("EstimateRepair", receipt.Skill);
  Assert.Equal(AgentTaskStatus.Done, receipt.Status);
  Assert.Equal(Mia.Id, receipt.Requester!.Id);
  Assert.Equal(Actor.Of<Estimator>().Id, receipt.Actor.Id);
  Assert.Equal(600, receipt.SpentMicros);
  Assert.Equal(50000, receipt.LimitMicros);              // the app's TaskBudget: $0.05
  Assert.Equal(3, receipt.ModelCalls);
  Assert.Equal(2, receipt.ToolCalls);
}

[Test(Shop)]
[runas(Mia)]
void The_record_says_every_step_in_order_and_verifies() {
  var repair = Repair.Single(r => r.Ticket == "T-7");
  var script = EstimateScript(repair);
  ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
  var task = (await Agents.Of<Estimator>().Do(new EstimateRepair(), repair, about: "Ticket T-7")).Task;

  Assert.Equal(["Started", "ModelCall", "ToolCall", "ToolResult", "ModelCall", "ToolCall", "ToolResult", "ModelCall", "Ended"],
               Trajectory.Kinds(task));
  Assert.Contains("It asked for NoteRepair (changes data); it is one of this work's tools, so it runs as the agent, for whoever asked.",
                  Trajectory.Said(task));

  var receipt = AgentTask.Single(t => t.Id == task);
  Assert.True(VerifyTrace(receipt).Intact);

  // A sentence rewritten afterwards is found, and which line it was.
  var lines = TraceOf(receipt);
  lines[3].Summary = "PartsFor was never asked for.";
  var check = CheckChain(lines);
  Assert.False(check.Intact);
  Assert.Equal(4, check.BrokenAt);
}

[Test(Shop)]
void Only_the_kit_writes_a_receipt_and_the_content_is_read_by_fewer() {
  Guid task;
  runas (Mia) {
    var repair = Repair.Single(r => r.Ticket == "T-7");
    var script = EstimateScript(repair);
    ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
    task = (await Agents.Of<Estimator>().Do(new EstimateRepair(), repair, about: "Ticket T-7")).Task;
    var receipt = AgentTask.Single(t => t.Id == task);
    Assert.Denied(() => { RewriteReceipt(receipt); UnitOfWork.Commit(); });
    UnitOfWork.Discard();
    // Whoever asked reads the content.
    Assert.NotNull(TraceEvent.First(e => e.Task.Id == task && e.Kind == TraceKind.ToolCall).Payload);
  }
  runas (Noor) {
    // The manager supervises and audits here, so she reads it too.
    Assert.Equal(9, TraceEvent.Count(e => e.Task.Id == task));
  }
}

See also#

Related

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…

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

Asking for help — an agent that consults a more capable model (Agents kit)

An agent can name one of the app's models to consult. The model doing the work may then ask it one focused question…

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…

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…

Privacy for the agent family — agents, the model meter, assignments and work repositories under a person's request

An app that uses `Osysharp.Privacy` beside the agent kits gets a holder for each without writing one:…

[On] — hear an event, after the commit that raised it

`[On]` on a function makes it a HANDLER of the event its one parameter is. Every handler of an event — in the app and…

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…