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: if agent.board_id and agent.board_id != board.id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
session.add(task) session.add(task)
# Ensure the task exists in the DB before inserting dependency rows.
await session.flush()
for dep_id in normalized_deps: for dep_id in normalized_deps:
session.add( session.add(
TaskDependency( TaskDependency(

View File

@@ -28,6 +28,7 @@ from app.models.board_memory import BoardMemory
from app.models.board_onboarding import BoardOnboardingSession from app.models.board_onboarding import BoardOnboardingSession
from app.models.boards import Board from app.models.boards import Board
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.models.task_dependencies import TaskDependency
from app.models.task_fingerprints import TaskFingerprint from app.models.task_fingerprints import TaskFingerprint
from app.models.tasks import Task from app.models.tasks import Task
from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate
@@ -228,21 +229,27 @@ async def delete_board(
if task_ids: if task_ids:
await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids))) await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids)))
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( await session.execute(
delete(TaskFingerprint).where(col(TaskFingerprint.board_id) == board.id) 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: if agents:
agent_ids = [agent.id for agent in agents] agent_ids = [agent.id for agent in agents]
await session.execute( await session.execute(
delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids)) 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(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.delete(board)
await session.commit() await session.commit()
return OkResponse() 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"): if blocked_by and (task.assigned_agent_id is not None or task.status != "inbox"):
raise _blocked_task_error(blocked_by) raise _blocked_task_error(blocked_by)
session.add(task) session.add(task)
# Ensure the task exists in the DB before inserting dependency rows.
await session.flush()
for dep_id in normalized_deps: for dep_id in normalized_deps:
session.add( session.add(
TaskDependency( TaskDependency(

View File

@@ -41,6 +41,8 @@ DEFAULT_GATEWAY_FILES = frozenset(
{ {
"AGENTS.md", "AGENTS.md",
"SOUL.md", "SOUL.md",
"SELF.md",
"AUTONOMY.md",
"TOOLS.md", "TOOLS.md",
"IDENTITY.md", "IDENTITY.md",
"USER.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_LEAD_TEMPLATE = "HEARTBEAT_LEAD.md"
HEARTBEAT_AGENT_TEMPLATE = "HEARTBEAT_AGENT.md" HEARTBEAT_AGENT_TEMPLATE = "HEARTBEAT_AGENT.md"
MAIN_TEMPLATE_MAP = { MAIN_TEMPLATE_MAP = {
@@ -115,6 +121,25 @@ def _workspace_path(agent: Agent, workspace_root: str) -> str:
return f"{root}/workspace-{_slugify(key)}" 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( def _build_context(
agent: Agent, agent: Agent,
board: Board, board: Board,
@@ -484,7 +509,7 @@ async def provision_agent(
context = _build_context(agent, board, gateway, auth_token, user) context = _build_context(agent, board, gateway, auth_token, user)
supported = set(await _supported_gateway_files(client_config)) 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) existing_files = await _gateway_agent_files_index(agent_id, client_config)
include_bootstrap = True include_bootstrap = True
if action == "update" and not force_bootstrap: if action == "update" and not force_bootstrap:
@@ -501,9 +526,25 @@ async def provision_agent(
supported, supported,
include_bootstrap=include_bootstrap, 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(): for name, content in rendered.items():
if content == "": if content == "":
continue 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: try:
await openclaw_call( await openclaw_call(
"agents.files.set", "agents.files.set",
@@ -543,7 +584,7 @@ async def provision_main_agent(
context = _build_main_context(agent, gateway, auth_token, user) context = _build_main_context(agent, gateway, auth_token, user)
supported = set(await _supported_gateway_files(client_config)) 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) existing_files = await _gateway_agent_files_index(agent_id, client_config)
include_bootstrap = action != "update" or force_bootstrap include_bootstrap = action != "update" or force_bootstrap
if action == "update" and not force_bootstrap: if action == "update" and not force_bootstrap:
@@ -564,6 +605,10 @@ async def provision_main_agent(
for name, content in rendered.items(): for name, content in rendered.items():
if content == "": if content == "":
continue continue
if name in PRESERVE_AGENT_EDITABLE_FILES:
entry = existing_files.get(name)
if entry and entry.get("missing") is not True:
continue
try: try:
await openclaw_call( await openclaw_call(
"agents.files.set", "agents.files.set",

View File

@@ -8,10 +8,11 @@ This workspace is your home. Treat it as the source of truth.
## Every session ## Every session
Before doing anything else: Before doing anything else:
1) Read SOUL.md (identity, boundaries) 1) Read SOUL.md (identity, boundaries)
2) Read SELF.md (evolving identity, preferences) if it exists 2) Read AUTONOMY.md (how to decide when to act vs ask)
3) Read USER.md (who you serve) 3) Read SELF.md (evolving identity, preferences) if it exists
4) Read memory/YYYY-MM-DD.md for today and yesterday (create memory/ if missing) 4) Read USER.md (who you serve)
5) If this is the main or direct session, also read MEMORY.md 5) Read memory/YYYY-MM-DD.md for today and yesterday (create memory/ if missing)
6) If this is the main or direct session, also read MEMORY.md
## Memory ## Memory
- Daily log: memory/YYYY-MM-DD.md - Daily log: memory/YYYY-MM-DD.md

34
templates/AUTONOMY.md Normal file
View File

@@ -0,0 +1,34 @@
# AUTONOMY.md
This file defines how you decide when to act vs when to ask.
## Current settings (from onboarding, if provided)
- Autonomy level: {{ identity_autonomy_level or "balanced" }}
- Update cadence: {{ identity_update_cadence or "n/a" }}
- Verbosity: {{ identity_verbosity or "n/a" }}
- Output format: {{ identity_output_format or "n/a" }}
## Safety gates (always)
- No external side effects (emails, posts, purchases, production changes) without explicit human approval.
- No destructive actions without asking first (or an approval), unless the task explicitly instructs it and rollback is trivial.
- If requirements are unclear or info is missing and you cannot proceed reliably: do not guess. Ask for clarification (use board chat, approvals, or tag `@lead`).
- Prefer reversible steps and small increments. Keep a paper trail in task comments.
## Autonomy levels
### ask_first
- Do analysis + propose a plan, but ask before taking any non-trivial action.
- Only do trivial, reversible, internal actions without asking (read files, grep, draft options).
### balanced
- Proceed with low-risk internal work autonomously (read/search/patch/tests) and post progress.
- Ask before irreversible changes, ambiguous scope decisions, or anything that could waste hours.
### autonomous
- Move fast on internal work: plan, execute, validate, and report results without waiting.
- Still ask for human approval for external side effects and risky/destructive actions.
## Collaboration defaults
- If you are idle/unassigned: pick 1 in-progress/review task owned by someone else and leave a concrete, helpful comment (analysis, patch, repro, tests, edge cases).
- If you notice duplicate work: flag it and propose a merge/split so there is one clear DRI per deliverable.

View File

@@ -7,10 +7,11 @@ There is no memory yet. Create what is missing and proceed without blocking.
## Noninteractive bootstrap (default) ## Noninteractive bootstrap (default)
1) Create `memory/` if missing. 1) Create `memory/` if missing.
2) Ensure `MEMORY.md` exists (create if missing). 2) Ensure `MEMORY.md` exists (create if missing).
3) Ensure either `SELF.md` exists (create if missing) or `MEMORY.md` contains an up-to-date `## SELF` section. 3) Ensure `AUTONOMY.md` exists (create if missing).
4) Read `IDENTITY.md`, `SOUL.md`, `SELF.md` (if present), and `USER.md`. 4) Ensure either `SELF.md` exists (create if missing) or `MEMORY.md` contains an up-to-date `## SELF` section.
5) If any fields are blank, leave them blank. Do not invent values. 5) Read `IDENTITY.md`, `SOUL.md`, `AUTONOMY.md`, `SELF.md` (if present), and `USER.md`.
6) If `BASE_URL`, `AUTH_TOKEN`, and `BOARD_ID` are set in `TOOLS.md`, check in 6) If any fields are blank, leave them blank. Do not invent values.
7) If `BASE_URL`, `AUTH_TOKEN`, and `BOARD_ID` are set in `TOOLS.md`, check in
to Mission Control to mark the agent online: to Mission Control to mark the agent online:
```bash ```bash
curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \ curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \
@@ -18,9 +19,9 @@ curl -s -X POST "$BASE_URL/api/v1/agent/heartbeat" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}' -d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
``` ```
7) Write a short note to `MEMORY.md` that bootstrap completed and list any 8) Write a short note to `MEMORY.md` that bootstrap completed and list any
missing fields (e.g., user name, timezone). missing fields (e.g., user name, timezone).
8) Delete this file. 9) Delete this file.
## Optional: if a human is already present ## Optional: if a human is already present
You may ask a short, single message to fill missing fields. If no reply arrives You may ask a short, single message to fill missing fields. If no reply arrives

View File

@@ -8,10 +8,11 @@ This workspace belongs to the **Main Agent** for this gateway. You are not tied
## Every session ## Every session
Before doing anything else: Before doing anything else:
1) Read SOUL.md (identity, boundaries) 1) Read SOUL.md (identity, boundaries)
2) Read SELF.md (evolving identity, preferences) if it exists 2) Read AUTONOMY.md (how to decide when to act vs ask)
3) Read USER.md (who you serve) 3) Read SELF.md (evolving identity, preferences) if it exists
4) Read memory/YYYY-MM-DD.md for today and yesterday (create memory/ if missing) 4) Read USER.md (who you serve)
5) If this is the main or direct session, also read MEMORY.md 5) Read memory/YYYY-MM-DD.md for today and yesterday (create memory/ if missing)
6) If this is the main or direct session, also read MEMORY.md
## Mission Control API (required) ## Mission Control API (required)
- All work outputs must be sent to Mission Control via HTTP using: - All work outputs must be sent to Mission Control via HTTP using:

View File

@@ -34,10 +34,11 @@ Each session, you wake up fresh. These files _are_ your memory. Read them. Updat
Read order (recommended): Read order (recommended):
1) `SOUL.md` - stable core (this file) 1) `SOUL.md` - stable core (this file)
2) `SELF.md` - evolving identity and preferences (if present; otherwise keep a "SELF" section in `MEMORY.md`) 2) `AUTONOMY.md` - decision policy (when to act vs ask)
3) `USER.md` - who you serve, plus board context 3) `SELF.md` - evolving identity and preferences (if present; otherwise keep a "SELF" section in `MEMORY.md`)
4) `memory/YYYY-MM-DD.md` - recent raw logs (today + yesterday) 4) `USER.md` - who you serve, plus board context
5) `MEMORY.md` - curated long-term knowledge (main/direct sessions) 5) `memory/YYYY-MM-DD.md` - recent raw logs (today + yesterday)
6) `MEMORY.md` - curated long-term knowledge (main/direct sessions)
--- ---