AgentsStep agents.step@1
Generated at from
openvibe-contracts v0.419.0 and
openvibe-sdk v0.161.0.
- Version
- 1.2.0
- Owner
- actor
- Visibility
- public
- Status
- active
- Compatibility
- backward
- Decision
- ADR-044
- Schema
https://openvibe.network/contracts/agents/step.v1.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). |
|
seq | integer | yes | The run's own order for this entry, starting at 1 and never renumbered. ?after=SEQ reads the entries after it. |
|
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. |
|
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. |
| |
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 |
| |
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. |
| |
child_run | string | null | The child run this step started, on KIND child. Null elsewhere. |
| |
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. |
| |
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. |
| |
author.id | string |
| ||
author.name | string | yes |
| |
author.avatar_url | string | Their picture. |
| |
status | enum | Whether the entry is still under way (a tool that runs, a message being written). Absent means done. |
| |
title | string | The entry in a few words, for a tool or a thought: "Searching the web", "Reading 3 files". |
| |
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. |
| |
attachments | array of object |
| ||
attachments[].kind | enum | yes |
| |
attachments[].media_id | string |
| ||
attachments[].url | string | An https address the reader may fetch. |
| |
attachments[].name | string |
| ||
attachments[].mime | string |
| ||
attachments[].bytes | integer |
| ||
attachments[].width | integer |
| ||
attachments[].height | integer |
| ||
attachments[].alt | string | What a picture shows, in words. |
| |
sources | array of object | The sources a message cites, numbered as its text cites them ([1], [2]). |
| |
sources[].n | integer | yes |
| |
sources[].title | string |
| ||
sources[].url | string | yes | The source's address. |
|
sources[].site | string |
| ||
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. |
| |
ui.type | string | yes |
| |
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. |
| |
desk.id | string | yes |
| |
desk.still_url | string | The picture the step saw. |
| |
desk.action | object |
| ||
desk.action.type | string | yes |
| |
desk.action.x | number | |||
desk.action.y | number | |||
desk.action.text | string |
| ||
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). |
|
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 }] }