---
title: "agents.step@1 · OpenVibe.Services"
canonical: "https://openvibe.services/docs/contracts/agents.step"
updated: null
---

# agents.step@1 · OpenVibe.Services

HTML: [agents.step@1 · OpenVibe.Services](https://openvibe.services/docs/contracts/agents.step)

# AgentsStep `agents.step@1`

Generated at 2026-11-13 06:25:48 UTC from [`openvibe-contracts v0.419.0`](https://github.com/OpenVibers/OpenVibe.Contracts/tree/v0.419.0) and [`openvibe-sdk v0.161.0`](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.161.0).

- Version: 1.2.0
- Owner: actor
- Visibility: public
- Status: active
- Compatibility: backward
- Decision: [ADR-044](/docs/adr/ADR-044)
- Schema: [`https://openvibe.network/contracts/agents/step.v1.json`](/docs/contracts/agents.step.json)

agents.step@1 (one agent system, 2026-11-12): one entry of a run's append-only step log, as GET /api/v1/runs/{id}/steps returns it and as agents.step.appended@1 publishes it. SEQ is the run's own order (1, 2, …), never renumbered; a reader pages with ?after=SEQ. KIND says what the entry is: think (a plan, a check, a note to self), tool (a tool call or its result), message (words for the person), approval (the run asked), child (a sub-agent or a hand-off) or file (a file the run made). TEXT is what a person reads; TOOL\_CALL is the tool's name and input on a tool entry; RESULT is what came back. The product's own event may split a call and its result into two steps: both carry KIND tool and the call's seq is the earlier one. A step carries no credential, no key and no token; a secret a tool saw is redacted before it is stored. Since 1.1.0 (agent-engine.md) a step also says who spoke (FROM: the person's own words are steps too), whether it is still under way (STATUS), a short TITLE and GLYPH for a tool or a thought, the files that go with it (ATTACHMENTS), numbered SOURCES, a rich card (UI) and, where a computer was used, the DESK picture and action for replay. Every added field is optional. Since 1.2.0 a step says the TURN it belongs to.

## Fields

| Field | Type | Required | Description | Constraints |
| --- | --- | --- | --- | --- |
| `run_id` | string | yes | The run this step belongs to (agents.run@1's id). | - `minLength 1`<br>- `maxLength 160` |
| `seq` | integer | yes | The run's own order for this entry, starting at 1 and never renumbered. ?after=SEQ reads the entries after it. | - `minimum 1` |
| `kind` | enum | yes | think: a plan, a check or reasoning. tool: a tool call or its result. message: words for the person. approval: the run asked its person. child: a sub-agent run or a hand-off. file: a file the run made. | - `one of "think", "tool", "message", "approval", "child", "file"` |
| `text` | string \| null |  | What a person reads for this step: the plan line, the message, the short tool answer or the file's name. Never a prompt or a secret. | - `maxLength 65536` |
| `tool_call` | object \| null |  | The tool this entry called. On a product that logs a call and its result as two steps, the call names the tool and the result entry has no TOOL\_CALL. |  |
| `result` | string \| object \| array \| number \| boolean \| null |  | What the step answered: a tool's output, a check's verdict, a file's reference. Any JSON; never a credential. |  |
| `cost` | object \| null |  | What this step spent, when the product meters per step. Null when it meters per run only. |  |
| `started_at` | string | yes |  | - `format date-time` |
| `ended_at` | string \| null |  | When the step ended, when the product records an end. A product that logs a point event sets it to the event's time; null when the step is still open. | - `format date-time` |
| `child_run` | string \| null |  | The child run this step started, on KIND child. Null elsewhere. | - `maxLength 160` |
| `from` | enum |  | Who this entry is from. person: the run's person (their message). agent: the agent. system: the product (a notice, a limit reached). tool: a tool's own words. Absent means agent. | - `one of "person", "agent", "system", "tool"` |
| `author` | object |  | Who spoke, when a thread has several people or agents: a name and a picture. Absent for the run's own person and agent. | - `no other fields` |
| `author.id` | string |  |  | - `maxLength 160` |
| `author.name` | string | yes |  | - `minLength 1`<br>- `maxLength 160` |
| `author.avatar_url` | string |  | Their picture. | - `format uri`<br>- `maxLength 2000` |
| `status` | enum |  | Whether the entry is still under way (a tool that runs, a message being written). Absent means done. | - `one of "running", "done", "failed"` |
| `title` | string |  | The entry in a few words, for a tool or a thought: "Searching the web", "Reading 3 files". | - `minLength 1`<br>- `maxLength 200` |
| `glyph` | string |  | A glyph's name in the openvibe-shared glyph registry (openvibe.design/icons). A reader that does not know it draws its default. | - `pattern ^[a-z][a-z0-9-]{0,40}$` |
| `attachments` | array of object |  |  | - `maxItems 20`<br>- `items: no other fields` |
| `attachments[].kind` | enum | yes |  | - `one of "image", "file", "audio", "video"` |
| `attachments[].media_id` | string |  |  | - `minLength 1`<br>- `maxLength 60` |
| `attachments[].url` | string |  | An https address the reader may fetch. | - `format uri`<br>- `maxLength 2000` |
| `attachments[].name` | string |  |  | - `maxLength 300` |
| `attachments[].mime` | string |  |  | - `maxLength 120` |
| `attachments[].bytes` | integer |  |  | - `minimum 0` |
| `attachments[].width` | integer |  |  | - `minimum 1` |
| `attachments[].height` | integer |  |  | - `minimum 1` |
| `attachments[].alt` | string |  | What a picture shows, in words. | - `maxLength 500` |
| `sources` | array of object |  | The sources a message cites, numbered as its text cites them (\[1\], \[2\]). | - `maxItems 40`<br>- `items: no other fields` |
| `sources[].n` | integer | yes |  | - `minimum 1` |
| `sources[].title` | string |  |  | - `maxLength 300` |
| `sources[].url` | string | yes | The source's address. | - `format uri`<br>- `maxLength 2000` |
| `sources[].site` | string |  |  | - `maxLength 120` |
| `ui` | object |  | A rich card for this entry. TYPE names it (link, table, code, diff, image, choice, timer, repo, desk, run, or a product's own, prefixed with its product: live-clip); DATA is that card's own content. A reader that does not know the type shows TEXT instead, so TEXT always says the same thing in words. | - `no other fields` |
| `ui.type` | string | yes |  | - `pattern ^[a-z][a-z0-9-]{0,40}$` |
| `ui.data` | object |  |  |  |
| `desk` | object |  | The computer this step used, for the screen tile and the replay: the desk, the picture it saw and what it did. A secret field is blanked before a still is stored. | - `no other fields` |
| `desk.id` | string | yes |  | - `minLength 1`<br>- `maxLength 160` |
| `desk.still_url` | string |  | The picture the step saw. | - `format uri`<br>- `maxLength 2000` |
| `desk.action` | object |  |  | - `no other fields` |
| `desk.action.type` | string | yes |  | - `pattern ^[a-z][a-z0-9-]{0,40}$` |
| `desk.action.x` | number |  |  |  |
| `desk.action.y` | number |  |  |  |
| `desk.action.text` | string |  |  | - `maxLength 500` |
| `turn` | integer |  | The turn the step belongs to. A person's message opens a turn (turn 0 is what the run started with); every step until their next message is in it. A fork, a hand-off or a regenerated answer starts at a turn boundary (agents.run@1's LINEAGE.AT\_TURN). | - `minimum 0` |

## Examples

From the contract's own test fixtures: valid ones validate, rejected ones must fail.

 Valid: a-persons-message-with-a-picture

~~~~
{
  "run_id": "c_8f3k2m",
  "seq": 1,
  "kind": "message",
  "from": "person",
  "text": "Find me a good deal on a mechanical keyboard like this one.",
  "attachments": [
    {
      "kind": "image",
      "media_id": "med_01K9R2V6M8Q4T0B7C3D5E9F2GH",
      "name": "keyboard.jpg",
      "mime": "image/jpeg",
      "bytes": 182044,
      "width": 1200,
      "height": 800,
      "alt": "A grey 75% mechanical keyboard"
    }
  ],
  "started_at": "2026-11-12T10:00:00Z",
  "ended_at": "2026-11-12T10:00:00Z"
}
~~~~

Valid: a-step-in-turn-two

~~~~
{
  "run_id": "tsk_01K9R2V6M8Q4T0B7C3D5E9F2GH",
  "seq": 7,
  "kind": "tool",
  "from": "tool",
  "status": "done",
  "title": "Typing in the search box",
  "glyph": "search",
  "text": "Typed \"mechanical keyboard 75%\" in the search box.",
  "tool_call": {
    "name": "computer.type",
    "input": {
      "text": "mechanical keyboard 75%"
    }
  },
  "desk": {
    "id": "desk_private_1",
    "still_url": "https://openvibe.media/s/still_01K9R2V6M8Q4T0B7C3D5E9F2GH.jpg",
    "action": {
      "type": "type",
      "x": 512,
      "y": 96,
      "text": "mechanical keyboard 75%"
    }
  },
  "started_at": "2026-11-12T10:03:00Z",
  "ended_at": "2026-11-12T10:03:01Z",
  "turn": 2
}
~~~~

Valid: a-tool-on-a-desk

~~~~
{
  "run_id": "tsk_01K9R2V6M8Q4T0B7C3D5E9F2GH",
  "seq": 7,
  "kind": "tool",
  "from": "tool",
  "status": "done",
  "title": "Typing in the search box",
  "glyph": "search",
  "text": "Typed \"mechanical keyboard 75%\" in the search box.",
  "tool_call": {
    "name": "computer.type",
    "input": {
      "text": "mechanical keyboard 75%"
    }
  },
  "desk": {
    "id": "desk_private_1",
    "still_url": "https://openvibe.media/s/still_01K9R2V6M8Q4T0B7C3D5E9F2GH.jpg",
    "action": {
      "type": "type",
      "x": 512,
      "y": 96,
      "text": "mechanical keyboard 75%"
    }
  },
  "started_at": "2026-11-12T10:03:00Z",
  "ended_at": "2026-11-12T10:03:01Z"
}
~~~~

Valid: an-answer-being-written-with-sources-and-a-card

~~~~
{
  "run_id": "c_8f3k2m",
  "seq": 4,
  "kind": "message",
  "from": "agent",
  "status": "running",
  "text": "Three picks under $90 [1][2]:",
  "sources": [
    {
      "n": 1,
      "title": "Keychron K2 at $74",
      "url": "https://openvibe.deals/d/keychron-k2",
      "site": "openvibe.deals"
    },
    {
      "n": 2,
      "title": "r/MechanicalKeyboards buying guide",
      "url": "https://www.reddit.com/r/MechanicalKeyboards/wiki/buying_guide",
      "site": "reddit.com"
    }
  ],
  "ui": {
    "type": "table",
    "data": {
      "columns": [
        "Keyboard",
        "Price",
        "Switches"
      ],
      "rows": [
        [
          "Keychron K2",
          "$74",
          "Gateron Brown"
        ],
        [
          "Royal Kludge RK84",
          "$59",
          "Red"
        ],
        [
          "Epomaker TH80",
          "$89",
          "Budgerigar"
        ]
      ]
    }
  },
  "started_at": "2026-11-12T10:01:40Z",
  "ended_at": null
}
~~~~

Valid: tool

~~~~
{
  "run_id": "tsk_01K9R2V6M8Q4T0B7C3D5E9F2GH",
  "seq": 4,
  "kind": "tool",
  "text": "Ran the test suite",
  "tool_call": {
    "name": "terminal",
    "input": {
      "command": "npm test",
      "cwd": "/home/person/app"
    }
  },
  "result": "124 checks passed",
  "cost": {
    "usd": 0.002,
    "tokens": 310,
    "seconds": 18
  },
  "started_at": "2026-11-12T10:00:10Z",
  "ended_at": "2026-11-12T10:00:28Z",
  "child_run": null
}
~~~~

Rejected: a-card-without-a-type

~~~~
{
  "run_id": "c_8f3k2m",
  "seq": 2,
  "kind": "message",
  "text": "hi",
  "ui": {
    "data": {}
  },
  "started_at": "2026-11-12T10:00:00Z"
}
~~~~

Rejected: a-turn-before-the-first

~~~~
{
  "run_id": "tsk_01K9R2V6M8Q4T0B7C3D5E9F2GH",
  "seq": 7,
  "kind": "tool",
  "from": "tool",
  "status": "done",
  "title": "Typing in the search box",
  "glyph": "search",
  "text": "Typed \"mechanical keyboard 75%\" in the search box.",
  "tool_call": {
    "name": "computer.type",
    "input": {
      "text": "mechanical keyboard 75%"
    }
  },
  "desk": {
    "id": "desk_private_1",
    "still_url": "https://openvibe.media/s/still_01K9R2V6M8Q4T0B7C3D5E9F2GH.jpg",
    "action": {
      "type": "type",
      "x": 512,
      "y": 96,
      "text": "mechanical keyboard 75%"
    }
  },
  "started_at": "2026-11-12T10:03:00Z",
  "ended_at": "2026-11-12T10:03:01Z",
  "turn": -1
}
~~~~

Rejected: from-nobody-known

~~~~
{
  "run_id": "c_8f3k2m",
  "seq": 2,
  "kind": "message",
  "from": "narrator",
  "text": "hi",
  "started_at": "2026-11-12T10:00:00Z"
}
~~~~

Rejected: unknown-kind

~~~~
{
  "run_id": "tsk_01K9R2V6M8Q4T0B7C3D5E9F2GH",
  "seq": 5,
  "kind": "thought",
  "text": "The test suite is the next thing to run.",
  "started_at": "2026-11-12T10:00:09Z"
}
~~~~

## Validate

~~~~
const contracts = require('openvibe-contracts');
contracts.validate('agents.step@1', value);   // { valid, errors: [{ path, message }] }
~~~~
