Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Agent

watching a task run (task.Watch)

task.Watch() → stream<AgentStep>

Everything a task has done so far, then everything it does next, as one stream that ends when the task does. A watcher can arrive late, leave, and come back — the work is driven by the task, not by whoever is looking at it, so watching costs the task nothing and stopping watching costs it nothing either.

preview1 example compiled by CIagenttaskstreamingui

Summary#

foreach (var step in task.Watch()) {
  // step.Text — "Using Search", "recommending approval"
}

One foreach. It replays what already happened, keeps going as more happens, and finishes when the task reaches a terminal state. Arriving late is not a special case — a watcher who opens a task halfway through sees the same thing as one who was there from the start.

Signature#

task.Watch()   // → stream<AgentStep>
AgentStep
Sequenceorder within the whole job, from 1
KindSaid · ToolCall · ToolResult · Asked · Answered · Finished
Textthe line a progress view shows
Detailthe tool name, the terminal state, the failure
IsErrorthe tool failed (ToolResult only)
Atwhen it happened

⚠ It is a method, not a property — the parens are carrying meaning. task.Calls is a value, complete when you get it; this opens something with a lifetime.

Description#

Watching is free, and so is looking away#

The task's work is driven by its loop, not by you. So:

  • open it and you see everything so far, then everything next;
  • close it and the task carries on exactly as before;
  • come back and call Watch() again — it catches you up.

There is no cursor to keep, no reconnect to write, and no state on the client at all. That is the whole reason this is a single member rather than a history call plus a live subscription: the moment they are two, every caller has to join them, and get the join right.

⚑ A watcher must live inside something that can pass items on as they arrive — so a function reading a stream is itself declared stream<T>. The compiler enforces this, which is what stops a watcher being written as an ordinary function that quietly blocks until the task ends.

stream<string> Progress(Guid taskId) {
  var task = ReviewTask.Where(t => t.Id == taskId).FirstOrDefault();
  foreach (var step in task.Watch()) {
    yield return step.Text;
  }
}

Does it cover child tasks too?#

A task driven by a loop makes no model call itself — the run it starts is a child task, and the work is recorded against the child. Watch() therefore covers the task and everything beneath it, which is what makes it show anything at all on the task you were handed. Same reason [[agent-task-calls#allcalls|AllCalls]] exists.

It ends when the task ends#

Completed, failed, or stopped — the stream finishes and the foreach exits. A task that is Waiting on a person is not terminal and the stream stays open: that is the case a watcher most wants to be attached for, because the thing being waited on is usually them.

Who can read it#

If you can read the task, you can read its progress. Steps hang off the task, so there is no second rule to declare and none to forget:

partial entity AgentTask {
  security { allow read when IsAuthenticated; }
}

⚠ A step says what the agent did — "Using Search", "recommending approval". It never carries what a tool returned, because a tool result is the agent's own read performed with the agent's authority. That is task.Transcript, behind its own gate.

Examples#

using Osyrin.Agents;

[Principal] entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

entity ReviewTask : AgentTask {
  security { allow read, update when IsAuthenticated; }
}

/// A progress feed a screen can bind straight to.
stream<string> Progress(Guid taskId) {
  var task = ReviewTask.Where(t => t.Id == taskId).FirstOrDefault();
  foreach (var step in task.Watch()) {
    yield return step.Sequence.ToString() + ". " + step.Text;
  }
}

See also#

Related

stopping work (task.Stop)

End a task and everything beneath it, including the model call it is in the middle of. Answers whether a live run was…

the agent task log (AgentTask)

Every piece of work an agent does is recorded as an `AgentTask` — which agent, what set it going, when it started and…

what a task cost, and what it did (task.Calls)

Every model call an agent task paid for, read off the task itself — the model, the turn, the tokens, the cache hits…

the agent loop (app.Agent, Loop)

The workflow an agent's work runs INSIDE. A step of it means "run the agent until it finishes or asks for help" — the…