Pause data generation for human review

This commit is contained in:
武阳
2026-04-29 19:31:29 +08:00
parent 12f6ced252
commit c1951d6e4d
2 changed files with 129 additions and 0 deletions
+76
View File
@@ -1117,6 +1117,39 @@ class LocalCodingAgent:
)
if history_entry is not None:
file_history.append(history_entry)
if tool_result.metadata.get('requires_user_review') is True:
stream_events.append(
{
'type': 'user_review_required',
'tool_name': tool_call.name,
'tool_call_id': tool_call.id,
'message_id': session.messages[tool_message_index].message_id,
'metadata': dict(tool_result.metadata),
}
)
result = AgentRunResult(
final_output=self._build_user_review_required_output(tool_result),
turns=turn_index + 1,
tool_calls=tool_calls,
transcript=session.transcript(),
events=tuple(stream_events),
usage=total_usage,
total_cost_usd=total_cost_usd,
stop_reason='user_review_required',
file_history=tuple(file_history),
session_id=session_id,
scratchpad_directory=(
str(scratchpad_directory) if scratchpad_directory is not None else None
),
)
result = self._append_runtime_after_turn_events(
result,
prompt=effective_prompt,
turn_index=turn_index,
)
result = self._persist_session(session, result)
self.last_run_result = result
return result
result = AgentRunResult(
final_output=(
@@ -1862,6 +1895,49 @@ class LocalCodingAgent:
entry['history_kind'] = 'tool'
return entry
def _build_user_review_required_output(self, tool_result: ToolExecutionResult) -> str:
fallback = (
'已生成待 review 的计划。本轮已暂停,请检查上面的计划;'
'如果需要修改,请直接回复修改意见;如果认可,请回复“确认,开始生成”。'
)
try:
payload = json.loads(tool_result.content)
except json.JSONDecodeError:
return fallback
plan = payload.get('plan') if isinstance(payload, dict) else None
if not isinstance(plan, dict):
return fallback
lines = [
'已生成待 review 的数据生成计划,本轮已暂停。',
'',
f"- plan_id: `{plan.get('plan_id', '')}`",
f"- revision: `{plan.get('revision', '')}`",
f"- dataset_label: {plan.get('dataset_label', '')}",
]
targets = plan.get('target_definitions')
if isinstance(targets, list) and targets:
lines.append('- target_definitions:')
for item in targets:
if isinstance(item, dict):
lines.append(
f" - {item.get('name', '')}: `{item.get('target', '')}`;规则:{item.get('rule', '')}"
)
elif plan.get('target'):
lines.append(f"- target: `{plan.get('target')}`")
lines.extend(
[
f"- 生成数量: {plan.get('total_count', '')}",
f"- 单轮/多轮: {plan.get('turn_mix', '')}",
f"- 覆盖范围: {plan.get('coverage', '')}",
f"- 排除项: {plan.get('exclusions', '')}",
f"- 落盘路径: {plan.get('output_path', '')}",
'',
'请 review 这份计划:需要修改就直接回复修改意见;认可的话回复“确认,开始生成”。',
]
)
return '\n'.join(lines)
def _compact_prefix_count(self, session: AgentSessionState) -> int:
prefix_count = 0
for message in session.messages:
+53
View File
@@ -274,6 +274,59 @@ class AgentRuntimeTests(unittest.TestCase):
self.assertGreaterEqual(len(result.transcript), 5)
self.assertGreaterEqual(len(result.file_history), 0)
def test_agent_stops_after_data_agent_plan_requires_review(self) -> None:
responses = [
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'I will prepare a reviewable plan.',
'tool_calls': [
{
'id': 'call_1',
'type': 'function',
'function': {
'name': 'data_agent_prepare_generation_plan',
'arguments': json.dumps(
{
'dataset_label': '地图和生活边界数据',
'target': 'Agent(tag="life_service")',
'total_count': 2,
'turn_mix': '1 条单轮,1 条多轮',
'coverage': '附近吃喝玩乐',
'exclusions': '不要生成导航路线类 query',
'output_path': 'tasks/demo/artifacts',
}
),
},
}
],
},
'finish_reason': 'tool_calls',
}
]
}
]
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
with patch('src.openai_compat.request.urlopen', side_effect=make_urlopen_side_effect(responses)):
agent = LocalCodingAgent(
model_config=ModelConfig(
model='Qwen/Qwen3-Coder-30B-A3B-Instruct',
base_url='http://127.0.0.1:8000/v1',
),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
result = agent.run('Prepare data generation plan')
self.assertEqual(result.stop_reason, 'user_review_required')
self.assertEqual(result.tool_calls, 1)
self.assertIn('本轮已暂停', result.final_output)
self.assertTrue(
any(event.get('type') == 'user_review_required' for event in result.events)
)
def test_write_tool_is_blocked_without_permission(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
config = AgentRuntimeConfig(cwd=Path(tmp_dir))