diff --git a/src/bub/agent/core.py b/src/bub/agent/core.py
index fc5bb55f..b1c648ae 100644
--- a/src/bub/agent/core.py
+++ b/src/bub/agent/core.py
@@ -10,6 +10,7 @@
from republic import LLM, Tool
from ..errors import ModelNotConfiguredError
+from ..skills import build_skills_prompt_section
from .context import Context
MODEL_NOT_CONFIGURED_ERROR = "Model not configured. Set BUB_MODEL (e.g., 'openai:gpt-4o-mini')."
@@ -105,11 +106,20 @@ def respond(
on_event: Callable[[ToolEvent], None] | None = None,
) -> str:
"""Run a tool-aware response and return assistant text."""
- if self._system_prompt:
- messages = [{"role": "system", "content": self._system_prompt}, *messages]
-
+ inserted_system = False
observation_fingerprints: dict[str, str] = {}
while True:
+ system_prompt = self._build_system_prompt()
+ if system_prompt:
+ if inserted_system:
+ messages[0] = {"role": "system", "content": system_prompt}
+ else:
+ messages = [{"role": "system", "content": system_prompt}, *messages]
+ inserted_system = True
+ elif inserted_system:
+ messages = messages[1:]
+ inserted_system = False
+
response = self._llm.chat.raw(
messages=messages,
tools=self._tools,
@@ -134,6 +144,13 @@ def respond(
return text or ""
+ def _build_system_prompt(self) -> str:
+ base_prompt = self._system_prompt.strip()
+ skills_section = build_skills_prompt_section(self._context.workspace_path)
+ if not skills_section:
+ return base_prompt
+ return f"{base_prompt}\n\n{skills_section}"
+
def _execute_tools(
self,
tool_calls: list[dict[str, Any]],
diff --git a/src/bub/config.py b/src/bub/config.py
index c465ff5d..fb427e97 100644
--- a/src/bub/config.py
+++ b/src/bub/config.py
@@ -5,8 +5,6 @@
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
-from .skills import build_skills_prompt_section
-
class Settings(BaseSettings):
"""Bub application settings."""
@@ -87,16 +85,4 @@ def get_settings(workspace_path: Path | None = None) -> Settings:
if agents_md_content:
settings.system_prompt = agents_md_content.strip()
- skills_section = build_skills_prompt_section(workspace_path)
- if skills_section:
- settings.system_prompt = _append_prompt_section(settings.system_prompt, skills_section)
-
return settings
-
-
-def _append_prompt_section(base: str | None, section: str) -> str:
- base_text = (base or "").strip()
- section_text = section.strip()
- if not base_text:
- return section_text
- return f"{base_text}\n\n{section_text}"
diff --git a/src/bub/skills/skill-creator/scripts/init_skill.py b/src/bub/skills/skill-creator/scripts/init_skill.py
index f90703ec..f722b98d 100644
--- a/src/bub/skills/skill-creator/scripts/init_skill.py
+++ b/src/bub/skills/skill-creator/scripts/init_skill.py
@@ -18,7 +18,6 @@
import sys
from pathlib import Path
-from generate_openai_yaml import write_openai_yaml
MAX_SKILL_NAME_LENGTH = 64
ALLOWED_RESOURCES = {"scripts", "references", "assets"}
@@ -296,15 +295,6 @@ def init_skill(skill_name, path, resources, include_examples, interface_override
print(f"[ERROR] Error creating SKILL.md: {e}")
return None
- # Create agents/openai.yaml
- try:
- result = write_openai_yaml(skill_dir, skill_name, interface_overrides)
- if not result:
- return None
- except Exception as e:
- print(f"[ERROR] Error creating agents/openai.yaml: {e}")
- return None
-
# Create resource directories if requested
if resources:
try:
diff --git a/tests/test_agent_contract.py b/tests/test_agent_contract.py
index 376d821d..850ded9e 100644
--- a/tests/test_agent_contract.py
+++ b/tests/test_agent_contract.py
@@ -4,6 +4,7 @@
import json
from dataclasses import dataclass
+from pathlib import Path
from types import SimpleNamespace
from typing import Any, ClassVar
@@ -77,6 +78,15 @@ def _handler(event: ToolEvent) -> None:
return _handler
+def _write_skill(root: Path, folder: str, *, name: str, description: str) -> None:
+ skill_dir = root / folder
+ skill_dir.mkdir(parents=True, exist_ok=True)
+ (skill_dir / "SKILL.md").write_text(
+ (f"---\nname: {name}\ndescription: {description}\n---\n\nSkill instructions.\n"),
+ encoding="utf-8",
+ )
+
+
def test_tool_result_payload_is_structured_json(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("BUB_MODEL", "openai:gpt-4o-mini")
monkeypatch.setattr("bub.agent.core.LLM", _FakeLLM)
@@ -163,3 +173,96 @@ def test_agent_allows_completion_with_verification_evidence(tmp_path, monkeypatc
result = agent.respond([{"role": "user", "content": "please verify completion and then conclude"}])
assert result == "verified completion"
+
+
+def test_agent_refreshes_skills_per_respond(tmp_path, monkeypatch) -> None:
+ monkeypatch.setenv("BUB_MODEL", "openai:gpt-4o-mini")
+ monkeypatch.setenv("HOME", str(tmp_path / "home"))
+
+ captured_system_prompts: list[str] = []
+
+ class _CaptureChat:
+ def raw(self, *, messages: list[dict[str, Any]], tools: list[Any], max_tokens: int | None = None) -> object:
+ _ = (tools, max_tokens)
+ content = messages[0].get("content", "")
+ captured_system_prompts.append(str(content))
+ return _text_response("done")
+
+ class _CaptureLLM:
+ def __init__(self, model: str, api_key: str | None = None, api_base: str | None = None) -> None:
+ _ = (api_key, api_base)
+ provider, model_name = model.split(":", 1)
+ self.provider = provider
+ self.model = model_name
+ self.chat = _CaptureChat()
+ self.tools = _FakeTools(outputs=["ok"])
+
+ monkeypatch.setattr("bub.agent.core.LLM", _CaptureLLM)
+
+ project_skills = tmp_path / ".agent" / "skills"
+ _write_skill(project_skills, "alpha", name="alpha-skill", description="alpha")
+
+ agent = Agent(context=Context(tmp_path), tools=[SimpleNamespace(name="fs_read")])
+ first = agent.respond([{"role": "user", "content": "first"}])
+ assert first == "done"
+
+ _write_skill(project_skills, "beta", name="beta-skill", description="beta")
+ second = agent.respond([{"role": "user", "content": "second"}])
+ assert second == "done"
+
+ assert len(captured_system_prompts) == 2
+ assert "alpha-skill" in captured_system_prompts[0]
+ assert "beta-skill" not in captured_system_prompts[0]
+ assert "beta-skill" in captured_system_prompts[1]
+
+
+def test_agent_refreshes_skills_within_same_respond_loop(tmp_path, monkeypatch) -> None:
+ monkeypatch.setenv("BUB_MODEL", "openai:gpt-4o-mini")
+ monkeypatch.setenv("HOME", str(tmp_path / "home"))
+ project_skills = tmp_path / ".agent" / "skills"
+ _write_skill(project_skills, "alpha", name="alpha-skill", description="alpha")
+
+ captured_system_prompts: list[str] = []
+
+ class _LoopChat:
+ def __init__(self) -> None:
+ self.calls = 0
+
+ def raw(self, *, messages: list[dict[str, Any]], tools: list[Any], max_tokens: int | None = None) -> object:
+ _ = max_tokens
+ content = messages[0].get("content", "")
+ captured_system_prompts.append(str(content))
+ self.calls += 1
+ if tools and self.calls == 1:
+ return _tool_call_response(name="fs_read", arguments='{"path":"foo.txt"}')
+ return _text_response("done")
+
+ class _LoopTools:
+ def __init__(self) -> None:
+ self.calls = 0
+
+ def execute(self, call: dict[str, Any], *, tools: list[Any] | None = None) -> str:
+ _ = (call, tools)
+ if self.calls == 0:
+ _write_skill(project_skills, "beta", name="beta-skill", description="beta")
+ self.calls += 1
+ return "ok"
+
+ class _LoopLLM:
+ def __init__(self, model: str, api_key: str | None = None, api_base: str | None = None) -> None:
+ _ = (api_key, api_base)
+ provider, model_name = model.split(":", 1)
+ self.provider = provider
+ self.model = model_name
+ self.chat = _LoopChat()
+ self.tools = _LoopTools()
+
+ monkeypatch.setattr("bub.agent.core.LLM", _LoopLLM)
+
+ agent = Agent(context=Context(tmp_path), tools=[SimpleNamespace(name="fs_read")])
+ result = agent.respond([{"role": "user", "content": "check"}])
+
+ assert result == "done"
+ assert len(captured_system_prompts) >= 2
+ assert "beta-skill" not in captured_system_prompts[0]
+ assert "beta-skill" in captured_system_prompts[1]
diff --git a/tests/test_bub.py b/tests/test_bub.py
index 95ee8dee..4814feab 100644
--- a/tests/test_bub.py
+++ b/tests/test_bub.py
@@ -22,12 +22,10 @@ def test_settings_with_agents_md(self, tmp_path, monkeypatch):
agents_md = tmp_path / "AGENTS.md"
agents_md.write_text("System prompt from AGENTS.md")
settings = get_settings(tmp_path)
- assert settings.system_prompt is not None
- assert settings.system_prompt.startswith("System prompt from AGENTS.md")
- assert "" in settings.system_prompt
+ assert settings.system_prompt == "System prompt from AGENTS.md"
def test_settings_with_skills_section(self, tmp_path, monkeypatch):
- """Test settings include available skills metadata."""
+ """Test settings do not inline available skills metadata."""
monkeypatch.setenv("HOME", str(tmp_path / "home"))
skill_dir = tmp_path / ".agent" / "skills" / "code_review"
skill_dir.mkdir(parents=True)
@@ -38,9 +36,7 @@ def test_settings_with_skills_section(self, tmp_path, monkeypatch):
settings = get_settings(tmp_path)
assert settings.system_prompt is not None
- assert "" in settings.system_prompt
- assert "code-review" in settings.system_prompt
- assert "code_review/SKILL.md" in settings.system_prompt
+ assert "" not in settings.system_prompt
class TestTools: