# watching a task run (task.Watch)

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

<!-- id: agent-task-watch · area: agent · stability: preview · html: https://osysharp.com/reference/agent/task-watch/ -->

## Summary        {#summary}

```osy syntax
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      {#signature}

```osy syntax
task.Watch()   // → stream<AgentStep>
```

| `AgentStep` | |
|---|---|
| `Sequence` | order within the whole job, from 1 |
| `Kind` | `Said` · `ToolCall` · `ToolResult` · `Asked` · `Answered` · `Finished` |
| `Text` | the line a progress view shows |
| `Detail` | the tool name, the terminal state, the failure |
| `IsError` | the tool failed (`ToolResult` only) |
| `At` | when it happened |

⚠ It is a **method, not a property** — the parens are carrying meaning. [`task.Calls`](https://osysharp.com/reference/agent/task-calls/) is a
*value*, complete when you get it; this opens something with a lifetime.

## Description    {#description}

### Watching is free, and so is looking away   {#detach}

The task's work is driven by its [loop](https://osysharp.com/reference/agent/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.

```osy syntax
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?   {#subtree}

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   {#terminal}

Completed, failed, or [stopped](https://osysharp.com/reference/agent/task-stop/) — 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   {#security}

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

```osy syntax
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`](https://osysharp.com/reference/agent/task-transcript/), behind its own gate.

## Examples       {#examples}

```osy title="watching-a-review" test app=agent-task-watch
using Osysharp.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       {#see-also}
- [stopping work (task.Stop)](https://osysharp.com/reference/agent/task-stop/) — ending work that is still running, including the model call it is in
- [what a task cost, and what it did (task.Calls)](https://osysharp.com/reference/agent/task-calls/) — what the same work *cost*
- [what the agent saw (task.Transcript)](https://osysharp.com/reference/agent/task-transcript/) — what the agent actually read and wrote, behind its own gate
