mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Add AGENTS.md protocol guidance
Load active-project AGENTS.md path-chain guidance into the protocol area without duplicating the project root instructions. Move the AGENTS.md protocol wording into a prompt template, reuse existing file/path helpers, and cover direct-path discovery plus prompt assembly with focused tests.
This commit is contained in:
parent
f8b06e0b12
commit
bcf2634000
8 changed files with 203 additions and 10 deletions
|
|
@ -7,7 +7,7 @@
|
|||
## Ownership
|
||||
|
||||
- Ordered Python files own main, tools, MCP, secrets, skills, and project prompt sections.
|
||||
- Active project instruction bodies are moved into prompt protocol; the system prompt keeps project metadata and stable project rules.
|
||||
- Active project instruction bodies and active-project AGENTS.md path-chain guidance are moved into prompt protocol; the system prompt keeps project metadata and stable project rules.
|
||||
|
||||
## Local Contracts
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,16 @@ async def build_prompt(agent: Agent, loop_data: LoopData | None = None) -> str:
|
|||
result = agent.read_prompt("agent.system.projects.main.md")
|
||||
project_name = agent.context.get_data(projects.CONTEXT_DATA_KEY_PROJECT)
|
||||
if loop_data:
|
||||
loop_data.protocol_persistent.pop("agents_md_instructions", None)
|
||||
loop_data.protocol_persistent.pop("project_instructions", None)
|
||||
if project_name:
|
||||
project_vars = projects.build_system_prompt_vars(project_name)
|
||||
if loop_data and project_vars.get("include_agents_md", True):
|
||||
agents_md_protocol = projects.build_agents_md_protocol(project_name)
|
||||
if agents_md_protocol:
|
||||
loop_data.protocol_persistent["agents_md_instructions"] = (
|
||||
agents_md_protocol
|
||||
)
|
||||
if loop_data and project_vars.get("project_instructions"):
|
||||
loop_data.protocol_persistent["project_instructions"] = agent.read_prompt(
|
||||
"agent.protocol.projects.instructions.md",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
- Preserve public helper APIs used by core code and plugins unless all callers, docs, and tests are updated.
|
||||
- Use structured parsers and serializers for YAML, JSON, paths, and URLs instead of ad hoc string handling.
|
||||
- Keep path handling constrained to intended roots for user files, uploads, downloads, projects, and workdirs.
|
||||
- Project metadata defaults must remain backwards-compatible; missing `include_agents_md` is treated as enabled and project instruction file content is injected with an explicit source path.
|
||||
- Project metadata defaults must remain backwards-compatible; missing `include_agents_md` is treated as enabled, project instruction file content is injected with an explicit source path, and active-project AGENTS.md path-chain guidance is assembled into prompt protocol without duplicating the project root AGENTS.md.
|
||||
- Do not hardcode secrets, provider keys, local absolute paths, or environment-specific values.
|
||||
- Use `RepairableException` for errors an agent may be able to fix.
|
||||
- This directory is a file-documented DOX profile: every direct `*.py` helper module must have a same-directory `*.py.dox.md` file named by appending `.dox.md` to the full Python filename.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from typing import Literal, NotRequired, TypedDict, TYPE_CHECKING, cast
|
||||
from typing import NotRequired, TypedDict, TYPE_CHECKING, cast
|
||||
|
||||
from helpers import files, dirty_json, persist_chat, file_tree, extension
|
||||
from helpers.print_style import PrintStyle
|
||||
|
|
@ -15,7 +15,13 @@ PROJECT_KNOWLEDGE_DIR = "knowledge"
|
|||
PROJECT_SKILLS_DIR = "skills"
|
||||
PROJECT_HEADER_FILE = "project.json"
|
||||
PROJECT_MCP_SERVERS_FILE = "mcp_servers.json"
|
||||
PROJECT_AGENTS_MD_FILES = ("AGENTS.md", "Agents.md", "agents.md")
|
||||
PROJECT_AGENTS_MD_FILES = (
|
||||
"AGENTS.override.md",
|
||||
"AGENTS.Override.md",
|
||||
"AGENTS.md",
|
||||
"Agents.md",
|
||||
"agents.md",
|
||||
)
|
||||
DEFAULT_MCP_SERVERS_CONFIG = '{\n "mcpServers": {}\n}'
|
||||
|
||||
CONTEXT_DATA_KEY_PROJECT = "project"
|
||||
|
|
@ -466,9 +472,10 @@ def deactivate_project_in_chats(name: str):
|
|||
def build_system_prompt_vars(name: str):
|
||||
project_data = load_basic_project_data(name)
|
||||
main_instructions = project_data.get("instructions", "") or ""
|
||||
include_agents_md = project_data.get("include_agents_md", True)
|
||||
instruction_files = get_project_instruction_files(
|
||||
name,
|
||||
include_agents_md=project_data.get("include_agents_md", True),
|
||||
include_agents_md=include_agents_md,
|
||||
)
|
||||
instruction_parts = [
|
||||
main_instructions,
|
||||
|
|
@ -481,11 +488,75 @@ def build_system_prompt_vars(name: str):
|
|||
"project_name": project_data.get("title", ""),
|
||||
"project_description": project_data.get("description", ""),
|
||||
"project_instructions": complete_instructions or "",
|
||||
"include_agents_md": include_agents_md,
|
||||
"project_path": files.normalize_a0_path(get_project_folder(name)),
|
||||
"project_git_url": project_data.get("git_url", ""),
|
||||
}
|
||||
|
||||
|
||||
def get_agents_md_chain(root: str, target: str) -> list[tuple[str, str]]:
|
||||
root_real = os.path.realpath(files.fix_dev_path(root))
|
||||
target_real = os.path.realpath(files.fix_dev_path(target))
|
||||
if os.path.isfile(target_real):
|
||||
target_real = os.path.dirname(target_real)
|
||||
|
||||
if files.is_in_dir(target_real, root_real):
|
||||
dirs = []
|
||||
cursor = target_real
|
||||
while True:
|
||||
dirs.append(cursor)
|
||||
if cursor == root_real:
|
||||
break
|
||||
parent = os.path.dirname(cursor)
|
||||
if parent == cursor:
|
||||
break
|
||||
cursor = parent
|
||||
dirs.reverse()
|
||||
else:
|
||||
dirs = [root_real]
|
||||
|
||||
chain = []
|
||||
for dir_path in dirs:
|
||||
for filename in PROJECT_AGENTS_MD_FILES:
|
||||
matches = files.read_text_files_in_dir(dir_path, pattern=filename)
|
||||
if filename not in matches:
|
||||
continue
|
||||
chain.append((files.get_abs_path(dir_path, filename), matches[filename]))
|
||||
break
|
||||
return chain
|
||||
|
||||
|
||||
def build_agents_md_protocol(name: str, target: str | None = None) -> str:
|
||||
project_folder = get_project_folder(name)
|
||||
project_agents_md = get_project_agents_md_instruction_file(name)
|
||||
project_agents_md_path = (
|
||||
os.path.realpath(files.fix_dev_path(project_agents_md[0]))
|
||||
if project_agents_md
|
||||
else ""
|
||||
)
|
||||
entries = [
|
||||
(path, content)
|
||||
for path, content in get_agents_md_chain(
|
||||
files.get_abs_path(""),
|
||||
target or project_folder,
|
||||
)
|
||||
if os.path.realpath(path) != project_agents_md_path
|
||||
]
|
||||
if not entries:
|
||||
return ""
|
||||
|
||||
instructions = []
|
||||
for path, content in entries:
|
||||
instructions.append(
|
||||
f"### path: {files.normalize_a0_path(path)}\n\n{content.strip()}"
|
||||
)
|
||||
return files.read_prompt_file(
|
||||
"agent.protocol.projects.agents_md.md",
|
||||
_directories=["prompts"],
|
||||
agents_md_instructions="\n\n".join(instructions),
|
||||
).strip()
|
||||
|
||||
|
||||
def get_additional_instructions_files(name: str):
|
||||
instructions_folder = files.get_abs_path(
|
||||
get_project_folder(name), PROJECT_META_DIR, PROJECT_INSTRUCTIONS_DIR
|
||||
|
|
@ -522,11 +593,8 @@ def get_project_instruction_files(
|
|||
|
||||
def get_project_agents_md_instruction_file(name: str) -> tuple[str, str] | None:
|
||||
project_folder = get_project_folder(name)
|
||||
for filename in PROJECT_AGENTS_MD_FILES:
|
||||
matches = files.read_text_files_in_dir(project_folder, pattern=filename)
|
||||
if filename in matches:
|
||||
path = files.get_abs_path(project_folder, filename)
|
||||
return (files.normalize_a0_path(path), matches[filename])
|
||||
for path, content in get_agents_md_chain(project_folder, project_folder):
|
||||
return (files.normalize_a0_path(path), content)
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@
|
|||
- `reactivate_project_in_chats(name: str)`
|
||||
- `deactivate_project_in_chats(name: str)`
|
||||
- `build_system_prompt_vars(name: str)`
|
||||
- `get_agents_md_chain(root: str, target: str) -> list[tuple[str, str]]`
|
||||
- `build_agents_md_protocol(name: str, target: str | None=...) -> str`
|
||||
- `get_additional_instructions_files(name: str)`
|
||||
- `get_project_instruction_files(name: str, include_agents_md: bool=...) -> list[tuple[str, str]]`
|
||||
- `get_project_agents_md_instruction_file(name: str) -> tuple[str, str] | None`
|
||||
|
|
@ -63,6 +65,8 @@
|
|||
- Project extension data may add named top-level sections such as `llm`, but it must not overwrite core project fields owned by `EditProjectData`.
|
||||
- Project extension save payloads exclude core project fields and transient inputs such as `git_token`; plugins needing core metadata should load it by project name.
|
||||
- Project metadata setup creates and repairs `.a0proj/instructions`, `.a0proj/knowledge`, and `.a0proj/skills` so settings surfaces can open those folders consistently.
|
||||
- AGENTS.md discovery is a linear root-to-target chain walk with `AGENTS.override.md` precedence; sibling directories are not scanned.
|
||||
- Active-project AGENTS.md protocol guidance excludes the exact project root AGENTS.md because `build_system_prompt_vars(...)` already loads it into project instructions; prose for that protocol block lives in `prompts/agent.protocol.projects.agents_md.md`.
|
||||
- Project MCP config uses the same JSON string shape as global MCP settings: an object with `mcpServers`.
|
||||
- Project MCP load/save paths validate project names as simple folder basenames before touching `.a0proj/mcp_servers.json`.
|
||||
- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, plugin state, settings/state persistence, secret handling.
|
||||
|
|
|
|||
6
prompts/agent.protocol.projects.agents_md.md
Normal file
6
prompts/agent.protocol.projects.agents_md.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# AGENTS.md instructions
|
||||
- these path-owned instructions are binding for the current project/workdir chain
|
||||
- active project root AGENTS.md is loaded separately in project instructions
|
||||
- before editing a deeper target, read AGENTS.md files on the direct path to that target; ignore siblings
|
||||
|
||||
{{agents_md_instructions}}
|
||||
|
|
@ -107,6 +107,20 @@ def test_project_system_prompt_includes_root_agents_md_with_path(monkeypatch, tm
|
|||
assert "Folder instruction rule." in instructions
|
||||
|
||||
|
||||
def test_project_system_prompt_prefers_agents_override_md(monkeypatch, tmp_path):
|
||||
_prepare_project_tree(monkeypatch, tmp_path)
|
||||
projects.create_project("demo", {"title": "Demo"})
|
||||
project_root = tmp_path / "usr" / "projects" / "demo"
|
||||
(project_root / "AGENTS.md").write_text("Standard rule.", encoding="utf-8")
|
||||
(project_root / "AGENTS.override.md").write_text("Override rule.", encoding="utf-8")
|
||||
|
||||
instructions = projects.build_system_prompt_vars("demo")["project_instructions"]
|
||||
|
||||
assert "### path: /a0/usr/projects/demo/AGENTS.override.md" in instructions
|
||||
assert "Override rule." in instructions
|
||||
assert "Standard rule." not in instructions
|
||||
|
||||
|
||||
def test_project_system_prompt_respects_disabled_agents_md(monkeypatch, tmp_path):
|
||||
_prepare_project_tree(monkeypatch, tmp_path)
|
||||
projects.create_project(
|
||||
|
|
@ -123,3 +137,51 @@ def test_project_system_prompt_respects_disabled_agents_md(monkeypatch, tmp_path
|
|||
|
||||
assert "Root AGENTS rule." not in prompt_vars["project_instructions"]
|
||||
assert "AGENTS.md" not in prompt_vars["project_instructions"]
|
||||
|
||||
|
||||
def test_agents_md_chain_walks_direct_path_only(monkeypatch, tmp_path):
|
||||
_prepare_project_tree(monkeypatch, tmp_path)
|
||||
root = tmp_path
|
||||
(root / "AGENTS.md").write_text("root doc", encoding="utf-8")
|
||||
target = root / "services" / "payments"
|
||||
sibling = root / "services" / "auth"
|
||||
target.mkdir(parents=True)
|
||||
sibling.mkdir(parents=True)
|
||||
(root / "services" / "AGENTS.md").write_text("services doc", encoding="utf-8")
|
||||
(target / "AGENTS.md").write_text("payments doc", encoding="utf-8")
|
||||
(sibling / "AGENTS.md").write_text("auth doc", encoding="utf-8")
|
||||
|
||||
chain = projects.get_agents_md_chain(str(root), str(target / "handler.py"))
|
||||
contents = [content for _, content in chain]
|
||||
|
||||
assert contents == ["root doc", "services doc", "payments doc"]
|
||||
|
||||
|
||||
def test_agents_md_protocol_excludes_project_root_and_keeps_subdir(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
_prepare_project_tree(monkeypatch, tmp_path)
|
||||
prompt_name = "agent.protocol.projects.agents_md.md"
|
||||
prompt_source = Path(__file__).resolve().parents[1] / "prompts" / prompt_name
|
||||
prompt_dir = tmp_path / "prompts"
|
||||
prompt_dir.mkdir()
|
||||
(prompt_dir / prompt_name).write_text(
|
||||
prompt_source.read_text(encoding="utf-8"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
projects.create_project("demo", {"title": "Demo"})
|
||||
(tmp_path / "AGENTS.md").write_text("framework doc", encoding="utf-8")
|
||||
project_root = tmp_path / "usr" / "projects" / "demo"
|
||||
(project_root / "AGENTS.md").write_text("project root doc", encoding="utf-8")
|
||||
api_dir = project_root / "api"
|
||||
api_dir.mkdir()
|
||||
(api_dir / "AGENTS.md").write_text("api doc", encoding="utf-8")
|
||||
|
||||
protocol = projects.build_agents_md_protocol(
|
||||
"demo",
|
||||
target=str(api_dir / "handler.py"),
|
||||
)
|
||||
|
||||
assert "framework doc" in protocol
|
||||
assert "api doc" in protocol
|
||||
assert "project root doc" not in protocol
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ async def test_project_prompt_moves_project_instructions_to_protocol(monkeypatch
|
|||
"project_name": "Demo",
|
||||
"project_description": "",
|
||||
"project_instructions": "Project rule.",
|
||||
"include_agents_md": True,
|
||||
"project_path": "/a0/usr/projects/demo",
|
||||
"project_git_url": "",
|
||||
}
|
||||
|
|
@ -121,6 +122,11 @@ async def test_project_prompt_moves_project_instructions_to_protocol(monkeypatch
|
|||
"build_system_prompt_vars",
|
||||
lambda _name: project_vars,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_prompt.projects,
|
||||
"build_agents_md_protocol",
|
||||
lambda _name: "AGENTS path rule.",
|
||||
)
|
||||
|
||||
prompt = await project_prompt.build_prompt.__wrapped__( # type: ignore[attr-defined]
|
||||
FakeAgent(),
|
||||
|
|
@ -128,4 +134,44 @@ async def test_project_prompt_moves_project_instructions_to_protocol(monkeypatch
|
|||
)
|
||||
|
||||
assert "Project rule." not in prompt
|
||||
assert "AGENTS path rule." not in prompt
|
||||
assert list(loop_data.protocol_persistent) == [
|
||||
"agents_md_instructions",
|
||||
"project_instructions",
|
||||
]
|
||||
assert "AGENTS path rule." in loop_data.protocol_persistent["agents_md_instructions"]
|
||||
assert "Project rule." in loop_data.protocol_persistent["project_instructions"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_project_prompt_does_not_load_agents_md_without_project(monkeypatch):
|
||||
loop_data = LoopData()
|
||||
|
||||
class FakeContext:
|
||||
def get_data(self, key):
|
||||
assert key == project_prompt.projects.CONTEXT_DATA_KEY_PROJECT
|
||||
return None
|
||||
|
||||
class FakeAgent:
|
||||
context = FakeContext()
|
||||
|
||||
def read_prompt(self, prompt_file: str, **kwargs) -> str:
|
||||
if prompt_file == "agent.system.projects.main.md":
|
||||
return "project context may be active"
|
||||
if prompt_file == "agent.system.projects.inactive.md":
|
||||
return "no active project"
|
||||
raise AssertionError(f"Unexpected prompt file: {prompt_file}")
|
||||
|
||||
monkeypatch.setattr(
|
||||
project_prompt.projects,
|
||||
"build_agents_md_protocol",
|
||||
lambda _name: (_ for _ in ()).throw(AssertionError("unexpected AGENTS load")),
|
||||
)
|
||||
|
||||
await project_prompt.build_prompt.__wrapped__( # type: ignore[attr-defined]
|
||||
FakeAgent(),
|
||||
loop_data=loop_data,
|
||||
)
|
||||
|
||||
assert "agents_md_instructions" not in loop_data.protocol_persistent
|
||||
assert "project_instructions" not in loop_data.protocol_persistent
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue