spriteforge 0.1.1__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.
- spriteforge/__init__.py +1 -0
- spriteforge/__main__.py +3 -0
- spriteforge/agents/__init__.py +0 -0
- spriteforge/agents/assets.py +109 -0
- spriteforge/agents/cancellation.py +12 -0
- spriteforge/agents/director.py +50 -0
- spriteforge/agents/models.py +63 -0
- spriteforge/agents/roles/art-director.md +15 -0
- spriteforge/agents/roles/art-reviewer.md +14 -0
- spriteforge/agents/roles/director.md +14 -0
- spriteforge/agents/roles/game-designer.md +15 -0
- spriteforge/agents/roles/level-tile-designer.md +16 -0
- spriteforge/agents/roles/librarian.md +13 -0
- spriteforge/agents/roles/menu-designer.md +15 -0
- spriteforge/agents/roles/pixel-artist.md +14 -0
- spriteforge/agents/roles/programmer.md +14 -0
- spriteforge/agents/roles/qa-playtester.md +13 -0
- spriteforge/agents/roles/sound-director.md +14 -0
- spriteforge/agents/roles.py +39 -0
- spriteforge/agents/router.py +233 -0
- spriteforge/agents/router_eval.py +145 -0
- spriteforge/agents/routing_state.py +133 -0
- spriteforge/agents/runtime.py +430 -0
- spriteforge/agents/workflow.py +389 -0
- spriteforge/app/__init__.py +0 -0
- spriteforge/app/agents_view.py +248 -0
- spriteforge/app/chat.py +114 -0
- spriteforge/app/extensions.py +115 -0
- spriteforge/app/index.html +279 -0
- spriteforge/app/local_only.py +49 -0
- spriteforge/app/server.py +358 -0
- spriteforge/app/static/assets/index-L2s1wlcp.css +1 -0
- spriteforge/app/static/assets/index-aBfzgLQl.js +71 -0
- spriteforge/app/static/assets/press-start-2p-latin-400-normal-BKfmdmKK.woff +0 -0
- spriteforge/app/static/assets/press-start-2p-latin-400-normal-xj9lRvVs.woff2 +0 -0
- spriteforge/app/static/index.html +13 -0
- spriteforge/app/static/press-start-2p-LICENSE.txt +93 -0
- spriteforge/app/studio.py +278 -0
- spriteforge/app/workers.py +73 -0
- spriteforge/cli.py +241 -0
- spriteforge/config.py +193 -0
- spriteforge/config.yaml +157 -0
- spriteforge/core/__init__.py +0 -0
- spriteforge/core/acting.py +16 -0
- spriteforge/core/cost.py +236 -0
- spriteforge/core/credentials.py +24 -0
- spriteforge/core/demo.py +79 -0
- spriteforge/core/forms.py +20 -0
- spriteforge/core/gates.py +175 -0
- spriteforge/core/security.py +42 -0
- spriteforge/core/state.py +222 -0
- spriteforge/core/team.py +15 -0
- spriteforge/core/usage.py +68 -0
- spriteforge/game/__init__.py +0 -0
- spriteforge/game/actors.py +377 -0
- spriteforge/game/agent_tools.py +178 -0
- spriteforge/game/art.py +2097 -0
- spriteforge/game/art_checks.py +190 -0
- spriteforge/game/art_review.py +518 -0
- spriteforge/game/build.py +134 -0
- spriteforge/game/bundle.py +1178 -0
- spriteforge/game/cli.py +119 -0
- spriteforge/game/codegen.py +433 -0
- spriteforge/game/codeinfo.py +154 -0
- spriteforge/game/dbsource.py +137 -0
- spriteforge/game/design.py +342 -0
- spriteforge/game/export.py +62 -0
- spriteforge/game/gamedb.py +191 -0
- spriteforge/game/image_cache.py +61 -0
- spriteforge/game/lessons.py +259 -0
- spriteforge/game/level_gen.py +118 -0
- spriteforge/game/level_preview.py +97 -0
- spriteforge/game/levels.py +934 -0
- spriteforge/game/loops.py +38 -0
- spriteforge/game/menus.py +712 -0
- spriteforge/game/mock_game.js +109 -0
- spriteforge/game/mock_platformer.js +58 -0
- spriteforge/game/motion.py +881 -0
- spriteforge/game/pipeline.py +1768 -0
- spriteforge/game/plan.py +502 -0
- spriteforge/game/playtest.py +318 -0
- spriteforge/game/qa.py +664 -0
- spriteforge/game/qa_checks.py +223 -0
- spriteforge/game/qa_runtime.py +565 -0
- spriteforge/game/reach.py +807 -0
- spriteforge/game/record.py +138 -0
- spriteforge/game/research.py +262 -0
- spriteforge/game/revision.py +225 -0
- spriteforge/game/sdk/API.md +104 -0
- spriteforge/game/sdk/FLOW.md +19 -0
- spriteforge/game/sdk/LEVELS.md +65 -0
- spriteforge/game/sdk/sf.js +418 -0
- spriteforge/game/sdk/sfflow.js +442 -0
- spriteforge/game/sdk/sflevel.js +683 -0
- spriteforge/game/sdk/sfsound-editor.js +225 -0
- spriteforge/game/sdk/sfsound.js +260 -0
- spriteforge/game/sdk/sounds.html +67 -0
- spriteforge/game/sounds.py +793 -0
- spriteforge/game/story.py +188 -0
- spriteforge/game/structure.py +98 -0
- spriteforge/game/team_reports.py +394 -0
- spriteforge/game/tilesets.py +70 -0
- spriteforge/game/title_art.py +186 -0
- spriteforge/genres/__init__.py +0 -0
- spriteforge/library/__init__.py +15 -0
- spriteforge/library/cli.py +126 -0
- spriteforge/library/describe.py +242 -0
- spriteforge/library/looks.py +154 -0
- spriteforge/library/models.py +79 -0
- spriteforge/library/store.py +422 -0
- spriteforge/library/tilesets/README.md +32 -0
- spriteforge/library/tilesets/gandalf-seasons.json +111 -0
- spriteforge/library/tilesets/pixel-crawler-dungeon.json +66 -0
- spriteforge/mcp/__init__.py +0 -0
- spriteforge/mcp/cli.py +58 -0
- spriteforge/mcp/client.py +57 -0
- spriteforge/mcp/filesystem_read.py +32 -0
- spriteforge/mcp/gamedb.py +56 -0
- spriteforge/mcp/library.py +179 -0
- spriteforge/mcp/memory.py +37 -0
- spriteforge/mcp/pixel_tools.py +145 -0
- spriteforge/mcp/registry.py +145 -0
- spriteforge/mcp/service.py +224 -0
- spriteforge/mcp/templates/context7.json +6 -0
- spriteforge/mcp/templates/fetch.json +6 -0
- spriteforge/mcp/templates/filesystem-read.json +6 -0
- spriteforge/mcp/templates/pixel-tools.json +6 -0
- spriteforge/memory/README.md +10 -0
- spriteforge/memory/__init__.py +0 -0
- spriteforge/memory/cli.py +131 -0
- spriteforge/memory/okf/__init__.py +1 -0
- spriteforge/memory/okf/catalog.py +331 -0
- spriteforge/memory/okf/codec.py +63 -0
- spriteforge/memory/okf/git.py +104 -0
- spriteforge/memory/okf/indexes.py +50 -0
- spriteforge/memory/okf/models.py +114 -0
- spriteforge/memory/okf/search.py +53 -0
- spriteforge/memory/okf/seed.py +81 -0
- spriteforge/memory/okf/seeds/genres.yaml +121 -0
- spriteforge/memory/okf/seeds/other.yaml +150 -0
- spriteforge/memory/okf/store.py +402 -0
- spriteforge/memory/okf/validate.py +115 -0
- spriteforge/pixel/__init__.py +17 -0
- spriteforge/pixel/atlas.py +110 -0
- spriteforge/pixel/grid.py +174 -0
- spriteforge/pixel/palette.py +156 -0
- spriteforge/pixel/palettes/dungeon.json +9 -0
- spriteforge/pixel/postprocess.py +170 -0
- spriteforge/pixel/preview.py +40 -0
- spriteforge/pixel/schemas.py +139 -0
- spriteforge/pixel/source.py +237 -0
- spriteforge/pixel/terrain.py +48 -0
- spriteforge/pixel/validate.py +138 -0
- spriteforge/prompts/anim_frame.txt +1 -0
- spriteforge/prompts/character.txt +1 -0
- spriteforge/prompts/item.txt +1 -0
- spriteforge/prompts/tile.txt +1 -0
- spriteforge/prompts/ui.txt +1 -0
- spriteforge/providers/__init__.py +0 -0
- spriteforge/providers/base.py +97 -0
- spriteforge/providers/http.py +38 -0
- spriteforge/providers/mock.py +111 -0
- spriteforge/providers/muse_image.py +60 -0
- spriteforge/providers/muse_search.py +91 -0
- spriteforge/providers/muse_spark.py +66 -0
- spriteforge/skills/__init__.py +0 -0
- spriteforge/skills/builtin/LICENSE-awesome-gamedev-agent-skills.txt +201 -0
- spriteforge/skills/builtin/NOTICE-awesome-gamedev-agent-skills.txt +15 -0
- spriteforge/skills/builtin/THIRD_PARTY.md +21 -0
- spriteforge/skills/builtin/audio-design/SKILL.md +190 -0
- spriteforge/skills/builtin/game-feel/SKILL.md +174 -0
- spriteforge/skills/builtin/game-ui-ux/SKILL.md +167 -0
- spriteforge/skills/builtin/level-design/SKILL.md +169 -0
- spriteforge/skills/builtin/phaser-arcade-physics/SKILL.md +17 -0
- spriteforge/skills/builtin/physics-tuning/SKILL.md +164 -0
- spriteforge/skills/builtin/pixel-art-prompting/SKILL.md +12 -0
- spriteforge/skills/builtin/platformer/SKILL.md +153 -0
- spriteforge/skills/builtin/puzzle/SKILL.md +143 -0
- spriteforge/skills/builtin/sprite-animation-frames/SKILL.md +11 -0
- spriteforge/skills/builtin/tileset-design/SKILL.md +10 -0
- spriteforge/skills/catalog.py +200 -0
- spriteforge/skills/cli.py +41 -0
- spriteforge/stages/__init__.py +0 -0
- spriteforge/stages/asset_context.py +301 -0
- spriteforge/stages/asset_models.py +192 -0
- spriteforge/stages/asset_render.py +366 -0
- spriteforge/stages/asset_review.py +140 -0
- spriteforge/stages/asset_store.py +94 -0
- spriteforge/stages/chat_changes.py +236 -0
- spriteforge/stages/concept.py +42 -0
- spriteforge/stages/game.py +1699 -0
- spriteforge/stages/game_confirm.py +112 -0
- spriteforge/stages/game_final.py +220 -0
- spriteforge/stages/game_record.py +246 -0
- spriteforge/stages/gameplay.py +218 -0
- spriteforge/stages/intake.py +372 -0
- spriteforge/stages/preview_assets.py +212 -0
- spriteforge/stages/preview_models.py +167 -0
- spriteforge/stages/preview_render.py +169 -0
- spriteforge/stages/preview_repair.py +61 -0
- spriteforge/stages/preview_review.py +169 -0
- spriteforge/stages/production.py +170 -0
- spriteforge/stages/references.py +105 -0
- spriteforge/stages/rewind.py +128 -0
- spriteforge/stages/workflow.py +440 -0
- spriteforge/templates/phaser/audit.js +45 -0
- spriteforge/templates/phaser/font.png +0 -0
- spriteforge/templates/phaser/font.xml +1 -0
- spriteforge/templates/phaser/preview.html +9 -0
- spriteforge/templates/phaser/room.js +49 -0
- spriteforge/templates/phaser/vendor/LICENSE.md +20 -0
- spriteforge/templates/phaser/vendor/phaser.min.js +1 -0
- spriteforge-0.1.1.dist-info/METADATA +364 -0
- spriteforge-0.1.1.dist-info/RECORD +218 -0
- spriteforge-0.1.1.dist-info/WHEEL +5 -0
- spriteforge-0.1.1.dist-info/entry_points.txt +2 -0
- spriteforge-0.1.1.dist-info/licenses/LICENSE +27 -0
- spriteforge-0.1.1.dist-info/top_level.txt +1 -0
spriteforge/cli.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import threading
|
|
3
|
+
import webbrowser
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
from spriteforge.app.server import create_app
|
|
10
|
+
from spriteforge.config import init_workspace, load_config
|
|
11
|
+
from spriteforge.core.credentials import load_credentials
|
|
12
|
+
from spriteforge.core.state import Store
|
|
13
|
+
from spriteforge.game.cli import app as game_app
|
|
14
|
+
from spriteforge.library.cli import app as library_app
|
|
15
|
+
from spriteforge.mcp.cli import app as mcp_app
|
|
16
|
+
from spriteforge.memory.cli import app as memory_app
|
|
17
|
+
from spriteforge.skills.cli import app as skills_app
|
|
18
|
+
|
|
19
|
+
app = typer.Typer()
|
|
20
|
+
app.add_typer(memory_app, name="memory")
|
|
21
|
+
app.add_typer(skills_app, name="skills")
|
|
22
|
+
app.add_typer(mcp_app, name="mcp")
|
|
23
|
+
app.add_typer(library_app, name="library")
|
|
24
|
+
app.add_typer(game_app, name="game")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def setup(config: Path | None, workspace: Path | None):
|
|
28
|
+
load_credentials()
|
|
29
|
+
cfg = load_config(config)
|
|
30
|
+
root = init_workspace(workspace or cfg.workspace)
|
|
31
|
+
return cfg, Store(root / "state.sqlite3")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def serve(store, run_id, port, open_browser, cfg=None):
|
|
35
|
+
url = f"http://127.0.0.1:{port}"
|
|
36
|
+
typer.echo(f"SpriteForge Studio · {url}" + (f" · run {run_id}" if run_id else ""))
|
|
37
|
+
if open_browser:
|
|
38
|
+
timer = threading.Timer(1.5, webbrowser.open, args=(url,))
|
|
39
|
+
timer.daemon = True
|
|
40
|
+
timer.start()
|
|
41
|
+
app = create_app(store, run_id, cfg=cfg)
|
|
42
|
+
uvicorn.run(app, host="127.0.0.1", port=port, access_log=False)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.callback(invoke_without_command=True)
|
|
46
|
+
def main(ctx: typer.Context):
|
|
47
|
+
"""SpriteForge. With no command, opens the studio in the browser."""
|
|
48
|
+
if ctx.invoked_subcommand is None:
|
|
49
|
+
studio()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
def studio(
|
|
54
|
+
config: Path | None = None,
|
|
55
|
+
workspace: Path | None = None,
|
|
56
|
+
port: int = 8765,
|
|
57
|
+
open_browser: bool = True,
|
|
58
|
+
):
|
|
59
|
+
"""Open the studio: every project in the workspace, chat-first (the default command)."""
|
|
60
|
+
cfg, store = setup(config, workspace)
|
|
61
|
+
serve(store, None, port, open_browser, cfg)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
def start(
|
|
66
|
+
mock: bool = False,
|
|
67
|
+
agents: bool = False,
|
|
68
|
+
intake: bool = False,
|
|
69
|
+
assets: bool = False,
|
|
70
|
+
legacy: bool = False,
|
|
71
|
+
goal: str = "",
|
|
72
|
+
config: Path | None = None,
|
|
73
|
+
workspace: Path | None = None,
|
|
74
|
+
port: int = 8765,
|
|
75
|
+
open_browser: bool = True,
|
|
76
|
+
):
|
|
77
|
+
"""Start a local UI; --agents uses the Phaser gameplay preview and human gates."""
|
|
78
|
+
cfg, store = setup(config, workspace)
|
|
79
|
+
if legacy:
|
|
80
|
+
typer.echo("DEPRECATED: --legacy enables the old concept/demo preview.", err=True)
|
|
81
|
+
if intake or assets or (agents and not legacy):
|
|
82
|
+
from spriteforge.stages.workflow import start_run
|
|
83
|
+
|
|
84
|
+
run = start_run(
|
|
85
|
+
store,
|
|
86
|
+
cfg,
|
|
87
|
+
mock=mock,
|
|
88
|
+
assets=assets,
|
|
89
|
+
gameplay_preview=not legacy,
|
|
90
|
+
legacy=legacy,
|
|
91
|
+
goal=goal,
|
|
92
|
+
)
|
|
93
|
+
serve(store, run.id, port, open_browser, cfg)
|
|
94
|
+
return
|
|
95
|
+
if agents and not mock:
|
|
96
|
+
raise typer.BadParameter("The agent acceptance workflow requires --mock")
|
|
97
|
+
if agents:
|
|
98
|
+
from spriteforge.agents.workflow import start_run
|
|
99
|
+
|
|
100
|
+
run = start_run(
|
|
101
|
+
store,
|
|
102
|
+
goal or "A top-down dungeon crawler with a hero and stone rooms",
|
|
103
|
+
cfg.budget,
|
|
104
|
+
router=cfg.router,
|
|
105
|
+
)
|
|
106
|
+
store.append_event(run.id, "Director", "preview.deprecated", {"legacy": True})
|
|
107
|
+
serve(store, run.id, port, open_browser, cfg)
|
|
108
|
+
return
|
|
109
|
+
run = (
|
|
110
|
+
store.create_run({"workflow": "mock-demo-v1", "budget": cfg.budget.model_dump()})
|
|
111
|
+
if mock
|
|
112
|
+
else None
|
|
113
|
+
)
|
|
114
|
+
serve(store, run.id if run else None, port, open_browser, cfg)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.command()
|
|
118
|
+
def router_eval(
|
|
119
|
+
live: bool = False,
|
|
120
|
+
max_calls: int = typer.Option(5, min=1, max=20),
|
|
121
|
+
offset: int = typer.Option(0, min=0),
|
|
122
|
+
cases: Path = Path("tests/router_cases.yaml"),
|
|
123
|
+
config: Path | None = None,
|
|
124
|
+
workspace: Path | None = None,
|
|
125
|
+
):
|
|
126
|
+
"""Report router precision/recall; --live makes at most --max-calls Spark requests."""
|
|
127
|
+
from uuid import uuid4
|
|
128
|
+
|
|
129
|
+
from spriteforge.agents.router_eval import evaluate, read_cases
|
|
130
|
+
from spriteforge.core.cost import Budget
|
|
131
|
+
from spriteforge.core.state import Artifact
|
|
132
|
+
from spriteforge.providers.mock import MockLLMProvider
|
|
133
|
+
from spriteforge.providers.muse_spark import MuseSparkProvider
|
|
134
|
+
|
|
135
|
+
cfg, store = setup(config, workspace)
|
|
136
|
+
suite = read_cases(cases)[offset:]
|
|
137
|
+
limits = (
|
|
138
|
+
cfg.budget.model_copy(
|
|
139
|
+
update={
|
|
140
|
+
"max_total_calls": min(
|
|
141
|
+
cfg.budget.max_total_calls
|
|
142
|
+
if cfg.budget.max_total_calls is not None
|
|
143
|
+
else max_calls,
|
|
144
|
+
max_calls,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
if live
|
|
149
|
+
else cfg.budget
|
|
150
|
+
)
|
|
151
|
+
run = store.create_run(
|
|
152
|
+
{
|
|
153
|
+
"workflow": "router-eval",
|
|
154
|
+
"budget": limits.model_dump(),
|
|
155
|
+
"settings": cfg.model_dump(mode="json"),
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
budget = Budget(store, run.id, limits)
|
|
159
|
+
provider = MuseSparkProvider(cfg.llm, budget) if live else MockLLMProvider(budget)
|
|
160
|
+
try:
|
|
161
|
+
report = evaluate(
|
|
162
|
+
provider, suite, cfg.router, store.path.parent, live=live, max_calls=max_calls
|
|
163
|
+
)
|
|
164
|
+
finally:
|
|
165
|
+
if live:
|
|
166
|
+
provider.close()
|
|
167
|
+
store.save_artifact(
|
|
168
|
+
Artifact(
|
|
169
|
+
id=uuid4().hex,
|
|
170
|
+
run_id=run.id,
|
|
171
|
+
name="router-eval.json",
|
|
172
|
+
mime="application/json",
|
|
173
|
+
data=report.model_dump_json(indent=2).encode(),
|
|
174
|
+
),
|
|
175
|
+
"Router",
|
|
176
|
+
"router.eval",
|
|
177
|
+
)
|
|
178
|
+
with store.connect() as db:
|
|
179
|
+
db.execute("UPDATE runs SET status='completed' WHERE id=?", (run.id,))
|
|
180
|
+
typer.echo(f"{report.mode}: {report.evaluated} cases, {report.skipped} skipped")
|
|
181
|
+
typer.echo(f"Spark calls attempted: {report.evaluated if live else 0} (cap {max_calls})")
|
|
182
|
+
typer.echo(f"Precision: {report.precision:.3f} Recall: {report.recall:.3f}")
|
|
183
|
+
typer.echo(f"Failures: {len(report.failures)}")
|
|
184
|
+
for failure in report.failures:
|
|
185
|
+
typer.echo(failure.model_dump_json())
|
|
186
|
+
typer.echo(f"Run: {run.id} (router-eval.json contains all results)")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@app.command()
|
|
190
|
+
def report(id: str, config: Path | None = None, workspace: Path | None = None):
|
|
191
|
+
"""Report successful image/Spark usage and failed attempts from the durable ledger."""
|
|
192
|
+
from spriteforge.core.usage import usage_report
|
|
193
|
+
|
|
194
|
+
_, store = setup(config, workspace)
|
|
195
|
+
typer.echo(usage_report(store, id).model_dump_json(indent=2))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@app.command()
|
|
199
|
+
def runs(config: Path | None = None, workspace: Path | None = None):
|
|
200
|
+
"""List runs persisted in the workspace database."""
|
|
201
|
+
_, store = setup(config, workspace)
|
|
202
|
+
for run in store.runs():
|
|
203
|
+
typer.echo(f"{run.id} {run.status:10} checkpoint={run.cursor}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.command()
|
|
207
|
+
def resume(
|
|
208
|
+
id: str,
|
|
209
|
+
config: Path | None = None,
|
|
210
|
+
workspace: Path | None = None,
|
|
211
|
+
port: int = 8765,
|
|
212
|
+
open_browser: bool = True,
|
|
213
|
+
assets: bool = False,
|
|
214
|
+
legacy: bool = False,
|
|
215
|
+
):
|
|
216
|
+
"""Resume from the database checkpoint and show its timeline."""
|
|
217
|
+
cfg, store = setup(config, workspace)
|
|
218
|
+
try:
|
|
219
|
+
run = store.get_run(id)
|
|
220
|
+
except KeyError:
|
|
221
|
+
raise typer.BadParameter(f"Run {id} does not exist") from None
|
|
222
|
+
old_preview = run.config.get("workflow") == "agent-runtime-mock-v1" or (
|
|
223
|
+
run.config.get("workflow") == "intake-concept-v1" and not run.config.get("gameplay_preview")
|
|
224
|
+
)
|
|
225
|
+
if old_preview:
|
|
226
|
+
if not legacy:
|
|
227
|
+
raise typer.BadParameter("This deprecated preview requires explicit --legacy")
|
|
228
|
+
typer.echo("DEPRECATED: --legacy enables the old concept/demo preview.", err=True)
|
|
229
|
+
with store.connect() as db:
|
|
230
|
+
db.execute(
|
|
231
|
+
"UPDATE runs SET config=? WHERE id=?",
|
|
232
|
+
(json.dumps({**run.config, "legacy_preview": True}), id),
|
|
233
|
+
)
|
|
234
|
+
store._event(db, id, "Director", "preview.deprecated", {"legacy": True})
|
|
235
|
+
if assets:
|
|
236
|
+
from spriteforge.stages.production import enable_production
|
|
237
|
+
|
|
238
|
+
enable_production(store, id)
|
|
239
|
+
if run.status == "failed" and run.config.get("workflow") == "intake-concept-v1":
|
|
240
|
+
store.retry_failed(id)
|
|
241
|
+
serve(store, id, port, open_browser, cfg)
|
spriteforge/config.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Schema(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ImageConfig(Schema):
|
|
13
|
+
base_url: HttpUrl
|
|
14
|
+
model: str
|
|
15
|
+
generate_path: str
|
|
16
|
+
edit_path: str
|
|
17
|
+
# Sizes Muse Image has accepted live; requests pick the one nearest the subject's aspect.
|
|
18
|
+
sizes: list[tuple[int, int]] = Field(default_factory=lambda: [(1024, 1024)], min_length=1)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Effort = Literal["minimal", "low", "medium", "high", "xhigh", "max"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LLMConfig(Schema):
|
|
25
|
+
base_url: HttpUrl
|
|
26
|
+
model: str
|
|
27
|
+
chat_path: str
|
|
28
|
+
# Spark `reasoning_effort` per task (intake, art_plan, revision, design, code); a task
|
|
29
|
+
# not listed sends none and the model picks its own effort.
|
|
30
|
+
reasoning: dict[str, Effort] = Field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BudgetConfig(Schema):
|
|
34
|
+
max_image_calls: int = Field(default=150, ge=0)
|
|
35
|
+
max_calls_per_model: int = Field(default=200, ge=0)
|
|
36
|
+
max_total_calls: int | None = Field(default=None, ge=0)
|
|
37
|
+
image_cost: float = Field(default=0.01, ge=0)
|
|
38
|
+
# Meta standard Spark rates: https://dev.meta.ai/docs/pricing-rate-limits
|
|
39
|
+
spark_input_per_million: float = Field(default=1.25, ge=0)
|
|
40
|
+
spark_cached_input_per_million: float = Field(default=0.15, ge=0)
|
|
41
|
+
spark_output_per_million: float = Field(default=4.25, ge=0)
|
|
42
|
+
# Muse Spark web search: "$2.50 per 1,000 search queries, in addition to the request's
|
|
43
|
+
# token cost" (https://dev.meta.ai/docs/pricing-rate-limits, 2026-11-02).
|
|
44
|
+
web_search_cost: float = Field(default=0.0025, ge=0)
|
|
45
|
+
max_estimated_cost: float = Field(default=5, ge=0)
|
|
46
|
+
# True (config.yaml): each agent may spend `max_agent_cost` (USD, estimated) since the
|
|
47
|
+
# human's last action (a gate answer, Go back, Continue); the caps above do not apply. Its
|
|
48
|
+
# loops run until that is used up or a try brings no improvement (game.patience), and work
|
|
49
|
+
# the user asks for always starts. False: the caps above, per run (the legacy concept and
|
|
50
|
+
# production stages, budgets made in code).
|
|
51
|
+
per_request: bool = False
|
|
52
|
+
max_agent_cost: float = Field(default=10, ge=0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ResearchConfig(Schema):
|
|
56
|
+
"""Muse Spark web search for the Game Designer's research (L2.2), on the Responses API."""
|
|
57
|
+
|
|
58
|
+
path: str = "/v1/responses"
|
|
59
|
+
# Built-in tool calls per research (searches and page opens; `max_tool_calls`).
|
|
60
|
+
max_searches: int = Field(default=8, ge=1, le=20)
|
|
61
|
+
search_context_size: Literal["low", "medium", "high"] = "medium"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MemoryConfig(Schema):
|
|
65
|
+
min_score: float = Field(default=0, ge=0, allow_inf_nan=False)
|
|
66
|
+
relative_score: float = Field(default=0.5, ge=0, le=1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PreviewConfig(Schema):
|
|
70
|
+
max_image_calls: int = Field(default=16, ge=0, le=150)
|
|
71
|
+
mood_board: bool = True
|
|
72
|
+
# game: after G1 Spark writes a design (G2 gate) and then the game's code (G4 playtest
|
|
73
|
+
# gate); art comes from the library first, Muse Image for the rest (`game` settings).
|
|
74
|
+
# muse: the original Muse Image building-block preview.
|
|
75
|
+
mode: Literal["game", "muse"] = "game"
|
|
76
|
+
|
|
77
|
+
@model_validator(mode="before")
|
|
78
|
+
@classmethod
|
|
79
|
+
def legacy_kit_settings(cls, data):
|
|
80
|
+
"""Runs saved before kits were removed: mode 'kit' and preview.max_art_calls."""
|
|
81
|
+
if isinstance(data, dict):
|
|
82
|
+
data = {k: v for k, v in data.items() if k != "max_art_calls"}
|
|
83
|
+
if data.get("mode") == "kit":
|
|
84
|
+
data["mode"] = "game"
|
|
85
|
+
return data
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class GameConfig(Schema):
|
|
89
|
+
"""Prompt-driven games: Spark writes the code, the harness checks and playtests it."""
|
|
90
|
+
|
|
91
|
+
# Agent loops (Programmer repairs, Muse redraws, QA check fixes, Menu and Level retries,
|
|
92
|
+
# Art Reviewer rounds) try again while they improve: they stop after `patience` tries in a
|
|
93
|
+
# row without improvement, or when the agent's allowance (budget.max_agent_cost) is used
|
|
94
|
+
# up. The counts below are optional fixed limits on top (None: no fixed limit).
|
|
95
|
+
patience: int = Field(default=2, ge=1, le=10)
|
|
96
|
+
max_repairs: int | None = Field(default=None, ge=0) # Programmer fix rounds after failed checks
|
|
97
|
+
max_art_calls: int | None = Field(default=None, ge=0) # Muse Image calls per build
|
|
98
|
+
image_workers: int = Field(default=4, ge=1, le=8) # Muse Image calls in flight at once
|
|
99
|
+
image_cache: bool = True # identical Muse requests reuse workspace/cache/muse-image
|
|
100
|
+
# The game's screen: 320x180 design pixels times screen_scale (6: 1920x1080, a laptop's
|
|
101
|
+
# full-HD screen). Every sprite, tile, text and distance is that many times bigger, so a
|
|
102
|
+
# sprite keeps more of the detail Muse drew and words stay readable.
|
|
103
|
+
screen_scale: int = Field(default=6, ge=1, le=8)
|
|
104
|
+
# The Level/Tile Designer studies the level design of similar platformers with web search
|
|
105
|
+
# (one search call per game, before the first level) and its level prompts read it.
|
|
106
|
+
level_research: bool = True
|
|
107
|
+
# Muse Image's strength for every first draw (the API has low and high; redraws and the
|
|
108
|
+
# Art Reviewer's fixes use high too).
|
|
109
|
+
image_reasoning: Literal["low", "high"] = "high"
|
|
110
|
+
art_retry: int | None = Field(default=None, ge=0) # redraws of a Muse frame that fails checks
|
|
111
|
+
native_grid_confidence: float = Field(default=0.6, ge=0, le=1) # big-pixel grid read-back
|
|
112
|
+
# Muse sprites as sheets: frames per animation row (1 = one image per sprite)
|
|
113
|
+
sheet_frames: int = Field(default=4, ge=1, le=6)
|
|
114
|
+
adapt_size: bool = True # a Muse sprite's frame takes the drawn art's shape
|
|
115
|
+
# Art Reviewer (M10.3): one vision call after the playtest; blocking issues are fixed once
|
|
116
|
+
# (at most review_max_fixes Muse Image edits at review_image_reasoning) before G4.
|
|
117
|
+
review: bool = True
|
|
118
|
+
review_max_fixes: int | None = Field(default=None, ge=0)
|
|
119
|
+
review_image_reasoning: Literal["low", "high"] = "high"
|
|
120
|
+
# Review call plus fixes stay within this estimate: fewer fixes when the review costs more.
|
|
121
|
+
review_cost_cap: float | None = Field(default=None, ge=0)
|
|
122
|
+
# QA tests (M10.4): the QA Playtester writes tests from each design version; every build runs
|
|
123
|
+
# them (and earlier versions' tests) with real key presses after the bot playtest.
|
|
124
|
+
qa: bool = True
|
|
125
|
+
# Code checks (L2.8): after the Programmer's first answer the QA Playtester reads the design
|
|
126
|
+
# and the code and writes checks of this game's own rules as JavaScript run in the game.
|
|
127
|
+
qa_checks: bool = True
|
|
128
|
+
# The bot playtest also looks at the HUD: texts drawn over each other or off the screen
|
|
129
|
+
# fail the build and go to the Programmer.
|
|
130
|
+
hud_check: bool = True
|
|
131
|
+
motion: bool = True # the Art Director's motion design: rows, forms, effects (L2.4)
|
|
132
|
+
# Menus and flow (L2.5): the Menu Designer's screens rendered by the SDK (title, pause,
|
|
133
|
+
# results, game over, victory, saved progress) and G3; title_art: Muse draws the title
|
|
134
|
+
# background and emblem (2 image calls; false or offline: code draws them).
|
|
135
|
+
menus: bool = True
|
|
136
|
+
title_art: bool = True
|
|
137
|
+
# Sounds (L2.7): the Sound Director's synthesised sounds (one Spark call per game), played by
|
|
138
|
+
# the SDK on the game's moments and edited in the build's sound editor or at G4.
|
|
139
|
+
sounds: bool = True
|
|
140
|
+
# Agent tools: in a live build each agent's Spark call may look things up with the tools its
|
|
141
|
+
# role grants (the built-in library and game-record MCP servers, its skills, and the user's
|
|
142
|
+
# MCP tools granted to it that need no confirmation), for up to `tool_rounds` rounds.
|
|
143
|
+
agent_tools: bool = True
|
|
144
|
+
tool_rounds: int = Field(default=3, ge=0, le=10)
|
|
145
|
+
max_code_kb: int = Field(default=200, ge=10, le=1000)
|
|
146
|
+
playtest_fps: int = Field(default=30, ge=10, le=60)
|
|
147
|
+
record_seconds: int = Field(default=20, ge=5, le=60)
|
|
148
|
+
|
|
149
|
+
@model_validator(mode="before")
|
|
150
|
+
@classmethod
|
|
151
|
+
def legacy_story_switch(cls, data):
|
|
152
|
+
"""Runs saved before L2.1 had `game.story`; the G1 brief now decides (a story only
|
|
153
|
+
when the human asks for one)."""
|
|
154
|
+
if isinstance(data, dict):
|
|
155
|
+
data = {k: v for k, v in data.items() if k != "story"}
|
|
156
|
+
return data
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class RouterConfig(Schema):
|
|
160
|
+
enabled: bool = True
|
|
161
|
+
max_skills: int = Field(default=3, ge=0, le=100)
|
|
162
|
+
max_tools: int = Field(default=5, ge=0, le=100)
|
|
163
|
+
token_budget: int = Field(default=4096, ge=0)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class Config(Schema):
|
|
167
|
+
workspace: Path
|
|
168
|
+
memory: MemoryConfig = Field(default_factory=MemoryConfig)
|
|
169
|
+
preview: PreviewConfig = Field(default_factory=PreviewConfig)
|
|
170
|
+
game: GameConfig = Field(default_factory=GameConfig)
|
|
171
|
+
router: RouterConfig = Field(default_factory=RouterConfig)
|
|
172
|
+
image: ImageConfig
|
|
173
|
+
llm: LLMConfig
|
|
174
|
+
research: ResearchConfig = Field(default_factory=ResearchConfig)
|
|
175
|
+
budget: BudgetConfig = Field(default_factory=BudgetConfig)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def load_config(path: Path | None = None) -> Config:
|
|
179
|
+
return Config.model_validate(
|
|
180
|
+
yaml.safe_load(
|
|
181
|
+
(path or Path(__file__).with_name("config.yaml")).read_text(encoding="utf-8")
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def init_workspace(path: Path) -> Path:
|
|
187
|
+
root = path.expanduser().resolve()
|
|
188
|
+
for name in ("runs", "memory", "skills"):
|
|
189
|
+
(root / name).mkdir(parents=True, exist_ok=True)
|
|
190
|
+
mcp = root / "mcp.json"
|
|
191
|
+
if not mcp.exists():
|
|
192
|
+
mcp.write_text('{"mcpServers": {}}\n', encoding="utf-8")
|
|
193
|
+
return root
|
spriteforge/config.yaml
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
workspace: ~/spriteforge-workspace
|
|
2
|
+
router:
|
|
3
|
+
enabled: true
|
|
4
|
+
max_skills: 3
|
|
5
|
+
max_tools: 5
|
|
6
|
+
token_budget: 4096 # conservative UTF-8 byte bound for loaded skill context
|
|
7
|
+
preview:
|
|
8
|
+
max_image_calls: 16
|
|
9
|
+
mood_board: true
|
|
10
|
+
mode: game # game = Spark writes the design and code (G2/G4 gates); muse = Muse blocks
|
|
11
|
+
game:
|
|
12
|
+
# Agent loops (Programmer repairs, Muse redraws, QA check fixes, Menu and Level retries, Art
|
|
13
|
+
# Reviewer rounds) try again while they improve. A loop stops after `patience` tries in a
|
|
14
|
+
# row without improvement, or when the agent has spent its budget.max_agent_cost ($10)
|
|
15
|
+
# since your last action. max_repairs, max_art_calls, art_retry and review_max_fixes are
|
|
16
|
+
# optional fixed limits on top (unset: none).
|
|
17
|
+
patience: 2
|
|
18
|
+
# Muse jobs run in parallel (Meta allows 150 image requests/min). Call slots are handed out
|
|
19
|
+
# in priority order before dispatch, so the cap and the greybox choice stay deterministic.
|
|
20
|
+
image_workers: 4
|
|
21
|
+
# The game's screen: 320x180 design pixels times screen_scale. 6 = 1920x1080 (a laptop's
|
|
22
|
+
# full-HD screen; sprites keep 6x the pixels Muse drew, text is 6x bigger); 4 = 1280x720.
|
|
23
|
+
screen_scale: 6
|
|
24
|
+
# The Level/Tile Designer studies the level design of similar platformers with web search
|
|
25
|
+
# (one search call per game, before the first level); its level prompts read the findings.
|
|
26
|
+
level_research: true
|
|
27
|
+
# Muse Image's strength for every first draw: high (the API has low and high). Redraws and
|
|
28
|
+
# the Art Reviewer's fixes use high as well.
|
|
29
|
+
image_reasoning: high
|
|
30
|
+
# Identical requests (model, endpoint, prompt, size, reasoning strength, n, reference
|
|
31
|
+
# hashes) reuse workspace/cache/muse-image/<sha256>.png: no call, no cost.
|
|
32
|
+
image_cache: true
|
|
33
|
+
# A frame that fails check_frame is redrawn (reasoning_strength high) while redraws improve.
|
|
34
|
+
native_grid_confidence: 0.6 # below this, Muse art is fitted instead of read cell by cell
|
|
35
|
+
# Muse sprites are drawn as sheets (a row of frames per animation, up to 4 rows): each
|
|
36
|
+
# frame gets a slot of the image, so Muse draws it with fewer, bigger pixels, close to the
|
|
37
|
+
# requested grid, and the sprite animates. 1 = one image per sprite.
|
|
38
|
+
sheet_frames: 4
|
|
39
|
+
# A Muse sprite's frame takes the shape Muse drew (about the design's area) instead of
|
|
40
|
+
# squeezing it into the design box; the Programmer sees the real size in assets.json.
|
|
41
|
+
adapt_size: true
|
|
42
|
+
# Art Reviewer: after the playtest, Spark (vision) looks at all the art together and at 3
|
|
43
|
+
# screenshots of the running game; blocking issues are fixed before G4 with Muse Image
|
|
44
|
+
# edits at its highest setting (the API has low and high), then it reviews again while the
|
|
45
|
+
# blocking issues go down (patience, its $10 allowance); the rest are G4 suggestions.
|
|
46
|
+
review: true
|
|
47
|
+
review_image_reasoning: high
|
|
48
|
+
# QA Playtester: tests written from each design version (one Spark call), played with real
|
|
49
|
+
# key presses after the bot playtest; failures join the repairs; exported per build as
|
|
50
|
+
# build/tests/test_game_qa.py (pytest + Playwright).
|
|
51
|
+
qa: true
|
|
52
|
+
# Motion design (L2.4): the Art Director writes every entity's animation rows (what each
|
|
53
|
+
# frame shows, fps, loop), how the player looks in each form and the effects the SDK plays
|
|
54
|
+
# (sf.fx); Muse draws the rows, code checks and repairs them. false = the design's names.
|
|
55
|
+
motion: true
|
|
56
|
+
# Menus and flow (L2.5): the Menu Designer writes the menus as data (one Spark call per game;
|
|
57
|
+
# feedback revises them), the SDK renders them in the art's colours and runs the flow (title,
|
|
58
|
+
# pause, results, game over, victory, progress saved in the browser); G3 shows every screen
|
|
59
|
+
# before the playtest. title_art: Muse draws the title background and an emblem (2 image
|
|
60
|
+
# calls, locked once drawn; false: code draws them from the game's art).
|
|
61
|
+
menus: true
|
|
62
|
+
title_art: true
|
|
63
|
+
# Sounds (L2.7): the Sound Director picks a synthesised sound for every moment (one Spark
|
|
64
|
+
# call; no audio files, no image calls); the SDK plays them, the build's sounds.html edits them.
|
|
65
|
+
sounds: true
|
|
66
|
+
# Agent tools: in a live build each agent may look things up before it answers, with the
|
|
67
|
+
# tools its role grants (allowed_tools in agents/roles/*.md: the built-in library and game
|
|
68
|
+
# record MCP servers), its skills (load_skill) and your MCP tools granted to it that need no
|
|
69
|
+
# confirmation. Each round is one more Spark call; false: one call per answer, no tools.
|
|
70
|
+
agent_tools: true
|
|
71
|
+
tool_rounds: 3
|
|
72
|
+
# (No story switch: the G1 brief decides. A story bible is written only when the human asks
|
|
73
|
+
# for one; every game keeps its full record as an OKF bundle in runs/<id>/game/db.)
|
|
74
|
+
max_code_kb: 200
|
|
75
|
+
playtest_fps: 30
|
|
76
|
+
record_seconds: 20
|
|
77
|
+
image:
|
|
78
|
+
# Live smoke 2026-10-28: both paths accept size=1024x1024, b64_json,
|
|
79
|
+
# output_format=png and reasoning_strength=low, but return 1600x1600 PNGs.
|
|
80
|
+
# One data-URL reference verified; the maximum reference count is unverified.
|
|
81
|
+
base_url: https://api.meta.ai
|
|
82
|
+
model: muse-image-1.0
|
|
83
|
+
generate_path: /v1/images/generations
|
|
84
|
+
edit_path: /v1/images/edits
|
|
85
|
+
# Request sizes (aspect ratio only). Art requests use the one nearest the subject's aspect.
|
|
86
|
+
# Live probe 2026-10-31, all accepted: 1024x1024 -> 1600x1600,
|
|
87
|
+
# 1792x1024 -> 2016x1152, 1024x1536 -> 1280x1920. With the grid wording Muse draws big
|
|
88
|
+
# square pixels but chooses its own cell count (16x16 asked: 21x27 cells, ~46 px each).
|
|
89
|
+
# Live probe 2026-10-31: 2048x512 (4:1) accepted -> 3200x800.
|
|
90
|
+
# Live probe 2026-10-31: 1536x512 (3:1) accepted -> 2736x912.
|
|
91
|
+
sizes: [[1024, 1024], [1792, 1024], [1024, 1536], [2048, 512], [1536, 512]]
|
|
92
|
+
llm:
|
|
93
|
+
base_url: https://api.meta.ai
|
|
94
|
+
model: muse-spark-1.3
|
|
95
|
+
chat_path: /v1/chat/completions
|
|
96
|
+
# Live probe 2026-10-30: strict json_schema {files:[{path,content}]} returned a complete
|
|
97
|
+
# 375-line Phaser game (8578 completion tokens, finish_reason stop, 43 s); it ran without
|
|
98
|
+
# errors. One call per game file set is enough; no output-length setting was needed.
|
|
99
|
+
# Live probe 2026-10-31: top-level reasoning_effort is accepted and
|
|
100
|
+
# works (same question: low 113-119 reasoning tokens, max 366-455). Docs list minimal,
|
|
101
|
+
# low, medium, high, xhigh, max ("none" -> HTTP 400). Prefix caching is automatic: a
|
|
102
|
+
# repeated 1558-token prompt reported cached_tokens 1521 on a second run, also across
|
|
103
|
+
# different efforts (0 on back-to-back calls of the very first run: the cache fills
|
|
104
|
+
# shortly after a request). Prompts start with their stable part and send a
|
|
105
|
+
# prompt_cache_key per task.
|
|
106
|
+
reasoning:
|
|
107
|
+
intake: low # G1 genre pick, brief draft, follow-up questions, style options
|
|
108
|
+
art_plan: low # Art Director's library picks
|
|
109
|
+
revision: low # Director splitting G4 feedback
|
|
110
|
+
design: high # Game Designer's design doc and its revisions
|
|
111
|
+
code: high # Programmer: new game, repairs, changes
|
|
112
|
+
review: max # Art Reviewer (vision): the strongest setting Spark accepts
|
|
113
|
+
describe: low # Librarian describing library images (spriteforge library describe)
|
|
114
|
+
qa: high # QA Playtester writing tests from the design
|
|
115
|
+
story: high # Game Designer writing the story bible
|
|
116
|
+
research: medium # Game Designer studying similar games with web search (L2.2)
|
|
117
|
+
plan: high # Game Designer writing the game plan from the requirements and research
|
|
118
|
+
level: high # Level/Tile Designer writing each level as data (L2.3)
|
|
119
|
+
motion: medium # Art Director's motion design: rows, forms, effects (L2.4)
|
|
120
|
+
menus: medium # Menu Designer: the menus as data and their revisions (L2.5)
|
|
121
|
+
sounds: low # Sound Director: a preset and tweaks per moment, and their revisions (L2.7)
|
|
122
|
+
lessons: low # Librarian: lessons from a finished game, shown at G5 (L2.6)
|
|
123
|
+
research:
|
|
124
|
+
# Live probe 2026-11-02: Chat Completions rejects {"type": "web_search"}
|
|
125
|
+
# (HTTP 400 "tools[0] did not match any supported type"); the Responses API accepts it.
|
|
126
|
+
# Citations: `url_citation` annotations on output_text (few), and with
|
|
127
|
+
# include ["web_search_call.action.sources"] every search lists the pages it returned
|
|
128
|
+
# (11-12 each). json_schema (text.format, strict) and web_search work in one call: the last
|
|
129
|
+
# `message` item holds the JSON (earlier ones are progress lines). A URL in the JSON is not
|
|
130
|
+
# always one a search returned, so code checks every reference URL against the sources.
|
|
131
|
+
# Searches are not in `usage`: each `web_search_call` item (search or open_page) is counted
|
|
132
|
+
# as one search (budget.web_search_cost). One probe research: 10.8k input + 2.1k output
|
|
133
|
+
# tokens (low effort) and 4 tool calls, about $0.03.
|
|
134
|
+
path: /v1/responses
|
|
135
|
+
max_searches: 8 # max_tool_calls per research
|
|
136
|
+
search_context_size: medium
|
|
137
|
+
budget:
|
|
138
|
+
max_image_calls: 150
|
|
139
|
+
max_calls_per_model: 200
|
|
140
|
+
image_cost: 0.01
|
|
141
|
+
# Standard Spark USD / million tokens, verified 2026-10-29:
|
|
142
|
+
# https://dev.meta.ai/docs/pricing-rate-limits
|
|
143
|
+
spark_input_per_million: 1.25
|
|
144
|
+
spark_cached_input_per_million: 0.15
|
|
145
|
+
spark_output_per_million: 4.25
|
|
146
|
+
# https://dev.meta.ai/docs/pricing-rate-limits 2026-11-02: $2.50 per 1,000 search queries
|
|
147
|
+
web_search_cost: 0.0025
|
|
148
|
+
max_estimated_cost: 5.0 # per run, legacy stages only (per_request: false)
|
|
149
|
+
# Each agent may spend max_agent_cost (USD) since your last action (a gate answer, Go back,
|
|
150
|
+
# Continue); its loops run until that is used up or they stop improving (game.patience).
|
|
151
|
+
# Work you ask for always starts.
|
|
152
|
+
per_request: true
|
|
153
|
+
max_agent_cost: 10
|
|
154
|
+
|
|
155
|
+
memory:
|
|
156
|
+
min_score: 0
|
|
157
|
+
relative_score: 0.5
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Which agent is making the model call now: the cost ledger records it with each call, so the
|
|
2
|
+
studio can show what every agent cost and which one is working."""
|
|
3
|
+
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
|
|
7
|
+
acting_agent: ContextVar[str | None] = ContextVar("acting_agent", default=None)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@contextmanager
|
|
11
|
+
def acting(agent: str):
|
|
12
|
+
token = acting_agent.set(agent)
|
|
13
|
+
try:
|
|
14
|
+
yield
|
|
15
|
+
finally:
|
|
16
|
+
acting_agent.reset(token)
|