devmate 2.5.1__tar.gz → 2.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devmate-2.5.1 → devmate-2.5.2}/PKG-INFO +1 -1
- {devmate-2.5.1 → devmate-2.5.2}/pyproject.toml +1 -1
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/__init__.py +1 -1
- devmate-2.5.2/src/devmate/sub_agent/__init__.py +237 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate.egg-info/PKG-INFO +1 -1
- devmate-2.5.1/src/devmate/sub_agent/__init__.py +0 -130
- {devmate-2.5.1 → devmate-2.5.2}/README.md +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/setup.cfg +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/__main__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/agent/validators.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/agent_eval/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/ai_config/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/ai_scenarios/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/app/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/app/cli.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/app/tui.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/app/web/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/app/web/cli_docs.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/bus/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/bus/bus.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/bus/command.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/core/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/core/config.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/core/logging.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/core/security.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/events/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/events/bus.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/events/domain_event.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/mcp/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/memory_store/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/module/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/module/registry.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/plugin/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/plugin_market/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/plugin_market/ratings.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/plugin_market/remote.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/rag/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/security/audit.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/security/vault.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/share/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/skill/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate/workflow/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate.egg-info/SOURCES.txt +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate.egg-info/dependency_links.txt +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate.egg-info/entry_points.txt +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate.egg-info/requires.txt +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate.egg-info/top_level.txt +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_agent/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_apidev/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_apihub/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_dbadmin/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_gitflow/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_monitor/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_notekeeper/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_plugins/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_plugins/code_stats.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_plugins/env_switch.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_plugins/file_watch.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_plugins/pomodoro.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_plugins/port_scan.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_publisher/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_regexlab/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_reporter/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_scaffold/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_sshman/__init__.py +0 -0
- {devmate-2.5.1 → devmate-2.5.2}/src/devmate_toolkit/__init__.py +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""devmate.sub_agent — Sub-agent 隔离(P0-⑥ · Context Isolate)
|
|
2
|
+
|
|
3
|
+
方法论:Context Engineering 五动作之一「隔离(Isolate)」
|
|
4
|
+
- 旁支任务委派给独立上下文的 Sub-agent
|
|
5
|
+
- 主 Agent 不承载子任务细节,只拿回结构化结果摘要
|
|
6
|
+
- 长任务 → 拆子任务 → 各子任务独立上下文 → 汇总
|
|
7
|
+
- 输出契约(多 Agent 工程三原则):每个子任务声明期望输出格式
|
|
8
|
+
缺失字段 → 标记 schema_failed(关键节点校验)
|
|
9
|
+
|
|
10
|
+
设计:
|
|
11
|
+
- Sub-agent 复用 AgentRuntime(独立 SessionMemory,互不污染)
|
|
12
|
+
- 结果自动截断/摘要,只回传关键信息给主上下文
|
|
13
|
+
- 支持并行执行多个子任务(ThreadPoolExecutor)
|
|
14
|
+
- 输出契约:expected_schema 声明字段名→类型,结果校验缺字段/类型错
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path # noqa: F401
|
|
21
|
+
|
|
22
|
+
from devmate.bus.command import CommandResult
|
|
23
|
+
|
|
24
|
+
# 允许的 schema 类型
|
|
25
|
+
_SCHEMA_TYPES = {"string": str, "int": int, "float": (int, float),
|
|
26
|
+
"bool": bool, "list": list, "dict": dict, "any": object}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class SubTask:
|
|
31
|
+
"""子任务定义"""
|
|
32
|
+
name: str # 子任务名
|
|
33
|
+
prompt: str # 子任务指令(独立上下文)
|
|
34
|
+
max_steps: int = 5 # 最大步数
|
|
35
|
+
tool_whitelist: list[str] = field(default_factory=list) # 允许的工具白名单
|
|
36
|
+
expected_schema: dict | None = None # 输出契约 {字段: 类型}(如 {"host": "string"})
|
|
37
|
+
schema_required: bool = True # 契约校验失败是否算任务失败
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict:
|
|
40
|
+
return {"name": self.name, "prompt": self.prompt,
|
|
41
|
+
"max_steps": self.max_steps,
|
|
42
|
+
"tool_whitelist": self.tool_whitelist,
|
|
43
|
+
"expected_schema": self.expected_schema}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def validate_schema(output: str, schema: dict) -> tuple[bool, list[str]]:
|
|
47
|
+
"""校验输出是否符合契约
|
|
48
|
+
|
|
49
|
+
策略(宽容解析):
|
|
50
|
+
1. 尝试 JSON 解析(输出可能是 JSON)
|
|
51
|
+
2. 否则尝试 key: value 逐行解析
|
|
52
|
+
3. 从文本中按字段名查找
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
(是否通过, 缺失/错误字段列表)
|
|
56
|
+
"""
|
|
57
|
+
if not schema:
|
|
58
|
+
return True, []
|
|
59
|
+
|
|
60
|
+
# 尝试 JSON 解析
|
|
61
|
+
obj = None
|
|
62
|
+
try:
|
|
63
|
+
obj = json.loads(output)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
issues = []
|
|
68
|
+
for field_name, field_type in schema.items():
|
|
69
|
+
# 定位值
|
|
70
|
+
value = None
|
|
71
|
+
if isinstance(obj, dict):
|
|
72
|
+
value = obj.get(field_name)
|
|
73
|
+
elif obj is not None:
|
|
74
|
+
continue # JSON 数组等,无法按字段校验
|
|
75
|
+
else:
|
|
76
|
+
# 行解析:key: value
|
|
77
|
+
for line in output.splitlines():
|
|
78
|
+
line = line.strip().lstrip("-• ")
|
|
79
|
+
if ":" in line:
|
|
80
|
+
k, _, v = line.partition(":")
|
|
81
|
+
if k.strip().strip('"').strip("'") == field_name:
|
|
82
|
+
value = v.strip()
|
|
83
|
+
break
|
|
84
|
+
if value is None:
|
|
85
|
+
# 全文包含字段名
|
|
86
|
+
if field_name in output:
|
|
87
|
+
continue # 出现但无法解析值,视为通过
|
|
88
|
+
|
|
89
|
+
if value is None or value == "":
|
|
90
|
+
issues.append(f"{field_name} 缺失")
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# 类型校验(宽容:去掉逗号/百分号/中文单位后缀)
|
|
94
|
+
expected_type = _SCHEMA_TYPES.get(field_type, object)
|
|
95
|
+
try:
|
|
96
|
+
clean = str(value).replace(",", "").replace("%", "")
|
|
97
|
+
# 去常见中英文单位后缀(核/GB/MB/秒/ms/元/%)
|
|
98
|
+
for unit in ("核", "个", "台", "GB", "MB", "KB", "秒", "ms", "元",
|
|
99
|
+
"字节", "张", "条"):
|
|
100
|
+
clean = clean.removesuffix(unit)
|
|
101
|
+
clean = clean.strip()
|
|
102
|
+
if field_type == "int":
|
|
103
|
+
int(clean)
|
|
104
|
+
elif field_type == "float":
|
|
105
|
+
float(clean)
|
|
106
|
+
elif field_type == "string":
|
|
107
|
+
str(value)
|
|
108
|
+
elif field_type in ("list", "dict"):
|
|
109
|
+
if not isinstance(value, expected_type):
|
|
110
|
+
issues.append(f"{field_name} 类型错误(期望 {field_type})")
|
|
111
|
+
elif field_type == "bool":
|
|
112
|
+
if str(value).lower() not in ("true", "false", "1", "0", "是", "否"):
|
|
113
|
+
issues.append(f"{field_name} 类型错误(期望 bool)")
|
|
114
|
+
elif field_type == "any":
|
|
115
|
+
pass
|
|
116
|
+
elif not isinstance(value, expected_type):
|
|
117
|
+
issues.append(f"{field_name} 类型错误(期望 {field_type})")
|
|
118
|
+
except (ValueError, TypeError):
|
|
119
|
+
issues.append(f"{field_name} 类型错误(期望 {field_type})")
|
|
120
|
+
|
|
121
|
+
return not issues, issues
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class SubTaskResult:
|
|
126
|
+
"""子任务结果"""
|
|
127
|
+
name: str
|
|
128
|
+
success: bool
|
|
129
|
+
summary: str = ""
|
|
130
|
+
duration_ms: float = 0.0
|
|
131
|
+
steps: int = 0
|
|
132
|
+
error: str = ""
|
|
133
|
+
schema_ok: bool = True # 输出契约校验是否通过
|
|
134
|
+
schema_missing: list[str] = field(default_factory=list) # 缺失/错误字段
|
|
135
|
+
|
|
136
|
+
def to_dict(self) -> dict:
|
|
137
|
+
return {"name": self.name, "success": self.success,
|
|
138
|
+
"summary": self.summary[:300],
|
|
139
|
+
"duration_ms": round(self.duration_ms, 1),
|
|
140
|
+
"steps": self.steps, "error": self.error[:200],
|
|
141
|
+
"schema_ok": self.schema_ok,
|
|
142
|
+
"schema_missing": self.schema_missing}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def run_subtask(task: SubTask, workdir: str = ".") -> SubTaskResult:
|
|
146
|
+
"""执行单个子任务(独立上下文)
|
|
147
|
+
|
|
148
|
+
用 AgentRuntime 跑,但:
|
|
149
|
+
- 独立 SessionMemory(不污染主 Agent)
|
|
150
|
+
- 独立工具环境(tool_whitelist 过滤)
|
|
151
|
+
- 结果只回传摘要 + 输出契约校验(expected_schema)
|
|
152
|
+
"""
|
|
153
|
+
start = time.time()
|
|
154
|
+
try:
|
|
155
|
+
from devmate_agent import AgentRuntime
|
|
156
|
+
|
|
157
|
+
runtime = AgentRuntime()
|
|
158
|
+
|
|
159
|
+
# 工具白名单过滤(隔离:子任务只看到允许的工具)
|
|
160
|
+
if task.tool_whitelist:
|
|
161
|
+
allowed = set(task.tool_whitelist)
|
|
162
|
+
for tname in list(runtime.tools.list_names()):
|
|
163
|
+
if tname not in allowed:
|
|
164
|
+
runtime.tools.unregister(tname)
|
|
165
|
+
|
|
166
|
+
# 运行子任务(dry_run=False 真实执行)
|
|
167
|
+
result = runtime.run(task.prompt, max_steps=task.max_steps)
|
|
168
|
+
|
|
169
|
+
if result.success:
|
|
170
|
+
data = result.data
|
|
171
|
+
summary = str(data.get("final_output", data.get("output", "")))[:300]
|
|
172
|
+
# 输出契约校验(关键节点校验:缺失字段标记失败)
|
|
173
|
+
schema_ok, missing = True, []
|
|
174
|
+
if task.expected_schema:
|
|
175
|
+
schema_ok, missing = validate_schema(summary, task.expected_schema)
|
|
176
|
+
if not schema_ok and task.schema_required:
|
|
177
|
+
return SubTaskResult(
|
|
178
|
+
name=task.name, success=False,
|
|
179
|
+
error=f"输出契约校验失败: 缺 {', '.join(missing)}",
|
|
180
|
+
summary=summary, schema_ok=False,
|
|
181
|
+
schema_missing=missing,
|
|
182
|
+
duration_ms=(time.time() - start) * 1000,
|
|
183
|
+
steps=data.get("step_count", data.get("total_steps", 0)),
|
|
184
|
+
)
|
|
185
|
+
return SubTaskResult(
|
|
186
|
+
name=task.name, success=True, summary=summary,
|
|
187
|
+
duration_ms=(time.time() - start) * 1000,
|
|
188
|
+
steps=data.get("step_count", data.get("total_steps", 0)),
|
|
189
|
+
schema_ok=schema_ok, schema_missing=missing,
|
|
190
|
+
)
|
|
191
|
+
return SubTaskResult(
|
|
192
|
+
name=task.name, success=False,
|
|
193
|
+
error=str(result.error or "任务失败"),
|
|
194
|
+
duration_ms=(time.time() - start) * 1000,
|
|
195
|
+
)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
return SubTaskResult(
|
|
198
|
+
name=task.name, success=False, error=str(e),
|
|
199
|
+
duration_ms=(time.time() - start) * 1000,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def run_subtasks(tasks: list[SubTask], workdir: str = ".",
|
|
204
|
+
parallel: bool = True) -> CommandResult:
|
|
205
|
+
"""并行/串行执行多个子任务
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
tasks: 子任务列表
|
|
209
|
+
workdir: 工作目录
|
|
210
|
+
parallel: 是否并行(默认 True)
|
|
211
|
+
"""
|
|
212
|
+
results = []
|
|
213
|
+
if parallel and len(tasks) > 1:
|
|
214
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
215
|
+
with ThreadPoolExecutor(max_workers=min(len(tasks), 4)) as pool:
|
|
216
|
+
futures = {pool.submit(run_subtask, t, workdir): t.name
|
|
217
|
+
for t in tasks}
|
|
218
|
+
# 保持任务顺序
|
|
219
|
+
ordered = [futures[f] for f in futures]
|
|
220
|
+
for f, name in zip(futures, ordered):
|
|
221
|
+
results.append(f.result())
|
|
222
|
+
else:
|
|
223
|
+
for t in tasks:
|
|
224
|
+
results.append(run_subtask(t, workdir))
|
|
225
|
+
|
|
226
|
+
success = sum(1 for r in results if r.success)
|
|
227
|
+
total = len(results)
|
|
228
|
+
return CommandResult(success=success == total, data={
|
|
229
|
+
"results": [r.to_dict() for r in results],
|
|
230
|
+
"success_count": success,
|
|
231
|
+
"total": total,
|
|
232
|
+
"message": f"Sub-agents: {success}/{total} 成功",
|
|
233
|
+
"summary": "\n".join(
|
|
234
|
+
f"[{'✅' if r.success else '❌'}] {r.name}: "
|
|
235
|
+
f"{r.summary or r.error}" for r in results
|
|
236
|
+
)[:800],
|
|
237
|
+
})
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
"""devmate.sub_agent — Sub-agent 隔离(P0-⑥ · Context Isolate)
|
|
2
|
-
|
|
3
|
-
方法论:Context Engineering 五动作之一「隔离(Isolate)」
|
|
4
|
-
- 旁支任务委派给独立上下文的 Sub-agent
|
|
5
|
-
- 主 Agent 不承载子任务细节,只拿回结构化结果摘要
|
|
6
|
-
- 长任务 → 拆子任务 → 各子任务独立上下文 → 汇总
|
|
7
|
-
|
|
8
|
-
设计:
|
|
9
|
-
- Sub-agent 复用 AgentRuntime(独立 SessionMemory,互不污染)
|
|
10
|
-
- 结果自动截断/摘要,只回传关键信息给主上下文
|
|
11
|
-
- 支持并行执行多个子任务(ThreadPoolExecutor)
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import time
|
|
15
|
-
from dataclasses import dataclass, field
|
|
16
|
-
from pathlib import Path # noqa: F401
|
|
17
|
-
|
|
18
|
-
from devmate.bus.command import CommandResult
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@dataclass
|
|
22
|
-
class SubTask:
|
|
23
|
-
"""子任务定义"""
|
|
24
|
-
name: str # 子任务名
|
|
25
|
-
prompt: str # 子任务指令(独立上下文)
|
|
26
|
-
max_steps: int = 5 # 最大步数
|
|
27
|
-
tool_whitelist: list[str] = field(default_factory=list) # 允许的工具白名单
|
|
28
|
-
|
|
29
|
-
def to_dict(self) -> dict:
|
|
30
|
-
return {"name": self.name, "prompt": self.prompt,
|
|
31
|
-
"max_steps": self.max_steps,
|
|
32
|
-
"tool_whitelist": self.tool_whitelist}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@dataclass
|
|
36
|
-
class SubTaskResult:
|
|
37
|
-
"""子任务结果"""
|
|
38
|
-
name: str
|
|
39
|
-
success: bool
|
|
40
|
-
summary: str = ""
|
|
41
|
-
duration_ms: float = 0.0
|
|
42
|
-
steps: int = 0
|
|
43
|
-
error: str = ""
|
|
44
|
-
|
|
45
|
-
def to_dict(self) -> dict:
|
|
46
|
-
return {"name": self.name, "success": self.success,
|
|
47
|
-
"summary": self.summary[:300],
|
|
48
|
-
"duration_ms": round(self.duration_ms, 1),
|
|
49
|
-
"steps": self.steps, "error": self.error[:200]}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def run_subtask(task: SubTask, workdir: str = ".") -> SubTaskResult:
|
|
53
|
-
"""执行单个子任务(独立上下文)
|
|
54
|
-
|
|
55
|
-
用 AgentRuntime 跑,但:
|
|
56
|
-
- 独立 SessionMemory(不污染主 Agent)
|
|
57
|
-
- 独立工具环境(tool_whitelist 过滤)
|
|
58
|
-
- 结果只回传摘要
|
|
59
|
-
"""
|
|
60
|
-
start = time.time()
|
|
61
|
-
try:
|
|
62
|
-
from devmate_agent import AgentRuntime
|
|
63
|
-
|
|
64
|
-
runtime = AgentRuntime()
|
|
65
|
-
|
|
66
|
-
# 工具白名单过滤(隔离:子任务只看到允许的工具)
|
|
67
|
-
if task.tool_whitelist:
|
|
68
|
-
allowed = set(task.tool_whitelist)
|
|
69
|
-
for tname in list(runtime.tools.list_names()):
|
|
70
|
-
if tname not in allowed:
|
|
71
|
-
runtime.tools.unregister(tname)
|
|
72
|
-
|
|
73
|
-
# 运行子任务(dry_run=False 真实执行)
|
|
74
|
-
result = runtime.run(task.prompt, max_steps=task.max_steps)
|
|
75
|
-
|
|
76
|
-
if result.success:
|
|
77
|
-
data = result.data
|
|
78
|
-
summary = str(data.get("final_output", data.get("output", "")))[:300]
|
|
79
|
-
return SubTaskResult(
|
|
80
|
-
name=task.name, success=True, summary=summary,
|
|
81
|
-
duration_ms=(time.time() - start) * 1000,
|
|
82
|
-
steps=data.get("step_count", data.get("total_steps", 0)),
|
|
83
|
-
)
|
|
84
|
-
return SubTaskResult(
|
|
85
|
-
name=task.name, success=False,
|
|
86
|
-
error=str(result.error or "任务失败"),
|
|
87
|
-
duration_ms=(time.time() - start) * 1000,
|
|
88
|
-
)
|
|
89
|
-
except Exception as e:
|
|
90
|
-
return SubTaskResult(
|
|
91
|
-
name=task.name, success=False, error=str(e),
|
|
92
|
-
duration_ms=(time.time() - start) * 1000,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def run_subtasks(tasks: list[SubTask], workdir: str = ".",
|
|
97
|
-
parallel: bool = True) -> CommandResult:
|
|
98
|
-
"""并行/串行执行多个子任务
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
tasks: 子任务列表
|
|
102
|
-
workdir: 工作目录
|
|
103
|
-
parallel: 是否并行(默认 True)
|
|
104
|
-
"""
|
|
105
|
-
results = []
|
|
106
|
-
if parallel and len(tasks) > 1:
|
|
107
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
108
|
-
with ThreadPoolExecutor(max_workers=min(len(tasks), 4)) as pool:
|
|
109
|
-
futures = {pool.submit(run_subtask, t, workdir): t.name
|
|
110
|
-
for t in tasks}
|
|
111
|
-
# 保持任务顺序
|
|
112
|
-
ordered = [futures[f] for f in futures]
|
|
113
|
-
for f, name in zip(futures, ordered):
|
|
114
|
-
results.append(f.result())
|
|
115
|
-
else:
|
|
116
|
-
for t in tasks:
|
|
117
|
-
results.append(run_subtask(t, workdir))
|
|
118
|
-
|
|
119
|
-
success = sum(1 for r in results if r.success)
|
|
120
|
-
total = len(results)
|
|
121
|
-
return CommandResult(success=success == total, data={
|
|
122
|
-
"results": [r.to_dict() for r in results],
|
|
123
|
-
"success_count": success,
|
|
124
|
-
"total": total,
|
|
125
|
-
"message": f"Sub-agents: {success}/{total} 成功",
|
|
126
|
-
"summary": "\n".join(
|
|
127
|
-
f"[{'✅' if r.success else '❌'}] {r.name}: "
|
|
128
|
-
f"{r.summary or r.error}" for r in results
|
|
129
|
-
)[:800],
|
|
130
|
-
})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|