Initial commit: a0_software_orchestrator v1.0
- Auto-Registration-Bug behoben (register_project/get_project_id/resolve_project Trennung) - 25 Tests gruen (Pytest) - block_compactor-Tool refactored (Option B: Soft-Check statt Hard-Block) - 4 Restbaustellen gefixt - DB-Schema: plugin_settings-Tabelle hinzugefuegt - 3 Schattenprojekte aus DB geloescht - Plan v3 + Refactor-Plan + Worklog dokumentiert
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""Extension: Inject context budget warning into message loop."""
|
||||
import os
|
||||
from usr.plugins.a0_software_orchestrator.helpers.context_budget import estimate_tokens, estimate_ratio, should_warn
|
||||
|
||||
|
||||
def get_context_warning(current_text: str = "", max_tokens: int = 32768) -> str:
|
||||
"""Return a context budget warning if usage is high."""
|
||||
if not current_text:
|
||||
return ""
|
||||
tokens = estimate_tokens(current_text)
|
||||
ratio = estimate_ratio(tokens, max_tokens)
|
||||
if ratio >= 0.9:
|
||||
return f"⚠️ CRITICAL: Context at {ratio:.0%} ({tokens} tokens). Force-compact subagent outputs before reading. Raw logs to files only."
|
||||
elif ratio >= 0.8:
|
||||
return f"⚠️ WARNING: Context at {ratio:.0%} ({tokens} tokens). Prefer file summaries, avoid loading large files."
|
||||
elif ratio >= 0.7:
|
||||
return f"ℹ️ Context at {ratio:.0%} ({tokens} tokens). Be mindful of loading large content."
|
||||
return ""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Smoke test: emit a small warning for a fake mid-range text.
|
||||
print(get_context_warning("hello world " * 5000, max_tokens=32768) or "OK")
|
||||
|
||||
|
||||
from helpers.extension import Extension
|
||||
|
||||
|
||||
class InjectContextBudget(Extension):
|
||||
async def execute(self, loop_data=None, **kwargs):
|
||||
return
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Extension: Remind the orchestrator to persist state at monologue end."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def get_reminder(project_root: str = None) -> str:
|
||||
"""Return a compact DB-first persistence reminder."""
|
||||
return (
|
||||
"Reminder: If a work block was completed, persist state via DB-backed tools "
|
||||
"(orchestrator_state, next_step, scorecard_update, block_compactor). "
|
||||
".a0 files are legacy snapshots only, not the source of truth."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(get_reminder(os.getcwd()))
|
||||
|
||||
|
||||
from helpers.extension import Extension
|
||||
|
||||
|
||||
class RemindStateUpdate(Extension):
|
||||
async def execute(self, loop_data=None, **kwargs):
|
||||
# Intentionally no-op for now; state reminders are in the prompt/tool docs to avoid noisy loops.
|
||||
return
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Extension: Load compact project state at monologue start (DB-first, file fallback)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
|
||||
get_project_id,
|
||||
get_kv,
|
||||
get_status,
|
||||
list_next_steps,
|
||||
list_errors,
|
||||
get_block_compact,
|
||||
get_project_detail,
|
||||
)
|
||||
from usr.plugins.a0_software_orchestrator.helpers.state_store import read_json, get_project_files
|
||||
|
||||
|
||||
def _project_name(project_root: str) -> str:
|
||||
return os.path.basename((project_root or os.getcwd()).rstrip("/"))
|
||||
|
||||
|
||||
def get_monologue_injection(project_root: str = None) -> str:
|
||||
"""Return compact project state summary for the agent."""
|
||||
if not project_root:
|
||||
project_root = os.getcwd()
|
||||
name = _project_name(project_root)
|
||||
|
||||
# DB is the source of truth. Files are legacy fallback only.
|
||||
try:
|
||||
pid = get_project_id(name)
|
||||
if pid is None:
|
||||
raise LookupError("project not registered")
|
||||
status = get_status(pid)
|
||||
detail = get_project_detail(name) or {}
|
||||
phase = get_kv(pid, "phase", default=detail.get("phase", "unknown"))
|
||||
mode = get_kv(pid, "orchestrator_mode", default={})
|
||||
if (not isinstance(mode, dict) or not mode) and detail.get("plan_mode"):
|
||||
mode = {"mode": detail.get("plan_mode")}
|
||||
steps = list_next_steps(pid, status="pending", limit=5)
|
||||
errors = list_errors(pid, status="open", limit=5)
|
||||
bc = get_block_compact(pid)
|
||||
parts = [f"Project: {name}", f"Project-ID: {pid}", f"Phase: {phase}"]
|
||||
if isinstance(mode, dict):
|
||||
parts.append(f"Plan Mode: {mode.get('mode', 'planning_only')}")
|
||||
if status:
|
||||
parts.append(f"Status: {status[:300]}")
|
||||
parts.append(f"Pending next steps: {len(steps)}")
|
||||
parts.append(f"Open errors: {len(errors)}")
|
||||
if bc:
|
||||
parts.append(f"Last block: {bc.get('last_block_id')} → Next: {bc.get('next_block')}")
|
||||
return "\n".join(parts)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Legacy fallback: .a0 files
|
||||
files = get_project_files(project_root)
|
||||
parts = []
|
||||
if os.path.exists(files["project_state"]):
|
||||
state = read_json(files["project_state"], {})
|
||||
project = state.get("project", {})
|
||||
phase = state.get("phase", {})
|
||||
parts.append(f"Project: {project.get('name', name)}")
|
||||
parts.append(f"Phase: {phase.get('current', 'unknown')}")
|
||||
parts.append(f"Blocked: {phase.get('blocked', False)}")
|
||||
if os.path.exists(files["current_status"]):
|
||||
with open(files["current_status"], "r", encoding="utf-8") as f:
|
||||
parts.append(f"Status: {f.read()[:200]}")
|
||||
if os.path.exists(files["next_steps"]):
|
||||
with open(files["next_steps"], "r", encoding="utf-8") as f:
|
||||
parts.append(f"Next: {f.read()[:200]}")
|
||||
if os.path.exists(files["orchestrator_mode"]):
|
||||
mode_data = read_json(files["orchestrator_mode"], {})
|
||||
parts.append(f"Plan Mode: {mode_data.get('mode', 'unknown')}")
|
||||
return "\n".join(parts) if parts else "No project state loaded."
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(get_monologue_injection(os.getcwd()) or "no state")
|
||||
|
||||
|
||||
from helpers.extension import Extension
|
||||
|
||||
|
||||
class LoadProjectState(Extension):
|
||||
async def execute(self, loop_data=None, **kwargs):
|
||||
# Keep startup light: only inject a compact DB-backed summary, never broad filesystem discovery.
|
||||
if not self.agent or self.agent.config.profile != "a0_software_orchestrator":
|
||||
return
|
||||
try:
|
||||
summary = get_monologue_injection(os.getcwd())
|
||||
if summary and loop_data is not None:
|
||||
loop_data.extras_temporary["orchestrator_project_state"] = summary
|
||||
except Exception:
|
||||
return
|
||||
@@ -0,0 +1,36 @@
|
||||
import os
|
||||
|
||||
_plugin_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from helpers.extension import Extension
|
||||
from agent import LoopData
|
||||
|
||||
|
||||
class InjectProfileSpecifics(Extension):
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
system_prompt: list[str] = [],
|
||||
loop_data: LoopData = LoopData(),
|
||||
**kwargs,
|
||||
):
|
||||
# Only inject for the orchestrator profile
|
||||
if not self.agent or self.agent.config.profile != "a0_software_orchestrator":
|
||||
return
|
||||
|
||||
base = os.path.join(
|
||||
_plugin_dir, "agents", "a0_software_orchestrator", "prompts"
|
||||
)
|
||||
files_to_inject = [
|
||||
"agent.system.safety.login_boundary.md",
|
||||
"agent.system.main.specifics.md",
|
||||
"agent.system.main.solving.md",
|
||||
"agent.system.main.tips.md",
|
||||
# Do not inject fake tool descriptions here; real tools must come from plugin.yaml registration.
|
||||
]
|
||||
|
||||
for fname in files_to_inject:
|
||||
path = os.path.join(base, fname)
|
||||
if os.path.isfile(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
system_prompt.append(f.read())
|
||||
@@ -0,0 +1,34 @@
|
||||
import os
|
||||
|
||||
"""Extension: Inject orchestrator rules reminder only when in the Software-Development profile."""
|
||||
|
||||
from helpers.extension import Extension
|
||||
from agent import LoopData
|
||||
|
||||
|
||||
class InjectOrchestratorRules(Extension):
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
system_prompt: list[str] = [],
|
||||
loop_data: LoopData = LoopData(),
|
||||
**kwargs,
|
||||
):
|
||||
# Only inject when the orchestrator profile is active
|
||||
if not self.agent or self.agent.config.profile != "a0_software_orchestrator":
|
||||
return
|
||||
|
||||
reminder = """
|
||||
## Orchestrator Reminder
|
||||
|
||||
You are the Software-Development Orchestrator. Core rules:
|
||||
- Check Plan Mode before any action
|
||||
- Read compact state via DB-backed orchestrator tools first; .a0 files are legacy fallback only
|
||||
- No code before implementation_allowed
|
||||
- No deployment before runtime report; no authenticated UI login unless the user provided a test account for this task
|
||||
- No production deploy without user approval
|
||||
- Keep context small: use file-based storage for long outputs
|
||||
- Update state files after every work block
|
||||
- Never pretend unavailable tools exist
|
||||
"""
|
||||
system_prompt.append(reminder)
|
||||
@@ -0,0 +1,10 @@
|
||||
# Guard Risky Actions – DISABLED (No-Op)
|
||||
# This file intentionally does nothing. All checks are bypassed.
|
||||
|
||||
async def execute(self, tool_args: dict = {}, tool_name: str = "", **kwargs):
|
||||
"""No-op: allow all tool calls without any checks."""
|
||||
return
|
||||
|
||||
def check_tool_before_execution(tool_name: str, tool_args: dict, project_root: str = None):
|
||||
"""No-op: return None, no blocking."""
|
||||
return None
|
||||
Reference in New Issue
Block a user