Files
jxxghp 6d4ce83837 fix(types): clear host mypy debt and prevent typing regressions
严格检查全部宿主源码,清零类型错误预算并移除按模块类型豁免。
按原因、所属方法、错误码及语法指纹锁定必要兼容例外和站点类型桩,
CI 阻断新增忽略、检查范围退化、本地桩替代真实接口及新的类型错误。

修复空值、异步和供应方类型边界,保持插件与 API 兼容合同,
并补充存储、订阅、通知、Agent 和工作流边界的回归覆盖。
2026-10-21 22:03:18 +08:00

132 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
将 Python 子进程的逐次调用接回当前 Agent 的真实工具链。
"""
import json
import uuid
from collections.abc import Awaitable, Callable
from dataclasses import replace
from typing import Any, cast
from langchain.agents.middleware.types import AgentMiddleware, ToolCallRequest
from langchain_core.messages import AIMessage, ToolMessage
from app.agent.code.authority import CURRENT_CELL, RPC_RESULT_KEY, CellAuthority, allowed_tools
from app.agent.middleware.policy import AgentPolicyMiddleware
from app.agent.policy.contracts import ExecutionOutcome
from app.agent.terminal.ownership import TerminalScope, current_terminal_scope
from app.agent.tools.impl.execute_code import ExecuteCodeTool
from app.agent.tools.result import inspect_tool_result
class CodeExecutionMiddleware(AgentMiddleware):
"""
外层绑定执行窗口;内层调用保留策略、Skill、日志和原始结果归档行为。
"""
def __init__(self, policy: AgentPolicyMiddleware) -> None:
"""
共享同一个宿主策略实例,不复制权限或跳过当前身份事实源。
"""
self.policy = policy
self.selections: dict[TerminalScope, dict[str, dict[str, Any]]] = {}
async def awrap_tool_call(self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], Awaitable[Any]]) -> Any:
"""
只为模型真实请求中已启用的原生 execute_code 绑定当前窗口,默认直接执行。
"""
if type(request.tool) is not ExecuteCodeTool:
return await handler(request)
scope = current_terminal_scope()
selections = self.selections.get(scope, {}) if scope is not None else {}
tools = selections.pop(str(request.tool_call['id']), None)
if scope is None or tools is None:
return ToolMessage(content='当前 Python 请求缺少宿主工具快照,未执行代码。',
name='execute_code', tool_call_id=request.tool_call['id'], status='error')
async def dispatch(tool: Any, arguments: dict[str, Any]) -> Any:
"""
重入当前 handler,逐次经过 Skill 和工具门禁,而不是直接调用工具方法。
"""
return await self._dispatch(request, handler, tool, arguments)
authority = CellAuthority(scope, self.policy.context, tools, dispatch)
token = CURRENT_CELL.set(authority)
try:
return await handler(request)
finally:
CURRENT_CELL.reset(token)
await authority.retire()
if not selections:
self.selections.pop(scope, None)
async def _dispatch(self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], Awaitable[Any]],
tool: Any, arguments: dict[str, Any]) -> Any:
"""
每次内部调用使用独立回执 ID;原文只交给 Python,外层模型只接收程序输出。
"""
identifier = uuid.uuid4().hex
raw: dict[str, str] = {}
state = {**request.state, RPC_RESULT_KEY: raw}
runtime = replace(request.runtime, tool_call_id=identifier, state=state)
nested = ToolCallRequest(tool=tool, tool_call={'name': tool.name, 'args': arguments, 'id': identifier, 'type': 'tool_call'},
state=state, runtime=runtime)
allowed, result = await self.policy.execute_tool_call(
tool=tool, arguments=arguments, invocation_id=identifier,
handler=lambda: handler(nested), enforce_decision=True,
)
if not allowed:
return {'success': False, 'error': str(result)}
if not isinstance(result, ToolMessage):
return {'success': False, 'error': '代码内调用未返回可读取的工具回执。'}
content = raw.get(tool.name, result.content)
if not isinstance(content, str):
return {'success': False, 'error': '代码内调用只支持文本或 JSON 结果。'}
if len(content.encode('utf-8')) > 16 * 1024 * 1024:
return {'success': False, 'error': '单次工具结果超过16MB,请使用工具的分页或过滤参数缩小结果。'}
try:
payload = json.loads(content)
except ValueError:
payload = content
if inspect_tool_result(result) is not ExecutionOutcome.SUCCEEDED:
return {'success': False, 'error': payload}
return payload
class CodeCaptureMiddleware(AgentMiddleware):
"""
筛选后、压缩前生成真实 helper 合同,新增说明必须计入最终请求预算。
"""
def __init__(self, execution: CodeExecutionMiddleware) -> None:
"""
捕获器与工具包装器共享短生命周期快照,不增加图节点或注册重复工具。
"""
self.execution = execution
async def awrap_model_call(self, request: Any, handler: Callable[[Any], Awaitable[Any]]) -> Any:
"""
只为本次模型实际发出的代码调用保存快照,下一次请求替换同 owner 的旧快照。
"""
scope = current_terminal_scope()
if scope is not None:
self.execution.selections.pop(scope, None)
tools = allowed_tools(request.tools)
signatures = []
for name, tool in tools.items():
schema = tool.args_schema.model_json_schema()
parameters = [field if field in schema.get('required', []) else f'{field}=...'
for field in schema.get('properties', {})]
signatures.append(f'{name}({", ".join(parameters)})')
description = '\nCurrent Python helpers: ' + ('; '.join(signatures) or 'none; only local Python computation is available.')
request = request.override(tools=[tool.model_copy(update={'description': tool.description + description})
if type(tool) is ExecuteCodeTool else tool for tool in request.tools])
result = await handler(request)
if scope is None or not any(type(tool) is ExecuteCodeTool for tool in request.tools):
return result
selections = {cast(str, call['id']): tools for message in result.result if isinstance(message, AIMessage)
for call in message.tool_calls if call['name'] == 'execute_code'}
if selections:
self.execution.selections[scope] = selections
return result