Steps

A Step is the atomic unit in an opentine run graph. It is a frozen dataclass with immutable identity inputs and recorded runtime metadata.

Step Fields

The current Step dataclass is frozen. The artifact stores parent_ids; parent_id exists as a compatibility property that returns the last parent, or None for root steps.

  • id — full 64-character SHA-256 digest.
  • parent_ids — zero, one, or many parent step IDs.
  • kindthink, tool, model, done, or error.
  • inputs and outputs — structured payloads for the step.
  • model_info and tool_info — model/tool metadata included in the step identity.
  • error — structured error payload included in the step identity.
  • timestamp, duration, and cost — recorded runtime data, outside the identity hash.
step_fields.py
1from opentine import Step, StepKind
2
3step = Step(
4    id="f3a7c9e12b04..." * 4,
5    parent_ids=[],
6    kind=StepKind.think,
7    inputs={"text": "I should search for recent papers."},
8    outputs={},
9    model_info="claude-sonnet-4-20250514",
10    tool_info={},
11    error={},
12    timestamp=1717200000.0,
13    duration=0.0,
14    cost=0.0,
15)

Step Kinds

step_kinds.py
from opentine import StepKind

StepKind.think  # Agent reasoning or intermediate text
StepKind.tool   # Tool invocation
StepKind.model  # Explicit model/harness entry step
StepKind.done   # Successful terminal or model text step
StepKind.error  # Failure step

Content Addressing

step_id() hashes a canonical immutable payload containing the step kind, parent_ids, inputs, outputs, model/tool metadata, and error payload. Timestamps, duration, and cost are recorded but do not change the ID.

content_addressing.py
1from opentine import StepKind, step_id
2
3sid = step_id(
4    kind=StepKind.tool,
5    parent_ids=["a1b2c3d4e5f6..."],
6    inputs={"name": "search", "arguments": {"query": "RLHF papers"}},
7    outputs={"result": "Top results..."},
8    model_info="claude-sonnet-4-20250514",
9    tool_info={"name": "search"},
10    error={},
11)
12
13assert len(sid) == 64

The display helpers shorten IDs to 12 characters, but the persisted graph uses full SHA-256 digests as keys.

Creating Steps

Application code usually calls Run.add_step(). It acceptsparent_ids for graph ancestry and also accepts a legacy parent_id argument for simple chains.

add_step.py
1step = run.add_step(
2    kind=StepKind.tool,
3    inputs={"name": "fetch", "arguments": {"url": "https://example.com"}},
4    outputs={"result": "Example Domain..."},
5    parent_ids=[parent_step.id],
6    tool_info={"name": "fetch"},
7    duration=0.8,
8)
9
10print(step.id)         # full 64-character SHA-256 digest
11print(step.short_id)   # first 12 characters for display
12print(step.parent_ids) # graph ancestry list
13print(step.parent_id)  # compatibility accessor for the last parent

Immutability

Steps are frozen so their identity cannot drift after insertion into the content-addressed graph. To correct or retry a decision, add a new child or fork the run from a known-good step.

immutability.py
1step = run.add_step(kind=StepKind.think, inputs={"text": "hello"})
2
3step.outputs = {"new": "data"}
4# dataclasses.FrozenInstanceError: cannot assign to field 'outputs'
5
6corrected = run.add_step(
7    kind=StepKind.think,
8    inputs={"text": "Actually, let me reconsider."},
9    parent_ids=[step.id],
10)