feat(engine): 添加运行时上下文支持并重构工具迭代限制
添加 RuntimeContext 类用于捕获模型运行时的日期时间信息, 包括UTC时间、本地时间和时区信息,并在系统提示中显示这些信息。 同时增加最大上下文消息数和工具迭代次数的配置选项, 将验证服务从引擎加载器中移除,并更新相关的数据结构和接口。 BREAKING CHANGE: 移除了验证服务,相关字段被替换为证据状态和接受状态。 - 添加 RuntimeContext 类和相关渲染方法 - 增加 max_context_messages 和 max_tool_iterations 配置 - 移除 ValidationService 相关代码 - 更新消息记录中的验证状态字段 - 添加原始工具调用检测和回退处理
This commit is contained in:
@ -35,6 +35,7 @@ class StubProvider(LLMProvider):
|
||||
model: str | None = None,
|
||||
max_tokens: int = 4096,
|
||||
temperature: float = 0.7,
|
||||
thinking_enabled: bool | None = None,
|
||||
) -> LLMResponse:
|
||||
if not self._responses:
|
||||
raise AssertionError("No stubbed provider responses left")
|
||||
@ -47,11 +48,22 @@ class StubProvider(LLMProvider):
|
||||
class StubSkillAssembler:
|
||||
def __init__(self, activated_skills: list[SkillContext]) -> None:
|
||||
self.activated_skills = activated_skills
|
||||
self.calls: list[dict] = []
|
||||
|
||||
async def assemble(self, **kwargs) -> SkillAssemblyResult:
|
||||
self.calls.append(kwargs)
|
||||
return SkillAssemblyResult(activated_skills=list(self.activated_skills))
|
||||
|
||||
|
||||
class RecordingToolAssembler:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict] = []
|
||||
|
||||
async def assemble(self, **kwargs):
|
||||
self.calls.append(kwargs)
|
||||
return kwargs["registry"].get_specs(["memory"])
|
||||
|
||||
|
||||
def _tool_call(*, name: str = "echo", arguments: dict | None = None, call_id: str = "call-1") -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
id=call_id,
|
||||
@ -576,6 +588,48 @@ def test_agent_loop_records_skill_receipts_and_effects(tmp_path: Path) -> None:
|
||||
assert effect_records[-1].run_id == result.run_id
|
||||
|
||||
|
||||
def test_thinking_disabled_still_uses_skill_and_tool_assembly(tmp_path: Path) -> None:
|
||||
skill = SkillContext(
|
||||
name="docker-debug",
|
||||
content="Use docker logs before editing config.",
|
||||
version="v0007",
|
||||
content_hash="hash-v7",
|
||||
activation_reason="llm_selected",
|
||||
tool_hints=["terminal"],
|
||||
)
|
||||
skill_assembler = StubSkillAssembler([skill])
|
||||
tool_assembler = RecordingToolAssembler()
|
||||
loader = EngineLoader(
|
||||
workspace=tmp_path,
|
||||
skill_assembler=skill_assembler,
|
||||
tool_assembler=tool_assembler,
|
||||
)
|
||||
loop = AgentLoop(loader=loader)
|
||||
bundle = ProviderBundle(
|
||||
main_runtime=SimpleNamespace(model="stub-model", provider_name="stub"),
|
||||
main_provider=StubProvider(
|
||||
[LLMResponse(content="Done", finish_reason="stop", provider_name="stub", model="stub-model")]
|
||||
),
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
loop.process_direct(
|
||||
"Why is the Docker container crashing?",
|
||||
provider_bundle=bundle,
|
||||
thinking_enabled=False,
|
||||
)
|
||||
)
|
||||
loaded = loop.boot()
|
||||
events = loaded.session_manager.get_run_event_records(result.session_id, result.run_id)
|
||||
tool_selection = next(event for event in events if event.event_type == "tool_selection_snapshotted")
|
||||
|
||||
assert skill_assembler.calls
|
||||
assert skill_assembler.calls[0]["thinking_enabled"] is False
|
||||
assert tool_assembler.calls
|
||||
assert [skill.name for skill in tool_assembler.calls[0]["activated_skills"]] == ["docker-debug"]
|
||||
assert tool_selection.event_payload["tool_names"] == ["memory"]
|
||||
|
||||
|
||||
def test_agent_loop_records_max_tool_iterations_as_failed_skill_effect(tmp_path: Path) -> None:
|
||||
skill = SkillContext(
|
||||
name="docker-debug",
|
||||
@ -635,6 +689,52 @@ def test_agent_loop_records_max_tool_iterations_as_failed_skill_effect(tmp_path:
|
||||
assert effect_records[-1].success is False
|
||||
|
||||
|
||||
def test_agent_loop_suppresses_raw_tool_call_when_finalizing_after_tool_limit(tmp_path: Path) -> None:
|
||||
loader = EngineLoader(
|
||||
workspace=tmp_path,
|
||||
skill_assembler=StubSkillAssembler([]),
|
||||
)
|
||||
loop = AgentLoop(loader=loader)
|
||||
bundle = ProviderBundle(
|
||||
main_runtime=SimpleNamespace(model="stub-model", provider_name="stub"),
|
||||
main_provider=StubProvider(
|
||||
[
|
||||
LLMResponse(
|
||||
content="Need a tool.",
|
||||
finish_reason="tool_calls",
|
||||
tool_calls=[_tool_call()],
|
||||
provider_name="stub",
|
||||
model="stub-model",
|
||||
),
|
||||
LLMResponse(
|
||||
content=(
|
||||
"<tool_call>\n"
|
||||
"<function=mcp_local_web_mcp_web_fetch>\n"
|
||||
"<parameter=url>https://example.com</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>"
|
||||
),
|
||||
finish_reason="stop",
|
||||
provider_name="stub",
|
||||
model="stub-model",
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
loop.process_direct(
|
||||
"Fetch the latest result",
|
||||
provider_bundle=bundle,
|
||||
max_tool_iterations=0,
|
||||
)
|
||||
)
|
||||
|
||||
assert result.finish_reason == "max_tool_iterations"
|
||||
assert "<tool_call>" not in result.output_text
|
||||
assert "raw tool call was suppressed" in result.output_text
|
||||
|
||||
|
||||
def test_llm_request_snapshot_defaults_to_compact_payload(tmp_path: Path) -> None:
|
||||
loop = AgentLoop(loader=EngineLoader(workspace=tmp_path, skill_assembler=StubSkillAssembler([])))
|
||||
bundle = ProviderBundle(
|
||||
|
||||
Reference in New Issue
Block a user