Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Multi-Rollout Parallel Task Execution

Spawn N independent agent attempts for the same task, run them in parallel with different strategies, and return the best result.

## Problem

A single agent trajectory may get stuck in a local optimum. When faced with a hard bug fix, the first approach the agent tries might not be the best one. Re-trying with a different strategy requires restarting the entire task, which is slow and wasteful.

## Solution

**Multi-rollout** generates N isolated execution attempts from the same starting point:

1. Clone the agent workspace into N isolated sub-workspaces.
2. Inject a different strategy prompt into each attempt.
3. Run all attempts in parallel (each is a full `DeepAgent.invoke()`).
4. Collect the N results.
5. Run a selector to pick the best one.
6. Return the winner.

## Configuration

Add `multi_rollout` to your `DeepAgentConfig`:

```python
from openjiuwen.harness import DeepAgentConfig, MultiRolloutConfig

config = DeepAgentConfig(
# ... other fields ...
multi_rollout=MultiRolloutConfig(
enabled=True,
n_rollouts=3,
max_parallel=3,
timeout_per_rollout=600.0,
selector_kind="first_successful",
)
)
```

| Field | Default | Description |
|-------|---------|-------------|
| `enabled` | `False` | Turn multi-rollout on/off |
| `n_rollouts` | `3` | Number of parallel attempts |
| `max_parallel` | `0` | Max concurrent rollouts (`0` = unlimited) |
| `timeout_per_rollout` | `600.0` | Timeout per attempt in seconds |
| `selector_kind` | `"first_successful"` | How to pick the winner |

## Strategy Variants

Each attempt receives the same task but prefixed with a different strategy instruction. The default three strategies are:

1. **Correctness-focused** — explore deeply, consider all implications
2. **Minimal-diff** — change as few lines as possible
3. **Edge-case-focused** — consider boundaries, errors, defensive code

You can replace them:

```python
config = MultiRolloutConfig(
strategy_variants=[
"Focus on speed. Get a working fix quickly.",
"Focus on robustness. Handle every edge case.",
"Focus on minimal changes. Preserve existing style.",
]
)
```

## Result Selectors

| Selector | Behavior | Best for |
|----------|----------|----------|
| `first_successful` | Return the first non-error result | Speed; safest default |
| `longest_output` | Return the successful result with longest output | When completeness matters |
| `shortest_output` | Return the successful result with shortest output | When minimal diffs matter |

## How It Works

```python
# When DeepAgent.invoke() is called:
if multi_rollout.enabled and n_rollouts > 1:
for i in range(n_rollouts):
subagent = parent.create_subagent(
"general-purpose",
subsession_id=f"rollout-{i:03d}"
)
# Each subagent gets its own isolated workspace

# Run all subagents in parallel via asyncio.gather
# Apply strategy prefix to each attempt's query
# Select best result via configured selector
return winner
else:
return await normal_invoke()
```

## Using Without DeepAgentConfig

You can also use `MultiRolloutExecutor` directly:

```python
from openjiuwen.harness.multi_rollout import MultiRolloutExecutor, MultiRolloutConfig

executor = MultiRolloutExecutor(parent_agent, MultiRolloutConfig(
enabled=True,
n_rollouts=3,
))
result = await executor.invoke({"query": "fix bug"})
```

## Caveats

- **Streaming**: Multi-rollout only works with `invoke()`, not `stream()`. If you need streaming with rollouts, run the executor first, then stream the winning result separately.
- **Cost**: Each rollout consumes full LLM tokens. 3 rollouts is approximately 3x cost.
- **Workspace state**: The parent workspace is untouched. The winning result is returned as text; copying files back to the parent workspace is the caller's responsibility.
1 change: 1 addition & 0 deletions docs/en/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
- [Security Guardrail](2.Development%20Guide/Advanced%20Usage/Security%20Guardrail.md)
- [MCP Tool](2.Development%20Guide/Advanced%20Usage/MCP%20Tool.md)
- [Store Plugin Development](2.Development%20Guide/Advanced%20Usage/Store%20Plugin%20Development.md)
- [Multi-Rollout Parallel Task Execution](2.Development%20Guide/Advanced%20Usage/Multi-Rollout%20Parallel%20Task%20Execution.md)
- [IntelliRouter](2.Development%20Guide/Advanced%20Usage/IntelliRouter.md)
- [API Docs](2.Development%20Guide/API%20Docs/README.md)
- [openjiuwen.harness](2.Development%20Guide/API%20Docs/openjiuwen.harness.README.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Multi-Rollout 并行任务执行

为同一任务生成 N 个独立的智能体尝试,以不同策略并行运行,返回最优结果。

## 问题

单一智能体执行路径可能陷入局部最优。面对复杂 bug 修复时,智能体的第一次尝试往往并非最佳方案。手动重试需要重启整个任务,既慢又浪费。

## 解决方案

**Multi-Rollout** 从同一出发点生成 N 个独立执行尝试:

1. 将智能体工作空间克隆为 N 个独立子空间
2. 为每个尝试注入不同策略提示
3. 并行运行所有尝试(每个都是完整的 `DeepAgent.invoke()`)
4. 收集 N 个结果
5. 通过选择器挑选最优
6. 返回胜者

## 配置

在 `DeepAgentConfig` 中添加 `multi_rollout`:

```python
from openjiuwen.harness import DeepAgentConfig, MultiRolloutConfig

config = DeepAgentConfig(
# ... 其他字段 ...
multi_rollout=MultiRolloutConfig(
enabled=True,
n_rollouts=3,
max_parallel=3,
timeout_per_rollout=600.0,
selector_kind="first_successful",
)
)
```

| 字段 | 默认值 | 说明 |
|------|--------|------|
| `enabled` | `False` | 开启/关闭多轮执行 |
| `n_rollouts` | `3` | 并行尝试次数 |
| `max_parallel` | `0` | 最大并发数(`0` = 不限) |
| `timeout_per_rollout` | `600.0` | 每次尝试超时(秒) |
| `selector_kind` | `"first_successful"` | 选择胜者的方式 |

## 策略变体

每个尝试接收相同任务,但前缀策略指令不同。默认三种策略:

1. **注重正确性** — 深入探索,考虑所有影响
2. **最小化改动** — 改动行数越少越好
3. **注重边界情况** — 考虑边界、错误、防御性代码

可自定义:

```python
config = MultiRolloutConfig(
strategy_variants=[
"Focus on speed. Get a working fix quickly.",
"Focus on robustness. Handle every edge case.",
"Focus on minimal changes. Preserve existing style.",
]
)
```

## 结果选择器

| 选择器 | 行为 | 适用场景 |
|--------|------|----------|
| `first_successful` | 返回第一个无错误结果 | 速度优先;最安全的默认 |
| `longest_output` | 返回输出最长的成功结果 | 完整性优先 |
| `shortest_output` | 返回输出最短的成功结果 | 最小化 diff 优先 |

## 工作原理

```
用户调用 DeepAgent.invoke()
└─ 如果 multi_rollout.enabled 且 n_rollouts > 1:
创建 N 个子智能体(各带独立工作空间)
为每个尝试的 query 添加策略前缀
并行运行(asyncio.gather)
选择最优结果
返回胜者
└─ 否则:
正常单路径执行
```

## 脱离 DeepAgentConfig 直接使用

也可直接使用 `MultiRolloutExecutor`:

```python
from openjiuwen.harness.multi_rollout import MultiRolloutExecutor, MultiRolloutConfig

executor = MultiRolloutExecutor(parent_agent, MultiRolloutConfig(
enabled=True,
n_rollouts=3,
))
result = await executor.invoke({"query": "fix bug"})
```

## 注意事项

- **流式输出**:Multi-Rollout 仅支持 `invoke()`,不支持 `stream()`。如需流式,先运行选择器,再对胜果单独流式输出。
- **成本**:每次尝试消耗完整 LLM token。3 次尝试 ≈ 3 倍成本。
- **工作空间状态**:父工作空间不受影响。返回结果为文本;如需将文件复制回父空间,由调用方负责。
1 change: 1 addition & 0 deletions docs/zh/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
- [安全护栏Guardrail](2.开发指南/高阶用法/安全护栏Guardrail.md)
- [MCP工具](2.开发指南/高阶用法/MCP工具.md)
- [插件开发-存储后端](2.开发指南/高阶用法/插件开发-存储后端.md)
- [Multi-Rollout 并行任务执行](2.开发指南/高阶用法/Multi-Rollout%20并行任务执行.md)
- [IntelliRouter智能路由](2.开发指南/高阶用法/IntelliRouter智能路由.md)
- [API文档](2.开发指南/API文档/README.md)
- [openjiuwen.harness](2.开发指南/API文档/openjiuwen.harness.README.md)
Expand Down
14 changes: 14 additions & 0 deletions openjiuwen/harness/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
VisionModelConfig,
)
from openjiuwen.harness.workspace.workspace import Workspace
from openjiuwen.harness.multi_rollout.config import MultiRolloutConfig
from openjiuwen.harness.multi_rollout.executor import MultiRolloutExecutor

__all__ = [
"DeepAgent",
Expand All @@ -28,6 +30,8 @@
"DeepAgentConfig",
"AudioModelConfig",
"VisionModelConfig",
"MultiRolloutConfig",
"MultiRolloutExecutor",
"create_deep_agent",
"Workspace",
]
Expand Down Expand Up @@ -75,6 +79,16 @@ def __getattr__(name: str) -> Any:
Workspace,
)
return Workspace
if name == "MultiRolloutConfig":
from openjiuwen.harness.multi_rollout.config import (
MultiRolloutConfig,
)
return MultiRolloutConfig
if name == "MultiRolloutExecutor":
from openjiuwen.harness.multi_rollout.executor import (
MultiRolloutExecutor,
)
return MultiRolloutExecutor
raise AttributeError(
f"module {__name__!r} has no attribute {name!r}"
)
23 changes: 22 additions & 1 deletion openjiuwen/harness/deep_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
from openjiuwen.core.common.exception.errors import build_error
from openjiuwen.core.common.logging import logger
from openjiuwen.core.common.security.user_config import UserConfig
from openjiuwen.harness.multi_rollout.executor import (
MultiRolloutExecutor,
)
from openjiuwen.core.context_engine import ContextEngine
from openjiuwen.core.context_engine.context.context_utils import ContextUtils
from openjiuwen.core.controller.config import ControllerConfig
Expand Down Expand Up @@ -2492,6 +2495,7 @@ async def invoke(
)

invoke_inputs = self._normalize_inputs(inputs)

ctx = AgentCallbackContext(agent=self, inputs=invoke_inputs, session=session)

self._invoke_active = True
Expand All @@ -2501,7 +2505,12 @@ async def invoke(
AgentCallbackEvent.BEFORE_INVOKE,
AgentCallbackEvent.AFTER_INVOKE,
):
if (
if self._use_multi_rollout():
executor = MultiRolloutExecutor(
self, self._deep_config.multi_rollout
)
result = await executor.invoke(invoke_inputs, session)
elif (
self._deep_config is not None
and self._deep_config.enable_task_loop
and not self._is_resume_input(invoke_inputs)
Expand All @@ -2518,6 +2527,18 @@ async def invoke(
finally:
self._invoke_active = False

def _use_multi_rollout(self) -> bool:
"""Return True when the multi-rollout executor should be used.

Folds the multi-rollout conditions (config present, multi_rollout
configured, enabled, and more than one rollout requested) into one
boolean so the invoke() branch stays a single expression.
"""
cfg = self._deep_config
if cfg is None or cfg.multi_rollout is None:
return False
return cfg.multi_rollout.enabled and cfg.multi_rollout.n_rollouts > 1

async def stream(
self,
inputs: Any,
Expand Down
22 changes: 22 additions & 0 deletions openjiuwen/harness/multi_rollout/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# coding: utf-8
"""Multi-rollout package for parallel task execution."""

from openjiuwen.harness.multi_rollout.config import MultiRolloutConfig
from openjiuwen.harness.multi_rollout.executor import MultiRolloutExecutor
from openjiuwen.harness.multi_rollout.selector import (
FirstSuccessfulSelector,
LongestOutputSelector,
RolloutResult,
ShortestOutputSelector,
get_selector,
)

__all__ = [
"FirstSuccessfulSelector",
"LongestOutputSelector",
"MultiRolloutConfig",
"MultiRolloutExecutor",
"RolloutResult",
"ShortestOutputSelector",
"get_selector",
]
45 changes: 45 additions & 0 deletions openjiuwen/harness/multi_rollout/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# coding: utf-8
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
"""Multi-rollout configuration."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any


@dataclass
class MultiRolloutConfig:
"""Configuration for parallel multi-rollout task execution.

When enabled, the agent spawns *n_rollouts* isolated subagents,
each with a different strategy, runs them in parallel, and returns
the best result.
"""

enabled: bool = False
n_rollouts: int = 3
max_parallel: int = 0 # 0 means unlimited (bounded by asyncio)
timeout_per_rollout: float = 600.0
strategy_variants: list[str] = field(
default_factory=lambda: [
(
"Approach: focus on correctness and thoroughness. "
"Explore deeply, consider all implications, and produce "
"a robust solution."
),
(
"Approach: focus on minimal changes. Change as few lines "
"as possible while still fixing the issue. Preserve existing "
"structure and style."
),
(
"Approach: focus on edge cases and defensive programming. "
"Consider boundary conditions, error handling, and "
"unexpected inputs."
),
]
)
selector_kind: str = "first_successful"
# Optional: extra kwargs passed to the selector
selector_kwargs: dict[str, Any] = field(default_factory=dict)
Loading