源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Creator App Tests / PR Spam Gate (push) Skipped
Frontend Tests / PR Spam Gate (push) Skipped
NPM Format (website & console) / PR Spam Gate (push) Skipped
Pre-commit Checks / PR Spam Gate (push) Skipped
QwenPaw Creator Plugin Release / check-version (push) Canceled after 0s
QwenPaw Creator Plugin Release / upload-oss (push) Canceled after 0s
Tests / PR Spam Gate (push) Skipped
CodeQL / CodeQL (python) (push) Canceled after 0s
CodeQL / CodeQL (typescript) (push) Canceled after 0s
Creator App Tests / Creator Backend Tests (push) Canceled after 0s
Creator App Tests / Creator UI Build & Tests (push) Canceled after 0s
E2E Smoke Tests / Playwright UI Smoke (push) Canceled after 0s
Frontend Tests / Detect console changes (push) Canceled after 0s
NPM Format (website & console) / website format check (push) Canceled after 0s
NPM Format (website & console) / console format check (push) Canceled after 0s
Pre-commit Checks / Pre-commit (push) Canceled after 0s
QwenPaw Data Plugin Release / check-version (push) Canceled after 0s
Tests / Detect code changes (push) Canceled after 0s
Frontend Tests / Vitest Unit Tests (push) Canceled after 0s
Frontend Tests / Frontend Summary (push) Canceled after 0s
QwenPaw Data Plugin Release / upload-oss (push) Canceled after 0s
Tests / Maintainer Approval (push) Canceled after 0s
Tests / Unit Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - fallback (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p2 (push) Canceled after 0s
Tests / Coverage Report (push) Canceled after 0s
Tests / Test Summary (push) Canceled after 0s
Creator App Tests / PR Spam Gate (push) Skipped
Frontend Tests / PR Spam Gate (push) Skipped
NPM Format (website & console) / PR Spam Gate (push) Skipped
Pre-commit Checks / PR Spam Gate (push) Skipped
QwenPaw Creator Plugin Release / check-version (push) Canceled after 0s
QwenPaw Creator Plugin Release / upload-oss (push) Canceled after 0s
Tests / PR Spam Gate (push) Skipped
CodeQL / CodeQL (python) (push) Canceled after 0s
CodeQL / CodeQL (typescript) (push) Canceled after 0s
Creator App Tests / Creator Backend Tests (push) Canceled after 0s
Creator App Tests / Creator UI Build & Tests (push) Canceled after 0s
E2E Smoke Tests / Playwright UI Smoke (push) Canceled after 0s
Frontend Tests / Detect console changes (push) Canceled after 0s
NPM Format (website & console) / website format check (push) Canceled after 0s
NPM Format (website & console) / console format check (push) Canceled after 0s
Pre-commit Checks / Pre-commit (push) Canceled after 0s
QwenPaw Data Plugin Release / check-version (push) Canceled after 0s
Tests / Detect code changes (push) Canceled after 0s
Frontend Tests / Vitest Unit Tests (push) Canceled after 0s
Frontend Tests / Frontend Summary (push) Canceled after 0s
QwenPaw Data Plugin Release / upload-oss (push) Canceled after 0s
Tests / Maintainer Approval (push) Canceled after 0s
Tests / Unit Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - fallback (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p2 (push) Canceled after 0s
Tests / Coverage Report (push) Canceled after 0s
Tests / Test Summary (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Autopilot gate — lifecycle pipeline with cleanup + completed."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from qwenpaw.loop.gates.base import StopAction, StopHandlerResult
|
||||
from qwenpaw.loop.gates.loop_gate import LoopGate
|
||||
|
||||
from ..shared.constants import (
|
||||
AUTOPILOT_MAX_ITERATIONS,
|
||||
AUTOPILOT_MAX_PHASE_ITERATIONS,
|
||||
AUTOPILOT_MAX_VALIDATION_ROUNDS,
|
||||
)
|
||||
from ..shared.fork_guard import forks_integrated, merge_blocked_continuation
|
||||
from ..shared.role_prompts import FORK_MERGE_PROTOCOL
|
||||
from ..shared.state import WorkflowState
|
||||
from .prompts import build_continuation as _build_prompt
|
||||
|
||||
_POST_FORK_PHASES = frozenset(
|
||||
{"qa", "validation", "cleanup", "completed"},
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _AutopilotState:
|
||||
loop_dir: Path
|
||||
workspace_dir: Path
|
||||
active: bool = True
|
||||
iteration: int = 0
|
||||
max_iterations: int = AUTOPILOT_MAX_ITERATIONS
|
||||
phase_entry_iteration: dict[str, int] = field(
|
||||
default_factory=dict,
|
||||
)
|
||||
skip_qa: bool = False
|
||||
skip_validation: bool = False
|
||||
validation_round: int = 0
|
||||
max_validation_rounds: int = AUTOPILOT_MAX_VALIDATION_ROUNDS
|
||||
phase: str = "expansion"
|
||||
blocked_on_merge: bool = False
|
||||
|
||||
|
||||
class AutopilotGate(LoopGate):
|
||||
"""Stop gate for the 6-phase Autopilot pipeline."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "autopilot"
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
return 50
|
||||
|
||||
def activate_for_autopilot(
|
||||
self,
|
||||
workspace_dir: Path,
|
||||
skip_qa: bool = False,
|
||||
skip_validation: bool = False,
|
||||
) -> Path:
|
||||
try:
|
||||
from qwenpaw.agents.fork_project import begin_fork_scope
|
||||
|
||||
begin_fork_scope(workspace_dir)
|
||||
except ImportError:
|
||||
import logging
|
||||
|
||||
logging.getLogger(__name__).warning(
|
||||
"begin_fork_scope unavailable; fork merge scope disabled",
|
||||
)
|
||||
wf = WorkflowState(workspace_dir, "autopilot")
|
||||
loop_dir = wf.create_instance()
|
||||
state = _AutopilotState(
|
||||
loop_dir=loop_dir,
|
||||
workspace_dir=workspace_dir,
|
||||
skip_qa=skip_qa,
|
||||
skip_validation=skip_validation,
|
||||
)
|
||||
wf.write_state(
|
||||
{
|
||||
"phase": "expansion",
|
||||
"validation_round": 0,
|
||||
},
|
||||
)
|
||||
self.activate(state)
|
||||
return loop_dir
|
||||
|
||||
async def check( # pylint: disable=too-many-return-statements
|
||||
self,
|
||||
ctx: Any,
|
||||
) -> Optional[StopHandlerResult]:
|
||||
if isinstance(ctx, dict) and ctx.get("has_tool_calls"):
|
||||
return StopHandlerResult(action=StopAction.BYPASS)
|
||||
|
||||
st: _AutopilotState | None = self._state()
|
||||
if st is None:
|
||||
return StopHandlerResult(
|
||||
action=StopAction.BYPASS,
|
||||
)
|
||||
|
||||
wf = WorkflowState.from_existing(
|
||||
st.workspace_dir,
|
||||
"autopilot",
|
||||
st.loop_dir,
|
||||
)
|
||||
data = await asyncio.to_thread(wf.read_state)
|
||||
|
||||
phase = data.get("phase", "expansion")
|
||||
st.phase = phase
|
||||
st.validation_round = data.get(
|
||||
"validation_round",
|
||||
st.validation_round,
|
||||
)
|
||||
|
||||
if phase in _POST_FORK_PHASES:
|
||||
integrated = await asyncio.to_thread(
|
||||
forks_integrated,
|
||||
data,
|
||||
st.workspace_dir,
|
||||
)
|
||||
if not integrated:
|
||||
# Preserve target phase; do not burn iteration/stall budget
|
||||
# while waiting on merge (same as Ultrawork).
|
||||
st.blocked_on_merge = True
|
||||
st.phase = phase
|
||||
await asyncio.to_thread(
|
||||
wf.update_state,
|
||||
{
|
||||
"merge_blocked": True,
|
||||
"resume_phase": phase,
|
||||
},
|
||||
)
|
||||
return StopHandlerResult(
|
||||
action=StopAction.INTERRUPT_AND_CONTINUE,
|
||||
reason="Autopilot blocked: forks not integrated",
|
||||
)
|
||||
|
||||
if data.get("merge_blocked"):
|
||||
await asyncio.to_thread(
|
||||
wf.update_state,
|
||||
{"merge_blocked": False},
|
||||
)
|
||||
st.blocked_on_merge = False
|
||||
|
||||
st.iteration += 1
|
||||
if st.iteration > st.max_iterations:
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason=f"Total iteration limit ({st.max_iterations})",
|
||||
)
|
||||
|
||||
if phase == "completed":
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason="Autopilot completed",
|
||||
)
|
||||
|
||||
if (
|
||||
phase == "validation"
|
||||
and st.validation_round > st.max_validation_rounds
|
||||
):
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason=(
|
||||
f"Validation round limit " f"({st.max_validation_rounds})"
|
||||
),
|
||||
)
|
||||
|
||||
if phase not in st.phase_entry_iteration:
|
||||
st.phase_entry_iteration[phase] = st.iteration
|
||||
elif (
|
||||
st.iteration - st.phase_entry_iteration[phase]
|
||||
> AUTOPILOT_MAX_PHASE_ITERATIONS
|
||||
):
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason=f"Phase '{phase}' stalled",
|
||||
)
|
||||
|
||||
return StopHandlerResult(
|
||||
action=StopAction.INTERRUPT_AND_CONTINUE,
|
||||
reason="Autopilot in progress",
|
||||
)
|
||||
|
||||
def build_continuation(self) -> str:
|
||||
"""Build Autopilot continuation from gate state."""
|
||||
st: _AutopilotState | None = self._state()
|
||||
if st is None:
|
||||
return ""
|
||||
if st.blocked_on_merge:
|
||||
return merge_blocked_continuation(FORK_MERGE_PROTOCOL)
|
||||
return _build_prompt(
|
||||
phase=st.phase,
|
||||
iteration=st.iteration,
|
||||
max_iterations=st.max_iterations,
|
||||
loop_dir=st.loop_dir,
|
||||
skip_qa=st.skip_qa,
|
||||
skip_validation=st.skip_validation,
|
||||
validation_round=st.validation_round,
|
||||
max_validation_rounds=st.max_validation_rounds,
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""AutopilotMode — full lifecycle pipeline."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from qwenpaw.runtime.slash_command_registry import CommandSpec
|
||||
|
||||
from ..shared.args import split_args
|
||||
from ..shared.loop_ui_i18n import loop_command_metadata, loop_help_text
|
||||
from ..shared.mode_base import OMPModeBase, info_msg, rewrite_user_msg
|
||||
from .gate import AutopilotGate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from agentscope.message import Msg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_HELP = loop_help_text("autopilot")
|
||||
|
||||
|
||||
class AutopilotMode(OMPModeBase):
|
||||
"""AgentMode for the Autopilot pipeline."""
|
||||
|
||||
name = "autopilot"
|
||||
gate_cls = AutopilotGate
|
||||
plugin_id = "__omp_autopilot__"
|
||||
handler_name = "autopilot-stop-handler"
|
||||
scope = "omp-autopilot"
|
||||
|
||||
def commands(self) -> list[CommandSpec]:
|
||||
return [
|
||||
CommandSpec(
|
||||
name="autopilot",
|
||||
handler=self._handler,
|
||||
category="builtin",
|
||||
help_text=_HELP,
|
||||
metadata=loop_command_metadata("autopilot"),
|
||||
),
|
||||
]
|
||||
|
||||
async def _handler(self, ctx: "Any", args: str) -> Optional["Msg"]:
|
||||
if not args or not args.strip() or args.strip().lower() == "help":
|
||||
return info_msg(_HELP)
|
||||
|
||||
parsed = _parse_args(args)
|
||||
if parsed is None:
|
||||
return info_msg("Invalid arguments. " + _HELP)
|
||||
task = parsed["task"]
|
||||
if len(task) < 5:
|
||||
return info_msg("Please provide a task description.\n\n" + _HELP)
|
||||
|
||||
workspace_dir = getattr(ctx, "workspace_dir", None)
|
||||
if not workspace_dir:
|
||||
return info_msg("ERROR: no workspace directory available.")
|
||||
|
||||
self.claim_workflow()
|
||||
loop_dir = await asyncio.to_thread(
|
||||
self._gate.activate_for_autopilot,
|
||||
Path(workspace_dir),
|
||||
parsed["skip_qa"],
|
||||
parsed["skip_validation"],
|
||||
)
|
||||
|
||||
prompt = (
|
||||
f"Autopilot activated.\n"
|
||||
f"Task: {task}\n"
|
||||
f"State directory: {loop_dir}\n"
|
||||
f"Phase: expansion — analyze requirements and create spec.md."
|
||||
)
|
||||
rewrite_user_msg(ctx, prompt)
|
||||
logger.info("Autopilot started: %s", loop_dir)
|
||||
return None
|
||||
|
||||
|
||||
def _parse_args(raw: str) -> dict | None:
|
||||
"""Parse /autopilot arguments. ``None`` means invalid input."""
|
||||
tokens = split_args(raw)
|
||||
if tokens is None:
|
||||
return None
|
||||
|
||||
skip_qa = False
|
||||
skip_validation = False
|
||||
task_parts: list[str] = []
|
||||
|
||||
for t in tokens:
|
||||
if t == "--skip-qa":
|
||||
skip_qa = True
|
||||
elif t == "--skip-validation":
|
||||
skip_validation = True
|
||||
else:
|
||||
task_parts.append(t)
|
||||
|
||||
return {
|
||||
"task": " ".join(task_parts),
|
||||
"skip_qa": skip_qa,
|
||||
"skip_validation": skip_validation,
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Autopilot continuation prompt templates — one per phase."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from ..shared.role_prompts import (
|
||||
fork_merge_instructions,
|
||||
format_batch_item,
|
||||
format_spawn_call,
|
||||
)
|
||||
|
||||
PHASES = (
|
||||
"expansion",
|
||||
"planning",
|
||||
"execution",
|
||||
"qa",
|
||||
"validation",
|
||||
"cleanup",
|
||||
"completed",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AutopilotPromptCtx:
|
||||
"""Typed context for Autopilot phase prompt builders."""
|
||||
|
||||
loop_dir: Path
|
||||
iteration: int
|
||||
max_iterations: int
|
||||
skip_qa: bool = False
|
||||
skip_validation: bool = False
|
||||
validation_round: int = 0
|
||||
max_validation_rounds: int = 3
|
||||
|
||||
|
||||
def build_continuation(
|
||||
phase: str,
|
||||
iteration: int,
|
||||
max_iterations: int,
|
||||
loop_dir: Path,
|
||||
skip_qa: bool = False,
|
||||
skip_validation: bool = False,
|
||||
validation_round: int = 0,
|
||||
max_validation_rounds: int = 3,
|
||||
) -> str:
|
||||
"""Return the controller prompt for the current phase."""
|
||||
ctx = AutopilotPromptCtx(
|
||||
loop_dir=loop_dir,
|
||||
iteration=iteration,
|
||||
max_iterations=max_iterations,
|
||||
skip_qa=skip_qa,
|
||||
skip_validation=skip_validation,
|
||||
validation_round=validation_round,
|
||||
max_validation_rounds=max_validation_rounds,
|
||||
)
|
||||
builders = {
|
||||
"expansion": _expansion,
|
||||
"planning": _planning,
|
||||
"execution": _execution,
|
||||
"qa": _qa,
|
||||
"validation": _validation,
|
||||
"cleanup": _cleanup,
|
||||
}
|
||||
fn = builders.get(phase)
|
||||
if fn is None:
|
||||
return (
|
||||
f"Unknown phase: {phase}. " f"Valid phases: {', '.join(PHASES)}."
|
||||
)
|
||||
return fn(ctx)
|
||||
|
||||
|
||||
def _expansion(ctx: AutopilotPromptCtx) -> str:
|
||||
analyst = format_spawn_call(
|
||||
"analyst",
|
||||
"Analyze requirements and produce spec.md...",
|
||||
)
|
||||
return f"""\
|
||||
Autopilot Controller — phase: expansion.
|
||||
Iteration: {ctx.iteration}/{ctx.max_iterations}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute:
|
||||
1. Analyze the task requirements.
|
||||
2. Dispatch an analyst subagent to extract requirements:
|
||||
{analyst}
|
||||
3. Dispatch an architect subagent to validate and expand the spec.
|
||||
4. Write the final spec to {ctx.loop_dir}/spec.md.
|
||||
5. Update {ctx.loop_dir}/state.json: set phase="planning"."""
|
||||
|
||||
|
||||
def _planning(ctx: AutopilotPromptCtx) -> str:
|
||||
architect = format_spawn_call(
|
||||
"architect",
|
||||
"Create implementation plan from spec...",
|
||||
)
|
||||
critic = format_spawn_call(
|
||||
"critic",
|
||||
"Review the implementation plan; challenge gaps and risks...",
|
||||
)
|
||||
return f"""\
|
||||
Autopilot Controller — phase: planning.
|
||||
Iteration: {ctx.iteration}/{ctx.max_iterations}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute:
|
||||
1. Read {ctx.loop_dir}/spec.md.
|
||||
2. Dispatch an architect subagent to create an implementation plan:
|
||||
{architect}
|
||||
3. Dispatch a critic subagent to review the plan:
|
||||
{critic}
|
||||
4. Write the final plan to {ctx.loop_dir}/plan.md.
|
||||
5. Update {ctx.loop_dir}/state.json: set phase="execution"."""
|
||||
|
||||
|
||||
def _execution(ctx: AutopilotPromptCtx) -> str:
|
||||
item1 = format_batch_item("executor", "<task 1 from plan>", fork=True)
|
||||
item2 = format_batch_item("executor", "<task 2 from plan>", fork=True)
|
||||
return f"""\
|
||||
Autopilot Controller — phase: execution.
|
||||
Iteration: {ctx.iteration}/{ctx.max_iterations}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute:
|
||||
1. Read {ctx.loop_dir}/plan.md for the task list.
|
||||
2. Identify independent tasks that can run in parallel.
|
||||
3. Dispatch executor workers via batch mode:
|
||||
spawn_subagent(task="", batch=[
|
||||
{item1},
|
||||
{item2},
|
||||
...
|
||||
])
|
||||
4. Poll each worker with check_agent_task (wait >= 30s between polls).
|
||||
5. Integrate forked worker results:
|
||||
{fork_merge_instructions()}
|
||||
6. After successful merges, update {ctx.loop_dir}/state.json:
|
||||
set forks_integrated=true.
|
||||
7. Verify outputs in the main workspace.
|
||||
8. Update {ctx.loop_dir}/state.json: set phase="qa".
|
||||
The gate rejects qa/later phases until forks_integrated=true."""
|
||||
|
||||
|
||||
def _qa(ctx: AutopilotPromptCtx) -> str:
|
||||
if ctx.skip_qa:
|
||||
return f"""\
|
||||
Autopilot Controller — phase: qa (SKIPPED via --skip-qa).
|
||||
Update {ctx.loop_dir}/state.json: set phase="validation"."""
|
||||
|
||||
architect = format_spawn_call(
|
||||
"architect",
|
||||
"DIAGNOSE FAILURE: paste test output; give root cause + fixes.",
|
||||
)
|
||||
executor = format_spawn_call(
|
||||
"executor",
|
||||
"FIX: apply the architect recommendations precisely.",
|
||||
)
|
||||
return f"""\
|
||||
Autopilot Controller — phase: qa.
|
||||
Iteration: {ctx.iteration}/{ctx.max_iterations}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute the UltraQA-style 3-agent cycle:
|
||||
1. Run the project test suite.
|
||||
2. If all tests pass, update {ctx.loop_dir}/state.json: set phase="validation".
|
||||
3. If tests fail:
|
||||
a. Dispatch architect subagent:
|
||||
{architect}
|
||||
b. Dispatch executor subagent:
|
||||
{executor}
|
||||
c. Re-run tests. Repeat up to 5 cycles."""
|
||||
|
||||
|
||||
def _validation(ctx: AutopilotPromptCtx) -> str:
|
||||
if ctx.skip_validation:
|
||||
return f"""\
|
||||
Autopilot Controller — phase: validation \
|
||||
(SKIPPED via --skip-validation).
|
||||
Update {ctx.loop_dir}/state.json: set phase="cleanup"."""
|
||||
|
||||
verifier = format_batch_item(
|
||||
"verifier",
|
||||
"REVIEW - Functional Completeness: Verify all spec requirements...",
|
||||
)
|
||||
security = format_batch_item(
|
||||
"security-reviewer",
|
||||
"REVIEW - Security: Check for vulnerabilities, injection vectors...",
|
||||
)
|
||||
code = format_batch_item(
|
||||
"code-reviewer",
|
||||
"REVIEW - Code Quality: Review quality and maintainability...",
|
||||
)
|
||||
return f"""\
|
||||
Autopilot Controller — phase: validation.
|
||||
Validation round: {ctx.validation_round}/{ctx.max_validation_rounds}
|
||||
|
||||
WARNING: You are the Controller — do NOT review
|
||||
code yourself. Dispatch subagents.
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute parallel validation:
|
||||
1. Use spawn_subagent batch mode for three reviewers:
|
||||
spawn_subagent(task="", batch=[
|
||||
{verifier},
|
||||
{security},
|
||||
{code}
|
||||
])
|
||||
2. Wait for all reviewers to complete.
|
||||
3. If ALL approve -> update {ctx.loop_dir}/state.json: set phase="cleanup".
|
||||
4. If any reject -> fix issues -> increment validation_round -> re-validate.
|
||||
5. If validation_round > {ctx.max_validation_rounds} -> STOP and report."""
|
||||
|
||||
|
||||
def _cleanup(ctx: AutopilotPromptCtx) -> str:
|
||||
return f"""\
|
||||
Autopilot Controller — phase: cleanup.
|
||||
Iteration: {ctx.iteration}/{ctx.max_iterations}
|
||||
|
||||
This is a real model turn. Perform final cleanup before completion.
|
||||
|
||||
Execute:
|
||||
1. Review changed files; run deslop / remove debug leftovers if needed.
|
||||
2. Re-run tests/build/lint for a final verification pass.
|
||||
3. Confirm no unresolved merge conflicts remain.
|
||||
4. Write a short completion note to {ctx.loop_dir}/progress.txt.
|
||||
5. Update {ctx.loop_dir}/state.json: set phase="completed".
|
||||
Do NOT set phase="completed" until verification succeeds."""
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "omp-workflows",
|
||||
"name": "OMP Workflows",
|
||||
"version": "0.1.1",
|
||||
"type": "bundle",
|
||||
"description": "Oh My Paw workflow modes: Autopilot, Ralph, UltraQA, Ultrawork, Team",
|
||||
"author": "QwenPaw Team",
|
||||
"entry": {
|
||||
"backend": "plugin.py"
|
||||
},
|
||||
"dependencies": [],
|
||||
"qwenpaw_version": {
|
||||
"min": "2.0.0",
|
||||
"max": "3.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""OMP Workflows plugin — registers five workflow AgentModes."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger("qwenpaw").getChild("plugin.omp_workflows")
|
||||
|
||||
_PLUGIN_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
class OMPWorkflowsPlugin:
|
||||
"""Plugin entry point for Oh My Paw workflow modes."""
|
||||
|
||||
def register(self, api) -> None:
|
||||
from .autopilot.mode import AutopilotMode
|
||||
from .ralph.mode import RalphMode
|
||||
from .team.mode import TeamMode
|
||||
from .ultraqa.mode import UltraQAMode
|
||||
from .ultrawork.mode import UltraworkMode
|
||||
|
||||
for mode_cls in (
|
||||
UltraQAMode,
|
||||
RalphMode,
|
||||
UltraworkMode,
|
||||
AutopilotMode,
|
||||
TeamMode,
|
||||
):
|
||||
api.register_mode(mode_cls)
|
||||
logger.info("Registered OMP mode: %s", mode_cls.name)
|
||||
|
||||
api.register_skill_provider(
|
||||
skills_dir=_PLUGIN_DIR / "skills",
|
||||
enabled_by_default=True,
|
||||
)
|
||||
|
||||
|
||||
plugin = OMPWorkflowsPlugin()
|
||||
@@ -0,0 +1,164 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Ralph gate — PRD-driven continuous loop with reviewer + deslop."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from qwenpaw.loop.gates.base import StopAction, StopHandlerResult
|
||||
from qwenpaw.loop.gates.loop_gate import LoopGate
|
||||
|
||||
from ..shared.constants import RALPH_MAX_ITERATIONS
|
||||
from ..shared.state import WorkflowState
|
||||
from .prompts import build_continuation as _build_prompt
|
||||
|
||||
|
||||
@dataclass
|
||||
class _RalphState:
|
||||
loop_dir: Path
|
||||
workspace_dir: Path
|
||||
active: bool = True
|
||||
iteration: int = 0
|
||||
max_iterations: int = RALPH_MAX_ITERATIONS
|
||||
no_deslop: bool = False
|
||||
critic_type: str = "architect"
|
||||
prd_summary: str = ""
|
||||
prd_cache: dict[str, Any] = field(default_factory=dict)
|
||||
prd_mtime_ns: int = -1
|
||||
|
||||
|
||||
class RalphGate(LoopGate):
|
||||
"""Stop gate for the Ralph PRD-driven loop."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "ralph"
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
return 50
|
||||
|
||||
def activate_for_ralph(
|
||||
self,
|
||||
workspace_dir: Path,
|
||||
no_deslop: bool = False,
|
||||
critic_type: str = "architect",
|
||||
max_iterations: int = RALPH_MAX_ITERATIONS,
|
||||
) -> Path:
|
||||
wf = WorkflowState(workspace_dir, "ralph")
|
||||
loop_dir = wf.create_instance()
|
||||
state = _RalphState(
|
||||
loop_dir=loop_dir,
|
||||
workspace_dir=workspace_dir,
|
||||
max_iterations=max_iterations,
|
||||
no_deslop=no_deslop,
|
||||
critic_type=critic_type,
|
||||
)
|
||||
wf.write_state(
|
||||
{
|
||||
"iteration": 0,
|
||||
"completed": False,
|
||||
},
|
||||
)
|
||||
self.activate(state)
|
||||
return loop_dir
|
||||
|
||||
async def check(self, ctx: Any) -> Optional[StopHandlerResult]:
|
||||
if isinstance(ctx, dict) and ctx.get("has_tool_calls"):
|
||||
return StopHandlerResult(action=StopAction.BYPASS)
|
||||
|
||||
st: _RalphState | None = self._state()
|
||||
if st is None:
|
||||
return StopHandlerResult(
|
||||
action=StopAction.BYPASS,
|
||||
)
|
||||
|
||||
wf = WorkflowState.from_existing(
|
||||
st.workspace_dir,
|
||||
"ralph",
|
||||
st.loop_dir,
|
||||
)
|
||||
data = await asyncio.to_thread(wf.read_state)
|
||||
prd = await asyncio.to_thread(_read_prd_cached, st, wf)
|
||||
|
||||
# Gate owns iteration in memory.
|
||||
st.iteration += 1
|
||||
if st.iteration > st.max_iterations:
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason=f"Reached max iterations ({st.max_iterations})",
|
||||
)
|
||||
|
||||
st.prd_summary = _summarize_prd(prd)
|
||||
completed = bool(data.get("completed")) or _all_stories_passed(prd)
|
||||
if completed:
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason="All stories completed and verified",
|
||||
)
|
||||
|
||||
await asyncio.to_thread(
|
||||
wf.update_state,
|
||||
{"iteration": st.iteration},
|
||||
)
|
||||
|
||||
return StopHandlerResult(
|
||||
action=StopAction.INTERRUPT_AND_CONTINUE,
|
||||
reason="Ralph iteration in progress",
|
||||
)
|
||||
|
||||
def build_continuation(self) -> str:
|
||||
"""Build Ralph continuation from gate state."""
|
||||
st: _RalphState | None = self._state()
|
||||
if st is None:
|
||||
return ""
|
||||
return _build_prompt(
|
||||
iteration=st.iteration,
|
||||
max_iterations=st.max_iterations,
|
||||
critic_type=st.critic_type,
|
||||
no_deslop=st.no_deslop,
|
||||
loop_dir=st.loop_dir,
|
||||
prd_summary=st.prd_summary,
|
||||
)
|
||||
|
||||
|
||||
def _read_prd_cached(
|
||||
st: _RalphState,
|
||||
wf: WorkflowState,
|
||||
) -> dict[str, Any]:
|
||||
"""Reload prd.json only when the file mtime changes."""
|
||||
prd_path = st.loop_dir / "prd.json"
|
||||
try:
|
||||
mtime_ns = prd_path.stat().st_mtime_ns if prd_path.exists() else -1
|
||||
except OSError:
|
||||
mtime_ns = -1
|
||||
if mtime_ns == st.prd_mtime_ns:
|
||||
return st.prd_cache
|
||||
prd = wf.read_prd()
|
||||
st.prd_cache = prd
|
||||
st.prd_mtime_ns = mtime_ns
|
||||
return prd
|
||||
|
||||
|
||||
def _all_stories_passed(prd: dict) -> bool:
|
||||
"""Return True when PRD exists and every story has passes=true."""
|
||||
stories = prd.get("stories") or []
|
||||
if not stories:
|
||||
return False
|
||||
return all(s.get("passes") for s in stories)
|
||||
|
||||
|
||||
def _summarize_prd(prd: dict) -> str:
|
||||
"""Build a one-line PRD progress summary."""
|
||||
stories = prd.get("stories", [])
|
||||
if not stories:
|
||||
return "PRD: not yet created."
|
||||
done = sum(1 for s in stories if s.get("passes"))
|
||||
return f"PRD progress: {done}/{len(stories)} stories completed."
|
||||
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""RalphMode — PRD-driven continuous implementation loop."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from qwenpaw.runtime.slash_command_registry import CommandSpec
|
||||
|
||||
from ..shared.args import split_args
|
||||
from ..shared.loop_ui_i18n import loop_command_metadata, loop_help_text
|
||||
from ..shared.mode_base import OMPModeBase, info_msg, rewrite_user_msg
|
||||
from .gate import RalphGate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from agentscope.message import Msg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_HELP = loop_help_text("ralph")
|
||||
|
||||
|
||||
class RalphMode(OMPModeBase):
|
||||
"""AgentMode for the Ralph workflow."""
|
||||
|
||||
name = "ralph"
|
||||
gate_cls = RalphGate
|
||||
plugin_id = "__omp_ralph__"
|
||||
handler_name = "ralph-stop-handler"
|
||||
scope = "omp-ralph"
|
||||
|
||||
def commands(self) -> list[CommandSpec]:
|
||||
return [
|
||||
CommandSpec(
|
||||
name="ralph",
|
||||
handler=self._handler,
|
||||
category="builtin",
|
||||
help_text=_HELP,
|
||||
metadata=loop_command_metadata("ralph"),
|
||||
),
|
||||
]
|
||||
|
||||
async def _handler(self, ctx: "Any", args: str) -> Optional["Msg"]:
|
||||
if not args or not args.strip() or args.strip().lower() == "help":
|
||||
return info_msg(_HELP)
|
||||
|
||||
parsed = _parse_args(args)
|
||||
if parsed is None:
|
||||
return info_msg("Invalid arguments. " + _HELP)
|
||||
task = parsed["task"]
|
||||
if len(task) < 5:
|
||||
return info_msg("Please provide a task description.\n\n" + _HELP)
|
||||
|
||||
workspace_dir = getattr(ctx, "workspace_dir", None)
|
||||
if not workspace_dir:
|
||||
return info_msg("ERROR: no workspace directory available.")
|
||||
|
||||
self.claim_workflow()
|
||||
loop_dir = await asyncio.to_thread(
|
||||
self._gate.activate_for_ralph,
|
||||
Path(workspace_dir),
|
||||
parsed["no_deslop"],
|
||||
parsed["critic_type"],
|
||||
)
|
||||
|
||||
from .prompts import build_initial_prd_prompt
|
||||
|
||||
prompt = build_initial_prd_prompt(task, loop_dir)
|
||||
rewrite_user_msg(ctx, prompt)
|
||||
logger.info("Ralph started: %s", loop_dir)
|
||||
return None
|
||||
|
||||
|
||||
def _parse_args(raw: str) -> dict | None:
|
||||
"""Parse /ralph arguments. ``None`` means invalid input."""
|
||||
tokens = split_args(raw)
|
||||
if tokens is None:
|
||||
return None
|
||||
|
||||
no_deslop = False
|
||||
critic_type = "architect"
|
||||
task_parts: list[str] = []
|
||||
|
||||
for t in tokens:
|
||||
if t == "--no-deslop":
|
||||
no_deslop = True
|
||||
elif t.startswith("--critic="):
|
||||
critic_type = t.split("=", 1)[1]
|
||||
if critic_type not in ("architect", "critic", "codex"):
|
||||
critic_type = "architect"
|
||||
else:
|
||||
task_parts.append(t)
|
||||
|
||||
return {
|
||||
"task": " ".join(task_parts),
|
||||
"no_deslop": no_deslop,
|
||||
"critic_type": critic_type,
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Ralph continuation prompt templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ..shared.role_prompts import format_spawn_call, resolve_role
|
||||
|
||||
|
||||
def build_continuation(
|
||||
iteration: int,
|
||||
max_iterations: int,
|
||||
critic_type: str,
|
||||
no_deslop: bool,
|
||||
loop_dir: Path,
|
||||
prd_summary: str,
|
||||
) -> str:
|
||||
"""Build the controller continuation for a Ralph iteration."""
|
||||
deslop_block = ""
|
||||
if not no_deslop:
|
||||
deslop_block = (
|
||||
"7.5. Run deslop cleanup on changed files.\n"
|
||||
"7.6. Re-run tests/build/lint to verify "
|
||||
"no regressions from deslop.\n"
|
||||
)
|
||||
|
||||
reviewer = resolve_role(critic_type)
|
||||
# Sequential story loop runs in the main workspace (no fork): each
|
||||
# story must land on the shared tree before the next story starts.
|
||||
executor_spawn = format_spawn_call(
|
||||
"executor",
|
||||
"Implement the following user story:\\n"
|
||||
"<story details + acceptance criteria>",
|
||||
fork=False,
|
||||
)
|
||||
reviewer_spawn = format_spawn_call(
|
||||
reviewer,
|
||||
"REVIEW: Verify the complete implementation against the PRD...",
|
||||
)
|
||||
|
||||
return f"""\
|
||||
Ralph PRD-driven loop — iteration {iteration}/{max_iterations}.
|
||||
{prd_summary}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute the current step:
|
||||
|
||||
1. Read {loop_dir}/prd.json for the user stories list.
|
||||
2. Pick the highest-priority story with passes=false.
|
||||
3. Dispatch an executor subagent to implement it:
|
||||
{executor_spawn}
|
||||
4. After completion, verify every acceptance criterion (run tests/build/lint).
|
||||
5. If verified, update prd.json: set passes=true for this story.
|
||||
Write progress to {loop_dir}/progress.txt.
|
||||
6. If all stories pass, proceed to step 7. Otherwise loop to step 2.
|
||||
7. Dispatch a {reviewer} reviewer to verify the overall implementation:
|
||||
{reviewer_spawn}
|
||||
{deslop_block}\
|
||||
8. When the reviewer approves, update {loop_dir}/state.json:
|
||||
set completed=true (required for the loop to terminate).
|
||||
9. Then report completion to the user.
|
||||
|
||||
WARNING — POLITE-STOP ANTI-PATTERN:
|
||||
After the reviewer approves, do NOT stop to report results.
|
||||
Continue immediately through deslop -> regression check -> cleanup.
|
||||
Stopping mid-chain to ask the user is a known failure mode."""
|
||||
|
||||
|
||||
def build_initial_prd_prompt(task: str, loop_dir: Path) -> str:
|
||||
"""Build the prompt for the initial PRD creation step."""
|
||||
return f"""\
|
||||
Ralph activated. Task: {task}
|
||||
|
||||
Step 1 — PRD Setup:
|
||||
1. Analyze the task and create {loop_dir}/prd.json with this structure:
|
||||
{{
|
||||
"title": "<task title>",
|
||||
"stories": [
|
||||
{{
|
||||
"id": "S1",
|
||||
"title": "<story title>",
|
||||
"description": "<what to implement>",
|
||||
"acceptance_criteria": ["<criterion 1>", "..."],
|
||||
"priority": 1,
|
||||
"passes": false
|
||||
}}
|
||||
]
|
||||
}}
|
||||
2. Break the task into concrete user stories
|
||||
with measurable acceptance criteria.
|
||||
3. After creating prd.json, begin the implementation loop."""
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Shared slash-command argument parsing helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shlex
|
||||
|
||||
|
||||
def split_args(raw: str) -> list[str] | None:
|
||||
"""Split *raw* with shlex; return ``None`` on unbalanced quotes."""
|
||||
try:
|
||||
return shlex.split(raw)
|
||||
except ValueError:
|
||||
return None
|
||||
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Shared constants for OMP workflow modes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registered tool names used in OMP role configs / controller prompts.
|
||||
# Keep in sync with QwenPaw ToolRegistry names (not display labels).
|
||||
# ---------------------------------------------------------------------------
|
||||
TOOL_READ_FILE = "read_file"
|
||||
TOOL_GREP_SEARCH = "grep_search"
|
||||
TOOL_GLOB_SEARCH = "glob_search"
|
||||
TOOL_WRITE_FILE = "write_file"
|
||||
TOOL_AST_SEARCH = "ast_search"
|
||||
TOOL_EXECUTE_SHELL = "execute_shell_command"
|
||||
|
||||
# Read-oriented defaults used by multiple reviewer roles.
|
||||
_TOOLS_READ = (
|
||||
TOOL_READ_FILE,
|
||||
TOOL_GREP_SEARCH,
|
||||
TOOL_GLOB_SEARCH,
|
||||
)
|
||||
_TOOLS_READ_AST = _TOOLS_READ + (TOOL_AST_SEARCH,)
|
||||
_TOOLS_READ_SHELL = _TOOLS_READ + (TOOL_EXECUTE_SHELL,)
|
||||
_TOOLS_READ_AST_SHELL = _TOOLS_READ_AST + (TOOL_EXECUTE_SHELL,)
|
||||
_TOOLS_READ_WRITE_SHELL = _TOOLS_READ + (
|
||||
TOOL_WRITE_FILE,
|
||||
TOOL_EXECUTE_SHELL,
|
||||
)
|
||||
_TOOLS_ARCHITECT = _TOOLS_READ + (
|
||||
TOOL_WRITE_FILE,
|
||||
TOOL_AST_SEARCH,
|
||||
TOOL_EXECUTE_SHELL,
|
||||
)
|
||||
_TOOLS_PLANNER = _TOOLS_READ + (TOOL_WRITE_FILE, TOOL_AST_SEARCH)
|
||||
|
||||
# Role -> allowed_tools. ``None`` means inherit the parent's full set.
|
||||
ROLE_ALLOWED_TOOLS: dict[str, list[str] | None] = {
|
||||
"executor": None,
|
||||
"debugger": None,
|
||||
"architect": list(_TOOLS_ARCHITECT),
|
||||
"analyst": list(_TOOLS_READ_WRITE_SHELL),
|
||||
"critic": list(_TOOLS_READ_AST),
|
||||
"security-reviewer": list(_TOOLS_READ_AST_SHELL),
|
||||
"code-reviewer": list(_TOOLS_READ_AST_SHELL),
|
||||
"qa-tester": list(_TOOLS_READ_SHELL),
|
||||
"planner": list(_TOOLS_PLANNER),
|
||||
"explore": list(_TOOLS_READ_AST),
|
||||
"verifier": list(_TOOLS_READ_AST_SHELL),
|
||||
}
|
||||
|
||||
# Role -> skills whitelist. Empty list clears inherited skills.
|
||||
ROLE_SKILLS: dict[str, list[str] | None] = {
|
||||
"executor": None,
|
||||
"debugger": [],
|
||||
"architect": [],
|
||||
"analyst": [],
|
||||
"critic": [],
|
||||
"security-reviewer": [],
|
||||
"code-reviewer": [],
|
||||
"qa-tester": [],
|
||||
"planner": [],
|
||||
"explore": [],
|
||||
"verifier": [],
|
||||
}
|
||||
|
||||
ULTRAQA_MAX_CYCLES = 5
|
||||
ULTRAQA_MAX_SAME_FAILURE = 3
|
||||
|
||||
AUTOPILOT_MAX_ITERATIONS = 60
|
||||
AUTOPILOT_MAX_PHASE_ITERATIONS = 10
|
||||
AUTOPILOT_MAX_VALIDATION_ROUNDS = 3
|
||||
|
||||
RALPH_MAX_ITERATIONS = 20
|
||||
RALPH_MAX_SAME_ISSUE = 3
|
||||
|
||||
TEAM_MAX_FIX_ATTEMPTS = 3
|
||||
TEAM_MAX_ITERATIONS = 30
|
||||
|
||||
ULTRAWORK_MAX_ITERATIONS = 30
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Hard checks for forked-worker integration before phase advance."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def forks_integrated(
|
||||
state: dict[str, Any] | None,
|
||||
workspace_dir: Path | str | None = None,
|
||||
) -> bool:
|
||||
"""Return True when the controller flag is set and forks are merged.
|
||||
|
||||
- ``forks_integrated`` must be the JSON/Python boolean ``True``.
|
||||
- When an integration project can be resolved, verification uses the
|
||||
coding-project registry (via workspace pointer / git root) and the
|
||||
active fork *scope* started at mode activation.
|
||||
- When no integration project can be resolved (non-git workspace, no
|
||||
pointer), treat as "no registry forks are possible" and allow — the
|
||||
protocol still sets ``forks_integrated=true`` on no-fork paths.
|
||||
- Import failures fail closed. Missing *workspace_dir* fails closed.
|
||||
- Only active (pending/finalized) forks in the current scope are checked;
|
||||
failed/superseded/merged leftovers cannot block later runs.
|
||||
"""
|
||||
if not isinstance(state, dict):
|
||||
return False
|
||||
if state.get("forks_integrated") is not True:
|
||||
return False
|
||||
if workspace_dir is None:
|
||||
return False
|
||||
try:
|
||||
from qwenpaw.agents.fork_project import (
|
||||
forks_merged_into_head,
|
||||
get_active_fork_scope,
|
||||
resolve_integration_project_dir,
|
||||
)
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
project_dir = resolve_integration_project_dir(workspace_dir)
|
||||
if project_dir is None:
|
||||
# No git project / pointer → cannot register registry forks; the
|
||||
# explicit boolean flag is enough for the no-fork protocol path.
|
||||
return True
|
||||
scope_id = get_active_fork_scope(workspace_dir) or None
|
||||
return forks_merged_into_head(project_dir, scope_id=scope_id)
|
||||
|
||||
|
||||
FORKS_INTEGRATED_REMINDER = """\
|
||||
BLOCKED: forked worker results are not integrated yet.
|
||||
|
||||
{protocol}
|
||||
|
||||
Keep the current workflow phase unchanged (resume_phase / target phase
|
||||
must stay as written — do NOT rewrite it to execution/exec/working).
|
||||
After merges succeed, update state.json with the JSON boolean
|
||||
forks_integrated=true (not a string). Then the gate resumes the
|
||||
original target phase — do not re-dispatch executor workers.
|
||||
"""
|
||||
|
||||
|
||||
def merge_blocked_continuation(protocol: str) -> str:
|
||||
"""Controller prompt when the gate rejects a premature phase advance."""
|
||||
return FORKS_INTEGRATED_REMINDER.format(protocol=protocol)
|
||||
@@ -0,0 +1,161 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""OMP loop UI copy: names, menu summaries, and full help texts.
|
||||
|
||||
Menu short copy is bilingual (en / zh-CN). Full ``help`` is English for now;
|
||||
zh-CN can be added later without changing call sites.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
LOOP_UI_I18N: dict[str, dict[str, Any]] = {
|
||||
"ultrawork": {
|
||||
"name": {
|
||||
"en": "Ultrawork",
|
||||
"zh-CN": "Ultrawork",
|
||||
},
|
||||
"description": {
|
||||
"en": "**Ultrawork** — parallel task execution engine",
|
||||
"zh-CN": "**Ultrawork** — 并行任务执行引擎",
|
||||
},
|
||||
"help": {
|
||||
"en": (
|
||||
"**Ultrawork** — parallel task execution engine\n\n"
|
||||
"Usage: `/ultrawork <task description>`\n\n"
|
||||
"Decomposes the task into independent sub-tasks and executes\n"
|
||||
"them in parallel via spawn_subagent batch mode."
|
||||
),
|
||||
},
|
||||
},
|
||||
"ralph": {
|
||||
"name": {
|
||||
"en": "Ralph",
|
||||
"zh-CN": "Ralph",
|
||||
},
|
||||
"description": {
|
||||
"en": ("**Ralph** — PRD-driven continuous implementation loop"),
|
||||
"zh-CN": "**Ralph** — PRD 驱动的持续实现循环",
|
||||
},
|
||||
"help": {
|
||||
"en": (
|
||||
"**Ralph** — PRD-driven continuous implementation loop\n\n"
|
||||
"Usage: `/ralph [--no-deslop] "
|
||||
"[--critic=architect|critic|codex] <task>`\n\n"
|
||||
"Creates a PRD with user stories, implements each one,\n"
|
||||
"verifies acceptance criteria, and runs reviewer verification."
|
||||
),
|
||||
},
|
||||
},
|
||||
"autopilot": {
|
||||
"name": {
|
||||
"en": "Autopilot",
|
||||
"zh-CN": "Autopilot",
|
||||
},
|
||||
"description": {
|
||||
"en": "**Autopilot** — full lifecycle pipeline",
|
||||
"zh-CN": "**Autopilot** — 全生命周期流水线",
|
||||
},
|
||||
"help": {
|
||||
"en": (
|
||||
"**Autopilot** — full lifecycle pipeline\n\n"
|
||||
"Usage: `/autopilot [--skip-qa] "
|
||||
"[--skip-validation] <task>`\n\n"
|
||||
"Phases: expansion -> planning -> execution "
|
||||
"-> qa -> validation -> cleanup -> completed\n"
|
||||
"Validation uses 3 parallel reviewers "
|
||||
"(architect + security + code)."
|
||||
),
|
||||
},
|
||||
},
|
||||
"ultraqa": {
|
||||
"name": {
|
||||
"en": "UltraQA",
|
||||
"zh-CN": "UltraQA",
|
||||
},
|
||||
"description": {
|
||||
"en": "**UltraQA** — automated QA cycle engine",
|
||||
"zh-CN": "**UltraQA** — 自动化 QA 循环引擎",
|
||||
},
|
||||
"help": {
|
||||
"en": (
|
||||
"**UltraQA** — automated QA cycle engine\n\n"
|
||||
"Usage:\n"
|
||||
" `/ultraqa [--tests|--build|--lint|"
|
||||
'--typecheck|--custom "cmd"]'
|
||||
" [--interactive]`\n\n"
|
||||
"Runs repeated QA cycles: check → diagnose → fix → re-check.\n"
|
||||
"Stops when all checks pass or max cycles reached."
|
||||
),
|
||||
},
|
||||
},
|
||||
"team": {
|
||||
"name": {
|
||||
"en": "Team",
|
||||
"zh-CN": "Team",
|
||||
},
|
||||
"description": {
|
||||
"en": "**Team** — multi-agent collaboration pipeline",
|
||||
"zh-CN": "**Team** — 多智能体协作流水线",
|
||||
},
|
||||
"help": {
|
||||
"en": (
|
||||
"**Team** — multi-agent collaboration pipeline\n\n"
|
||||
"Usage: `/team [N:role] <task>`\n\n"
|
||||
"Examples:\n"
|
||||
" `/team 3:executor Implement authentication`\n"
|
||||
" `/team ralph Build the REST API`\n\n"
|
||||
"Phases: plan -> prd -> exec -> verify -> fix (retry)"
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def loop_ui(mode_key: str) -> dict[str, Any]:
|
||||
"""Return the UI copy bundle for one OMP mode."""
|
||||
try:
|
||||
return LOOP_UI_I18N[mode_key]
|
||||
except KeyError as exc:
|
||||
raise KeyError(f"Unknown OMP loop UI key: {mode_key}") from exc
|
||||
|
||||
|
||||
def _pick_locale(mapping: dict[str, str], lang: str = "en") -> str:
|
||||
if not mapping:
|
||||
return ""
|
||||
if lang in mapping and mapping[lang]:
|
||||
return mapping[lang]
|
||||
short = lang.split("-", 1)[0].lower()
|
||||
for key, value in mapping.items():
|
||||
if key.split("-", 1)[0].lower() == short and value:
|
||||
return value
|
||||
for key in ("en", "en-US", "zh-CN", "zh"):
|
||||
if mapping.get(key):
|
||||
return mapping[key]
|
||||
for value in mapping.values():
|
||||
if value:
|
||||
return value
|
||||
return ""
|
||||
|
||||
|
||||
def loop_help_text(mode_key: str, lang: str = "en") -> str:
|
||||
"""Full help text for CommandSpec / slash help handlers."""
|
||||
help_map = loop_ui(mode_key).get("help") or {}
|
||||
if not isinstance(help_map, dict):
|
||||
return ""
|
||||
return _pick_locale(help_map, lang)
|
||||
|
||||
|
||||
def loop_command_metadata(mode_key: str) -> dict[str, Any]:
|
||||
"""Metadata for CommandSpec: loop_name + i18n maps for GET /loops."""
|
||||
ui = loop_ui(mode_key)
|
||||
name_map = ui.get("name") or {}
|
||||
desc_map = ui.get("description") or {}
|
||||
return {
|
||||
"builtin": True,
|
||||
"loop_name": _pick_locale(name_map, "en") or mode_key,
|
||||
"name_i18n": dict(name_map) if isinstance(name_map, dict) else {},
|
||||
"description_i18n": dict(desc_map)
|
||||
if isinstance(desc_map, dict)
|
||||
else {},
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Shared AgentMode scaffolding for OMP workflow modes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import weakref
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from agentscope.message import Msg, TextBlock
|
||||
|
||||
from qwenpaw.modes.base import AgentMode
|
||||
from qwenpaw.runtime.hooks import HookContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OMPModeBase(AgentMode):
|
||||
"""Common setup / lifecycle for OMP workflow modes.
|
||||
|
||||
Subclasses must set ``name``, ``gate_cls``, ``plugin_id``,
|
||||
``handler_name``, and ``scope``.
|
||||
"""
|
||||
|
||||
gate_cls: type
|
||||
plugin_id: str
|
||||
handler_name: str
|
||||
scope: str
|
||||
|
||||
_instances: ClassVar[list[weakref.ReferenceType]] = []
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._gate: Any = None
|
||||
self._workspace_ref: weakref.ReferenceType[Any] | None = None
|
||||
OMPModeBase._instances.append(weakref.ref(self))
|
||||
|
||||
def setup(self, workspace: object) -> None:
|
||||
super().setup(workspace)
|
||||
self._workspace_ref = weakref.ref(workspace)
|
||||
from qwenpaw.loop.gates import StopHandler, StopHandlerRegistration
|
||||
|
||||
handler = StopHandler()
|
||||
gate = self.gate_cls()
|
||||
handler.register(gate)
|
||||
self._gate = gate
|
||||
|
||||
plugins = getattr(workspace, "plugins", None)
|
||||
if plugins is not None:
|
||||
# Defensive: matches MissionMode / older WorkspacePlugins
|
||||
if not hasattr(plugins, "stop_handlers"):
|
||||
plugins.stop_handlers = []
|
||||
plugins.stop_handlers.append(
|
||||
StopHandlerRegistration(
|
||||
plugin_id=self.plugin_id,
|
||||
handler=handler,
|
||||
priority=0,
|
||||
name=self.handler_name,
|
||||
scope=self.scope,
|
||||
),
|
||||
)
|
||||
|
||||
def is_active(
|
||||
self,
|
||||
ctx: Any, # pylint: disable=unused-argument
|
||||
) -> bool:
|
||||
# Follows upstream MissionMode pattern (LoopGate lacks public API)
|
||||
# pylint: disable=protected-access
|
||||
return self._gate is not None and self._gate._state() is not None
|
||||
|
||||
async def on_conversation_reset(
|
||||
self,
|
||||
ctx: HookContext, # pylint: disable=unused-argument
|
||||
) -> None:
|
||||
"""Clear gate state on /new and /clear."""
|
||||
if self._gate is not None:
|
||||
self._gate.reset_session()
|
||||
|
||||
def claim_workflow(self) -> None:
|
||||
"""Deactivate peer OMP workflows so only this scope is active.
|
||||
|
||||
``_filter_by_scope`` keeps the first active non-default scope;
|
||||
without mutual exclusion a later ``/ralph`` would be ignored
|
||||
while an earlier ``/ultraqa`` gate still holds session state.
|
||||
|
||||
Only peers bound to the **same workspace** are deactivated —
|
||||
modes from other workspaces must keep their own gates.
|
||||
"""
|
||||
my_ws = self._workspace_ref() if self._workspace_ref else None
|
||||
alive: list[weakref.ReferenceType] = []
|
||||
for ref in OMPModeBase._instances:
|
||||
mode = ref()
|
||||
if mode is None:
|
||||
continue
|
||||
alive.append(ref)
|
||||
# pylint: disable=protected-access
|
||||
peer_gate = mode._gate
|
||||
if mode is self or peer_gate is None:
|
||||
continue
|
||||
peer_ws = mode._workspace_ref() if mode._workspace_ref else None
|
||||
if my_ws is not None and peer_ws is not my_ws:
|
||||
continue
|
||||
if peer_gate._state() is not None:
|
||||
logger.info(
|
||||
"Deactivating peer OMP mode '%s' for '%s'",
|
||||
mode.name,
|
||||
self.name,
|
||||
)
|
||||
peer_gate.reset_session()
|
||||
OMPModeBase._instances = alive
|
||||
|
||||
|
||||
def info_msg(text: str) -> Msg:
|
||||
"""Build a system info message for slash-command help/errors."""
|
||||
return Msg(
|
||||
name="system",
|
||||
content=[TextBlock(type="text", text=text)],
|
||||
role="system",
|
||||
)
|
||||
|
||||
|
||||
def rewrite_user_msg(ctx: Any, text: str) -> None:
|
||||
"""Replace the last user message content with *text*."""
|
||||
msgs = getattr(ctx, "input_msgs", None)
|
||||
if not msgs:
|
||||
return
|
||||
last = msgs[-1]
|
||||
if isinstance(last, Msg):
|
||||
last.content = [TextBlock(type="text", text=text)]
|
||||
|
||||
|
||||
__all__ = ["OMPModeBase", "info_msg", "rewrite_user_msg"]
|
||||
@@ -0,0 +1,240 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Reusable role identity prompts and spawn_subagent formatting."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .constants import ROLE_ALLOWED_TOOLS, ROLE_SKILLS
|
||||
|
||||
try:
|
||||
from qwenpaw.agents.fork_project import FORK_WORKER_COMMIT_PROTOCOL
|
||||
except ImportError: # pragma: no cover - offline / partial installs
|
||||
FORK_WORKER_COMMIT_PROTOCOL = (
|
||||
"## Fork worktree commit protocol (REQUIRED)\n"
|
||||
"Commit all changes in the worktree before finishing.\n"
|
||||
)
|
||||
|
||||
ROLE_PROMPTS: dict[str, str] = {
|
||||
"analyst": (
|
||||
"You are a requirements analyst. "
|
||||
"Extract concrete requirements, identify hidden constraints, "
|
||||
"define measurable acceptance criteria.\n"
|
||||
"DO NOT: write code, create files, run commands.\n"
|
||||
"OUTPUT: structured requirements in JSON format."
|
||||
),
|
||||
"architect": (
|
||||
"You are a system architect. "
|
||||
"Diagnose failures with root cause analysis, "
|
||||
"design system interfaces and module boundaries.\n"
|
||||
"OUTPUT: diagnosis with specific fix recommendations."
|
||||
),
|
||||
"executor": (
|
||||
"You are a code executor. "
|
||||
"Implement the assigned task following the design spec. "
|
||||
"Run quality checks. Follow existing patterns.\n"
|
||||
"OUTPUT: working code + progress report."
|
||||
),
|
||||
"qa-tester": (
|
||||
"You are an interactive QA tester. "
|
||||
"Test CLI/service interactions by starting the service, "
|
||||
"running test cases, and verifying expected behavior.\n"
|
||||
"OUTPUT: test results with PASS/FAIL per scenario."
|
||||
),
|
||||
"security-reviewer": (
|
||||
"You are a security reviewer. "
|
||||
"Identify vulnerabilities, injection vectors, "
|
||||
"auth/authz weaknesses, and data exposure risks.\n"
|
||||
"DO NOT modify any project files.\n"
|
||||
"OUTPUT: security findings with severity ratings."
|
||||
),
|
||||
"code-reviewer": (
|
||||
"You are a code reviewer. "
|
||||
"Review code quality, correctness, maintainability, "
|
||||
"and adherence to project conventions.\n"
|
||||
"DO NOT modify any project files.\n"
|
||||
"OUTPUT: review findings with actionable feedback."
|
||||
),
|
||||
"critic": (
|
||||
"You are a plan/design critic. "
|
||||
"Challenge assumptions, find gaps, identify risks.\n"
|
||||
"DO NOT create plans or write code.\n"
|
||||
"OUTPUT: critique with specific concerns and alternatives."
|
||||
),
|
||||
"planner": (
|
||||
"You are a strategic planner. "
|
||||
"Create implementation plans from specifications. "
|
||||
"Define task breakdown and dependency order.\n"
|
||||
"DO NOT write implementation code.\n"
|
||||
"OUTPUT: ordered task list with dependencies."
|
||||
),
|
||||
"explore": (
|
||||
"You are a code explorer. "
|
||||
"Search and read the codebase to map structure, "
|
||||
"dependencies, and relevant code locations.\n"
|
||||
"DO NOT modify any files.\n"
|
||||
"OUTPUT: structured findings with file paths and relationships."
|
||||
),
|
||||
"debugger": (
|
||||
"You are a debugging expert. "
|
||||
"Perform root cause analysis on build/test failures. "
|
||||
"Apply targeted fixes.\n"
|
||||
"DO NOT add new features.\n"
|
||||
"OUTPUT: root cause + applied fix summary."
|
||||
),
|
||||
"verifier": (
|
||||
"You are an adversarial verifier. "
|
||||
"Your job is to BREAK the implementation. "
|
||||
"Try every edge case, invalid input, and race condition.\n"
|
||||
"DO NOT modify any project files.\n"
|
||||
"OUTPUT: VERDICT: PASS/FAIL/PARTIAL with evidence."
|
||||
),
|
||||
}
|
||||
|
||||
# UX / flag aliases → ROLE_PROMPTS keys.
|
||||
_ROLE_ALIASES = {
|
||||
"codex": "critic", # Ralph --critic=codex
|
||||
"ralph": "executor", # Team `/team ralph ...`
|
||||
}
|
||||
|
||||
|
||||
def resolve_role(role: str) -> str:
|
||||
"""Map *role* (and aliases) onto a known ROLE_PROMPTS key."""
|
||||
key = _ROLE_ALIASES.get(role, role)
|
||||
if key in ROLE_PROMPTS:
|
||||
return key
|
||||
return "executor"
|
||||
|
||||
|
||||
def get_role_prompt(role: str) -> str:
|
||||
"""Return the prompt fragment for *role*, falling back to executor."""
|
||||
return ROLE_PROMPTS[resolve_role(role)]
|
||||
|
||||
|
||||
def build_worker_prompt(
|
||||
role: str,
|
||||
task: str,
|
||||
context: str = "",
|
||||
*,
|
||||
fork: bool = False,
|
||||
) -> str:
|
||||
"""Build a complete worker prompt for spawn_subagent's *task* param."""
|
||||
parts = [get_role_prompt(role)]
|
||||
if fork:
|
||||
parts.append("\n" + FORK_WORKER_COMMIT_PROTOCOL)
|
||||
parts.append(f"\n## Task\n{task}")
|
||||
if context:
|
||||
parts.append(f"\n## Context\n{context}")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def tools_literal(role: str) -> str | None:
|
||||
"""Return a Python-list literal for *role*'s tools, or None to omit."""
|
||||
tools = ROLE_ALLOWED_TOOLS.get(resolve_role(role))
|
||||
if tools is None:
|
||||
return None
|
||||
inner = ", ".join(f'"{t}"' for t in tools)
|
||||
return f"[{inner}]"
|
||||
|
||||
|
||||
def skills_literal(role: str) -> str | None:
|
||||
"""Return a Python-list literal for *role*'s skills, or None to omit."""
|
||||
skills = ROLE_SKILLS.get(resolve_role(role))
|
||||
if skills is None:
|
||||
return None
|
||||
inner = ", ".join(f'"{s}"' for s in skills)
|
||||
return f"[{inner}]"
|
||||
|
||||
|
||||
def format_spawn_call(
|
||||
role: str,
|
||||
task_body: str,
|
||||
*,
|
||||
fork: bool = False,
|
||||
background: bool = True,
|
||||
indent: str = " ",
|
||||
) -> str:
|
||||
"""Format a ``spawn_subagent(...)`` example for controller prompts.
|
||||
|
||||
The *task* argument embeds the role identity from
|
||||
:func:`build_worker_prompt` so prompts stay aligned with
|
||||
``ROLE_PROMPTS`` / ``omp-roles`` skill config.
|
||||
"""
|
||||
# Keep the example readable: show role identity + task placeholder.
|
||||
task_preview = (
|
||||
f"<role:{resolve_role(role)} identity from omp-roles>\\n\\n"
|
||||
f"## Task\\n{task_body}"
|
||||
)
|
||||
pad = indent
|
||||
lines = [
|
||||
f"{pad}spawn_subagent(",
|
||||
f'{pad} task="{task_preview}",',
|
||||
]
|
||||
tools = tools_literal(role)
|
||||
if tools is not None:
|
||||
lines.append(f"{pad} allowed_tools={tools},")
|
||||
skills = skills_literal(role)
|
||||
if skills is not None:
|
||||
lines.append(f"{pad} skills={skills},")
|
||||
if fork:
|
||||
lines.append(f"{pad} fork=True,")
|
||||
if background:
|
||||
lines.append(f"{pad} background=True,")
|
||||
lines.append(f"{pad})")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_batch_item(
|
||||
role: str,
|
||||
task_body: str,
|
||||
*,
|
||||
fork: bool = False,
|
||||
indent: str = " ",
|
||||
) -> str:
|
||||
"""Format one batch-mode dict entry for controller prompts."""
|
||||
pad = indent
|
||||
task_preview = (
|
||||
f"<role:{resolve_role(role)} identity from omp-roles>\\n\\n"
|
||||
f"## Task\\n{task_body}"
|
||||
)
|
||||
lines = [
|
||||
f"{pad}{{",
|
||||
f'{pad} "task": "{task_preview}",',
|
||||
]
|
||||
tools = tools_literal(role)
|
||||
if tools is not None:
|
||||
lines.append(f'{pad} "allowed_tools": {tools},')
|
||||
skills = skills_literal(role)
|
||||
if skills is not None:
|
||||
lines.append(f'{pad} "skills": {skills},')
|
||||
if fork:
|
||||
lines.append(f'{pad} "fork": true,')
|
||||
lines.append(f"{pad}}}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
FORK_MERGE_PROTOCOL = """\
|
||||
Fork integration (REQUIRED when any worker used fork=True):
|
||||
- Each forked worker result includes [FORK_BRANCH: <branch>] and works
|
||||
in an isolated git worktree. Workers must commit before finishing;
|
||||
`git merge <branch>` only brings commits, not dirty files.
|
||||
- After ALL workers finish, for each [FORK_BRANCH]:
|
||||
1. `git merge --no-ff <branch>` (or cherry-pick the commits) into
|
||||
the current branch of the main workspace.
|
||||
2. On conflict: resolve or abort. Do NOT advance the workflow phase
|
||||
and do NOT report completion while conflicts remain.
|
||||
- Keep the target workflow phase as-is while merging (do not rewrite
|
||||
it back to execution/exec/working).
|
||||
- If no worker used fork=True, still set forks_integrated=true
|
||||
(JSON boolean, not a string).
|
||||
- After successful integration, update state.json with the JSON
|
||||
boolean forks_integrated=true.
|
||||
- The stop gate REJECTS phase completion until that flag is true AND
|
||||
registered fork commits are ancestors of HEAD."""
|
||||
|
||||
|
||||
def fork_merge_instructions(indent: str = "") -> str:
|
||||
"""Return the fork merge protocol block, optionally indented."""
|
||||
if not indent:
|
||||
return FORK_MERGE_PROTOCOL
|
||||
return "\n".join(
|
||||
indent + line for line in FORK_MERGE_PROTOCOL.splitlines()
|
||||
)
|
||||
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""WorkflowState — state directory and file management for OMP modes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorkflowState:
|
||||
"""Manage per-instance state directory and files.
|
||||
|
||||
Path convention:
|
||||
{workspace_dir}/.qwenpaw/omp_workflows/{mode_name}-{timestamp}/
|
||||
|
||||
Writers should prefer :meth:`update_state` so concurrent agent
|
||||
updates to other keys are preserved (read-merge-write + atomic
|
||||
replace).
|
||||
"""
|
||||
|
||||
def __init__(self, workspace_dir: Path, mode_name: str) -> None:
|
||||
self.workspace_dir = workspace_dir
|
||||
self.mode_name = mode_name
|
||||
self._instance_dir: Path | None = None
|
||||
|
||||
def create_instance(self) -> Path:
|
||||
"""Create a timestamped instance directory."""
|
||||
ts = time.strftime("%Y%m%d-%H%M%S")
|
||||
suffix = uuid.uuid4().hex[:6]
|
||||
base = self.workspace_dir / ".qwenpaw" / "omp_workflows"
|
||||
self._instance_dir = base / f"{self.mode_name}-{ts}-{suffix}"
|
||||
self._instance_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.append_log(f"[{self.mode_name}] instance created")
|
||||
return self._instance_dir
|
||||
|
||||
@property
|
||||
def instance_dir(self) -> Path | None:
|
||||
return self._instance_dir
|
||||
|
||||
@classmethod
|
||||
def from_existing(
|
||||
cls,
|
||||
workspace_dir: Path,
|
||||
mode_name: str,
|
||||
instance_dir: Path,
|
||||
) -> WorkflowState:
|
||||
"""Attach to an already-created instance directory."""
|
||||
wf = cls(workspace_dir, mode_name)
|
||||
wf._instance_dir = instance_dir
|
||||
return wf
|
||||
|
||||
def read_state(self) -> dict[str, Any]:
|
||||
"""Read state.json, returning empty dict if absent."""
|
||||
if not self._instance_dir:
|
||||
return {}
|
||||
p = self._instance_dir / "state.json"
|
||||
if not p.exists():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
logger.warning("Failed to read %s", p, exc_info=True)
|
||||
return {}
|
||||
|
||||
def write_state(self, data: dict[str, Any]) -> None:
|
||||
"""Atomically replace state.json with *data*."""
|
||||
if not self._instance_dir:
|
||||
return
|
||||
self._atomic_write_json(
|
||||
self._instance_dir / "state.json",
|
||||
data,
|
||||
)
|
||||
|
||||
def update_state(self, patch: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Merge *patch* into state.json and write atomically.
|
||||
|
||||
Returns the merged document. Prefer this over
|
||||
:meth:`write_state` when the gate only owns some keys.
|
||||
"""
|
||||
data = self.read_state()
|
||||
data.update(patch)
|
||||
self.write_state(data)
|
||||
return data
|
||||
|
||||
def read_prd(self) -> dict[str, Any]:
|
||||
"""Read prd.json, returning empty dict if absent."""
|
||||
if not self._instance_dir:
|
||||
return {}
|
||||
p = self._instance_dir / "prd.json"
|
||||
if not p.exists():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
logger.warning("Failed to read %s", p, exc_info=True)
|
||||
return {}
|
||||
|
||||
def append_log(self, entry: str) -> None:
|
||||
"""Append a line to progress.txt (survives :meth:`cleanup`)."""
|
||||
if not self._instance_dir:
|
||||
return
|
||||
p = self._instance_dir / "progress.txt"
|
||||
with p.open("a", encoding="utf-8") as f:
|
||||
f.write(entry + "\n")
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Remove temporary control files; keep audit artifacts.
|
||||
|
||||
Deletes ``state.json`` / ``prd.json`` (and ``*.tmp`` sidecars)
|
||||
only. Specs, plans, handoffs, worker results, and
|
||||
``progress.txt`` are retained for post-run inspection.
|
||||
"""
|
||||
if not self._instance_dir or not self._instance_dir.exists():
|
||||
return
|
||||
remove_names = {"state.json", "prd.json"}
|
||||
for child in list(self._instance_dir.iterdir()):
|
||||
should_remove = child.name in remove_names or (
|
||||
child.is_file() and child.name.endswith(".tmp")
|
||||
)
|
||||
if not should_remove:
|
||||
continue
|
||||
try:
|
||||
if child.is_dir():
|
||||
shutil.rmtree(child)
|
||||
else:
|
||||
child.unlink()
|
||||
except OSError:
|
||||
logger.warning(
|
||||
"Failed to remove %s during cleanup",
|
||||
child,
|
||||
exc_info=True,
|
||||
)
|
||||
self.append_log(f"[{self.mode_name}] cleanup complete")
|
||||
logger.info("Cleaned up control files in %s", self._instance_dir)
|
||||
|
||||
@staticmethod
|
||||
def _atomic_write_json(path: Path, data: dict[str, Any]) -> None:
|
||||
"""Write JSON via temp file + replace."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
tmp.write_text(
|
||||
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
tmp.replace(path)
|
||||
@@ -0,0 +1,93 @@
|
||||
# OMP Role Tool Configuration
|
||||
|
||||
When using spawn_subagent (single or batch mode) to dispatch sub-agents,
|
||||
set the corresponding allowed_tools and skills parameters based on the
|
||||
sub-agent's role.
|
||||
|
||||
**Canonical source of truth in code:**
|
||||
`plugins/bundle/omp_workflows/shared/constants.py`
|
||||
(`ROLE_ALLOWED_TOOLS`, `ROLE_SKILLS`, `TOOL_*` names) and
|
||||
`shared/role_prompts.py` (`ROLE_PROMPTS`, `build_worker_prompt`,
|
||||
`format_spawn_call`). Tool strings must match registered ToolRegistry
|
||||
names — do not invent aliases.
|
||||
|
||||
## Role Definitions
|
||||
|
||||
### executor (Code Executor)
|
||||
- **allowed_tools**: null (inherits all tools)
|
||||
- **skills**: null (inherits all skills)
|
||||
- **Boundary**: Implement features, run quality checks. Follow existing patterns.
|
||||
|
||||
### architect (System Architect)
|
||||
- **allowed_tools**: ["read_file", "grep_search", "glob_search",
|
||||
"write_file", "ast_search", "execute_shell_command"]
|
||||
- **skills**: []
|
||||
- **Boundary**: Design interfaces and module boundaries, diagnose complex issues. No implementation code.
|
||||
|
||||
### analyst (Requirements Analyst)
|
||||
- **allowed_tools**: ["read_file", "grep_search", "glob_search",
|
||||
"write_file", "execute_shell_command"]
|
||||
- **skills**: []
|
||||
- **Boundary**: Extract requirements, identify constraints, define acceptance criteria. No code.
|
||||
|
||||
### critic (Plan/Design Critic)
|
||||
- **allowed_tools**: ["read_file", "grep_search", "glob_search",
|
||||
"ast_search"]
|
||||
- **skills**: []
|
||||
- **Boundary**: Review plans and designs, challenge assumptions. No plans, no code.
|
||||
|
||||
### security-reviewer (Security Reviewer)
|
||||
- **allowed_tools**: ["read_file", "grep_search", "glob_search",
|
||||
"execute_shell_command", "ast_search"]
|
||||
- **skills**: []
|
||||
- **Boundary**: Identify vulnerabilities, injection vectors, auth weaknesses. No file modifications.
|
||||
|
||||
### code-reviewer (Code Reviewer)
|
||||
- **allowed_tools**: ["read_file", "grep_search", "glob_search",
|
||||
"execute_shell_command", "ast_search"]
|
||||
- **skills**: []
|
||||
- **Boundary**: Review code quality, maintainability, convention adherence. No file modifications.
|
||||
|
||||
### qa-tester (QA Tester)
|
||||
- **allowed_tools**: ["read_file", "grep_search", "glob_search",
|
||||
"execute_shell_command"]
|
||||
- **skills**: []
|
||||
- **Boundary**: Interactive CLI/service testing — start services and verify behavior.
|
||||
|
||||
### planner (Strategic Planner)
|
||||
- **allowed_tools**: ["read_file", "grep_search", "glob_search",
|
||||
"write_file", "ast_search"]
|
||||
- **skills**: []
|
||||
- **Boundary**: Create implementation plans, define task breakdown and dependencies. No implementation code.
|
||||
|
||||
### explore (Code Explorer)
|
||||
- **allowed_tools**: ["read_file", "grep_search", "glob_search",
|
||||
"ast_search"]
|
||||
- **skills**: []
|
||||
- **Boundary**: Read-only exploration. Map codebase structure and dependencies.
|
||||
|
||||
### debugger (Debugging Expert)
|
||||
- **allowed_tools**: null (inherits all tools)
|
||||
- **skills**: []
|
||||
- **Boundary**: Root cause analysis, fix build/test failures. No new features.
|
||||
|
||||
### verifier (Adversarial Verifier)
|
||||
- **allowed_tools**: ["read_file", "grep_search", "glob_search",
|
||||
"execute_shell_command", "ast_search"]
|
||||
- **skills**: []
|
||||
- **Boundary**: Verification only — no file modifications. Physical isolation via read-only tool set.
|
||||
|
||||
## Usage
|
||||
|
||||
```text
|
||||
spawn_subagent(
|
||||
task="<role identity from ROLE_PROMPTS>\n\n## Task\n<task description>",
|
||||
allowed_tools=<ROLE_ALLOWED_TOOLS[role] or omit if null>,
|
||||
skills=<ROLE_SKILLS[role] or omit if null>,
|
||||
fork=True, # if worktree isolation needed
|
||||
background=True, # if background execution needed
|
||||
)
|
||||
```
|
||||
|
||||
When allowed_tools or skills is null, omit the parameter (uses default None).
|
||||
For batch mode, pass `task=""` and put per-item role config in each batch dict.
|
||||
@@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Team pipeline gate — 5-phase with fix retry limit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from qwenpaw.loop.gates.base import StopAction, StopHandlerResult
|
||||
from qwenpaw.loop.gates.loop_gate import LoopGate
|
||||
|
||||
from ..shared.constants import (
|
||||
TEAM_MAX_FIX_ATTEMPTS,
|
||||
TEAM_MAX_ITERATIONS,
|
||||
)
|
||||
from ..shared.fork_guard import forks_integrated, merge_blocked_continuation
|
||||
from ..shared.role_prompts import FORK_MERGE_PROTOCOL
|
||||
from ..shared.state import WorkflowState
|
||||
from .prompts import build_continuation as _build_prompt
|
||||
|
||||
_POST_FORK_PHASES = frozenset({"verify", "fix", "completed"})
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TeamState:
|
||||
loop_dir: Path
|
||||
workspace_dir: Path
|
||||
active: bool = True
|
||||
iteration: int = 0
|
||||
max_iterations: int = TEAM_MAX_ITERATIONS
|
||||
agent_count: int = 3
|
||||
agent_role: str = "executor"
|
||||
fix_attempts: int = 0
|
||||
max_fix_attempts: int = TEAM_MAX_FIX_ATTEMPTS
|
||||
phase: str = "plan"
|
||||
blocked_on_merge: bool = False
|
||||
|
||||
|
||||
class TeamPipelineGate(LoopGate):
|
||||
"""Stop gate for the Team multi-agent pipeline."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "team"
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
return 50
|
||||
|
||||
def activate_for_team(
|
||||
self,
|
||||
workspace_dir: Path,
|
||||
agent_count: int = 3,
|
||||
agent_role: str = "executor",
|
||||
) -> Path:
|
||||
try:
|
||||
from qwenpaw.agents.fork_project import begin_fork_scope
|
||||
|
||||
begin_fork_scope(workspace_dir)
|
||||
except ImportError:
|
||||
import logging
|
||||
|
||||
logging.getLogger(__name__).warning(
|
||||
"begin_fork_scope unavailable; fork merge scope disabled",
|
||||
)
|
||||
wf = WorkflowState(workspace_dir, "team")
|
||||
loop_dir = wf.create_instance()
|
||||
|
||||
handoffs = loop_dir / "handoffs"
|
||||
handoffs.mkdir(exist_ok=True)
|
||||
results = loop_dir / "results"
|
||||
results.mkdir(exist_ok=True)
|
||||
|
||||
state = _TeamState(
|
||||
loop_dir=loop_dir,
|
||||
workspace_dir=workspace_dir,
|
||||
agent_count=agent_count,
|
||||
agent_role=agent_role,
|
||||
)
|
||||
wf.write_state(
|
||||
{
|
||||
"current_phase": "plan",
|
||||
"fix_attempts": 0,
|
||||
},
|
||||
)
|
||||
self.activate(state)
|
||||
return loop_dir
|
||||
|
||||
async def check( # pylint: disable=too-many-return-statements
|
||||
self,
|
||||
ctx: Any,
|
||||
) -> Optional[StopHandlerResult]:
|
||||
if isinstance(ctx, dict) and ctx.get("has_tool_calls"):
|
||||
return StopHandlerResult(action=StopAction.BYPASS)
|
||||
|
||||
st: _TeamState | None = self._state()
|
||||
if st is None:
|
||||
return StopHandlerResult(
|
||||
action=StopAction.BYPASS,
|
||||
)
|
||||
|
||||
wf = WorkflowState.from_existing(
|
||||
st.workspace_dir,
|
||||
"team",
|
||||
st.loop_dir,
|
||||
)
|
||||
data = await asyncio.to_thread(wf.read_state)
|
||||
|
||||
prev_phase = st.phase
|
||||
phase = data.get("current_phase", "plan")
|
||||
st.phase = phase
|
||||
|
||||
if phase in _POST_FORK_PHASES:
|
||||
integrated = await asyncio.to_thread(
|
||||
forks_integrated,
|
||||
data,
|
||||
st.workspace_dir,
|
||||
)
|
||||
if not integrated:
|
||||
# Preserve target phase; do not burn iteration while merging.
|
||||
st.blocked_on_merge = True
|
||||
st.phase = phase
|
||||
await asyncio.to_thread(
|
||||
wf.update_state,
|
||||
{
|
||||
"merge_blocked": True,
|
||||
"resume_phase": phase,
|
||||
},
|
||||
)
|
||||
return StopHandlerResult(
|
||||
action=StopAction.INTERRUPT_AND_CONTINUE,
|
||||
reason="Team blocked: forks not integrated",
|
||||
)
|
||||
|
||||
if data.get("merge_blocked"):
|
||||
await asyncio.to_thread(
|
||||
wf.update_state,
|
||||
{"merge_blocked": False},
|
||||
)
|
||||
st.blocked_on_merge = False
|
||||
|
||||
st.iteration += 1
|
||||
if st.iteration > st.max_iterations:
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason=f"Iteration limit ({st.max_iterations})",
|
||||
)
|
||||
|
||||
if phase == "completed":
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason="Team pipeline completed",
|
||||
)
|
||||
|
||||
# Count fix *rounds* (phase entry), not every agent turn.
|
||||
if phase == "fix" and prev_phase != "fix":
|
||||
st.fix_attempts += 1
|
||||
if st.fix_attempts > st.max_fix_attempts:
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason=f"Fix retry limit ({st.max_fix_attempts})",
|
||||
)
|
||||
await asyncio.to_thread(
|
||||
wf.update_state,
|
||||
{"fix_attempts": st.fix_attempts},
|
||||
)
|
||||
|
||||
return StopHandlerResult(
|
||||
action=StopAction.INTERRUPT_AND_CONTINUE,
|
||||
reason="Team pipeline in progress",
|
||||
)
|
||||
|
||||
def build_continuation(self) -> str:
|
||||
"""Build Team continuation from gate state."""
|
||||
st: _TeamState | None = self._state()
|
||||
if st is None:
|
||||
return ""
|
||||
if st.blocked_on_merge:
|
||||
return merge_blocked_continuation(FORK_MERGE_PROTOCOL)
|
||||
return _build_prompt(
|
||||
phase=st.phase,
|
||||
iteration=st.iteration,
|
||||
max_iterations=st.max_iterations,
|
||||
agent_count=st.agent_count,
|
||||
agent_role=st.agent_role,
|
||||
loop_dir=st.loop_dir,
|
||||
fix_attempts=st.fix_attempts,
|
||||
max_fix_attempts=st.max_fix_attempts,
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""TeamMode — multi-agent collaboration pipeline."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from qwenpaw.runtime.slash_command_registry import CommandSpec
|
||||
|
||||
from ..shared.args import split_args
|
||||
from ..shared.loop_ui_i18n import loop_command_metadata, loop_help_text
|
||||
from ..shared.mode_base import OMPModeBase, info_msg, rewrite_user_msg
|
||||
from ..shared.role_prompts import resolve_role
|
||||
from .gate import TeamPipelineGate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from agentscope.message import Msg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_HELP = loop_help_text("team")
|
||||
|
||||
|
||||
class TeamMode(OMPModeBase):
|
||||
"""AgentMode for the Team pipeline."""
|
||||
|
||||
name = "team"
|
||||
gate_cls = TeamPipelineGate
|
||||
plugin_id = "__omp_team__"
|
||||
handler_name = "team-stop-handler"
|
||||
scope = "omp-team"
|
||||
|
||||
def commands(self) -> list[CommandSpec]:
|
||||
return [
|
||||
CommandSpec(
|
||||
name="team",
|
||||
handler=self._handler,
|
||||
category="builtin",
|
||||
help_text=_HELP,
|
||||
metadata=loop_command_metadata("team"),
|
||||
),
|
||||
]
|
||||
|
||||
async def _handler(self, ctx: "Any", args: str) -> Optional["Msg"]:
|
||||
if not args or not args.strip() or args.strip().lower() == "help":
|
||||
return info_msg(_HELP)
|
||||
|
||||
parsed = _parse_args(args)
|
||||
if parsed is None:
|
||||
return info_msg("Invalid arguments. " + _HELP)
|
||||
task = parsed["task"]
|
||||
if len(task) < 5:
|
||||
return info_msg("Please provide a task description.\n\n" + _HELP)
|
||||
|
||||
workspace_dir = getattr(ctx, "workspace_dir", None)
|
||||
if not workspace_dir:
|
||||
return info_msg("ERROR: no workspace directory available.")
|
||||
|
||||
self.claim_workflow()
|
||||
loop_dir = await asyncio.to_thread(
|
||||
self._gate.activate_for_team,
|
||||
Path(workspace_dir),
|
||||
parsed["agent_count"],
|
||||
parsed["agent_role"],
|
||||
)
|
||||
|
||||
prompt = (
|
||||
f"Team pipeline activated.\n"
|
||||
f"Task: {task}\n"
|
||||
f"Workers: {parsed['agent_count']}, Role: {parsed['agent_role']}\n"
|
||||
f"State directory: {loop_dir}\n"
|
||||
"Phase: plan \u2014 explore the codebase and "
|
||||
"create a task breakdown."
|
||||
)
|
||||
rewrite_user_msg(ctx, prompt)
|
||||
logger.info("Team started: %s", loop_dir)
|
||||
return None
|
||||
|
||||
|
||||
_TEAM_SPEC_RE = re.compile(r"^(\d+):(\w[\w-]*)$")
|
||||
|
||||
|
||||
def _parse_args(raw: str) -> dict | None:
|
||||
"""Parse /team arguments. ``None`` means invalid input."""
|
||||
tokens = split_args(raw)
|
||||
if tokens is None:
|
||||
return None
|
||||
|
||||
agent_count = 3
|
||||
agent_role = "executor"
|
||||
task_parts: list[str] = []
|
||||
|
||||
for i, t in enumerate(tokens):
|
||||
m = _TEAM_SPEC_RE.match(t)
|
||||
if m and i == 0:
|
||||
agent_count = int(m.group(1))
|
||||
agent_role = m.group(2)
|
||||
elif t in ("executor", "ralph") and i == 0:
|
||||
agent_role = t
|
||||
else:
|
||||
task_parts.append(t)
|
||||
|
||||
return {
|
||||
"task": " ".join(task_parts),
|
||||
"agent_count": max(1, min(agent_count, 10)),
|
||||
"agent_role": resolve_role(agent_role),
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Team pipeline continuation prompt templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from ..shared.role_prompts import (
|
||||
fork_merge_instructions,
|
||||
format_batch_item,
|
||||
format_spawn_call,
|
||||
tools_literal,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TeamPromptCtx:
|
||||
"""Typed context for Team phase prompt builders."""
|
||||
|
||||
loop_dir: Path
|
||||
iteration: int
|
||||
max_iterations: int
|
||||
agent_count: int = 3
|
||||
agent_role: str = "executor"
|
||||
fix_attempts: int = 0
|
||||
max_fix_attempts: int = 3
|
||||
|
||||
|
||||
def build_continuation(
|
||||
phase: str,
|
||||
iteration: int,
|
||||
max_iterations: int,
|
||||
agent_count: int,
|
||||
agent_role: str,
|
||||
loop_dir: Path,
|
||||
fix_attempts: int = 0,
|
||||
max_fix_attempts: int = 3,
|
||||
) -> str:
|
||||
"""Return the controller prompt for the current pipeline phase."""
|
||||
ctx = TeamPromptCtx(
|
||||
loop_dir=loop_dir,
|
||||
iteration=iteration,
|
||||
max_iterations=max_iterations,
|
||||
agent_count=agent_count,
|
||||
agent_role=agent_role,
|
||||
fix_attempts=fix_attempts,
|
||||
max_fix_attempts=max_fix_attempts,
|
||||
)
|
||||
builders = {
|
||||
"plan": _plan,
|
||||
"prd": _prd,
|
||||
"exec": _exec,
|
||||
"verify": _verify,
|
||||
"fix": _fix,
|
||||
}
|
||||
fn = builders.get(phase)
|
||||
if fn is None:
|
||||
return f"Unknown phase: {phase}. Update state.json."
|
||||
return fn(ctx)
|
||||
|
||||
|
||||
def _plan(ctx: TeamPromptCtx) -> str:
|
||||
explore = format_spawn_call(
|
||||
"explore",
|
||||
"Explore the codebase and map relevant "
|
||||
"files, modules, and dependencies.",
|
||||
)
|
||||
planner = format_spawn_call(
|
||||
"planner",
|
||||
"Create an implementation plan with "
|
||||
"task breakdown and dependencies...",
|
||||
)
|
||||
return f"""\
|
||||
Team Pipeline Controller — phase: plan.
|
||||
Pipeline iteration: {ctx.iteration}/{ctx.max_iterations}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute:
|
||||
1. Dispatch an explore subagent to map the codebase:
|
||||
{explore}
|
||||
2. After exploration, dispatch a planner subagent:
|
||||
{planner}
|
||||
3. Write the plan to {ctx.loop_dir}/handoffs/plan.md.
|
||||
4. Update {ctx.loop_dir}/state.json: set current_phase="prd"."""
|
||||
|
||||
|
||||
def _prd(ctx: TeamPromptCtx) -> str:
|
||||
analyst = format_spawn_call(
|
||||
"analyst",
|
||||
(
|
||||
"Read the plan and define acceptance "
|
||||
"criteria for each sub-task..."
|
||||
),
|
||||
)
|
||||
return f"""\
|
||||
Team Pipeline Controller — phase: prd.
|
||||
Pipeline iteration: {ctx.iteration}/{ctx.max_iterations}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute:
|
||||
1. Read {ctx.loop_dir}/handoffs/plan.md for the task breakdown.
|
||||
2. Dispatch an analyst subagent:
|
||||
{analyst}
|
||||
3. Write the PRD to {ctx.loop_dir}/handoffs/prd.md.
|
||||
4. Update {ctx.loop_dir}/state.json: set current_phase="exec"."""
|
||||
|
||||
|
||||
def _exec(ctx: TeamPromptCtx) -> str:
|
||||
role = ctx.agent_role
|
||||
item = format_batch_item(
|
||||
role,
|
||||
f"<sub-task 1>\\nWrite your result to "
|
||||
f"{ctx.loop_dir}/results/agent-001.json",
|
||||
fork=True,
|
||||
)
|
||||
tools_note = tools_literal(role)
|
||||
tools_hint = (
|
||||
"omit allowed_tools (inherit all)"
|
||||
if tools_note is None
|
||||
else f"allowed_tools={tools_note}"
|
||||
)
|
||||
return f"""\
|
||||
Team Pipeline Controller — phase: exec.
|
||||
Pipeline iteration: {ctx.iteration}/{ctx.max_iterations}
|
||||
Workers: {ctx.agent_count}, Role: {role} ({tools_hint})
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute:
|
||||
1. Read {ctx.loop_dir}/handoffs/prd.md for sub-tasks and acceptance criteria.
|
||||
2. Dispatch {ctx.agent_count} workers via batch mode:
|
||||
spawn_subagent(task="", batch=[
|
||||
{item},
|
||||
...repeat for each worker...
|
||||
])
|
||||
3. Poll each worker with check_agent_task (wait >= 30s between polls).
|
||||
4. Integrate forked worker results:
|
||||
{fork_merge_instructions()}
|
||||
5. After successful merges, update {ctx.loop_dir}/state.json:
|
||||
set forks_integrated=true.
|
||||
6. Read {ctx.loop_dir}/results/ files and summarize.
|
||||
7. Write the summary to {ctx.loop_dir}/handoffs/exec-summary.md.
|
||||
8. Update {ctx.loop_dir}/state.json: set current_phase="verify".
|
||||
The gate rejects verify/completed until forks_integrated=true."""
|
||||
|
||||
|
||||
def _verify(ctx: TeamPromptCtx) -> str:
|
||||
verifier = format_batch_item(
|
||||
"verifier",
|
||||
"VERIFY: Check code changes for correctness and completeness...",
|
||||
)
|
||||
security = format_batch_item(
|
||||
"security-reviewer",
|
||||
"SECURITY REVIEW: Check for security vulnerabilities...",
|
||||
)
|
||||
code = format_batch_item(
|
||||
"code-reviewer",
|
||||
"CODE REVIEW: Review code quality and conventions...",
|
||||
)
|
||||
return f"""\
|
||||
Team Pipeline Controller — phase: verify.
|
||||
Pipeline iteration: {ctx.iteration}/{ctx.max_iterations}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute:
|
||||
1. Read {ctx.loop_dir}/handoffs/exec-summary.md.
|
||||
2. Dispatch three reviewers via batch mode:
|
||||
spawn_subagent(task="", batch=[
|
||||
{verifier},
|
||||
{security},
|
||||
{code}
|
||||
])
|
||||
3. If ALL pass -> update {ctx.loop_dir}/state.json: \
|
||||
set current_phase="completed".
|
||||
4. If any fail -> write report to {ctx.loop_dir}/handoffs/verify-report.md
|
||||
-> update state.json: set current_phase="fix"."""
|
||||
|
||||
|
||||
def _fix(ctx: TeamPromptCtx) -> str:
|
||||
debugger = format_spawn_call(
|
||||
"debugger",
|
||||
"Fix the following issues from verification:\\n<issues>",
|
||||
)
|
||||
return f"""\
|
||||
Team Pipeline Controller — phase: fix.
|
||||
Pipeline iteration: {ctx.iteration}/{ctx.max_iterations}
|
||||
Fix attempt: {ctx.fix_attempts}/{ctx.max_fix_attempts}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
|
||||
Execute:
|
||||
1. Read {ctx.loop_dir}/handoffs/verify-report.md for failure details.
|
||||
2. Dispatch a debugger subagent to fix the issues:
|
||||
{debugger}
|
||||
3. After fix, update {ctx.loop_dir}/state.json: set current_phase="verify"."""
|
||||
@@ -0,0 +1,160 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""UltraQA gate — 3-agent QA cycle with stop conditions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from qwenpaw.loop.gates.base import StopAction, StopHandlerResult
|
||||
from qwenpaw.loop.gates.loop_gate import LoopGate
|
||||
|
||||
from ..shared.constants import ULTRAQA_MAX_CYCLES, ULTRAQA_MAX_SAME_FAILURE
|
||||
from ..shared.state import WorkflowState
|
||||
from .prompts import build_continuation as _build_prompt
|
||||
|
||||
|
||||
@dataclass
|
||||
class _UltraQAState:
|
||||
loop_dir: Path
|
||||
workspace_dir: Path
|
||||
active: bool = True
|
||||
cycle: int = 0
|
||||
max_cycles: int = ULTRAQA_MAX_CYCLES
|
||||
goal_type: str = "tests"
|
||||
custom_cmd: str = ""
|
||||
interactive: bool = False
|
||||
qa_passed: bool = False
|
||||
last_failures: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class UltraQAGate(LoopGate):
|
||||
"""Stop gate for the UltraQA 3-agent cycle."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "ultraqa"
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
return 50
|
||||
|
||||
def activate_for_qa(
|
||||
self,
|
||||
workspace_dir: Path,
|
||||
goal_type: str = "tests",
|
||||
custom_cmd: str = "",
|
||||
interactive: bool = False,
|
||||
max_cycles: int = ULTRAQA_MAX_CYCLES,
|
||||
) -> Path:
|
||||
"""Create state directory and activate the gate."""
|
||||
wf = WorkflowState(workspace_dir, "ultraqa")
|
||||
loop_dir = wf.create_instance()
|
||||
state = _UltraQAState(
|
||||
loop_dir=loop_dir,
|
||||
workspace_dir=workspace_dir,
|
||||
max_cycles=max_cycles,
|
||||
goal_type=goal_type,
|
||||
custom_cmd=custom_cmd,
|
||||
interactive=interactive,
|
||||
)
|
||||
wf.write_state(
|
||||
{
|
||||
"cycle": 0,
|
||||
"qa_passed": False,
|
||||
"last_failures": [],
|
||||
},
|
||||
)
|
||||
self.activate(state)
|
||||
return loop_dir
|
||||
|
||||
async def check(self, ctx: Any) -> Optional[StopHandlerResult]:
|
||||
if isinstance(ctx, dict) and ctx.get("has_tool_calls"):
|
||||
return StopHandlerResult(action=StopAction.BYPASS)
|
||||
|
||||
st: _UltraQAState | None = self._state()
|
||||
if st is None:
|
||||
return StopHandlerResult(
|
||||
action=StopAction.BYPASS,
|
||||
)
|
||||
|
||||
wf = WorkflowState.from_existing(
|
||||
st.workspace_dir,
|
||||
"ultraqa",
|
||||
st.loop_dir,
|
||||
)
|
||||
data = await asyncio.to_thread(wf.read_state)
|
||||
|
||||
# Agent-owned fields — never overwritten by the gate.
|
||||
st.qa_passed = data.get("qa_passed", False)
|
||||
st.last_failures = data.get(
|
||||
"last_failures",
|
||||
st.last_failures,
|
||||
)
|
||||
|
||||
if st.qa_passed:
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason="QA goals achieved",
|
||||
)
|
||||
|
||||
# Gate owns cycle in memory (single writer).
|
||||
if st.cycle >= st.max_cycles:
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason=f"Reached max cycles ({st.max_cycles})",
|
||||
)
|
||||
|
||||
if _repeated_failure(
|
||||
st.last_failures,
|
||||
ULTRAQA_MAX_SAME_FAILURE,
|
||||
):
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason="Same failure repeated too many times",
|
||||
)
|
||||
|
||||
st.cycle += 1
|
||||
await asyncio.to_thread(
|
||||
wf.update_state,
|
||||
{"cycle": st.cycle},
|
||||
)
|
||||
|
||||
return StopHandlerResult(
|
||||
action=StopAction.INTERRUPT_AND_CONTINUE,
|
||||
reason="QA cycle in progress",
|
||||
)
|
||||
|
||||
def build_continuation(self) -> str:
|
||||
"""Build QA cycle continuation from gate state."""
|
||||
st: _UltraQAState | None = self._state()
|
||||
if st is None:
|
||||
return ""
|
||||
return _build_prompt(
|
||||
cycle=st.cycle,
|
||||
max_cycles=st.max_cycles,
|
||||
goal_type=st.goal_type,
|
||||
custom_cmd=st.custom_cmd,
|
||||
last_failures=st.last_failures,
|
||||
loop_dir=st.loop_dir,
|
||||
interactive=st.interactive,
|
||||
)
|
||||
|
||||
|
||||
def _repeated_failure(
|
||||
failures: list[str],
|
||||
threshold: int,
|
||||
) -> bool:
|
||||
"""Check if the most recent failure repeated >= threshold."""
|
||||
if len(failures) < threshold:
|
||||
return False
|
||||
last = failures[-1]
|
||||
return all(f == last for f in failures[-threshold:])
|
||||
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""UltraQAMode — QA cycle engine with 3-agent collaboration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from qwenpaw.runtime.slash_command_registry import CommandSpec
|
||||
|
||||
from ..shared.args import split_args
|
||||
from ..shared.loop_ui_i18n import loop_command_metadata, loop_help_text
|
||||
from ..shared.mode_base import OMPModeBase, info_msg, rewrite_user_msg
|
||||
from .gate import UltraQAGate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from agentscope.message import Msg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_HELP = loop_help_text("ultraqa")
|
||||
|
||||
|
||||
class UltraQAMode(OMPModeBase):
|
||||
"""AgentMode for the UltraQA workflow."""
|
||||
|
||||
name = "ultraqa"
|
||||
gate_cls = UltraQAGate
|
||||
plugin_id = "__omp_ultraqa__"
|
||||
handler_name = "ultraqa-stop-handler"
|
||||
scope = "omp-ultraqa"
|
||||
|
||||
def commands(self) -> list[CommandSpec]:
|
||||
return [
|
||||
CommandSpec(
|
||||
name="ultraqa",
|
||||
handler=self._handler,
|
||||
category="builtin",
|
||||
help_text=_HELP,
|
||||
metadata=loop_command_metadata("ultraqa"),
|
||||
),
|
||||
]
|
||||
|
||||
async def _handler(self, ctx: "Any", args: str) -> Optional["Msg"]:
|
||||
if not args or not args.strip() or args.strip().lower() == "help":
|
||||
return info_msg(_HELP)
|
||||
|
||||
parsed = _parse_args(args)
|
||||
if parsed is None:
|
||||
return info_msg("Invalid arguments. " + _HELP)
|
||||
|
||||
workspace_dir = getattr(ctx, "workspace_dir", None)
|
||||
if not workspace_dir:
|
||||
return info_msg("ERROR: no workspace directory available.")
|
||||
|
||||
self.claim_workflow()
|
||||
loop_dir = await asyncio.to_thread(
|
||||
self._gate.activate_for_qa,
|
||||
Path(workspace_dir),
|
||||
parsed["goal_type"],
|
||||
parsed["custom_cmd"],
|
||||
parsed["interactive"],
|
||||
)
|
||||
|
||||
prompt = (
|
||||
f"UltraQA activated. Goal: {parsed['goal_type']}.\n"
|
||||
f"State directory: {loop_dir}\n"
|
||||
f"Read {loop_dir}/state.json and begin the QA cycle."
|
||||
)
|
||||
rewrite_user_msg(ctx, prompt)
|
||||
logger.info("UltraQA started: %s", loop_dir)
|
||||
return None
|
||||
|
||||
|
||||
def _parse_args(raw: str) -> dict | None:
|
||||
"""Parse /ultraqa arguments. ``None`` means invalid input."""
|
||||
tokens = split_args(raw)
|
||||
if tokens is None:
|
||||
return None
|
||||
|
||||
goal_type = "tests"
|
||||
custom_cmd = ""
|
||||
interactive = False
|
||||
|
||||
i = 0
|
||||
while i < len(tokens):
|
||||
t = tokens[i]
|
||||
if t == "--tests":
|
||||
goal_type = "tests"
|
||||
elif t == "--build":
|
||||
goal_type = "build"
|
||||
elif t == "--lint":
|
||||
goal_type = "lint"
|
||||
elif t == "--typecheck":
|
||||
goal_type = "typecheck"
|
||||
elif t == "--interactive":
|
||||
interactive = True
|
||||
elif t == "--custom" and i + 1 < len(tokens):
|
||||
goal_type = "custom"
|
||||
i += 1
|
||||
custom_cmd = tokens[i]
|
||||
i += 1
|
||||
|
||||
return {
|
||||
"goal_type": goal_type,
|
||||
"custom_cmd": custom_cmd,
|
||||
"interactive": interactive,
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""UltraQA continuation prompt templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ..shared.role_prompts import format_spawn_call
|
||||
|
||||
|
||||
def build_continuation(
|
||||
cycle: int,
|
||||
max_cycles: int,
|
||||
goal_type: str,
|
||||
custom_cmd: str,
|
||||
last_failures: list[str],
|
||||
loop_dir: Path,
|
||||
interactive: bool = False,
|
||||
) -> str:
|
||||
"""Build the controller continuation message for one QA cycle."""
|
||||
failures_summary = (
|
||||
"\n".join(f" - {f}" for f in last_failures[-5:])
|
||||
if last_failures
|
||||
else " (none)"
|
||||
)
|
||||
|
||||
qa_step = (
|
||||
_interactive_step()
|
||||
if interactive
|
||||
else _command_step(goal_type, custom_cmd)
|
||||
)
|
||||
architect_spawn = format_spawn_call(
|
||||
"architect",
|
||||
f"DIAGNOSE FAILURE:\\nGoal: {goal_type}\\n"
|
||||
"Output: <paste QA output>\\n"
|
||||
"Provide root cause analysis and fix recommendations.",
|
||||
)
|
||||
executor_spawn = format_spawn_call(
|
||||
"executor",
|
||||
"FIX:\\nIssue: <architect diagnosis>\\n"
|
||||
"Files: <affected files>\\n"
|
||||
"Apply the fix precisely as recommended.",
|
||||
)
|
||||
|
||||
return f"""\
|
||||
UltraQA cycle {cycle}/{max_cycles}.
|
||||
Goal: {goal_type}
|
||||
Previous failures:
|
||||
{failures_summary}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config.
|
||||
Role identity text is embedded in each spawn task below.
|
||||
|
||||
Execute this cycle:
|
||||
|
||||
1. {qa_step}
|
||||
2. If all checks PASS, update {loop_dir}/state.json: set qa_passed=true.
|
||||
3. If checks FAIL, dispatch an architect subagent to diagnose root cause:
|
||||
{architect_spawn}
|
||||
4. After diagnosis completes, dispatch an executor subagent to apply the fix:
|
||||
{executor_spawn}
|
||||
5. After the fix, update {loop_dir}/state.json
|
||||
with the latest last_failures."""
|
||||
|
||||
|
||||
def _command_step(goal_type: str, custom_cmd: str) -> str:
|
||||
cmd_map = {
|
||||
"tests": "Run the project test suite.",
|
||||
"build": "Run the project build.",
|
||||
"lint": "Run the linter.",
|
||||
"typecheck": "Run the type checker.",
|
||||
}
|
||||
if goal_type == "custom" and custom_cmd:
|
||||
return f"Run the QA command: `{custom_cmd}`"
|
||||
return cmd_map.get(goal_type, "Run the project test suite.")
|
||||
|
||||
|
||||
def _interactive_step() -> str:
|
||||
spawn = format_spawn_call(
|
||||
"qa-tester",
|
||||
"TEST:\\nGoal: <goal>\\nService: <how to start>\\n"
|
||||
"Test cases: <scenarios>",
|
||||
)
|
||||
return "Dispatch a qa-tester subagent for interactive testing:\n" + spawn
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Ultrawork gate — two-phase working/done gate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from qwenpaw.loop.gates.base import StopAction, StopHandlerResult
|
||||
from qwenpaw.loop.gates.loop_gate import LoopGate
|
||||
|
||||
from ..shared.constants import ULTRAWORK_MAX_ITERATIONS
|
||||
from ..shared.fork_guard import forks_integrated, merge_blocked_continuation
|
||||
from ..shared.role_prompts import FORK_MERGE_PROTOCOL
|
||||
from ..shared.state import WorkflowState
|
||||
from .prompts import build_continuation as _build_prompt
|
||||
|
||||
|
||||
@dataclass
|
||||
class _UltraworkState:
|
||||
loop_dir: Path
|
||||
workspace_dir: Path
|
||||
active: bool = True
|
||||
phase: str = "working"
|
||||
iteration: int = 0
|
||||
max_iterations: int = ULTRAWORK_MAX_ITERATIONS
|
||||
blocked_on_merge: bool = False
|
||||
|
||||
|
||||
class UltraworkGate(LoopGate):
|
||||
"""Stop gate for Ultrawork parallel execution."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "ultrawork"
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
return 50
|
||||
|
||||
def activate_for_work(self, workspace_dir: Path) -> Path:
|
||||
"""Create state directory and activate."""
|
||||
try:
|
||||
from qwenpaw.agents.fork_project import begin_fork_scope
|
||||
|
||||
begin_fork_scope(workspace_dir)
|
||||
except ImportError:
|
||||
import logging
|
||||
|
||||
logging.getLogger(__name__).warning(
|
||||
"begin_fork_scope unavailable; fork merge scope disabled",
|
||||
)
|
||||
wf = WorkflowState(workspace_dir, "ultrawork")
|
||||
loop_dir = wf.create_instance()
|
||||
state = _UltraworkState(
|
||||
loop_dir=loop_dir,
|
||||
workspace_dir=workspace_dir,
|
||||
)
|
||||
wf.write_state({"phase": "working", "iteration": 0})
|
||||
self.activate(state)
|
||||
return loop_dir
|
||||
|
||||
async def check(self, ctx: Any) -> Optional[StopHandlerResult]:
|
||||
if isinstance(ctx, dict) and ctx.get("has_tool_calls"):
|
||||
return StopHandlerResult(action=StopAction.BYPASS)
|
||||
|
||||
st: _UltraworkState | None = self._state()
|
||||
if st is None:
|
||||
return StopHandlerResult(
|
||||
action=StopAction.BYPASS,
|
||||
)
|
||||
|
||||
wf = WorkflowState.from_existing(
|
||||
st.workspace_dir,
|
||||
"ultrawork",
|
||||
st.loop_dir,
|
||||
)
|
||||
data = await asyncio.to_thread(wf.read_state)
|
||||
|
||||
phase = data.get("phase", st.phase)
|
||||
st.phase = phase
|
||||
|
||||
if phase == "done":
|
||||
integrated = await asyncio.to_thread(
|
||||
forks_integrated,
|
||||
data,
|
||||
st.workspace_dir,
|
||||
)
|
||||
if not integrated:
|
||||
# Keep target phase "done"; do not rewind to working.
|
||||
st.blocked_on_merge = True
|
||||
st.phase = "done"
|
||||
await asyncio.to_thread(
|
||||
wf.update_state,
|
||||
{
|
||||
"merge_blocked": True,
|
||||
"resume_phase": "done",
|
||||
},
|
||||
)
|
||||
return StopHandlerResult(
|
||||
action=StopAction.INTERRUPT_AND_CONTINUE,
|
||||
reason="Ultrawork blocked: forks not integrated",
|
||||
)
|
||||
await asyncio.to_thread(
|
||||
wf.update_state,
|
||||
{"merge_blocked": False},
|
||||
)
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason="Ultrawork completed",
|
||||
)
|
||||
|
||||
st.blocked_on_merge = False
|
||||
st.iteration += 1
|
||||
if st.iteration > st.max_iterations:
|
||||
await asyncio.to_thread(wf.cleanup)
|
||||
self.deactivate()
|
||||
return StopHandlerResult(
|
||||
action=StopAction.TERMINATE,
|
||||
reason=f"Reached max iterations ({st.max_iterations})",
|
||||
)
|
||||
|
||||
await asyncio.to_thread(
|
||||
wf.update_state,
|
||||
{"iteration": st.iteration},
|
||||
)
|
||||
|
||||
return StopHandlerResult(
|
||||
action=StopAction.INTERRUPT_AND_CONTINUE,
|
||||
reason="Ultrawork in progress",
|
||||
)
|
||||
|
||||
def build_continuation(self) -> str:
|
||||
"""Build Ultrawork continuation from gate state."""
|
||||
st: _UltraworkState | None = self._state()
|
||||
if st is None:
|
||||
return ""
|
||||
if st.blocked_on_merge:
|
||||
return merge_blocked_continuation(FORK_MERGE_PROTOCOL)
|
||||
return _build_prompt(
|
||||
st.loop_dir,
|
||||
iteration=st.iteration,
|
||||
max_iterations=st.max_iterations,
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""UltraworkMode — parallel execution engine."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from qwenpaw.runtime.slash_command_registry import CommandSpec
|
||||
|
||||
from ..shared.loop_ui_i18n import loop_command_metadata, loop_help_text
|
||||
from ..shared.mode_base import OMPModeBase, info_msg, rewrite_user_msg
|
||||
from .gate import UltraworkGate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from agentscope.message import Msg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_HELP = loop_help_text("ultrawork")
|
||||
|
||||
|
||||
class UltraworkMode(OMPModeBase):
|
||||
"""AgentMode for Ultrawork parallel execution."""
|
||||
|
||||
name = "ultrawork"
|
||||
gate_cls = UltraworkGate
|
||||
plugin_id = "__omp_ultrawork__"
|
||||
handler_name = "ultrawork-stop-handler"
|
||||
scope = "omp-ultrawork"
|
||||
|
||||
def commands(self) -> list[CommandSpec]:
|
||||
return [
|
||||
CommandSpec(
|
||||
name="ultrawork",
|
||||
handler=self._handler,
|
||||
category="builtin",
|
||||
help_text=_HELP,
|
||||
metadata=loop_command_metadata("ultrawork"),
|
||||
),
|
||||
]
|
||||
|
||||
async def _handler(self, ctx: "Any", args: str) -> Optional["Msg"]:
|
||||
task = (args or "").strip()
|
||||
if not task or len(task) < 5 or task.lower() == "help":
|
||||
return info_msg(_HELP)
|
||||
|
||||
workspace_dir = getattr(ctx, "workspace_dir", None)
|
||||
if not workspace_dir:
|
||||
return info_msg("ERROR: no workspace directory available.")
|
||||
|
||||
self.claim_workflow()
|
||||
loop_dir = await asyncio.to_thread(
|
||||
self._gate.activate_for_work,
|
||||
Path(workspace_dir),
|
||||
)
|
||||
|
||||
prompt = (
|
||||
f"Ultrawork activated.\n"
|
||||
f"Task: {task}\n"
|
||||
f"State directory: {loop_dir}\n"
|
||||
f"Decompose this task into independent sub-tasks and use "
|
||||
f"spawn_subagent batch mode to execute them in parallel."
|
||||
)
|
||||
rewrite_user_msg(ctx, prompt)
|
||||
logger.info("Ultrawork started: %s", loop_dir)
|
||||
return None
|
||||
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Ultrawork continuation prompt templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ..shared.constants import ULTRAWORK_MAX_ITERATIONS
|
||||
from ..shared.role_prompts import (
|
||||
fork_merge_instructions,
|
||||
format_batch_item,
|
||||
tools_literal,
|
||||
)
|
||||
|
||||
|
||||
def build_continuation(
|
||||
loop_dir: Path,
|
||||
iteration: int = 0,
|
||||
max_iterations: int = ULTRAWORK_MAX_ITERATIONS,
|
||||
) -> str:
|
||||
"""Build the controller prompt for the working phase."""
|
||||
item1 = format_batch_item(
|
||||
"executor",
|
||||
"<sub-task 1>",
|
||||
fork=True,
|
||||
)
|
||||
item2 = format_batch_item(
|
||||
"executor",
|
||||
"<sub-task 2>",
|
||||
fork=True,
|
||||
)
|
||||
# Shown so controllers know the canonical tool list source.
|
||||
executor_tools = tools_literal("executor")
|
||||
tools_note = (
|
||||
"omit allowed_tools (inherit all)"
|
||||
if executor_tools is None
|
||||
else f"allowed_tools={executor_tools}"
|
||||
)
|
||||
|
||||
return f"""\
|
||||
You are the Ultrawork parallel execution controller.
|
||||
|
||||
Current phase: working
|
||||
Iteration: {iteration}/{max_iterations}
|
||||
|
||||
Use the omp-roles skill for role tool/skill config
|
||||
(executor: {tools_note}).
|
||||
|
||||
Execute:
|
||||
1. Analyze the task and decompose it into independent sub-tasks.
|
||||
2. For each sub-task, determine whether it depends on other sub-tasks.
|
||||
3. Use spawn_subagent batch mode to dispatch all
|
||||
independent sub-tasks at once:
|
||||
spawn_subagent(task="", batch=[
|
||||
{item1},
|
||||
{item2},
|
||||
...
|
||||
])
|
||||
4. Use check_agent_task to poll each sub-task status
|
||||
(wait >= 30s between polls).
|
||||
5. Integrate forked worker results:
|
||||
{fork_merge_instructions()}
|
||||
6. After successful merges, update {loop_dir}/state.json:
|
||||
set forks_integrated=true.
|
||||
7. Summarize results.
|
||||
8. Update {loop_dir}/state.json: set phase="done".
|
||||
The gate rejects phase="done" until forks_integrated=true."""
|
||||
Reference in New Issue
Block a user