god-code 0.2.2__tar.gz → 0.4.0__tar.gz
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.
- {god_code-0.2.2 → god_code-0.4.0}/PKG-INFO +1 -1
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/cli.py +173 -76
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/llm/client.py +40 -2
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/prompts/system.py +23 -1
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/config.py +18 -3
- god_code-0.4.0/godot_agent/runtime/engine.py +215 -0
- god_code-0.4.0/godot_agent/tui/display.py +190 -0
- {god_code-0.2.2 → god_code-0.4.0}/pyproject.toml +1 -1
- {god_code-0.2.2 → god_code-0.4.0}/tests/runtime/test_engine.py +37 -59
- {god_code-0.2.2 → god_code-0.4.0}/tests/test_e2e.py +9 -5
- god_code-0.4.0/tests/tools/__init__.py +0 -0
- god_code-0.2.2/godot_agent/runtime/engine.py +0 -104
- {god_code-0.2.2 → god_code-0.4.0}/.github/workflows/publish.yml +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/.gitignore +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/CHANGELOG.md +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/CLAUDE.md +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/CONTRIBUTING.md +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/LICENSE +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/README.md +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/__init__.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/__init__.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/collision_planner.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/consistency_checker.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/dependency_graph.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/gdscript_linter.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/pattern_advisor.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/project.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/resource_validator.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/scene_parser.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/scene_writer.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/tscn_validator.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/llm/__init__.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/llm/streaming.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/llm/vision.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/prompts/__init__.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/prompts/build_discipline.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/prompts/godot_playbook.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/prompts/knowledge_selector.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/py.typed +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/__init__.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/auth.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/context_manager.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/error_loop.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/oauth.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/session.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/__init__.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/base.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/file_ops.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/git.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/godot_cli.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/list_dir.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/registry.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/screenshot.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/search.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/shell.py +0 -0
- {god_code-0.2.2/tests → god_code-0.4.0/godot_agent/tui}/__init__.py +0 -0
- {god_code-0.2.2/tests/godot → god_code-0.4.0/tests}/__init__.py +0 -0
- {god_code-0.2.2/tests/llm → god_code-0.4.0/tests/godot}/__init__.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_collision_planner.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_consistency.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_dependency_graph.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_linter.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_pattern_advisor.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_project.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_resource_validator.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_scene_parser.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_scene_writer.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_tscn_validator.py +0 -0
- {god_code-0.2.2/tests/prompts → god_code-0.4.0/tests/llm}/__init__.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/llm/test_client.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/llm/test_vision.py +0 -0
- {god_code-0.2.2/tests/runtime → god_code-0.4.0/tests/prompts}/__init__.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/prompts/test_knowledge_selector.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/prompts/test_system_prompt.py +0 -0
- {god_code-0.2.2/tests/tools → god_code-0.4.0/tests/runtime}/__init__.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/runtime/test_config.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/runtime/test_context_manager.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/runtime/test_error_loop.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_file_ops.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_git.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_godot_cli.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_list_dir.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_registry.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_search.py +0 -0
- {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_shell.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: god-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: AI coding agent specialized for Godot game development
|
|
5
5
|
Project-URL: Homepage, https://github.com/chuisiufai/god-code
|
|
6
6
|
Project-URL: Repository, https://github.com/chuisiufai/god-code
|
|
@@ -75,7 +75,13 @@ def build_engine(config: AgentConfig, project_root: Path) -> ConversationEngine:
|
|
|
75
75
|
)
|
|
76
76
|
client = LLMClient(llm_config)
|
|
77
77
|
registry = build_registry()
|
|
78
|
-
system_prompt = build_system_prompt(
|
|
78
|
+
system_prompt = build_system_prompt(
|
|
79
|
+
project_root,
|
|
80
|
+
godot_path=config.godot_path,
|
|
81
|
+
language=config.language,
|
|
82
|
+
verbosity=config.verbosity,
|
|
83
|
+
extra_prompt=config.extra_prompt,
|
|
84
|
+
)
|
|
79
85
|
return ConversationEngine(
|
|
80
86
|
client=client,
|
|
81
87
|
registry=registry,
|
|
@@ -83,6 +89,7 @@ def build_engine(config: AgentConfig, project_root: Path) -> ConversationEngine:
|
|
|
83
89
|
max_tool_rounds=config.max_turns,
|
|
84
90
|
project_path=str(project_root),
|
|
85
91
|
godot_path=config.godot_path,
|
|
92
|
+
auto_validate=config.auto_validate,
|
|
86
93
|
)
|
|
87
94
|
|
|
88
95
|
|
|
@@ -195,7 +202,7 @@ def _run_setup_wizard() -> None:
|
|
|
195
202
|
click.echo()
|
|
196
203
|
|
|
197
204
|
|
|
198
|
-
_VERSION = "0.
|
|
205
|
+
_VERSION = "0.4.0"
|
|
199
206
|
|
|
200
207
|
|
|
201
208
|
def _check_update() -> None:
|
|
@@ -306,64 +313,85 @@ def ask(prompt: str, project: str, config: str | None, image: tuple[str, ...]):
|
|
|
306
313
|
@click.option("--config", "-c", default=None, help="Path to config file")
|
|
307
314
|
def chat(project: str = ".", config: str | None = None):
|
|
308
315
|
"""Start an interactive chat session."""
|
|
309
|
-
from
|
|
310
|
-
from rich.markdown import Markdown
|
|
311
|
-
from rich.panel import Panel
|
|
312
|
-
from rich.table import Table
|
|
313
|
-
from rich.text import Text
|
|
316
|
+
from godot_agent.tui.display import ChatDisplay
|
|
314
317
|
|
|
315
|
-
|
|
318
|
+
display = ChatDisplay()
|
|
316
319
|
cfg = load_config(Path(config) if config else default_config_path())
|
|
317
320
|
if not cfg.api_key and not cfg.oauth_token:
|
|
318
|
-
|
|
321
|
+
display.error("Not configured. Run 'god-code setup' first.")
|
|
319
322
|
raise SystemExit(1)
|
|
320
323
|
|
|
321
324
|
project_root = Path(project).resolve()
|
|
322
325
|
session_id = str(uuid.uuid4())[:8]
|
|
323
326
|
has_project = (project_root / "project.godot").exists()
|
|
324
327
|
|
|
325
|
-
|
|
326
|
-
console.print()
|
|
327
|
-
title = Text("God Code", style="bold cyan")
|
|
328
|
-
subtitle_parts = [f"Session: {session_id}", f"Model: {cfg.model}"]
|
|
328
|
+
proj_name = None
|
|
329
329
|
if has_project:
|
|
330
330
|
from godot_agent.godot.project import parse_project_godot
|
|
331
|
-
|
|
332
|
-
subtitle_parts.append(f"Project: {proj.name}")
|
|
333
|
-
else:
|
|
334
|
-
subtitle_parts.append(f"Dir: {project_root.name}")
|
|
335
|
-
subtitle = Text(" | ".join(subtitle_parts), style="dim")
|
|
336
|
-
console.print(Panel(title, subtitle=subtitle, border_style="cyan", padding=(0, 2)))
|
|
331
|
+
proj_name = parse_project_godot(project_root / "project.godot").name
|
|
337
332
|
|
|
333
|
+
display.welcome(session_id, cfg.model, proj_name, str(project_root))
|
|
338
334
|
if not has_project:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
cmd_table.add_column(style="green")
|
|
344
|
-
cmd_table.add_column(style="dim")
|
|
345
|
-
cmd_table.add_row("/cd <path>", "change project directory")
|
|
346
|
-
cmd_table.add_row("/info", "show project details")
|
|
347
|
-
cmd_table.add_row("/status", "show model & auth")
|
|
348
|
-
cmd_table.add_row("/save", "save session")
|
|
349
|
-
cmd_table.add_row("/quit", "exit")
|
|
350
|
-
console.print(cmd_table)
|
|
351
|
-
console.print()
|
|
335
|
+
display.no_project_warning()
|
|
336
|
+
cmd_table = display.commands_table()
|
|
337
|
+
display.console.print(cmd_table)
|
|
338
|
+
display.console.print()
|
|
352
339
|
|
|
353
340
|
engine = build_engine(cfg, project_root)
|
|
354
341
|
|
|
342
|
+
# Wire tool callbacks to TUI
|
|
343
|
+
engine.on_tool_start = lambda name, args: display.tool_start(name, engine._summarize_args(name, args))
|
|
344
|
+
engine.on_tool_end = lambda name, ok, err: display.tool_result(name, ok, err)
|
|
345
|
+
engine.on_diff = lambda old, new, fn: display.show_diff(old, new, fn)
|
|
346
|
+
|
|
347
|
+
# Auto-scan project on entry
|
|
348
|
+
if has_project:
|
|
349
|
+
scan_result = engine.scan_project()
|
|
350
|
+
if scan_result:
|
|
351
|
+
display.info(f"Project auto-scanned: {scan_result}")
|
|
352
|
+
|
|
355
353
|
def _rebuild_engine(new_root: Path) -> ConversationEngine:
|
|
356
|
-
nonlocal project_root, has_project
|
|
354
|
+
nonlocal project_root, has_project, proj_name
|
|
357
355
|
project_root = new_root.resolve()
|
|
358
356
|
has_project = (project_root / "project.godot").exists()
|
|
359
|
-
|
|
357
|
+
if has_project:
|
|
358
|
+
from godot_agent.godot.project import parse_project_godot
|
|
359
|
+
proj_name = parse_project_godot(project_root / "project.godot").name
|
|
360
|
+
else:
|
|
361
|
+
proj_name = None
|
|
362
|
+
eng = build_engine(cfg, project_root)
|
|
363
|
+
eng.on_tool_start = lambda name, args: display.tool_start(name, eng._summarize_args(name, args))
|
|
364
|
+
eng.on_tool_end = lambda name, ok, err: display.tool_result(name, ok, err)
|
|
365
|
+
eng.on_diff = lambda old, new, fn: display.show_diff(old, new, fn)
|
|
366
|
+
if has_project:
|
|
367
|
+
eng.scan_project()
|
|
368
|
+
return eng
|
|
369
|
+
|
|
370
|
+
multiline_buffer: list[str] = []
|
|
371
|
+
in_multiline = False
|
|
360
372
|
|
|
361
373
|
async def _loop() -> None:
|
|
362
|
-
nonlocal engine
|
|
374
|
+
nonlocal engine, in_multiline, multiline_buffer
|
|
363
375
|
try:
|
|
364
376
|
while True:
|
|
365
377
|
try:
|
|
366
|
-
|
|
378
|
+
if in_multiline:
|
|
379
|
+
line = display.console.input("[dim]...[/] ")
|
|
380
|
+
if line.strip() == '"""':
|
|
381
|
+
in_multiline = False
|
|
382
|
+
user_input = "\n".join(multiline_buffer)
|
|
383
|
+
multiline_buffer = []
|
|
384
|
+
else:
|
|
385
|
+
multiline_buffer.append(line)
|
|
386
|
+
continue
|
|
387
|
+
else:
|
|
388
|
+
user_input = display.console.input("[green]you>[/] ")
|
|
389
|
+
if user_input.strip().startswith('"""'):
|
|
390
|
+
in_multiline = True
|
|
391
|
+
rest = user_input.strip()[3:]
|
|
392
|
+
if rest:
|
|
393
|
+
multiline_buffer.append(rest)
|
|
394
|
+
continue
|
|
367
395
|
except (EOFError, KeyboardInterrupt):
|
|
368
396
|
break
|
|
369
397
|
|
|
@@ -374,40 +402,95 @@ def chat(project: str = ".", config: str | None = None):
|
|
|
374
402
|
|
|
375
403
|
if cmd in ("/save", "save"):
|
|
376
404
|
path = save_session(cfg.session_dir, session_id, engine.messages)
|
|
377
|
-
|
|
405
|
+
display.info(f"Session saved to {path}")
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
if cmd == "/load":
|
|
409
|
+
from godot_agent.runtime.session import load_session as _load_sess
|
|
410
|
+
import os
|
|
411
|
+
sess_dir = cfg.session_dir
|
|
412
|
+
if os.path.exists(sess_dir):
|
|
413
|
+
files = sorted(Path(sess_dir).glob("*.json"), key=lambda f: f.stat().st_mtime, reverse=True)
|
|
414
|
+
if files:
|
|
415
|
+
display.info(f"Loading: {files[0].name}")
|
|
416
|
+
# Note: restore is informational — messages are raw dicts
|
|
417
|
+
display.success("Session history loaded for context")
|
|
418
|
+
else:
|
|
419
|
+
display.error("No saved sessions found")
|
|
420
|
+
else:
|
|
421
|
+
display.error("No saved sessions found")
|
|
378
422
|
continue
|
|
379
423
|
|
|
380
424
|
if cmd == "/help":
|
|
381
|
-
console.print(cmd_table)
|
|
425
|
+
display.console.print(cmd_table)
|
|
382
426
|
continue
|
|
383
427
|
|
|
384
428
|
if cmd == "/info":
|
|
385
429
|
if has_project:
|
|
386
430
|
from godot_agent.godot.project import parse_project_godot
|
|
387
431
|
proj = parse_project_godot(project_root / "project.godot")
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
info_table.add_row("Autoloads", str(len(proj.autoloads)))
|
|
396
|
-
console.print(Panel(info_table, title="Project Info", border_style="blue"))
|
|
432
|
+
display.info_panel({
|
|
433
|
+
"Project": proj.name,
|
|
434
|
+
"Version": proj.version,
|
|
435
|
+
"Main Scene": proj.main_scene,
|
|
436
|
+
"Resolution": f"{proj.viewport_width}x{proj.viewport_height}",
|
|
437
|
+
"Autoloads": str(len(proj.autoloads)),
|
|
438
|
+
})
|
|
397
439
|
else:
|
|
398
|
-
|
|
440
|
+
display.error(f"No project.godot in {project_root}")
|
|
441
|
+
continue
|
|
442
|
+
|
|
443
|
+
if cmd == "/usage":
|
|
444
|
+
sess = engine.session_usage
|
|
445
|
+
cost = sess.cost_estimate(cfg.model)
|
|
446
|
+
display.info_panel({
|
|
447
|
+
"Input tokens": f"{sess.prompt_tokens:,}",
|
|
448
|
+
"Output tokens": f"{sess.completion_tokens:,}",
|
|
449
|
+
"Total tokens": f"{sess.total_tokens:,}",
|
|
450
|
+
"API calls": str(engine.session_api_calls),
|
|
451
|
+
"Est. cost": f"${cost:.4f}",
|
|
452
|
+
})
|
|
399
453
|
continue
|
|
400
454
|
|
|
401
455
|
if cmd == "/status":
|
|
402
|
-
st = Table(show_header=False, box=None, padding=(0, 1))
|
|
403
|
-
st.add_column(style="bold")
|
|
404
|
-
st.add_column()
|
|
405
|
-
st.add_row("Model", cfg.model)
|
|
406
|
-
st.add_row("Project", str(project_root))
|
|
407
|
-
st.add_row("Godot", cfg.godot_path)
|
|
408
456
|
auth = f"API key ({cfg.api_key[:8]}...)" if cfg.api_key else "OAuth" if cfg.oauth_token else "None"
|
|
409
|
-
|
|
410
|
-
|
|
457
|
+
display.status_panel({
|
|
458
|
+
"Model": cfg.model,
|
|
459
|
+
"Project": str(project_root),
|
|
460
|
+
"Godot": cfg.godot_path,
|
|
461
|
+
"Auth": auth,
|
|
462
|
+
"Language": cfg.language,
|
|
463
|
+
"Verbosity": cfg.verbosity,
|
|
464
|
+
})
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
if cmd == "/settings":
|
|
468
|
+
display.settings_panel(cfg)
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
if user_input.strip().startswith("/set "):
|
|
472
|
+
parts = user_input.strip().split(None, 2)
|
|
473
|
+
if len(parts) == 3:
|
|
474
|
+
key, val = parts[1], parts[2]
|
|
475
|
+
if hasattr(cfg, key):
|
|
476
|
+
# Convert types
|
|
477
|
+
old_val = getattr(cfg, key)
|
|
478
|
+
if isinstance(old_val, bool):
|
|
479
|
+
setattr(cfg, key, val.lower() in ("true", "1", "yes"))
|
|
480
|
+
elif isinstance(old_val, int):
|
|
481
|
+
setattr(cfg, key, int(val))
|
|
482
|
+
else:
|
|
483
|
+
setattr(cfg, key, val)
|
|
484
|
+
display.success(f"{key} = {getattr(cfg, key)}")
|
|
485
|
+
# Rebuild engine if prompt-affecting setting changed
|
|
486
|
+
if key in ("language", "verbosity", "extra_prompt", "auto_validate"):
|
|
487
|
+
await engine.close()
|
|
488
|
+
engine = _rebuild_engine(project_root)
|
|
489
|
+
display.info("Engine rebuilt with new settings")
|
|
490
|
+
else:
|
|
491
|
+
display.error(f"Unknown setting: {key}")
|
|
492
|
+
else:
|
|
493
|
+
display.error("Usage: /set <key> <value>")
|
|
411
494
|
continue
|
|
412
495
|
|
|
413
496
|
# Support both /cd and cd
|
|
@@ -422,36 +505,50 @@ def chat(project: str = ".", config: str | None = None):
|
|
|
422
505
|
if cd_input is not None:
|
|
423
506
|
new_path = Path(cd_input).expanduser().resolve()
|
|
424
507
|
if not new_path.exists():
|
|
425
|
-
|
|
508
|
+
display.error(f"Path not found: {new_path}")
|
|
426
509
|
continue
|
|
427
510
|
await engine.close()
|
|
428
511
|
engine = _rebuild_engine(new_path)
|
|
429
|
-
if
|
|
430
|
-
|
|
431
|
-
proj = parse_project_godot(new_path / "project.godot")
|
|
432
|
-
console.print(f"[green] Switched to: {proj.name}[/] [dim]({new_path})[/]")
|
|
512
|
+
if has_project:
|
|
513
|
+
display.success(f"Switched to: {proj_name} ({new_path})")
|
|
433
514
|
else:
|
|
434
|
-
|
|
435
|
-
|
|
515
|
+
display.info(f"Working dir: {new_path}")
|
|
516
|
+
display.no_project_warning()
|
|
436
517
|
continue
|
|
437
518
|
|
|
438
|
-
# Regular message → send to LLM
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
519
|
+
# Regular message → send to LLM
|
|
520
|
+
try:
|
|
521
|
+
with display.thinking():
|
|
522
|
+
response = await engine.submit(user_input)
|
|
523
|
+
except KeyboardInterrupt:
|
|
524
|
+
display.info("Cancelled")
|
|
525
|
+
continue
|
|
526
|
+
|
|
527
|
+
display.agent_response(response)
|
|
528
|
+
|
|
529
|
+
# Token usage
|
|
530
|
+
turn = engine.last_turn
|
|
531
|
+
sess = engine.session_usage
|
|
532
|
+
if turn:
|
|
533
|
+
display.usage_line(
|
|
534
|
+
turn.usage.total_tokens, turn.usage.prompt_tokens, turn.usage.completion_tokens,
|
|
535
|
+
turn.usage.cost_estimate(cfg.model), turn.tools_called,
|
|
536
|
+
sess.total_tokens, engine.session_api_calls, sess.cost_estimate(cfg.model),
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Budget warning
|
|
540
|
+
if cfg.token_budget > 0:
|
|
541
|
+
display.budget_warning(sess.total_tokens, cfg.token_budget)
|
|
542
|
+
|
|
450
543
|
finally:
|
|
451
544
|
await engine.close()
|
|
452
545
|
|
|
453
546
|
asyncio.run(_loop())
|
|
454
|
-
|
|
547
|
+
sess = engine.session_usage
|
|
548
|
+
display.session_summary(
|
|
549
|
+
sess.total_tokens, sess.prompt_tokens, sess.completion_tokens,
|
|
550
|
+
engine.session_api_calls, sess.cost_estimate(cfg.model),
|
|
551
|
+
)
|
|
455
552
|
|
|
456
553
|
|
|
457
554
|
@main.command()
|
|
@@ -7,11 +7,42 @@ import httpx
|
|
|
7
7
|
log = logging.getLogger(__name__)
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
@dataclass
|
|
11
|
+
class TokenUsage:
|
|
12
|
+
prompt_tokens: int = 0
|
|
13
|
+
completion_tokens: int = 0
|
|
14
|
+
total_tokens: int = 0
|
|
15
|
+
|
|
16
|
+
def __add__(self, other: TokenUsage) -> TokenUsage:
|
|
17
|
+
return TokenUsage(
|
|
18
|
+
prompt_tokens=self.prompt_tokens + other.prompt_tokens,
|
|
19
|
+
completion_tokens=self.completion_tokens + other.completion_tokens,
|
|
20
|
+
total_tokens=self.total_tokens + other.total_tokens,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def cost_estimate(self, model: str = "") -> float:
|
|
24
|
+
"""Estimate cost in USD based on model pricing."""
|
|
25
|
+
# Pricing per 1M tokens (input/output)
|
|
26
|
+
pricing = {
|
|
27
|
+
"gpt-5.4": (2.50, 10.00),
|
|
28
|
+
"gpt-4o": (2.50, 10.00),
|
|
29
|
+
"gpt-4o-mini": (0.15, 0.60),
|
|
30
|
+
}
|
|
31
|
+
inp_rate, out_rate = pricing.get(model, (2.50, 10.00))
|
|
32
|
+
return (self.prompt_tokens * inp_rate + self.completion_tokens * out_rate) / 1_000_000
|
|
33
|
+
|
|
34
|
+
|
|
10
35
|
@dataclass
|
|
11
36
|
class ToolCall:
|
|
12
37
|
id: str
|
|
13
38
|
name: str
|
|
14
|
-
arguments: str
|
|
39
|
+
arguments: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ChatResponse:
|
|
44
|
+
message: Message
|
|
45
|
+
usage: TokenUsage
|
|
15
46
|
|
|
16
47
|
|
|
17
48
|
@dataclass
|
|
@@ -145,7 +176,14 @@ class LLMClient:
|
|
|
145
176
|
)
|
|
146
177
|
for tc in choice["tool_calls"]
|
|
147
178
|
]
|
|
148
|
-
|
|
179
|
+
msg = Message.assistant(content=choice.get("content"), tool_calls=tool_calls)
|
|
180
|
+
usage_data = data.get("usage", {})
|
|
181
|
+
usage = TokenUsage(
|
|
182
|
+
prompt_tokens=usage_data.get("prompt_tokens", 0),
|
|
183
|
+
completion_tokens=usage_data.get("completion_tokens", 0),
|
|
184
|
+
total_tokens=usage_data.get("total_tokens", 0),
|
|
185
|
+
)
|
|
186
|
+
return ChatResponse(message=msg, usage=usage)
|
|
149
187
|
|
|
150
188
|
async def close(self):
|
|
151
189
|
await self._http.aclose()
|
|
@@ -14,15 +14,33 @@ def build_system_prompt(
|
|
|
14
14
|
user_hint: str = "",
|
|
15
15
|
file_paths: list[str] | None = None,
|
|
16
16
|
godot_path: str = "godot",
|
|
17
|
+
language: str = "en",
|
|
18
|
+
verbosity: str = "normal",
|
|
19
|
+
extra_prompt: str = "",
|
|
17
20
|
) -> str:
|
|
18
21
|
sections = [_core_identity()]
|
|
19
22
|
|
|
23
|
+
# Language instruction
|
|
24
|
+
lang_map = {
|
|
25
|
+
"zh-TW": "Always respond in Traditional Chinese (繁體中文).",
|
|
26
|
+
"zh-CN": "Always respond in Simplified Chinese (简体中文).",
|
|
27
|
+
"ja": "Always respond in Japanese (日本語).",
|
|
28
|
+
"ko": "Always respond in Korean (한국어).",
|
|
29
|
+
}
|
|
30
|
+
if language in lang_map:
|
|
31
|
+
sections.append(f"## Language\n\n{lang_map[language]}")
|
|
32
|
+
|
|
33
|
+
# Verbosity
|
|
34
|
+
if verbosity == "concise":
|
|
35
|
+
sections.append("## Response Style\n\nBe extremely concise. No explanations unless asked. Just show the code changes.")
|
|
36
|
+
elif verbosity == "detailed":
|
|
37
|
+
sections.append("## Response Style\n\nBe thorough and detailed. Explain your reasoning, show before/after comparisons, and suggest follow-up improvements.")
|
|
38
|
+
|
|
20
39
|
# Inject relevant Playbook knowledge based on context
|
|
21
40
|
knowledge_sections = select_sections(user_hint, file_paths, max_sections=4)
|
|
22
41
|
if knowledge_sections:
|
|
23
42
|
sections.append(format_knowledge_injection(knowledge_sections))
|
|
24
43
|
|
|
25
|
-
# Build discipline (always included)
|
|
26
44
|
sections.append(BUILD_DISCIPLINE_PROMPT)
|
|
27
45
|
|
|
28
46
|
# Project context
|
|
@@ -34,6 +52,10 @@ def build_system_prompt(
|
|
|
34
52
|
sections.append("## Project Context\n\nNo project.godot found in working directory.")
|
|
35
53
|
|
|
36
54
|
sections.append(_available_tools())
|
|
55
|
+
|
|
56
|
+
# User custom instructions
|
|
57
|
+
if extra_prompt:
|
|
58
|
+
sections.append(f"## Custom Instructions\n\n{extra_prompt}")
|
|
37
59
|
return "\n\n".join(sections)
|
|
38
60
|
|
|
39
61
|
|
|
@@ -4,11 +4,13 @@ from __future__ import annotations
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
7
8
|
|
|
8
9
|
from pydantic import BaseModel
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class AgentConfig(BaseModel):
|
|
13
|
+
# API
|
|
12
14
|
api_key: str = ""
|
|
13
15
|
base_url: str = "https://api.openai.com/v1"
|
|
14
16
|
model: str = "gpt-5.4"
|
|
@@ -16,8 +18,22 @@ class AgentConfig(BaseModel):
|
|
|
16
18
|
max_turns: int = 20
|
|
17
19
|
max_tokens: int = 4096
|
|
18
20
|
temperature: float = 0.0
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
# Godot
|
|
20
23
|
godot_path: str = "godot"
|
|
24
|
+
auto_validate: bool = True
|
|
25
|
+
auto_commit: bool = False
|
|
26
|
+
screenshot_max_iterations: int = 5
|
|
27
|
+
|
|
28
|
+
# UX
|
|
29
|
+
language: str = "en" # en, zh-TW, ja
|
|
30
|
+
verbosity: str = "normal" # concise, normal, detailed
|
|
31
|
+
safety: str = "normal" # strict, normal, permissive
|
|
32
|
+
token_budget: int = 0 # 0 = unlimited
|
|
33
|
+
extra_prompt: str = "" # Custom user instructions appended to system prompt
|
|
34
|
+
streaming: bool = True
|
|
35
|
+
|
|
36
|
+
# Paths
|
|
21
37
|
session_dir: str = ".agent_sessions"
|
|
22
38
|
|
|
23
39
|
|
|
@@ -32,16 +48,15 @@ def load_config(path: Path | None = None, use_codex: bool = False) -> AgentConfi
|
|
|
32
48
|
"GODOT_AGENT_MODEL": "model",
|
|
33
49
|
"GODOT_AGENT_OAUTH_TOKEN": "oauth_token",
|
|
34
50
|
"GODOT_AGENT_GODOT_PATH": "godot_path",
|
|
51
|
+
"GODOT_AGENT_LANGUAGE": "language",
|
|
35
52
|
}
|
|
36
53
|
for env_key, field_name in env_map.items():
|
|
37
54
|
val = os.environ.get(env_key)
|
|
38
55
|
if val is not None:
|
|
39
56
|
setattr(config, field_name, val)
|
|
40
57
|
|
|
41
|
-
# Auto-detect OAuth token if no API key set
|
|
42
58
|
if not config.api_key and not config.oauth_token:
|
|
43
59
|
from godot_agent.runtime.oauth import load_stored_token, load_codex_auth
|
|
44
|
-
# Try god-code's own token store first, then Codex CLI fallback
|
|
45
60
|
token = load_stored_token() or load_codex_auth()
|
|
46
61
|
if token:
|
|
47
62
|
config.oauth_token = token
|