ReelHive turns a brief into a narrated video using two Strands graphs and a shared Python service. Agents write the story and plan the scenes; code handles files, measured audio timing, validation, and rendering. The agent harness is the combination of those execution rules, typed artifacts, checkpoints, and approval boundaries. That is the architecture I want to explain, including the places where its guarantees stop.

This walkthrough follows the public repository at commit e3021d3, checked on October 11, 2026. The companion Journal post covers what building it taught me. Here I want the files and the execution path.

Every transition needs a reason, an artifact, and a defined failure path.

One service owns the run

The CLI and the React studio use the same core service. The browser reaches it through the local server and workspace layer; the CLI calls it directly. Keeping orchestration in core/service.py means script approval, scene editing, regeneration, and resume have a common implementation. The UI doesn't get its own interpretation of what a completed run means.

ReelHive: entry points, graphs, saved artifacts, and rendering
CLI and React studio feed a shared core service. The draft graph validates the brief, researches, and writes a script. Production runs scenes then visuals alongside narration and music, joins before timing, checks quality, and either renders or attempts one repair and recheck. Failed rechecks stop with a report. Run artifacts and events are saved on disk.

Adapted from our original architecture sketch, with the join and repair paths corrected to match this implementation. Open the image for the full-size view.

The service creates a run directory, supplies dependencies, drives the graphs, and records status. An event stream connects the work to CLI progress, the studio, and run.log.jsonl. That separation matters when a run pauses: closing the browser does not turn a saved script into an untraceable chat response.

This is a local tool. Rendering, audio, and run artifacts live on the machine. Agent text still goes to the configured model provider unless that provider is local, and optional image generation has its own remote call. "Local" describes the workspace; it is not a promise that every operation is offline. The repository's privacy notes describe those boundaries.

Two graphs separate drafting from production

The draft graph runs brief, research, then script. Validation normalizes the input; research gathers product context when a source is available; the writer turns the notes into narration beats. Its useful output is script.json, which a person can review before downstream work starts.

Production begins with three branches. Scene planning leads to visual resolution, while narration and music selection can proceed alongside it. Timing joins those results, then the critic chooses between rendering and a repair attempt. The production graph definition is short enough to read without a framework tour.

The customization level controls approval. Low continues automatically. Medium stops after the draft for script approval. High also splits production into prepare and finish, with scene and image approval between them. Preparation includes timing; finishing starts at the critic. These are service-managed boundaries between graph invocations, so a human review doesn't depend on keeping the original process alive.

This is our application design, not a claim that Strands lacks interruption support. Strands also documents multi-agent interrupts. We use saved artifacts and explicit service commands because the editor needs to load, change, and approve those artifacts independently.

Typed state connects agents and deterministic nodes

Every ReelHive node uses the same MultiAgentBase contract through FunctionNode and AgentNode. A function node runs blocking work through asyncio.to_thread. An agent node builds a prompt, requests Pydantic-validated structured output, and applies it to the run. Both emit node events and checkpoint completion. A renderer can be a graph node without pretending to reason.

We pass a RunContext through invocation_state. Nodes use typed fields such as script, spec, narrated, and music, rather than extracting application state from the previous node's prose. During parallel preparation, the branches have distinct responsibilities; after the join, timing combines their results. That ownership is part of the design, not something a shared dictionary enforces for you.

Model output still needs boundaries. AgentNode passes structured_output_model to Strands and makes one extra repair call if validation still raises. Template text limits are schema constraints. The writer gets a chance to produce shorter copy; we don't silently chop the end off a sentence.

Provider configuration assigns strong and fast model tiers, with per-node overrides. The Copilot adapter is a separate SDK path, not a Strands model. It exposes a structured submit tool and rejects shell or file-write tool requests. The output contract stays the same even though the invocation mechanism differs.

Parallel branches need an explicit join in Python

In the Strands Python graph, a satisfied incoming edge can make a destination ready. For our timing node, readiness means visuals, narration, and music have all completed. We express that requirement on every incoming edge. Here is a runnable inspection of the real graph after installing the repository's dependencies:

uv run python - <<'PY'
from reelhive.graphs.production import build_production_graph

graph = build_production_graph()
print(graph)
PY

The condition in that module is small. This self-contained check uses the same completion rule and shows why the visual branch must finish too:

from types import SimpleNamespace

BRANCHES = {"visuals", "narrate", "music"}

def all_branches_done(state):
    done = {node.node_id for node in state.completed_nodes}
    return BRANCHES <= done

state = SimpleNamespace(completed_nodes=[
    SimpleNamespace(node_id="narrate"),
    SimpleNamespace(node_id="music"),
])
assert not all_branches_done(state)
state.completed_nodes.append(SimpleNamespace(node_id="visuals"))
assert all_branches_done(state)

The builder attaches that condition to visuals -> timing, narrate -> timing, and music -> timing. The Strands join documentation explains the Python-specific default. Check the SDK you are using before copying this assumption to another implementation.

Conditional routing also lets one render node serve both successful paths: the critic passes directly, or the repair's recheck passes. If the recheck fails, no render edge is satisfied. The graph can finish scheduling without producing a video; the service must distinguish that from a successful product outcome.

Hard checks come before judgment, with one repair pass

critic.py checks duration against a 5% tolerance, narration pace, template limits, matching audio, image paths and resolution, and whether the narration fits each scene. It also checks coverage indices and rejects generated imagery presented as product UI. At high customization, images need approval. These checks run before the critic spends model tokens.

When they pass, the editorial critic reviews the hook, clarity, audience fit, storyline, and call to action. Its verdict must pass and every rubric score must be at least three out of five. These are project rules, not evidence of independently measured video quality. Coverage indices, in particular, record what scenes claim to cover; they do not prove semantic coverage.

Failure goes to fix, then recheck. Recheck regenerates changed voice recordings, resolves visuals, recalculates timing, and reruns hard checks. There is no cycle back to fix. The builder also caps node executions, but the acyclic shape is what makes the single repair pass easy to see.

Here is the limitation I would keep visible in any review: recheck does not rerun the editorial critic. A repair can satisfy structural checks without proving it fixed the original editorial complaint. A useful next improvement would be a bounded editorial re-review. Adding an unlimited fix loop would not resolve that missing evidence.

The scene specification is the rendering contract

Agents produce data that an existing renderer understands. The contract is SceneSpec, exported from Pydantic to the Node renderer's JSON schema. The renderer reads template limits from that schema, and a contract test detects drift. Agents do not write a fresh animation program for every brief.

Kokoro supplies narration with measured durations. Timing uses those measurements to lay out the scenes. Revideo renders silent frames, then ffmpeg mixes narration and music, ducks the music under speech, and muxes the result. When narration changes, keeping the old timing would break that chain; refreshing dependent artifacts is part of applying an edit.

Visual resolution follows the same principle of constrained choices. A product-UI scene can use a supplied image or a captured screenshot. Concept scenes can use generated illustrations. Missing sources can fall back to text. The resolver makes those decisions executable. A prompt saying "don't invent the product" is backed by an asset rule and a later check.

Checkpoints and evals answer different questions

A run folder contains the normalized brief, provider configuration, script, scene specification, audio, visuals, status, checkpoint, and event log. The checkpoint stores completed nodes and the state needed to restore the run. Node wrappers skip completed work when it is restored. Temporary files followed by replacement reduce the chance of exposing a partly written checkpoint.

That is recovery at node boundaries, not exactly-once execution. If a process dies after a remote call but before its completion is checkpointed, that work can repeat. Saved outputs make inspection and resumption possible; they do not guarantee that a new model call would produce identical output.

The eval harness asks a different question: how do scripts and specifications compare across providers or prompt changes? It runs the graphs in spec-only mode, substitutes timed silent audio, and avoids rendering and paid image generation. That keeps the evaluation focused, but it cannot judge the actual voice performance or final video frames.

To inspect the project yourself, start from the repository checkout. The setup installs the Python and Node dependencies, including the renderer; choose and configure your model provider before running production or evals:

git clone https://github.com/d2k-klin/reelhive.git
cd reelhive
make setup
uv run reelhive doctor
uv run reelhive run examples/briefs/medium.yaml
# Review script.json in the run directory printed above.
uv run reelhive approve runs/YOUR_RUN_DIRECTORY

# Uses the configured provider and requires its credentials.
uv run reelhive eval --providers claude --set core

I would start an adaptation by defining its typed output, irreversible actions, and stop conditions. Then draw the dependencies that remain. Keep the graph where it exposes real parallel work and decisions, and make the rejected run as inspectable as the successful one. That is the part of this harness I would copy first.