Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

Assignments — a piece of work owned until its outcome is reached (Assignments kit)

OpenAssignment(new NewAssignment { Title, Goal, Criteria, … }) · MeetCriterion(criterion, metBy, evidence) · CompleteAssignment(a, outcome) · ReopenAssignment(a, why) · AttachWork(a, …) · ConditionsOf(a) · CostOf(a)

An assignment of the `Osysharp.Assignments` kit says who asked, the outcome wanted and what must be true for it to be done. Agent runs, processes, requests to people and work handed to other systems are attached to it; any of them can fail or be replaced, and the assignment stays. It completes only when every criterion is met — or somebody the app trusts accepts a written exception — and can be opened again with everything it had. It tells progress from activity, works out whether it is waiting, blocked, overdue or stalled with the reason for each, and knows what it has cost.

preview1 example compiled by CIagentskitassignments

Summary#

var a = OpenAssignment(new NewAssignment {
  Title = "Press P-200 faults intermittently",
  Goal = "The press runs a full shift without faulting.",
  Criteria = ["The cause is found", "The press ran clean for a shift"],
});

MeetCriterion(a.Criteria.Single(c => c.Position == 1), "A worn pilot valve", evidence: log.Id, evidenceType: "ServiceLog");
CompleteAssignment(a, "The valve was replaced.");     // CriteriaUnmet — one criterion is not met, and it says which

Signature#

Assignment OpenAssignment(NewAssignment ask)          // Title, Goal, Scope, Criteria, Owner, Assignees, DueAt,
                                                      // StalledAfter, Budget, Parent, ParentCriterion, Process
AssignmentCriterion AddCriterion(Assignment a, string text, string key = "", Assignment? metWhen = null)
void MeetCriterion(AssignmentCriterion c, string metBy, Guid? evidence = null, string evidenceType = "")
void ReopenCriterion(AssignmentCriterion c, string why)
void AcceptException(AssignmentCriterion c, string why)                 // policy AcceptsAssignmentExceptions
Completion CompletionOf(Assignment a)                                   // Ready, ByException, Unmet, Why

void CompleteAssignment(Assignment a, string outcome, string limitations = "", string obligations = "")
void CancelAssignment(Assignment a, string why)
void FailAssignment(Assignment a, string why, string limitations = "")
void ReopenAssignment(Assignment a, string why, List<AssignmentCriterion>? noLongerMet = null)

AssignmentWork AttachWork(Assignment a, WorkAttachment what)            // Kind, Key, Title, Subject, WaitingOn, DueAt, Branch
void CloseWork(AssignmentWork work, WorkState state, string result = "")
void BlockWork(AssignmentWork work, string why) · UnblockWork(work) · CountWorkCost(work, totalMicros)
AssignmentWork? DoneFor(Assignment a, string key) · RecordDone(a, key, title, result)

void NoteProgress(Assignment a, string what, Guid reference, string referenceType, string branch = "")
void NoteActivity(Assignment a, ActivityKind kind, string what)         // Note, Reminder, Retry, StatusSummary
AssignmentConditions ConditionsOf(Assignment a)                         // Waiting, Blocked, Overdue, Stalled + why
AssignmentLink LinkAssignments(Assignment from, Assignment to, LinkKind kind, string why = "")
LlmCall ChargedTo(Assignment a, string purpose = "") · AssignmentCost CostOf(Assignment a)

policy OpensAssignments · OverseesAssignments · AcceptsAssignmentExceptions      // the app answers each

policy ErasesAssignments                                                // nobody, until the app says
void TakePersonOffAssignment(Assignment a, Principal person, string standIn, string why)
void AnonymiseAssignment(Assignment a, AssignmentScrub how)             // Words, StandIn, Blank, Why — a closed assignment
int  EraseWordsFromActivity(Assignment a, List<string> words, string standIn, string why)
List<Assignment> AssignmentsOf(Principal person) · bool MayEraseAssignments() · string FormerPersonWords()

Description#

When is an assignment done?#

When every criterion is met or excepted — and it has at least one. A criterion is met with what met it: words, and optionally the id of the evidence or decision it rests on. Finishing every piece of attached work completes nothing. CompleteAssignment refuses with CriteriaUnmet, naming each criterion in the way. A criterion that will not be met is settled by AcceptException, which only somebody the app's AcceptsAssignmentExceptions policy admits may call; the assignment then closes as CompletedByException.

What happens when the outcome does not hold?#

ReopenAssignment(a, why, noLongerMet) opens the same assignment again. It keeps its criteria, its activity, what is attached to it and what it cost; the criteria named go back to unmet (with none named, every one that was met by hand does). Whoever asked, whoever owns it and whoever oversees assignments may.

What counts as progress?#

Something established: a criterion met, a piece of work done, an exception accepted, or NoteProgress — which refuses a note that points at nothing. A reminder, a retry, a status summary and a plain note are activity (NoteActivity). The assignment keeps LastActivityAt and LastProgressAt apart, and BranchesOf(a) keeps both per parallel branch.

How do I know why it is not moving?#

ConditionsOf(a) works out four conditions, each with its reasons. More than one can hold; a closed assignment has none.

conditionholds when
Waitingopen work is pending, inside the time allowed for it
Blockeda piece of work is blocked, or an assignment it depends on is not completed
Overduethe assignment's date, or a piece of work's, has passed
Stallednothing has been established for longer than it allows (StalledAfter; the app's default is three days)

What can be attached to it?#

Anything it waits on or has had done: an agent's piece of work, a process, a request to a person, a handoff, an action. AttachWork is safe to repeat — the same Key answers the piece of work already there — so a step that is run again after a crash attaches nothing twice. For something that must not HAPPEN twice, ask DoneFor(a, key) first and RecordDone in the same unit of work as the doing.

An assignment opened with Parent is under another; with ParentCriterion its completion meets that criterion on the parent, and reopening it puts the criterion back in question. Whoever is on an assignment reads what is opened under it. LinkAssignments(from, to, LinkKind.DependsOn) blocks from until to is completed.

Who may see and act on one?#

Whoever is on it reads it: whoever asked, its owner, and the people — or agents' accounts — put on it with AddParticipant. Assignees and contributors change it; observers do not. OverseesAssignments reads and acts on all of them. Its state, criteria, activity, people and attached work are written only by the kit's own functions.

Can a person be taken off it, or its words erased?#

Yes, and the assignment stays — it is the organisation's record of work it did.

  • TakePersonOffAssignment(a, person, standIn, why) takes somebody who was ON it off: their seat goes; who asked, who owns, who closed and who a piece of work waits on become nobody where it was them; each line they wrote and each criterion they settled is marked as a former person's, and a page shows Somebody no longer here (FormerPersonWords()) — never "The app", and never nothing. Their name and address are replaced by standIn in the assignment's sentences. AssignmentsOf(person) lists every assignment they are on or did something on.
  • AnonymiseAssignment(a, new AssignmentScrub { Words, StandIn, Why }) erases what a closed assignment says about the person it is ABOUT: each of Words — a name, an address, an order number — is replaced by StandIn ("a former customer") in its title, goal and scope, each criterion and what met it, each piece of work and its result, every line of activity and how it ended. With Blank = true every sentence is replaced whole. Either way what happened stays: each criterion and whether it was met, each line's kind and time, the cost, how it closed.
  • EraseWordsFromActivity(a, words, standIn, why) replaces words in the activity alone, on an open assignment too — what the repositories kit calls when an item whose title the activity named is erased.

Who may: a person the app's ErasesAssignments policy admits — nobody, until the app says. Whoever asked for an assignment, whoever owns it and whoever oversees every assignment 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 does it for a person who asks, and when a period ends.

What does it cost?#

ChargedTo(a) is an LlmCall whose scope is the assignment and whose limit is what is left of its budget; a call that does not fit is refused before anything is sent. CostOf(a) adds its own calls, what attached work reported (CountWorkCost) and what the assignments under it spent.

Agent work given with AssignedWork.Do carries that same call: every model call it makes is charged to the assignment as it returns and held to what the assignment has left, so work that would pass the budget is stopped while it runs. Its receipt is on the assignment before it starts, so CancelAssignment calls it off. When the work ends, its share is reported as the work's (CountWorkCost(work, total, chargedAsSpent: true)) and is counted once.

Work the agent hands on is the assignment's too: each piece is attached beside the piece that handed it on ("Resolver: investigate delivery — handed on by CaseLead"), for every specialist that is on the assignment, and settled when it ends. The whole tree is charged to the assignment as it spends; the piece at the top counts all of it, and a piece that was handed on counts nothing itself — its own figure is on its receipt.

Examples#

app Workshop {
  use Osysharp.Assignments;
  use Osysharp.Llm;
  use Osysharp.Identity;
  use Osysharp.Observability;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Assignments;
using Osysharp.Identity;

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

// The kit's questions, answered by this app.
policy OpensAssignments            => IsDesk || IsManager;
policy OverseesAssignments         => IsManager;
policy AcceptsAssignmentExceptions => IsManager;

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

[TestFixture]
void Shop() {
  var dana = new Person { Email = "[email protected]", DisplayName = "Dana" };
  var tariq = new Person { Email = "[email protected]", DisplayName = "Tariq" };
  var mona = new Person { Email = "[email protected]", DisplayName = "Mona" };
  new RoleGrant { Member = dana, Role = ShopRole.Desk };
  new RoleGrant { Member = tariq, Role = ShopRole.Technician };
  new RoleGrant { Member = mona, Role = ShopRole.Manager };
}
principal Dana  => Person.Single(p => p.Email == "[email protected]");
principal Tariq => Person.Single(p => p.Email == "[email protected]");
principal Mona  => Person.Single(p => p.Email == "[email protected]");

Assignment Job() { return Assignment.Single(); }
AssignmentCriterion Criterion(int position) { return Job().Criteria.Single(c => c.Position == position); }

[Test(Shop)]
void An_assignment_completes_by_its_criteria_and_is_opened_again_with_what_it_had() {
  TestClock.Set(new DateTime(2026, 5, 4, 9, 0, 0));
  runas (Dana) {
    OpenAssignment(new NewAssignment {
      Title = "Press P-200 faults intermittently",
      Goal = "The press runs a full shift without faulting.",
      Criteria = ["The cause is found", "The customer approved the cost", "The press ran clean for a shift"],
      Assignees = [Tariq],
    });
  }
  runas (Tariq) {
    // A visit is attached, blocked, and then done. None of that completes anything.
    var visit = AttachWork(Job(), new WorkAttachment { Key = "visit-1", Title = "First visit", WaitingOn = Tariq });
    BlockWork(visit, "The site is closed.");
    Assert.Equal(["Blocked"], ConditionsOf(Job()).Names());
    UnblockWork(visit);
    CloseWork(visit, WorkState.Done, "Valve replaced.");

    MeetCriterion(Criterion(1), "A worn pilot valve");
    MeetCriterion(Criterion(3), "Eight hours with no fault");
    var refused = Assert.Throws<CriteriaUnmet>(() => CompleteAssignment(Job(), "Fixed."));
    Assert.Contains("1 of 3 criteria is not met: “The customer approved the cost”.", refused.Message);
    // The technician may not wave the approval through.
    Assert.Denied(() => AcceptException(Criterion(2), "They always pay."));
  }
  runas (Mona) { AcceptException(Criterion(2), "Covered by the service contract."); }
  runas (Tariq) {
    CompleteAssignment(Job(), "The valve was replaced.");
    Assert.Equal(AssignmentState.CompletedByException, Job().State);
  }

  // Three days of reminders are activity; they establish nothing.
  runas (Dana) { ReopenAssignment(Job(), "The press faulted again.", [Criterion(1), Criterion(3)]); }
  TestClock.Advance(TimeSpan.FromDays(4));
  runas (Tariq) {
    NoteActivity(Job(), ActivityKind.Reminder, "Reminded the customer.");
    Assert.Equal(1, Job().Reopened);
    Assert.Equal(CriterionState.Excepted, Criterion(2).State);          // what still holds was kept
    Assert.Equal(WorkState.Done, WorkOf(Job(), "visit-1")!.State);      // and so was everything attached to it
    Assert.Equal(["Stalled"], ConditionsOf(Job()).Names());             // the reminder moved nothing
    Assert.Equal(new DateTime(2026, 5, 4, 9, 0, 0), Job().LastProgressAt);
  }
}

[Test(Shop)]
[runas(Tariq)]
void What_was_done_once_is_not_done_again() {
  runas (Dana) { OpenAssignment(new NewAssignment { Title = "Order a valve", Goal = "A valve is on the shelf.", Criteria = ["Delivered"], Assignees = [Tariq] }); }
  Assert.Null(DoneFor(Job(), "part-order"));
  RecordDone(Job(), "part-order", "Order the valve", "Ordered, reference 7731");
  // A step that is run again asks first, and finds it.
  Assert.Equal("Ordered, reference 7731", DoneFor(Job(), "part-order")!.Result);
  Assert.Equal(1, Job().Work.Count());
}

See also#

Related

Work repositories — what an assignment knows, kept as items (WorkRepositories kit)

The `Osysharp.WorkRepositories` kit keeps an assignment's evidence, correspondence, findings, documents, proposals and…

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…

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

[Actor] — a principal that code may run as

A class marked `[Actor]` declares a principal that code may run as: a robot, a nightly job, an integration, a digital…

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