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.
Files changed (85) hide show
  1. {god_code-0.2.2 → god_code-0.4.0}/PKG-INFO +1 -1
  2. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/cli.py +173 -76
  3. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/llm/client.py +40 -2
  4. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/prompts/system.py +23 -1
  5. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/config.py +18 -3
  6. god_code-0.4.0/godot_agent/runtime/engine.py +215 -0
  7. god_code-0.4.0/godot_agent/tui/display.py +190 -0
  8. {god_code-0.2.2 → god_code-0.4.0}/pyproject.toml +1 -1
  9. {god_code-0.2.2 → god_code-0.4.0}/tests/runtime/test_engine.py +37 -59
  10. {god_code-0.2.2 → god_code-0.4.0}/tests/test_e2e.py +9 -5
  11. god_code-0.4.0/tests/tools/__init__.py +0 -0
  12. god_code-0.2.2/godot_agent/runtime/engine.py +0 -104
  13. {god_code-0.2.2 → god_code-0.4.0}/.github/workflows/publish.yml +0 -0
  14. {god_code-0.2.2 → god_code-0.4.0}/.gitignore +0 -0
  15. {god_code-0.2.2 → god_code-0.4.0}/CHANGELOG.md +0 -0
  16. {god_code-0.2.2 → god_code-0.4.0}/CLAUDE.md +0 -0
  17. {god_code-0.2.2 → god_code-0.4.0}/CONTRIBUTING.md +0 -0
  18. {god_code-0.2.2 → god_code-0.4.0}/LICENSE +0 -0
  19. {god_code-0.2.2 → god_code-0.4.0}/README.md +0 -0
  20. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/__init__.py +0 -0
  21. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/__init__.py +0 -0
  22. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/collision_planner.py +0 -0
  23. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/consistency_checker.py +0 -0
  24. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/dependency_graph.py +0 -0
  25. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/gdscript_linter.py +0 -0
  26. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/pattern_advisor.py +0 -0
  27. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/project.py +0 -0
  28. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/resource_validator.py +0 -0
  29. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/scene_parser.py +0 -0
  30. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/scene_writer.py +0 -0
  31. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/godot/tscn_validator.py +0 -0
  32. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/llm/__init__.py +0 -0
  33. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/llm/streaming.py +0 -0
  34. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/llm/vision.py +0 -0
  35. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/prompts/__init__.py +0 -0
  36. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/prompts/build_discipline.py +0 -0
  37. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/prompts/godot_playbook.py +0 -0
  38. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/prompts/knowledge_selector.py +0 -0
  39. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/py.typed +0 -0
  40. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/__init__.py +0 -0
  41. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/auth.py +0 -0
  42. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/context_manager.py +0 -0
  43. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/error_loop.py +0 -0
  44. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/oauth.py +0 -0
  45. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/runtime/session.py +0 -0
  46. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/__init__.py +0 -0
  47. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/base.py +0 -0
  48. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/file_ops.py +0 -0
  49. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/git.py +0 -0
  50. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/godot_cli.py +0 -0
  51. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/list_dir.py +0 -0
  52. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/registry.py +0 -0
  53. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/screenshot.py +0 -0
  54. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/search.py +0 -0
  55. {god_code-0.2.2 → god_code-0.4.0}/godot_agent/tools/shell.py +0 -0
  56. {god_code-0.2.2/tests → god_code-0.4.0/godot_agent/tui}/__init__.py +0 -0
  57. {god_code-0.2.2/tests/godot → god_code-0.4.0/tests}/__init__.py +0 -0
  58. {god_code-0.2.2/tests/llm → god_code-0.4.0/tests/godot}/__init__.py +0 -0
  59. {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_collision_planner.py +0 -0
  60. {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_consistency.py +0 -0
  61. {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_dependency_graph.py +0 -0
  62. {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_linter.py +0 -0
  63. {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_pattern_advisor.py +0 -0
  64. {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_project.py +0 -0
  65. {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_resource_validator.py +0 -0
  66. {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_scene_parser.py +0 -0
  67. {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_scene_writer.py +0 -0
  68. {god_code-0.2.2 → god_code-0.4.0}/tests/godot/test_tscn_validator.py +0 -0
  69. {god_code-0.2.2/tests/prompts → god_code-0.4.0/tests/llm}/__init__.py +0 -0
  70. {god_code-0.2.2 → god_code-0.4.0}/tests/llm/test_client.py +0 -0
  71. {god_code-0.2.2 → god_code-0.4.0}/tests/llm/test_vision.py +0 -0
  72. {god_code-0.2.2/tests/runtime → god_code-0.4.0/tests/prompts}/__init__.py +0 -0
  73. {god_code-0.2.2 → god_code-0.4.0}/tests/prompts/test_knowledge_selector.py +0 -0
  74. {god_code-0.2.2 → god_code-0.4.0}/tests/prompts/test_system_prompt.py +0 -0
  75. {god_code-0.2.2/tests/tools → god_code-0.4.0/tests/runtime}/__init__.py +0 -0
  76. {god_code-0.2.2 → god_code-0.4.0}/tests/runtime/test_config.py +0 -0
  77. {god_code-0.2.2 → god_code-0.4.0}/tests/runtime/test_context_manager.py +0 -0
  78. {god_code-0.2.2 → god_code-0.4.0}/tests/runtime/test_error_loop.py +0 -0
  79. {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_file_ops.py +0 -0
  80. {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_git.py +0 -0
  81. {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_godot_cli.py +0 -0
  82. {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_list_dir.py +0 -0
  83. {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_registry.py +0 -0
  84. {god_code-0.2.2 → god_code-0.4.0}/tests/tools/test_search.py +0 -0
  85. {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.2.2
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(project_root, godot_path=config.godot_path)
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.2.2"
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 rich.console import Console
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
- console = Console()
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
- console.print("[yellow]Not configured. Run 'god-code setup' first.[/]")
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
- # Welcome banner
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
- proj = parse_project_godot(project_root / "project.godot")
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
- console.print("[yellow] No project.godot found. Use /cd to navigate to a Godot project.[/]")
340
-
341
- # Commands table
342
- cmd_table = Table(show_header=False, box=None, padding=(0, 2), show_edge=False)
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
- return build_engine(cfg, project_root)
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
- user_input = console.input("[green]you>[/] ")
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
- console.print(f" [dim]Session saved to {path}[/]")
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
- info_table = Table(show_header=False, box=None, padding=(0, 1))
389
- info_table.add_column(style="bold")
390
- info_table.add_column()
391
- info_table.add_row("Project", proj.name)
392
- info_table.add_row("Version", proj.version)
393
- info_table.add_row("Main Scene", proj.main_scene)
394
- info_table.add_row("Resolution", f"{proj.viewport_width}x{proj.viewport_height}")
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
- console.print(f"[yellow] No project.godot in {project_root}[/]")
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
- st.add_row("Auth", auth)
410
- console.print(Panel(st, title="Status", border_style="blue"))
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
- console.print(f"[red] Path not found: {new_path}[/]")
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 (new_path / "project.godot").exists():
430
- from godot_agent.godot.project import parse_project_godot
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
- console.print(f" Working dir: {new_path}")
435
- console.print("[yellow] No project.godot found here.[/]")
515
+ display.info(f"Working dir: {new_path}")
516
+ display.no_project_warning()
436
517
  continue
437
518
 
438
- # Regular message → send to LLM with spinner
439
- with console.status("[cyan]Thinking...[/]", spinner="dots"):
440
- response = await engine.submit(user_input)
441
-
442
- console.print()
443
- console.print(Panel(
444
- Markdown(response),
445
- title="[cyan]agent[/]",
446
- border_style="cyan",
447
- padding=(1, 2),
448
- ))
449
- console.print()
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
- console.print("[dim]Session ended.[/]")
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 # JSON string
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
- return Message.assistant(content=choice.get("content"), tool_calls=tool_calls)
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
- screenshot_max_iterations: int = 5
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