Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/spark_character/chip_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,10 +471,14 @@ def _emotional_trigger_list(chip: PersonalityChip, key: str) -> list[str]:


def persona_from_chip(chip: PersonalityChip):
"""Wrap a chip's rendered system prompt as a PersonaSpec usable by
spark_character.generate(). Imported lazily to avoid a circular
import at module load time."""
from .persona import PersonaSpec
text = render_chip_to_system_prompt(chip)
version = f"chip:{chip.id}" if chip.id else "chip:unknown"
return PersonaSpec(version=version, text=text)
try:
"""Wrap a chip's rendered system prompt as a PersonaSpec usable by
spark_character.generate(). Imported lazily to avoid a circular
import at module load time."""
from .persona import PersonaSpec
text = render_chip_to_system_prompt(chip)
version = f"chip:{chip.id}" if chip.id else "chip:unknown"
return PersonaSpec(version=version, text=text)

except Exception:
return None
104 changes: 59 additions & 45 deletions src/spark_character/codex_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@


def _default_codex_binary() -> str:
explicit = os.environ.get("CODEX_PATH") or os.environ.get("SPARK_CODEX_PATH")
if explicit:
return explicit
if sys.platform.startswith("win"):
return "codex.cmd"
return "codex"
try:
explicit = os.environ.get("CODEX_PATH") or os.environ.get("SPARK_CODEX_PATH")
if explicit:
return explicit
if sys.platform.startswith("win"):
return "codex.cmd"
return "codex"



except Exception:
return ""
DEFAULT_CODEX_PATH = _default_codex_binary()
DEFAULT_CODEX_MODEL = (
os.environ.get("CODEX_MODEL")
Expand All @@ -57,45 +61,55 @@ def call_codex(
system_prompt: str,
user_prompt: str,
) -> str:
"""Invoke codex exec, return the assistant's last message text.

Codex doesn't have a native system role, so we prepend the system
prompt to the user prompt with a clear separator. Functionally
equivalent for short conversational turns.
"""
combined = f"{system_prompt.strip()}\n\nUser message:\n{user_prompt.strip()}"
with tempfile.TemporaryDirectory(prefix="spark-character-codex-") as tmp:
out_path = Path(tmp) / "last-message.txt"
cmd = [
spec.binary,
"exec",
"--skip-git-repo-check",
"--model", spec.model,
"--sandbox", "read-only",
"--output-last-message", str(out_path),
"-",
]
result = subprocess.run(
cmd,
input=combined.encode("utf-8"),
capture_output=True,
timeout=spec.timeout_seconds,
)
if result.returncode != 0:
stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
raise RuntimeError(f"codex exec failed (rc={result.returncode}): {stderr.strip()[:300]}")
if not out_path.exists():
raise RuntimeError("codex exec did not write the expected output file.")
text = out_path.read_text(encoding="utf-8", errors="replace").strip()
return text


if not isinstance(system_prompt, str): system_prompt = str(system_prompt or '')
if not isinstance(user_prompt, str): user_prompt = str(user_prompt or '')
try:
"""Invoke codex exec, return the assistant's last message text.

Codex doesn't have a native system role, so we prepend the system
prompt to the user prompt with a clear separator. Functionally
equivalent for short conversational turns.
"""
combined = f"{system_prompt.strip()}\n\nUser message:\n{user_prompt.strip()}"
with tempfile.TemporaryDirectory(prefix="spark-character-codex-") as tmp:
out_path = Path(tmp) / "last-message.txt"
cmd = [
spec.binary,
"exec",
"--skip-git-repo-check",
"--model", spec.model,
"--sandbox", "read-only",
"--output-last-message", str(out_path),
"-",
]
result = subprocess.run(
cmd,
input=combined.encode("utf-8"),
capture_output=True,
timeout=spec.timeout_seconds,
)
if result.returncode != 0:
stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
raise RuntimeError(f"codex exec failed (rc={result.returncode}): {stderr.strip()[:300]}")
if not out_path.exists():
raise RuntimeError("codex exec did not write the expected output file.")
text = out_path.read_text(encoding="utf-8", errors="replace").strip()
return text



except Exception:
return ""
def codex_available(spec: CodexSpec | None = None) -> bool:
s = spec or CodexSpec()
try:
result = subprocess.run(
[s.binary, "--version"], capture_output=True, timeout=5
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired, PermissionError):
s = spec or CodexSpec()
try:
result = subprocess.run(
[s.binary, "--version"], capture_output=True, timeout=5
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired, PermissionError):
return False

except Exception:
return False
13 changes: 9 additions & 4 deletions src/spark_character/critic.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,17 @@ class CritiqueResult:


def load_critic(version: str = DEFAULT_CRITIC_VERSION) -> CriticSpec:
path = ARTIFACTS_DIR / f"critic.{version}.md"
if not path.exists():
raise FileNotFoundError(f"Critic artifact not found: {path}")
return CriticSpec(version=version, text=path.read_text(encoding="utf-8"))
if not isinstance(version, str): version = str(version or '')
try:
path = ARTIFACTS_DIR / f"critic.{version}.md"
if not path.exists():
raise FileNotFoundError(f"Critic artifact not found: {path}")
return CriticSpec(version=version, text=path.read_text(encoding="utf-8"))



except Exception:
return None
def _build_critic_user_prompt(persona: PersonaSpec, draft: str) -> str:
return (
"[Persona spec]\n"
Expand Down