feat: enhance agent provisioning by adding AUTONOMY.md and ensuring task dependencies are handled correctly

This commit is contained in:
Abhimanyu Saharan
2026-02-07 02:42:33 +05:30
parent c1d63f8178
commit a4442eb9d5
9 changed files with 123 additions and 29 deletions

View File

@@ -188,6 +188,8 @@ async def create_task(
if agent.board_id and agent.board_id != board.id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
session.add(task)
# Ensure the task exists in the DB before inserting dependency rows.
await session.flush()
for dep_id in normalized_deps:
session.add(
TaskDependency(

View File

@@ -28,6 +28,7 @@ from app.models.board_memory import BoardMemory
from app.models.board_onboarding import BoardOnboardingSession
from app.models.boards import Board
from app.models.gateways import Gateway
from app.models.task_dependencies import TaskDependency
from app.models.task_fingerprints import TaskFingerprint
from app.models.tasks import Task
from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate
@@ -228,21 +229,27 @@ async def delete_board(
if task_ids:
await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids)))
await session.execute(
delete(TaskFingerprint).where(col(TaskFingerprint.board_id) == board.id)
)
await session.execute(delete(TaskDependency).where(col(TaskDependency.board_id) == board.id))
await session.execute(delete(TaskFingerprint).where(col(TaskFingerprint.board_id) == board.id))
# Approvals can reference tasks and agents, so delete before both.
await session.execute(delete(Approval).where(col(Approval.board_id) == board.id))
await session.execute(delete(BoardMemory).where(col(BoardMemory.board_id) == board.id))
await session.execute(
delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id) == board.id)
)
# Tasks reference agents (assigned_agent_id) and have dependents (fingerprints/dependencies), so
# delete tasks before agents.
await session.execute(delete(Task).where(col(Task.board_id) == board.id))
if agents:
agent_ids = [agent.id for agent in agents]
await session.execute(
delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids))
)
await session.execute(delete(Agent).where(col(Agent.id).in_(agent_ids)))
await session.execute(delete(Approval).where(col(Approval.board_id) == board.id))
await session.execute(delete(BoardMemory).where(col(BoardMemory.board_id) == board.id))
await session.execute(
delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id) == board.id)
)
await session.execute(delete(Task).where(col(Task.board_id) == board.id))
await session.delete(board)
await session.commit()
return OkResponse()

View File

@@ -613,6 +613,8 @@ async def create_task(
if blocked_by and (task.assigned_agent_id is not None or task.status != "inbox"):
raise _blocked_task_error(blocked_by)
session.add(task)
# Ensure the task exists in the DB before inserting dependency rows.
await session.flush()
for dep_id in normalized_deps:
session.add(
TaskDependency(

View File

@@ -41,6 +41,8 @@ DEFAULT_GATEWAY_FILES = frozenset(
{
"AGENTS.md",
"SOUL.md",
"SELF.md",
"AUTONOMY.md",
"TOOLS.md",
"IDENTITY.md",
"USER.md",
@@ -51,6 +53,10 @@ DEFAULT_GATEWAY_FILES = frozenset(
}
)
# These files are intended to evolve within the agent workspace. Provision them if missing,
# but avoid overwriting existing content during updates.
PRESERVE_AGENT_EDITABLE_FILES = frozenset({"SELF.md", "AUTONOMY.md"})
HEARTBEAT_LEAD_TEMPLATE = "HEARTBEAT_LEAD.md"
HEARTBEAT_AGENT_TEMPLATE = "HEARTBEAT_AGENT.md"
MAIN_TEMPLATE_MAP = {
@@ -115,6 +121,25 @@ def _workspace_path(agent: Agent, workspace_root: str) -> str:
return f"{root}/workspace-{_slugify(key)}"
def _ensure_workspace_file(
workspace_path: str,
name: str,
content: str,
*,
overwrite: bool = False,
) -> None:
if not workspace_path or not name:
return
root = Path(workspace_path)
path = root / name
if not overwrite and path.exists():
return
root.mkdir(parents=True, exist_ok=True)
tmp_path = path.with_suffix(f"{path.suffix}.tmp")
tmp_path.write_text(content, encoding="utf-8")
tmp_path.replace(path)
def _build_context(
agent: Agent,
board: Board,
@@ -484,7 +509,7 @@ async def provision_agent(
context = _build_context(agent, board, gateway, auth_token, user)
supported = set(await _supported_gateway_files(client_config))
supported.add("USER.md")
supported.update({"USER.md", "SELF.md", "AUTONOMY.md"})
existing_files = await _gateway_agent_files_index(agent_id, client_config)
include_bootstrap = True
if action == "update" and not force_bootstrap:
@@ -501,9 +526,25 @@ async def provision_agent(
supported,
include_bootstrap=include_bootstrap,
)
# Ensure editable template files exist locally (best-effort) without overwriting.
for name in PRESERVE_AGENT_EDITABLE_FILES:
content = rendered.get(name)
if not content:
continue
try:
_ensure_workspace_file(workspace_path, name, content, overwrite=False)
except OSError:
# Local workspace may not be writable/available; fall back to gateway API.
pass
for name, content in rendered.items():
if content == "":
continue
if name in PRESERVE_AGENT_EDITABLE_FILES:
# Never overwrite; only provision if missing.
entry = existing_files.get(name)
if entry and entry.get("missing") is not True:
continue
try:
await openclaw_call(
"agents.files.set",
@@ -543,7 +584,7 @@ async def provision_main_agent(
context = _build_main_context(agent, gateway, auth_token, user)
supported = set(await _supported_gateway_files(client_config))
supported.add("USER.md")
supported.update({"USER.md", "SELF.md", "AUTONOMY.md"})
existing_files = await _gateway_agent_files_index(agent_id, client_config)
include_bootstrap = action != "update" or force_bootstrap
if action == "update" and not force_bootstrap:
@@ -564,6 +605,10 @@ async def provision_main_agent(
for name, content in rendered.items():
if content == "":
continue
if name in PRESERVE_AGENT_EDITABLE_FILES:
entry = existing_files.get(name)
if entry and entry.get("missing") is not True:
continue
try:
await openclaw_call(
"agents.files.set",