Skip to content

agents: creative-pipeline eval — one commission across brief, sketch, storyboard, cut and review - #4620

Merged
georgi merged 5 commits into
mainfrom
eval/creative-pipeline
Aug 1, 2026
Merged

agents: creative-pipeline eval — one commission across brief, sketch, storyboard, cut and review#4620
georgi merged 5 commits into
mainfrom
eval/creative-pipeline

Conversation

@georgi

@georgi georgi commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Ninth tool-loop suite. The other eight each score one surface; this one scores the seams — one commission carried through brief → ideation → sketch → storyboard → cut → review. That's where a model that passes sketch-tools and storyboard-tools individually still loses the client's aspect ratio between them.

IS_SANDBOX=1 npm run dev:nodetool -- eval creative-pipeline \
  -p claude_agent_sdk -m sonnet --max-iterations 220 --no-find-model

What one run produces

Brief: vertical social spot for a cold brew — 9:16, under 12s, must feature hands and sunrise, no logo.

sonnet read the brief, proposed three concepts, committed to one, built a style frame, wrote and rendered four shots, assembled at 16.20s, measured, trimmed and ripple-moved to 11.80s, then signed off with four review notes. 162 tool calls.

Clips: 1 · 2 · 3 · 4 — media checked in at docs/evals/creative-pipeline/, 1.3 MB compressed from 15 MB.

Design

Composed, not reimplemented. The bridge builds the real sketch, storyboard and timeline bridges and merges their tool arrays, so the model drives the same contract those suites already cover and this file cannot drift from them. Only ui_storyboard_assemble_timeline is replaced — its own version returns a sequence id and touches nothing, and the handoff is the thing under test.

ui_brief_* / ui_review_* are eval instrumentation, not a frontend contract. Nothing in web/ implements them. A brief passed only in the prompt cannot be told apart from one the model ignored.

Planted defect. Rendered clips come back 1.35× longer than requested, like a video model that only emits fixed-length takes. Trimming the last clip fixes the runtime; trimming an earlier one only opens a gap, since later clips keep their start times.

--live routes the same tool calls to fal for real media. Headless stays default, artifacts default to empty, no predicate reads them — a case asserting on artifacts would fail every CI run. Media is the cheap half: ~$0.17 for nine artifacts against ~$2.60 for the agent loop.

Three checks rewritten after live runs — all the same mistake

Each encoded one valid working order and failed models that used another.

  • Severity was a three-value enum that threw on "critical", failing a run on this harness's vocabulary. Synonyms now map.
  • Overrun detection grepped note prose for runtime/duration/length, and scored a run that found the overrun and fixed it as a miss on wording. Now reads the severity the model assigned.
  • reviewActedOn counted edits after the first note, requiring report-before-fix. A run assembled at 16.20s, trimmed to 12.00s, verified, then filed notes as a sign-off — a complete loop scored as "review changed nothing". Now cutRevisedAfterAssembly, which accepts either order. It also had no true positive withinBriefRuntime didn't already catch.

Tool-call ceilings were guesses too: 90 failed a case that did the job in 97 calls. Now 130/150, from measurement.

Two things the live run showed that the eval can't

The planted defect is conservative. LTX returned 4.84s takes for 3s requests — 1.61×, not the 1.35× simulated.

A per-shot brief violation the predicates miss. clip-shot_3 has neither hands nor sunrise. The model caught it in its own review; mustFeature only asks whether each element appears somewhere, so it passed.

Verification

  • 12 harness tests, scripted provider, no network — including the two failures the suite exists to catch
  • Full agents suite 1792 passed; packages/agents + packages/cli typecheck clean; lint clean
  • Live: full-pipeline 1.00 (93 calls), review-catches-overrun 1.00 (25), brief-constraints-hold 0.91 (97)
  • Replaying a recorded 93-call transcript through the corrected predicates scores 1.000

Known gap

In --live mode the timeline still lays clips at the simulated overshoot, so the scored runtime is not the runtime of the files on disk. Closing it needs the backend to report delivered duration. Documented in the source and in packages/agents/CLAUDE.md.

🤖 Generated with Claude Code

georgi and others added 4 commits July 31, 2026 13:10
The eight existing tool-loop suites each score one surface. A model can pass
sketch-tools and storyboard-tools outright and still lose the client's aspect
ratio between them, or deliver a cut that runs half again as long as it was
commissioned at. This suite scores the seams: one commission carried through
brief, ideation, sketch, storyboard, cut and review.

The three creative surfaces are composed, not reimplemented. The bridge builds
the real sketch, storyboard and timeline bridges and merges their tool arrays,
so the model drives the same contract those suites already cover and this file
cannot drift from them. Only ui_storyboard_assemble_timeline is replaced: the
storyboard bridge's version returns a sequence id and touches nothing, which is
fine when the board alone is under test and useless here, because the handoff
is the thing being measured. The replacement drives the timeline bridge's own
tools, so a later trim acts on the storyboard's output.

ui_brief_* and ui_review_* are eval instrumentation and nothing in web/
implements them. They exist because a brief passed only in the prompt cannot be
told apart from one the model ignored.

Rendered clips come back 1.35x the requested length, the way a video model that
emits fixed-length takes does, so a cut planned to exactly fill the brief
overruns. Trimming the last clip fixes it; trimming an earlier one only opens a
gap and leaves the runtime unchanged, since the clips after it keep their start
times. A model that re-measures sees that, and one that trims and declares
victory does not.

Two checks were rewritten after a live run against claude_agent_sdk/sonnet
rather than kept and explained away. The review severity was a three-value enum
that threw on "critical" — failing a run on this harness's vocabulary rather
than on anything about the cut — so synonyms now map. And overrun detection
grepped the note prose for runtime/duration/length, which scored a run that
found the overrun, trimmed four clips and ripple-moved three to close the gaps
as a miss, purely on wording; it now grades the severity the model assigned,
which survives paraphrase. Tool-call ceilings came from the same run: 90 failed
a case that did the job in 97 calls, so they are 130 and 150.

Measured: review-catches-overrun scores 1.00 in ~25 calls, brief-constraints-
hold 0.91 in 97. The full six-phase case spent 138 calls without converging at
--max-iterations 140 and needs a higher cap; the harness test proves the case
is satisfiable in 18 calls when driven optimally, so that is a finding about
efficiency, not an unsatisfiable case.

Harness tests use a scripted provider and no network. Alongside the passing
run they assert the two failures the suite exists to catch: notes filed with
nothing fixed afterwards, and a forbidden element reaching the board.

Co-Authored-By: Claude <noreply@anthropic.com>
A live sonnet run assembled the cut at 16.20s, trimmed and ripple-moved it to
exactly 12.00s, verified with ui_review_get_cut, then filed four notes as a
compliance sign-off. The suite scored that as "review changed nothing".

reviewActedOn counted timeline edits after the first review note, so it
required report-then-fix and failed fix-then-verify-then-report. It also had no
true positive that withinBriefRuntime did not already catch: a cut that
overruns fails the runtime check whatever order the model worked in. So the
check only ever fired on correct work done differently.

It is now cutRevisedAfterAssembly, keyed on the handoff rather than on the
report. Both orders pass; shipping the renderer's output unrevised still fails.

That is the third check in this suite rewritten for the same reason — encoding
one valid process shape and penalising the others. The first two were a
severity enum that threw on "critical" and an overrun check that grepped the
note prose. Replaying the recorded 93-call transcript through the corrected
predicates scores 1.000 with no failed checks.

Also adds scripts/dump-creative-run.ts, which writes what the model actually
made — concepts, style-frame prompt, shot list, assembled cut with timings,
review notes, phase snapshots, full transcript — to nodetool-debug/. The eval
report carries pass/fail and call counts, which cannot show the work.

full-pipeline is now verified live: 1.00 in 93 calls at --max-iterations 220.
The earlier 138-call non-convergence was the SDK turn cap, not the case.

Co-Authored-By: Claude <noreply@anthropic.com>
The suite fakes every generate and render, which is what makes it cost the
agent loop and nothing else. That also means it never proves the pipeline can
produce anything. --live wires a MediaBackend so the same tool calls
additionally hit fal, and the run leaves real stills and clips on disk without
changing a tool contract or a predicate.

MediaBackend is an interface in the bridge and the fal wiring lives in the
dump script: packages/agents has no fal dependency and should not grow one for
an opt-in path. Headless stays the default, artifacts default to empty, and no
predicate reads them — a case asserting on artifacts would fail every CI run.

Media is the cheap half. flux/schnell bills $0.003 per megapixel and
ltx-2-19b/distilled $0.0008, so nine artifacts cost about $0.17 against ~$2.60
for the loop that directs them. Verified end to end: a style frame, four
keyframes and four clips, cut delivered at 11.80s inside a 12s brief.

Two things the live run exposed, both documented rather than hidden. LTX
returned 4.84s takes for 3s requests — a 1.61x overshoot against the 1.35x this
eval simulates, so the planted defect is conservative rather than unfair. And
the timeline still lays clips at the simulated overshoot, so the scored runtime
is not the runtime of the files on disk; closing that needs the backend to
report delivered duration.

A generation failure is recorded on the artifact and never thrown. The model is
scored on directing the pipeline, and failing its run because fal rate-limited
would measure the weather.

Also fixes two stale editsAfterReview references in the report writer left by
the rename to editsAfterAssembly.

Co-Authored-By: Claude <noreply@anthropic.com>
The suite is headless, so nobody can see what it produces without paying for a
run. This is one --live run's output: a style frame, four keyframes and four
clips that claude_agent_sdk/sonnet directed from a 9:16 / under-12s / hands and
sunrise / no logo brief, delivered at 11.80s across 162 tool calls.

Compressed for the repo — stills to WebP at 720px, clips to h264 at 540px.
15 MB of raw output became 1.3 MB, which is the difference between a fixture
and a liability. Precedent for binaries this size already exists in
marketing/public; the originals are not kept.

The README records two things the eval itself cannot report. The planted
1.35x render overshoot is conservative: LTX returned 4.84s takes for 3s
requests, 1.61x. And clip-shot_3 has neither hands nor sunrise, a per-shot
brief violation the model caught in its own review while the suite's
mustFeature check passed it, because that check only asks whether each element
appears somewhere in the shot list.

Co-Authored-By: Claude <noreply@anthropic.com>
@georgi
georgi force-pushed the eval/creative-pipeline branch from 1db5b0c to 0256488 Compare July 31, 2026 13:13
flux/schnell was chosen on cost — $0.003 per megapixel — and it was the wrong
trade. It mangles hands, and this brief requires them in three of four shots. A
model that cannot draw the thing being commissioned is not cheap. Stills now
come from openai/gpt-image-2; clips stay on LTX-distilled, which was never the
problem. Both are overridable with CREATIVE_IMAGE_MODEL / CREATIVE_VIDEO_MODEL.

Regenerated from the prompts recorded in the run dump rather than by re-running
the agent, so this cost image and video calls and not another loop.

The swap introduced a different brief violation, which is the more interesting
half. flux could not render legible text, so it never breached the brief's
no-logo rule by accident. gpt-image-2 can, and branded the bottle in shot 3
with raised COLD BREW COFFEE lettering. Better capability, new failure mode.

The prompt is why: sonnet never carried no-logo into any image prompt. It read
the brief and the constraint did not survive into the generation call. Shot 3
was regenerated with it appended.

Neither violation is visible to the suite. forbiddenAvoided reads shot action
text and layer names, so it can see the word logo in a prompt and never what
the picture contains; mustFeature asks only whether each element appears
somewhere in the shot list, so a shot with neither hands nor sunrise passes.
The predicates grade the plan. Grading the artifact needs a human or a vision
model, and that is now written down in both the fixture README and the package
docs rather than left for someone to discover.

Co-Authored-By: Claude <noreply@anthropic.com>
@georgi
georgi merged commit 254ff1e into main Aug 1, 2026
23 checks passed
@georgi
georgi deleted the eval/creative-pipeline branch August 1, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant