Skip to content
Closed
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
13 changes: 13 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ agent:
# chat = direct text-only dataset generation via an OpenAI-compatible API
provider: pi

# Trace each agent session to Langfuse (https://langfuse.com). Works for Codex
# and Claude Code -- each uses its own native integration (Codex plugin, Claude
# Code Stop hook) and Teich passes the credentials into the container. Side-
# channel only: it reads transcripts, fails open, and doesn't change agent
# behavior or Teich's output files. All three credentials are required when
# enabled. base_url may be Langfuse Cloud or a self-hosted URL; for a host-local
# instance use http://host.docker.internal:<port>.
# langfuse:
# enabled: true
# public_key: pk-lf-...
# secret_key: sk-lf-...
# base_url: https://cloud.langfuse.com

model:
# Model id passed to the selected agent/provider.
model: deepseek/deepseek-v4-flash
Expand Down
22 changes: 22 additions & 0 deletions docker/codex-runtime.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,28 @@ RUN --mount=type=cache,target=/root/.cache/uv \
chmod +x /usr/local/bin/hermes && \
(hermes --version || hermes --help >/dev/null)

# Bake the Langfuse Codex observability plugin into a staging CODEX_HOME so Teich
# can seed it into each session offline (no per-run network on the sandboxes).
# Only used when agent.langfuse.enabled is set; side-channel/observability
# only. NOTE: pulls the plugin at its current HEAD -- the version is pinned at
# image-build time, not by a commit hash. chmod a+rX so the unprivileged in-
# container `codex` user (and the host `docker cp`) can read it.
RUN HOME=/opt/codex-langfuse CODEX_HOME=/opt/codex-langfuse/.codex \
sh -c 'mkdir -p "$CODEX_HOME" \
&& codex plugin marketplace add langfuse/codex-observability-plugin \
&& codex plugin add tracing@codex-observability-plugin \
&& chmod -R a+rX /opt/codex-langfuse'

# Langfuse hook + SDK for Claude Code, pinned to a known-good commit so the
# baked hook script is reproducible. The SDK goes in the venv because Claude
# strips it from a hook's PATH, so the hook calls it by full path.
RUN /opt/venv/bin/pip install --no-cache-dir "langfuse>=4.0,<5" && \
git clone https://github.com/langfuse/Claude-Observability-Plugin.git \
/opt/claude-langfuse-plugin && \
git -C /opt/claude-langfuse-plugin checkout 597af67d6c6b369f3e55db6cfa2ebe444f1af46c && \
rm -rf /opt/claude-langfuse-plugin/.git && \
chmod -R a+rX /opt/claude-langfuse-plugin

# Create working directory and user in one layer
WORKDIR /workspace
RUN useradd -m -s /bin/bash codex && \
Expand Down
23 changes: 23 additions & 0 deletions src/teich/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,10 +716,20 @@ def generate(
try:
if agent_provider == "codex":
runner = CodexRunner(cfg)
if cfg.agent.langfuse.enabled:
console.print(
"[cyan]Codex Langfuse tracing enabled: uploading each session to "
f"{cfg.agent.langfuse.base_url}.[/cyan]"
)
elif agent_provider == "pi":
runner = PiRunner(cfg)
elif agent_provider in {"claude", "claude-code", "claude_code"}:
runner = ClaudeCodeRunner(cfg)
if cfg.agent.langfuse.enabled:
console.print(
"[cyan]Claude Code Langfuse tracing enabled: uploading each session to "
f"{cfg.agent.langfuse.base_url}.[/cyan]"
)
elif agent_provider in {"hermes", "hermes-agent", "hermes_agent"}:
runner = HermesRunner(cfg)
elif agent_provider == "chat":
Expand Down Expand Up @@ -1021,6 +1031,19 @@ def init(
# chat = direct text-only dataset generation via an OpenAI-compatible API
provider: pi

# Trace each agent session to Langfuse (https://langfuse.com). Works for Codex
# and Claude Code -- each uses its own native integration (Codex plugin, Claude
# Code Stop hook) and Teich passes the credentials into the container. Side-
# channel only: it reads transcripts, fails open, and doesn't change agent
# behavior or Teich's output files. All three credentials are required when
# enabled. base_url may be Langfuse Cloud or a self-hosted URL; for a host-local
# instance use http://host.docker.internal:<port>.
# langfuse:
# enabled: true
# public_key: pk-lf-...
# secret_key: sk-lf-...
# base_url: https://cloud.langfuse.com

model:
# Model id passed to the selected agent/provider.
model: deepseek/deepseek-v4-flash
Expand Down
34 changes: 34 additions & 0 deletions src/teich/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,43 @@ class APIConfig(BaseModel):
wire_api: str = "responses"


class LangfuseConfig(BaseModel):
"""Langfuse tracing credentials, set under ``agent.langfuse``.

When enabled, Teich wires each agent's Langfuse integration (the Codex
plugin, the Claude Code Stop hook) and passes these credentials into the
container. Tracing is side-channel only: it reads transcripts, fails open,
and does not change agent behavior or Teich's output files.
"""
enabled: bool = False
public_key: str | None = None
secret_key: str | None = None
base_url: str | None = None

@model_validator(mode="after")
def require_credentials_when_enabled(self) -> LangfuseConfig:
if self.enabled:
missing = [
name
for name, value in (
("public_key", self.public_key),
("secret_key", self.secret_key),
("base_url", self.base_url),
)
if not (value and value.strip())
]
if missing:
raise ValueError(
"langfuse requires " + ", ".join(missing) + " when enabled"
)
return self


class AgentConfig(BaseModel):
"""Agent runtime selection."""
provider: str = "codex"
# Langfuse tracing, applied to every agent that supports it (Codex, Claude).
langfuse: LangfuseConfig = Field(default_factory=LangfuseConfig)


class ModelConfig(BaseModel):
Expand Down
Loading
Loading