brickwright 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brickwright/__init__.py +5 -0
- brickwright/__main__.py +6 -0
- brickwright/_build_template.py +25 -0
- brickwright/cli.py +446 -0
- brickwright/config.py +104 -0
- brickwright/driver/__init__.py +7 -0
- brickwright/driver/completion.py +89 -0
- brickwright/driver/critique.py +133 -0
- brickwright/driver/external_critique.py +114 -0
- brickwright/driver/gallery.py +289 -0
- brickwright/driver/loop.py +377 -0
- brickwright/driver/messages.py +168 -0
- brickwright/driver/narration.py +320 -0
- brickwright/driver/report.py +52 -0
- brickwright/driver/tracking.py +217 -0
- brickwright/driver/transcript.py +99 -0
- brickwright/driver/viewpoint.py +242 -0
- brickwright/prompts/__init__.py +3 -0
- brickwright/prompts/compilation.py +110 -0
- brickwright/prompts/dispatch_guidance.md +6 -0
- brickwright/prompts/general_instructions.md +68 -0
- brickwright/prompts/internal_agent_instructions.md +29 -0
- brickwright/prompts/internal_subagent_instructions.md +8 -0
- brickwright/prompts/render_guidance.md +4 -0
- brickwright/py.typed +0 -0
- brickwright/record.py +620 -0
- brickwright/tools/__init__.py +69 -0
- brickwright/tools/build_execution.py +113 -0
- brickwright/tools/connectivity_diagnostics.py +118 -0
- brickwright/tools/editing.py +314 -0
- brickwright/tools/exploration.py +85 -0
- brickwright/tools/finalization.py +74 -0
- brickwright/tools/registry.py +241 -0
- brickwright/tools/rendering.py +249 -0
- brickwright/tools/schemas.py +203 -0
- brickwright/tools/software_rendering.py +780 -0
- brickwright/tools/subagents.py +109 -0
- brickwright/tools/testing.py +128 -0
- brickwright/tools/validation.py +172 -0
- brickwright-0.1.0.dist-info/METADATA +89 -0
- brickwright-0.1.0.dist-info/RECORD +44 -0
- brickwright-0.1.0.dist-info/WHEEL +4 -0
- brickwright-0.1.0.dist-info/entry_points.txt +3 -0
- brickwright-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""The visual-critique step: an independent model judges the build against the target.
|
|
2
|
+
|
|
3
|
+
A separate agent first picks a camera angle whose render matches the reference's
|
|
4
|
+
viewpoint (see :mod:`.viewpoint`); a single call then sees the reference beside that
|
|
5
|
+
matched render and reports whether they match and what differs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
14
|
+
|
|
15
|
+
from ..config import Config
|
|
16
|
+
from ..record import Record
|
|
17
|
+
from .completion import bad_request_error, litellm_complete
|
|
18
|
+
from .messages import image_content_block, request_kwargs
|
|
19
|
+
from .viewpoint import match_viewpoint
|
|
20
|
+
|
|
21
|
+
VIEWPOINT_COMPARE_PROMPT = """\
|
|
22
|
+
You are an independent reviewer judging how faithfully a LEGO build reproduces a
|
|
23
|
+
target object. You are given:
|
|
24
|
+
|
|
25
|
+
1. The target reference image.
|
|
26
|
+
2. A single rendered image of the LEGO build, captured from a camera viewpoint chosen
|
|
27
|
+
to match the reference's viewpoint.
|
|
28
|
+
3. The original text prompt that requested the build.
|
|
29
|
+
|
|
30
|
+
First describe both objects in detail, then enumerate the concrete differences between
|
|
31
|
+
them. Judge only the main object that the prompt refers to -- ignore lighting, shadows,
|
|
32
|
+
background, camera angle, photographic style, and rendering artifacts. Focus on shape,
|
|
33
|
+
proportions, structure, parts and coloring, and any missing or extra features.
|
|
34
|
+
|
|
35
|
+
Reply with a single JSON object with exactly these keys:
|
|
36
|
+
- "differences_description": a string with your detailed descriptions of both objects
|
|
37
|
+
followed by the differences between them.
|
|
38
|
+
- "done": a boolean -- true if the build faithfully matches the reference (only minor
|
|
39
|
+
or cosmetic differences remain), false if it should be revised further.
|
|
40
|
+
|
|
41
|
+
Reply with only the JSON object, no prose around it."""
|
|
42
|
+
|
|
43
|
+
# What the critic is shown as the original prompt when the build had none.
|
|
44
|
+
NO_TEXT_PROMPT = "(no text prompt)"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CritiqueResult(BaseModel):
|
|
48
|
+
"""An independent reviewer's verdict comparing the build to the target image."""
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(frozen=True)
|
|
51
|
+
|
|
52
|
+
differences_description: str
|
|
53
|
+
done: bool
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def critique(
|
|
57
|
+
record: Record,
|
|
58
|
+
config: Config,
|
|
59
|
+
*,
|
|
60
|
+
complete: Callable[..., Any] | None = None,
|
|
61
|
+
on_response: Callable[[Any], None] | None = None,
|
|
62
|
+
) -> CritiqueResult | None:
|
|
63
|
+
"""Compare the build to the prompt image and return the verdict.
|
|
64
|
+
|
|
65
|
+
The build is shown from a camera angle matched to the reference's viewpoint.
|
|
66
|
+
The result is ``None`` when no comparison could be made: no matched render, or a
|
|
67
|
+
reply that won't parse into a verdict. Raises ``ValueError`` without a prompt
|
|
68
|
+
image.
|
|
69
|
+
|
|
70
|
+
``on_response`` observes every raw completion response immediately (the
|
|
71
|
+
viewpoint agent's and the critic's), so the caller can fold their usage and cost
|
|
72
|
+
into the run's tracker; it may raise to stop before another paid request.
|
|
73
|
+
"""
|
|
74
|
+
reference = record.prompt_image_path
|
|
75
|
+
if reference is None:
|
|
76
|
+
raise ValueError("critique requires a prompt image")
|
|
77
|
+
complete = complete or litellm_complete()
|
|
78
|
+
prompt = record.read_meta().prompt or NO_TEXT_PROMPT
|
|
79
|
+
|
|
80
|
+
result = match_viewpoint(record, config, complete=complete, on_response=on_response)
|
|
81
|
+
if result is None:
|
|
82
|
+
return None # no matched render to compare against
|
|
83
|
+
|
|
84
|
+
content: list[dict[str, Any]] = [
|
|
85
|
+
{"type": "text", "text": f"Original prompt:\n{prompt}"},
|
|
86
|
+
{"type": "text", "text": "Target reference image:"},
|
|
87
|
+
image_content_block(reference),
|
|
88
|
+
{
|
|
89
|
+
"type": "text",
|
|
90
|
+
"text": (
|
|
91
|
+
"The LEGO build, rendered from a viewpoint chosen to match the "
|
|
92
|
+
f"reference (azimuth {result.azimuth:g}, elevation {result.elevation:g}):"
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
image_content_block(record.viewpoint_path),
|
|
96
|
+
]
|
|
97
|
+
messages = [
|
|
98
|
+
{"role": "system", "content": VIEWPOINT_COMPARE_PROMPT},
|
|
99
|
+
{"role": "user", "content": content},
|
|
100
|
+
]
|
|
101
|
+
response = _complete(complete, config, messages)
|
|
102
|
+
if on_response is not None:
|
|
103
|
+
on_response(response)
|
|
104
|
+
try:
|
|
105
|
+
return CritiqueResult.model_validate_json(_text_of(response))
|
|
106
|
+
except ValidationError:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_RESPONSE_FORMATS = (CritiqueResult, {"type": "json_object"})
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _complete(
|
|
114
|
+
complete: Callable[..., Any], config: Config, messages: list[dict[str, Any]]
|
|
115
|
+
) -> Any:
|
|
116
|
+
"""Run one critic completion, weakening response_format until the provider takes it."""
|
|
117
|
+
kwargs = request_kwargs(config, "critique")
|
|
118
|
+
|
|
119
|
+
def ask(**asked: Any) -> Any:
|
|
120
|
+
return complete(messages=messages, **asked, **kwargs)
|
|
121
|
+
|
|
122
|
+
for response_format in _RESPONSE_FORMATS:
|
|
123
|
+
try:
|
|
124
|
+
return ask(response_format=response_format)
|
|
125
|
+
except bad_request_error() as exc:
|
|
126
|
+
if "response_format" not in str(exc):
|
|
127
|
+
raise
|
|
128
|
+
return ask()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _text_of(response: Any) -> str:
|
|
132
|
+
"""The text content of a completion response (``""`` if it has none)."""
|
|
133
|
+
return response.choices[0].message.content or ""
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""The playbook ``brickwright external critique-instructions`` prints.
|
|
2
|
+
|
|
3
|
+
It has the external agent dispatch two fresh subagents -- one to match the viewpoint
|
|
4
|
+
(rendering through ``brickwright external render-angle``), one to critique -- each
|
|
5
|
+
handed the driver's own system prompt.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ..record import Record
|
|
11
|
+
from .critique import NO_TEXT_PROMPT, VIEWPOINT_COMPARE_PROMPT
|
|
12
|
+
from .viewpoint import ANGLE_CONVENTIONS, ORIENTATION_HINT, VIEWPOINT_TASK
|
|
13
|
+
|
|
14
|
+
DEFAULT_CRITIQUE_ROUNDS = 2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def external_viewpoint_prompt(record_id: str, viewpoint_path: str) -> str:
|
|
18
|
+
"""The angle-finding subagent's system prompt: the driver's
|
|
19
|
+
:data:`~driver.viewpoint.VIEWPOINT_TASK`, rendering through
|
|
20
|
+
``brickwright external render-angle``."""
|
|
21
|
+
return f"""\
|
|
22
|
+
{VIEWPOINT_TASK}
|
|
23
|
+
|
|
24
|
+
Render the build from a camera angle by running this command from the repository root:
|
|
25
|
+
|
|
26
|
+
uv run brickwright external render-angle {record_id} --azimuth <A> --elevation <E>
|
|
27
|
+
|
|
28
|
+
{ANGLE_CONVENTIONS}
|
|
29
|
+
|
|
30
|
+
The command renders the build, writes the image to
|
|
31
|
+
{viewpoint_path}
|
|
32
|
+
(overwriting any previous one), and prints that path. Open the PNG, compare its viewpoint
|
|
33
|
+
to the reference, and re-run the command with adjusted angles to get closer.
|
|
34
|
+
{ORIENTATION_HINT} Use as few renders as you need. When the render is seen from essentially the same viewpoint as
|
|
35
|
+
the reference, stop -- your final render must be that matched view, so it is left at the
|
|
36
|
+
path above for the critic to compare against. Then reply with a plain-text message
|
|
37
|
+
reporting the matched azimuth and elevation and confirming the render at that path."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def external_critique_instructions(
|
|
41
|
+
record: Record, *, rounds: int = DEFAULT_CRITIQUE_ROUNDS
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Assemble the record-specific critique playbook for the external agent.
|
|
44
|
+
|
|
45
|
+
Raises ``ValueError`` when the record has no prompt image, since there is nothing to
|
|
46
|
+
critique the build against -- the external mirror of the driver skipping the loop.
|
|
47
|
+
"""
|
|
48
|
+
reference = record.prompt_image_path
|
|
49
|
+
if reference is None:
|
|
50
|
+
raise ValueError("external critique requires a prompt image")
|
|
51
|
+
|
|
52
|
+
record_id = record.id
|
|
53
|
+
reference_path = str(reference.absolute())
|
|
54
|
+
build_path = str(record.build_path.absolute())
|
|
55
|
+
viewpoint_path = str(record.viewpoint_path.absolute())
|
|
56
|
+
prompt = record.read_meta().prompt or NO_TEXT_PROMPT
|
|
57
|
+
viewpoint_prompt = external_viewpoint_prompt(record_id, viewpoint_path)
|
|
58
|
+
# Braces in the JSON schema example are doubled so the f-string emits them literally.
|
|
59
|
+
return f"""\
|
|
60
|
+
# BrickWright visual critique (image prompt)
|
|
61
|
+
|
|
62
|
+
This is the external-agent counterpart of the driver's `viewpoint` visual-critique loop.
|
|
63
|
+
Run it for image-prompt builds once the build validates and renders acceptably, to check
|
|
64
|
+
the build against the reference image and revise it before finalizing.
|
|
65
|
+
|
|
66
|
+
The loop has two steps, and each is run by a **fresh subagent** -- a new agent started
|
|
67
|
+
with no prior context, so it judges independently, exactly as the driver runs each step as
|
|
68
|
+
an independent model call. Dispatch the subagents yourself, and give each one the system
|
|
69
|
+
prompt quoted below **verbatim**. Do not do these steps in your own context.
|
|
70
|
+
|
|
71
|
+
- Record: {record_id}
|
|
72
|
+
- Reference image: {reference_path}
|
|
73
|
+
- build.py: {build_path}
|
|
74
|
+
- Original prompt: {prompt}
|
|
75
|
+
|
|
76
|
+
Repeat the loop below up to {rounds} time(s), or until the critic reports `"done": true`.
|
|
77
|
+
|
|
78
|
+
## Step A -- match the camera viewpoint (fresh subagent)
|
|
79
|
+
|
|
80
|
+
Dispatch a fresh subagent whose system prompt is exactly:
|
|
81
|
+
|
|
82
|
+
----------------------------------------------------------------------
|
|
83
|
+
{viewpoint_prompt}
|
|
84
|
+
----------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
Give that subagent this task: read the reference image at {reference_path} and the build
|
|
87
|
+
source at {build_path}, then find the matching camera angle as its system prompt
|
|
88
|
+
instructs. It leaves the matched render at {viewpoint_path} and reports the azimuth and
|
|
89
|
+
elevation it chose.
|
|
90
|
+
|
|
91
|
+
## Step B -- critique the build (fresh subagent)
|
|
92
|
+
|
|
93
|
+
Dispatch a second fresh subagent whose system prompt is exactly:
|
|
94
|
+
|
|
95
|
+
----------------------------------------------------------------------
|
|
96
|
+
{VIEWPOINT_COMPARE_PROMPT}
|
|
97
|
+
----------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
Give that subagent this task:
|
|
100
|
+
|
|
101
|
+
- Original prompt: {prompt}
|
|
102
|
+
- Read the target reference image at {reference_path}.
|
|
103
|
+
- Read the LEGO build's matched render at {viewpoint_path}.
|
|
104
|
+
|
|
105
|
+
It must reply with only the JSON object its system prompt describes
|
|
106
|
+
(`{{"differences_description": "...", "done": true|false}}`).
|
|
107
|
+
|
|
108
|
+
## Step C -- revise
|
|
109
|
+
|
|
110
|
+
Parse the critic's JSON. If `"done"` is true, the build matches the reference well enough:
|
|
111
|
+
stop the loop and finalize. Otherwise edit `build.py` to address the
|
|
112
|
+
`differences_description`, re-run `uv run brickwright external validate {record_id}` until
|
|
113
|
+
it passes, then repeat from Step A. Stop after {rounds} round(s) even if the critic is not
|
|
114
|
+
yet satisfied."""
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""A catalog of the Narrator's lifecycle, tool, and summary output."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from ..config import Config, Effort
|
|
11
|
+
from ..record import Timing, Usage
|
|
12
|
+
from ..tools.registry import ToolOutcome, subagent_outcome, validation_outcome
|
|
13
|
+
from ..tools.subagents import SubagentResult
|
|
14
|
+
from ..tools.testing import CheckResult
|
|
15
|
+
from ..tools.validation import BuildDimensions, ValidationReport
|
|
16
|
+
from .narration import Narrator
|
|
17
|
+
from .report import RunReport
|
|
18
|
+
|
|
19
|
+
_SAMPLE_SOURCE = """\
|
|
20
|
+
import bricklayer as bl
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_build():
|
|
24
|
+
build = bl.LegoBuild()
|
|
25
|
+
base = build.add(bl.brick(4, 2, color=bl.Color.RED), name="base")
|
|
26
|
+
build.stack(bl.brick(4, 2, color=bl.Color.RED), on=base)
|
|
27
|
+
return build
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
_TRACEBACK = """\
|
|
31
|
+
Traceback (most recent call last):
|
|
32
|
+
File "<explore>", line 1, in <module>
|
|
33
|
+
ValueError: no part matches id '9999'
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_gallery(console: Console | None = None) -> None:
|
|
38
|
+
"""Print one example of every Narrator output to ``console``."""
|
|
39
|
+
console = console or Console()
|
|
40
|
+
narrator = Narrator(console)
|
|
41
|
+
record = _sample_record()
|
|
42
|
+
config = _sample_config()
|
|
43
|
+
|
|
44
|
+
_section(console, "lifecycle")
|
|
45
|
+
narrator.run_started(record, config)
|
|
46
|
+
narrator.turn_started(1)
|
|
47
|
+
narrator.model_message(
|
|
48
|
+
"I'll lay a 4×2 footprint and stack two courses of red brick."
|
|
49
|
+
)
|
|
50
|
+
narrator.warn("the cost limit cannot be enforced for this provider")
|
|
51
|
+
|
|
52
|
+
_section(console, "tool calls & results")
|
|
53
|
+
for call in _tool_scenes():
|
|
54
|
+
narrator.tool_called(call.name, call.arguments)
|
|
55
|
+
for outcome in call.outcomes:
|
|
56
|
+
narrator.tool_finished(call.name, outcome)
|
|
57
|
+
print()
|
|
58
|
+
|
|
59
|
+
_section(console, "run summary")
|
|
60
|
+
for result in _summary_scenes():
|
|
61
|
+
narrator.run_finished(record, result)
|
|
62
|
+
print()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class _Call:
|
|
67
|
+
"""One tool's call line plus the result variants worth showing."""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
arguments: dict[str, Any] = field(default_factory=dict)
|
|
71
|
+
outcomes: list[ToolOutcome] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _tool_scenes() -> list[_Call]:
|
|
75
|
+
return [
|
|
76
|
+
_Call(
|
|
77
|
+
name="read_build",
|
|
78
|
+
outcomes=[ToolOutcome(_SAMPLE_SOURCE)],
|
|
79
|
+
),
|
|
80
|
+
_Call(
|
|
81
|
+
name="write_build",
|
|
82
|
+
arguments={"content": _SAMPLE_SOURCE},
|
|
83
|
+
outcomes=[
|
|
84
|
+
ToolOutcome("ok: build.py written"),
|
|
85
|
+
ToolOutcome("write failed: missing argument 'content'", ok=False),
|
|
86
|
+
],
|
|
87
|
+
),
|
|
88
|
+
_Call(
|
|
89
|
+
name="insert_build",
|
|
90
|
+
arguments={
|
|
91
|
+
"insert_line": 6,
|
|
92
|
+
"insert_text": " build.stack(bl.tile(4, 2, color=bl.Color.WHITE), on=base)",
|
|
93
|
+
},
|
|
94
|
+
outcomes=[
|
|
95
|
+
ToolOutcome(
|
|
96
|
+
"ok: build.py edited. Edited region:\n"
|
|
97
|
+
"7: build.stack(bl.tile(4, 2, color=bl.Color.WHITE), on=base)"
|
|
98
|
+
),
|
|
99
|
+
ToolOutcome(
|
|
100
|
+
"insert failed: invalid insert_line 99: must be in 0..12 "
|
|
101
|
+
"(0 inserts at the top of the file; N inserts after line N)",
|
|
102
|
+
ok=False,
|
|
103
|
+
),
|
|
104
|
+
],
|
|
105
|
+
),
|
|
106
|
+
_Call(
|
|
107
|
+
name="edit_build",
|
|
108
|
+
arguments={
|
|
109
|
+
"old_str": "bl.brick(4, 2, color=bl.Color.RED), name=",
|
|
110
|
+
"new_str": "bl.brick(4, 2, color=bl.Color.BLUE), name=",
|
|
111
|
+
},
|
|
112
|
+
outcomes=[
|
|
113
|
+
ToolOutcome("ok: build.py edited"),
|
|
114
|
+
ToolOutcome("edit failed: old_str not found", ok=False),
|
|
115
|
+
],
|
|
116
|
+
),
|
|
117
|
+
_Call(
|
|
118
|
+
name="multi_edit_build",
|
|
119
|
+
arguments={
|
|
120
|
+
"edits": [
|
|
121
|
+
{"old_str": "Color.RED", "new_str": "Color.BLUE"},
|
|
122
|
+
{"old_str": "bl.brick(4, 2,", "new_str": "bl.brick(4, 4,"},
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
outcomes=[
|
|
126
|
+
ToolOutcome("ok: applied 2 edits to build.py"),
|
|
127
|
+
ToolOutcome(
|
|
128
|
+
"multi-edit failed (build.py unchanged): "
|
|
129
|
+
"edit 2 of 2: old_str not found",
|
|
130
|
+
ok=False,
|
|
131
|
+
),
|
|
132
|
+
],
|
|
133
|
+
),
|
|
134
|
+
_Call(
|
|
135
|
+
name="validate",
|
|
136
|
+
outcomes=[
|
|
137
|
+
validation_outcome(
|
|
138
|
+
ValidationReport(
|
|
139
|
+
ok=True,
|
|
140
|
+
dimensions=BuildDimensions(width=4, depth=2, height=2.0),
|
|
141
|
+
brick_count=8,
|
|
142
|
+
checks=[
|
|
143
|
+
CheckResult(name="connected", passed=True, detail=""),
|
|
144
|
+
CheckResult(name="no_collisions", passed=True, detail=""),
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
),
|
|
148
|
+
validation_outcome(
|
|
149
|
+
ValidationReport(
|
|
150
|
+
ok=False,
|
|
151
|
+
dimensions=BuildDimensions(width=4, depth=2, height=2.0),
|
|
152
|
+
brick_count=8,
|
|
153
|
+
checks=[
|
|
154
|
+
CheckResult(name="connected", passed=True, detail=""),
|
|
155
|
+
CheckResult(
|
|
156
|
+
name="no_collisions",
|
|
157
|
+
passed=False,
|
|
158
|
+
detail="2 bricks overlap at (2, 0)",
|
|
159
|
+
),
|
|
160
|
+
],
|
|
161
|
+
)
|
|
162
|
+
),
|
|
163
|
+
validation_outcome(
|
|
164
|
+
ValidationReport.error(
|
|
165
|
+
"Traceback (most recent call last):\n"
|
|
166
|
+
"NameError: name 'Color' is not defined"
|
|
167
|
+
)
|
|
168
|
+
),
|
|
169
|
+
],
|
|
170
|
+
),
|
|
171
|
+
_Call(
|
|
172
|
+
name="explore",
|
|
173
|
+
arguments={"code": "print(bricklayer.search_parts('brick', limit=5))"},
|
|
174
|
+
outcomes=[
|
|
175
|
+
ToolOutcome("Brick 2x2"),
|
|
176
|
+
ToolOutcome("Brick 2x2 (3003)\nBrick 2x4 (3001)\nBrick 1x2 (3004)"),
|
|
177
|
+
ToolOutcome(f"explore error:\n{_TRACEBACK}", ok=False),
|
|
178
|
+
],
|
|
179
|
+
),
|
|
180
|
+
_Call(
|
|
181
|
+
name="render",
|
|
182
|
+
outcomes=[
|
|
183
|
+
ToolOutcome(
|
|
184
|
+
"ok: rendered the build to a multi-view contact sheet; "
|
|
185
|
+
"the image follows.",
|
|
186
|
+
image=Path("out/render.png"),
|
|
187
|
+
),
|
|
188
|
+
ToolOutcome(f"render failed:\n{_TRACEBACK}", ok=False),
|
|
189
|
+
],
|
|
190
|
+
),
|
|
191
|
+
_Call(
|
|
192
|
+
name="finalize",
|
|
193
|
+
outcomes=[
|
|
194
|
+
ToolOutcome("ok: build compiled to out/build.ldr", done=True),
|
|
195
|
+
ToolOutcome(f"compile failed:\n{_TRACEBACK}", ok=False, done=True),
|
|
196
|
+
],
|
|
197
|
+
),
|
|
198
|
+
_Call(
|
|
199
|
+
name="dispatch_subagents",
|
|
200
|
+
arguments={
|
|
201
|
+
"tasks": [
|
|
202
|
+
{"prompt": "Design the archway using arch parts.", "tier": "smart"},
|
|
203
|
+
{"prompt": "Design the two flanking towers.", "tier": "balanced"},
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
outcomes=[
|
|
207
|
+
subagent_outcome(
|
|
208
|
+
[
|
|
209
|
+
SubagentResult(ok=True, text="Use two 1x6 arch bricks (3455)."),
|
|
210
|
+
SubagentResult(ok=True, text="Stack 2x2 round bricks (3941)."),
|
|
211
|
+
]
|
|
212
|
+
),
|
|
213
|
+
subagent_outcome(
|
|
214
|
+
[
|
|
215
|
+
SubagentResult(ok=True, text="Use two 1x6 arch bricks (3455)."),
|
|
216
|
+
SubagentResult(
|
|
217
|
+
ok=False,
|
|
218
|
+
reason="subagent produced no final answer",
|
|
219
|
+
),
|
|
220
|
+
]
|
|
221
|
+
),
|
|
222
|
+
],
|
|
223
|
+
),
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _summary_scenes() -> list[RunReport]:
|
|
228
|
+
usage = Usage(
|
|
229
|
+
prompt_tokens=18_432,
|
|
230
|
+
completion_tokens=2_561,
|
|
231
|
+
total_tokens=20_993,
|
|
232
|
+
cost_usd=0.2317,
|
|
233
|
+
)
|
|
234
|
+
timing = Timing(
|
|
235
|
+
total=42.8, llm=31.2, tools={"validate": 8.1, "render": 2.9}, other=0.6
|
|
236
|
+
)
|
|
237
|
+
return [
|
|
238
|
+
RunReport(
|
|
239
|
+
done=True,
|
|
240
|
+
source_sha256="example-source",
|
|
241
|
+
report=ValidationReport(ok=True, source_sha256="example-source"),
|
|
242
|
+
validations=3,
|
|
243
|
+
turns=11,
|
|
244
|
+
usage=usage,
|
|
245
|
+
timing=timing,
|
|
246
|
+
),
|
|
247
|
+
RunReport(
|
|
248
|
+
done=True,
|
|
249
|
+
report=ValidationReport(ok=False),
|
|
250
|
+
validations=5,
|
|
251
|
+
turns=14,
|
|
252
|
+
usage=usage,
|
|
253
|
+
timing=timing,
|
|
254
|
+
),
|
|
255
|
+
RunReport(
|
|
256
|
+
done=False,
|
|
257
|
+
report=ValidationReport(ok=True),
|
|
258
|
+
validations=2,
|
|
259
|
+
turns=30,
|
|
260
|
+
usage=usage,
|
|
261
|
+
timing=timing,
|
|
262
|
+
),
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _section(console: Console, title: str) -> None:
|
|
267
|
+
console.print()
|
|
268
|
+
console.rule(f"[bold]{title}", style="blue")
|
|
269
|
+
console.print()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _sample_record() -> Any:
|
|
273
|
+
"""A stand-in record exposing just what the narration panels read."""
|
|
274
|
+
meta = SimpleNamespace(prompt="Build a 4×2 red brick wall, two courses high.")
|
|
275
|
+
return SimpleNamespace(
|
|
276
|
+
id="rec_3f9a2b-red-brick-wall",
|
|
277
|
+
path=Path("records/rec_3f9a2b-red-brick-wall"),
|
|
278
|
+
read_meta=lambda: meta,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _sample_config() -> Config:
|
|
283
|
+
return Config(
|
|
284
|
+
model="anthropic/claude-opus-4-8",
|
|
285
|
+
effort=Effort.HIGH,
|
|
286
|
+
max_validations=10,
|
|
287
|
+
max_turns=30,
|
|
288
|
+
max_cost=5.0,
|
|
289
|
+
)
|