Code Walkthrough

前文讲mini-sweagent的实现在 AIO action-level agentloop中;
本节概述siirl-agentic使能swe-agent时,具体的调用链;
同样的,在每个RolloutWorker的初始化阶段,会创建一个thread来实际负责每个sglang DP的完整数据流,具体创建thread执行的操作在_build_executor(…)中,其中,默认采用NaiveExecutor类;
具体来看,swe-agent的agentflow定义,其中,build_agentflow中首先需要获取对应的flow,其中,仍然采用原先的.swe.agentflow,具体的定义在siirl/execution/rollout/agentflow/swe/init.py中,其中根据BUILTIN_PROVIDERS以及对应的config中的name获取对应的subconf,其中对于sweagent,采用定义的RLTokenAgentBuilder来初始化agent,其中,runtime暂时skip不管,environment中采用k8s_adapter.py中的K8sEnvAdapterBuilder;

BUILTIN_PROVIDERS
# Use original SWE-ReX/SWE-agent implementations via adapters
BUILTIN_PROVIDERS = {
    "agent": {
        "minisweagent": ".swe.agent.minisweagent:MiniSWEAgentBuilder",
        # 推荐:使用 RLTokenAgent (有完整的 token tracking 支持)
        "sweagent": ".swe.agent.sii_sweagent:RLTokenAgentBuilder",
        # 实验性:使用原始 SWE-agent + 适配器 (缺少 token tracking,仅用于非 RL 场景)
        "sweagent_original": ".swe.agent.swe_adapter:SWEAgentAdapterBuilder",
        # 旧名称(兼容)
        "sweagent_legacy": ".swe.agent.sii_sweagent:RLTokenAgentBuilder",
        # 使用 DefaultAgent + LiteLLMModel (通过 sglang API,用于 validate 阶段)
        "litellm_agent": ".swe.agent.sii_sweagent:LiteLLMAgentBuilder",
    },
    "environment": {
        "docker": ".swe.environment.docker:DockerEnvBuilder",
        # Use original SWE-ReX K8sDeployment via adapter (recommended)
        "k8s": ".swe.environment.k8s_adapter:K8sEnvAdapterBuilder",
        # Legacy K8sEnvBuilder (custom implementation, 1895 lines)
        "k8s_legacy": ".swe.environment.k8s:K8sEnvBuilder",
        "kr8s": ".swe.environment.kr8s:Kr8sEnvBuilder",
    },
    "runtime": {
        "swefactory": ".swe.runtime.swefactory:SWEFactoryBuiler",
        "swebench": ".swe.runtime.swebench:SWEBenchBuiler",
        "swebench_sii": ".swe.runtime.swebench_sii:SWEBenchBuiler",
        "swebench_agent": ".swe.runtime.swebench_k8s_eval:SWEBenchK8sEvalBuilder",
    },
}

首先分析RLTokenAgentBuilder和K8sEnvAdapterBuilder的具体定义,其中,先看RLTokenAgentBuilder,核心提供build方法,返回RLTokenAgentWrapper类;

class RLTokenAgentBuilder(AgentBuilder):
    def __init__(self, config: dict):
        config.pop("name")
        # convert config to DefaultAgentConfig
        # Note: model field is required by DefaultAgentConfig but not used in training (sample.model is used instead)
 
        # Get model config from agent configuration
        model_config = config.get("model", {})
 
        # Filter out fields not supported by GenericAPIModelConfig
        # max_workers and tool_parser are SGLang-specific fields that GenericAPIModelConfig doesn't support
        # These fields are still used by build_agentflow() for SGLangModelConfig, just not here
        unsupported_fields = {"max_workers", "tool_parser"}
        clean_model_config = {k: v for k, v in model_config.items() if k not in unsupported_fields}
 
        # Ensure name field exists (required by GenericAPIModelConfig)
        # The actual model used in training comes from sample.model (SGLang), not this config
        if "name" not in clean_model_config:
            clean_model_config["name"] = "dummy"  # Placeholder, not used in training
 
        self.config = DefaultAgentConfig(
            name=config.get("name", "main"),
            templates=TemplateConfig.model_validate(config.get("templates", {})),
            tools=ToolConfig.model_validate(config.get("tools", {})),
            history_processors=config.get("history_processors", [DefaultHistoryProcessor()]),
            max_requeries=config.get("max_requeries", 3),
            action_sampler=config.get("action_sampler"),
            model=clean_model_config,
        )
 
    def build(self, sample: SWESample) -> Agent:
        # Pop problem_statement from instance to avoid passing it twice
        instance = sample.m.data.runtime_meta.instance.copy()
        if "problem_statement" in instance:
            instance.pop("problem_statement")
        # Pop repo to avoid duplicate keyword error in _get_format_dict
        # The original SWE-agent code computes repo from self._env.repo
        if "repo" in instance:
            instance.pop("repo")
 
        # Handle multimodal (with images) vs text-only
        if sample.m.data.issue_images:
            instance.pop("issue_images", None)
            problem_statement = SWEBenchMultimodalProblemStatement(
                text=sample.m.data.problem_statement,
                issue_images=sample.m.data.issue_images,
                id=instance["instance_id"],
                extra_fields=instance,
            )
        else:
            problem_statement = TextProblemStatement(
                text=sample.m.data.problem_statement,
                id=instance["instance_id"],
                extra_fields=instance,
            )
 
        # Use original RLTokenAgent via wrapper
        return RLTokenAgentWrapper(
            templates=self.config.templates,
            tools=ToolHandler(self.config.tools),
            history_processors=self.config.history_processors,
            model=sample.model,
            max_requeries=self.config.max_requeries,
            name=self.config.name,
            action_sampler_config=self.config.action_sampler,
            problem_statement=problem_statement,
            sample=sample,
        )

其中再看RLTokenAgentWrapper的定义,其中负责wrapper实际调用swe-agent 三方库中的RLTokenAgent对象,其中核心提供run(…)与resume(…)方法,resume用于partial rollout时提供,而run用于正常生成流程,其中调用swe-agent库中的RLTokenAgent进行生成,并提供异常捕捉SglangGenerationAborted,用于判断该请求被abort,调用_snapshot将历史状态保存在partial_state中,随后raise异常给上层捕获;

class RLTokenAgentWrapper(AbstractAgent):
 
    def __init__(
        self,
        *,
        templates: TemplateConfig,
        tools: ToolHandler,
        history_processors: list[HistoryProcessor],
        model: Model,
        max_requeries: int = 3,
        name: str = "main",
        problem_statement: ProblemStatement | ProblemStatementConfig,
        sample: SWESample,
        _catch_errors: bool = True,
        _always_require_zero_exit_code: bool = False,
        action_sampler_config: ActionSamplerConfig | None = None,
    ):
        """Initialize the wrapper."""
        self.sample = sample
        self.problem_statement = problem_statement
        self.model = model
 
        # Create state with tokenizer for original RLTokenAgent
        if hasattr(model, "tokenizer"):
            state = StateWithTokenizer(model.tokenizer)
        else:
            raise ValueError(f"Model {type(model).__name__} must have 'tokenizer' attribute for RLTokenAgent")
 
        # Create original RLTokenAgent with state
        # Note: original RLTokenAgent uses ToolHandler, not SiiToolHandler
        # We need to convert ToolHandler config to ToolHandler
        from sweagent.tools.tools import ToolHandler
 
        tool_handler = ToolHandler(tools.config)
 
        # ``model`` is a ``SweSglangModel`` — a faithful async port of the
        # pre-6564c63 ``query_for_swe`` wrapper. Upstream ``RLTokenAgent``
        # reads ``output.get("new_prompt_token_ids", [])`` etc. with defaults,
        # so missing keys (we don't set them) degrade cleanly to empty
        # ``sample.tokens / loss_mask``. Enabling proper RL token tracking is
        # a separate workstream — do NOT bolt it onto this wrapper, the
        # incremental-tokenise attempt is what caused the reward regression.
        self._agent = OriginalRLTokenAgent(
            templates=templates,
            tools=tool_handler,
            history_processors=history_processors,
            model=model,
            max_requeries=max_requeries,
            name=name,
            _catch_errors=_catch_errors,
            _always_require_zero_exit_code=_always_require_zero_exit_code,
            action_sampler_config=action_sampler_config,
            state=state,  # Critical: provide state with tokenizer - must be last
        )
 
        # Expose key attributes
        self.name = self._agent.name
        self.tools = tools
        self.history = self._agent.history
        self.trajectory = self._agent.trajectory
        self.info = self._agent.info
 
        # Populated by ``run`` / ``resume`` when SglangGenerationAborted
        # propagates — SWEAgentFlow reads this in the abort branch to build
        # ``sample.partial_agent_data`` and delay env cleanup.
        self.partial_state: dict[str, Any] | None = None
 
    @property
    def templates(self):
        """Access to agent's template configuration."""
        return self._agent.templates
 
    def add_hook(self, hook):
        """Add hook to the underlying agent."""
        self._agent.add_hook(hook)
 
    async def setup(
        self,
        env: ContainerEnv,
        problem_statement: ProblemStatement | ProblemStatementConfig,
        output_dir: Path = Path("."),
    ) -> None:
        """Setup the agent for a new instance."""
        swe_env = env._env if hasattr(env, "_env") else env
        await self._agent.setup(env=swe_env, problem_statement=problem_statement, output_dir=output_dir)
 
    async def run(
        self,
        env: ContainerEnv,
        output_dir: Path = Path("."),
    ) -> AgentRunResult:
        swe_env = env._env if hasattr(env, "_env") else env
 
        try:
            result = await self._agent.run(
                env=swe_env, problem_statement=self.problem_statement, output_dir=output_dir
            )
        except SglangGenerationAborted:
            self.partial_state = self._snapshot()
            raise
 
        self._backfill_sample(result)
        return result
 
    async def resume(
        self,
        env: ContainerEnv,
        partial: dict[str, Any],
        output_dir: Path = Path("."),
    ) -> AgentRunResult:
        """Partial-rollout entry point. Pairs with ``run``'s snapshot path.
 
        Call order: this method ``setup``s the upstream agent, restores
        model-side TokenManager state from ``partial``, restores upstream
        agent state, then calls ``upstream_agent.resume`` which skips
        ``setup``/``on_run_start`` and jumps straight into the step loop.
        """
        swe_env = env._env if hasattr(env, "_env") else env
 
        # 1) setup is still required — it binds env/problem_statement/tools on
        #    the upstream agent, installs tools on the pod, and seeds
        #    info/history. restore_state below overwrites the bits that
        #    setup would otherwise discard (history, trajectory, info, ...).
        await self._agent.setup(
            env=swe_env, problem_statement=self.problem_statement, output_dir=output_dir
        )
 
        # 2) model side first: TokenManager / _rid / stats / _processed_message_count
        self.model.restore_state(partial)
 
        # 3) upstream agent state
        self._agent.restore_state(partial["swe_agent_state"])
 
        try:
            result = await self._agent.resume(
                env=swe_env, problem_statement=self.problem_statement, output_dir=output_dir
            )
        except SglangGenerationAborted:
            self.partial_state = self._snapshot()
            raise
 
        self._backfill_sample(result)
        return result
 
    def _backfill_sample(self, result: AgentRunResult) -> None:
        """Write rollout outputs back onto the siirl Sample.
 
        Shared by both normal-completion paths in ``run`` and ``resume``.
        """
        # CRITICAL: Assign token tracking data to sample for RL training
        self.sample.prompts = self._agent.init_input_ids
        self.sample.tokens = self._agent.input_ids[len(self._agent.init_input_ids) :]
        self.sample.loss_mask = self._agent.loss_mask[len(self._agent.init_input_ids) :]
        self.sample.rollout_log_probs = getattr(self._agent, "rollout_log_probs", [])
        # Upstream now also surfaces base64-encoded routed experts; forward it
        # untouched so the training end can reshape it.
        self.sample.rollout_routed_experts = getattr(self._agent, "routed_experts_raw", "") or ""
 
        # Store patch / exit_status in rollout for evaluation.
        # 优先读 self._agent.info(写 traj 的同一个 dict),落回 result.info 兜底。
        # Train 阶段只对 "submitted" 做 eval;validate 阶段对 "submitted*" 都做 eval。
        agent_info = getattr(self._agent, "info", {}) or {}
        info = agent_info if agent_info else (result.info or {})
        self.sample.m.rollout.patch = info.get("submission", None)
        self.sample.m.rollout.exit_status = info.get("exit_status", None)
 
    def _snapshot(self) -> dict[str, Any]:
        """Build the partial_agent_data dict (naive_flow-aligned shape).
 
        naive_flow key convention (see naive_flow._save_partial_agent_data):
            rid / messages / prompts_ids / response_ids / response_mask /
            rollout_log_prob / assistant_turns / env_turns /
            env_rewards / env_kwargs / routed_experts
        SWE-specific additions: swe_agent_state, swe_model_stats,
        swe_processed_message_count. ``swe_env_handle`` and
        ``swe_runtime_bootstrapped`` are filled in by SWEAgentFlow.
        """
        model_state = self.model.dump_state()
        upstream_trajectory = getattr(self._agent, "_trajectory", None) or list(self._agent.trajectory)
        env_turns = sum(
            1 for step in upstream_trajectory if step.get("tool_calls")
        )
        return {
            # naive_flow-aligned
            "rid": model_state["rid"],
            "messages": copy.deepcopy(self._agent.history),
            "prompts_ids": model_state["prompts_ids"],
            "response_ids": model_state["response_ids"],
            "response_mask": model_state["response_mask"],
            "rollout_log_prob": model_state["rollout_log_prob"],
            "assistant_turns": len(upstream_trajectory),
            "env_turns": env_turns,
            "env_rewards": [],
            "env_kwargs": {},
            "routed_experts": getattr(self._agent, "routed_experts_raw", "") or "",
            # SWE-specific
            "swe_agent_state": self._agent.dump_state(),
            "swe_model_stats": model_state["swe_model_stats"],
            "swe_processed_message_count": model_state["swe_processed_message_count"],
        }

再看K8sEnvAdapterBuilder的定义,具体定义如下,核心提供start方法,

class K8sEnvAdapterBuilder(ContainerEnvBuilder):
    """Parses a SWE-bench sample into a ``K8sEnvAdapter``."""
 
    def __init__(self, config: dict):
        self.config = config
 
    async def build(self, args: ContainerBuildArgs):
        raise NotImplementedError("K8s uses pre-built images. Use 'start' instead.")
 
    def _make_adapter(self, runtime_meta: Any) -> K8sEnvAdapter:
        sample, is_eval_pod = _extract_sample_and_eval_flag(runtime_meta)
        resume_handle = _extract_resume_handle(runtime_meta)
        image_name = _resolve_image_name(sample)
        if is_eval_pod:
            logger.info("[K8sEnvAdapterBuilder] Starting eval pod (skip_reset=True)")
        if resume_handle is not None:
            logger.info(
                f"[K8sEnvAdapterBuilder] Partial-rollout resume: attaching to "
                f"pod {resume_handle.get('pod_name')} in namespace "
                f"{resume_handle.get('namespace')}"
            )
 
        container = ContainerStartArgs(
            image=image_name,
            cwd="/testbed",
            startup_timeout=self.config.get("startup_timeout", 1800.0),
        )
        return K8sEnvAdapter(
            args=container,
            instance=sample,
            repo_name=self.config.get("repo_name", "testbed"),
            namespace=self.config.get("namespace", "swe"),
            config=self.config,
            skip_reset=is_eval_pod,
            resume_handle=resume_handle,
        )
 
    async def start(self, args: ContainerStartArgs, runtime_meta: Any = None) -> K8sEnvAdapter:
        env = self._make_adapter(runtime_meta)
        await env._ensure_initialized()
        return env

随后在此处分析SWEAgentFlow的具体定义,完成类定义如下,可以看到,SWEAgentFlow核心分为preprocess, generate和reward三个函数,其中,在preprocess函数中,会初始化对应的runtime以及agent,注意,这里的runtime和 agent实际上都是per-sample概念的,所有sample信息实际上会被绑定到对应的某个Flow里,注意,如果该preprocess的sample含有partial_agent_data字段,代表其为partial样本,此时,需要将对应的历史状态meta.rollout.partial_state中,方便在generate时进行恢复;
在generate函数中,实则是调用generate_async(…)函数,在generate_async函数中,首先将partial_state通过调用_inject_resume_handle(…)函数,将swe_env_handle恢复到runtime_meta中,用于保证该sample能够连接上同一个环境 (pod);
随后调用self.env.start(…),也即K8sEnvAdapterBuilder的start(…)函数;随后调用await m.runtime.bootstrap(env)进行runtime连接,然后调用m.agent.run(env),也即RLTokenAgentWrapper的run方法,也即调用定义在swe-agent中的RLTokenAgent.run;根据返回结果,如果出现SglangGenerationAborted,则把对应的结果保存到partial_state中,并把错误异常raise上送;上送的异常最终在NaiveExecutor中被捕获,将对应的sample通过put_partial存入partial队列中;

class SWEAgentFlow(AgentFlow):
    def __init__(
        self,
        env: ContainerEnvBuilder,
        runtime: RuntimeBuilder,
        agent: AgentBuilder = None,  # 可选,validate 模式下可以为 None
        validate_agent: AgentBuilder = None,
    ):
        # 如果 agent 为 None,使用 validate_agent 作为主要的 agent
        self.agent = validate_agent if agent is None else agent
        self.validate_agent = validate_agent
        self.env = env
        self.runtime = runtime
 
    async def preprocess(self, sample: dict, model: Model, is_validate: bool = False) -> SWESample:
        """把数据集的一条数据 (dict) 处理成 Sample 对象;可能会 raise exception
 
        Args:
            sample (dict): 数据集的一条数据
            model (Model): 模型实例(每个样本独立)
            is_validate (bool): 是否为 validate 阶段
 
        Returns:
            Sample: rollout 并 evaluate 的算例
        """
        data = self.runtime.parse_sampledata(sample)
        meta = SWEAgentMeta(data)
        s = SWESample(meta, model)
        runtime = self.runtime.build(s)
 
        # 根据是否 validate 选择不同的 agent
        agent_builder = self.validate_agent if is_validate and self.validate_agent else self.agent
        agent = agent_builder.build(s)
 
        meta.runtime = runtime
        meta.agent = agent
        # 存下 is_validate,reward 依此决定 exit_status 过滤规则
        meta.rollout.is_validate = is_validate
 
        # Store weight_version (training step) from sample dict for eval to use
        if "weight_version" in sample:
            meta.weight_version = sample["weight_version"]
            logger.debug(f"[SWEAgentFlow.preprocess] Set weight_version={sample['weight_version']} for sample")
 
        # Partial-rollout inbound: if AgentFlowCallable forwarded a
        # ``partial_agent_data`` from the siirl Sample into this dict,
        # record it on the rollout meta. ``_generate_async`` picks it up
        # and branches into the resume path.
        partial = sample.get("partial_agent_data") if isinstance(sample, dict) else None
        if partial:
            meta.rollout.partial_state = partial
            # 置空出口字段,防止和本轮 _generate_async 新写入的 partial 互相污染
            meta.rollout.partial_agent_data = None
            logger.info(
                f"[SWEAgentFlow.preprocess] Resuming from partial "
                f"(rid={partial.get('rid')}, assistant_turns={partial.get('assistant_turns')})"
            )
 
        logger.debug(f"[SWEAgentFlow.preprocess] is_validate={is_validate}, agent type: {type(meta.agent)}")
        return s
 
    async def generate(self, sample: SWESample):
        """Dispatch the full rollout to a worker-thread event loop.
 
        The actual rollout body (``_generate_async``) runs in isolation — its
        SWEEnv/httpx/kubectl timers can't be starved by Sglang or Ray activity
        on the rollout main loop.  See the module header comment for background.
        """
        loop = asyncio.get_running_loop()
        await loop.run_in_executor(
            _get_flow_executor(), _run_async_in_thread, self._generate_async, sample
        )
 
    async def reward(self, sample: SWESample):
        loop = asyncio.get_running_loop()
        await loop.run_in_executor(
            _get_flow_executor(), _run_async_in_thread, self._reward_async, sample
        )
 
    async def _generate_async(self, sample: SWESample):
        m = sample.m
        partial = getattr(m.rollout, "partial_state", None)
        logger.warning(
            "[SWEAgentFlow.generate] Running agent.%s", "resume" if partial else "run"
        )
        logger.debug(f"[SWEAgentFlow.generate] m.agent type: {type(m.agent)}")
 
        # Inject the resume handle into runtime_meta so K8sEnvAdapter's
        # builder attaches to the existing pod instead of creating a new one.
        runtime_meta = m.data.runtime_meta
        if partial and partial.get("swe_env_handle"):
            runtime_meta = _inject_resume_handle(runtime_meta, partial["swe_env_handle"])
 
        env = None
        try:
            env = await self.env.start(m.data.container_args, runtime_meta)
        except Exception as start_exc:
            logger.error(f"[SWEAgentFlow.generate] Failed to start environment: {start_exc}", exc_info=True)
            raise EnvCreateError(f"env start failed: {start_exc}") from start_exc
 
        aborted = False
        try:
            await m.runtime.bootstrap(env)  # idempotent — resume path is a no-op
            if partial:
                await m.agent.resume(env, partial)
            else:
                await m.agent.run(env)
            await m.runtime.diff(env)
        except SglangGenerationAborted:
            # Build the outbound partial_agent_data. wrapper.partial_state is
            # populated by RLTokenAgentWrapper.run/resume in its own except.
            wrapper_partial = getattr(m.agent, "partial_state", None) or {}
            env_handle = None
            try:
                env_handle = env.get_handle() if env is not None else None
            except Exception as e:
                logger.error(
                    f"[SWEAgentFlow.generate] get_handle failed: {e}; "
                    "will fall through to cleanup and drop partial",
                    exc_info=True,
                )
            if env_handle is None or not wrapper_partial:
                # Can't resume without env handle or agent snapshot — let the
                # finally cleanup the pod and don't publish partial data.
                logger.warning(
                    f"[SWEAgentFlow.generate] Abort without usable partial "
                    f"(env_handle={bool(env_handle)}, wrapper_partial={bool(wrapper_partial)}); "
                    "releasing pod"
                )
                m.rollout.partial_agent_data = None
            else:
                m.rollout.partial_agent_data = {
                    **wrapper_partial,
                    "swe_env_handle": env_handle,
                    "swe_runtime_bootstrapped": True,
                }
                aborted = True
            raise
        finally:
            if env is not None:
                if aborted:
                    # Keep pod alive; close Python client only. Fallback to
                    # full cleanup if detach itself fails — we'd rather lose
                    # the partial than leak a pod into the cluster.
                    try:
                        await env.detach()
                    except Exception as e:
                        logger.error(
                            f"[SWEAgentFlow.generate] detach failed: {e}; "
                            "falling back to cleanup",
                            exc_info=True,
                        )
                        with contextlib.suppress(Exception):
                            await env.cleanup()
                        m.rollout.partial_agent_data = None
                else:
                    try:
                        await env.cleanup()
                    except Exception as e:
                        logger.warning(f"[SWEAgentFlow.generate] cleanup failed: {e}")
 
        # 按 exit_status 决定是否让该样本进入 eval 阶段(与 Agentic_RL 对齐)
        # 注意:TRUNCATED 样本依然会跑 eval(仅预置 status=TRUNCATED,训练时由上层按 status 排除)。
        # Abort 路径不会到这里(上面 raise 已经离开了函数)。
        do_eval, status_override = _should_eval(m.rollout.exit_status, m.rollout.is_validate)
        if status_override is not None:
            sample.status = status_override
            tag = "Pre-set status" if do_eval else "Skip eval"
            sample.errors.append(f"{tag}: exit_status={m.rollout.exit_status!r}, is_validate={m.rollout.is_validate}")
            logger.info(
                f"[SWEAgentFlow.generate] {tag} (status={status_override.value}, do_eval={do_eval}, "
                f"exit_status={m.rollout.exit_status!r}, is_validate={m.rollout.is_validate})"
            )
 
    async def _reward_async(self, sample: SWESample):
        """reward body — runs inside the worker thread's event loop."""
        m = sample.m
 
        # 与 generate 对齐的 exit_status 过滤:不合格样本跳过 eval,但设置 reward=0
        # 这样样本仍然进入 databuffer,保证 GRPO batch size 一致性
        do_eval, _ = _should_eval(m.rollout.exit_status, m.rollout.is_validate)
        if not do_eval:
            logger.info(
                f"[SWEAgentFlow.reward] Skipping eval, setting reward=0 (exit_status={m.rollout.exit_status!r}, "
                f"is_validate={m.rollout.is_validate}, sample.status={sample.status.value})"
            )
            sample.reward = 0.0
            return
 
        # Check if runtime needs external env (e.g., run_instance_k8s_for_rl creates its own pod)
        needs_external_env = getattr(m.runtime, "needs_external_env", True)
        logger.debug(f"[SWEAgentFlow.reward] needs_external_env={needs_external_env}")
 
        env = None
        if needs_external_env:
            try:
                # 标记为 eval pod,跳过 reset(eval pod 应该保持镜像中的原始状态)
                runtime_meta = m.data.runtime_meta
                logger.debug(f"[SWEAgentFlow.reward] runtime_meta type: {type(runtime_meta)}")
                if isinstance(runtime_meta, dict):
                    runtime_meta = {**runtime_meta, "_is_eval_pod": True}
                    logger.debug("[SWEAgentFlow.reward] Set _is_eval_pod=True in dict")
                else:
                    # runtime_meta 是 SBSample 对象(dataclass,可能 frozen),绕过 frozen 检查
                    object.__setattr__(runtime_meta, "_is_eval_pod", True)
                    logger.debug("[SWEAgentFlow.reward] Set _is_eval_pod=True on SBSample object")
                env = await self.env.start(m.data.container_args, runtime_meta)
            except Exception as start_exc:
                logger.error(f"[SWEAgentFlow.reward] Failed to start environment: {start_exc}", exc_info=True)
                raise EnvCreateError(f"env start failed: {start_exc}") from start_exc
        else:
            logger.info("[SWEAgentFlow.reward] Skipping external env creation (runtime handles its own pod)")
 
        try:
            if env is not None:
                await m.runtime.bootstrap(env)
            await m.runtime.eval(env)
        finally:
            if env is not None:
                try:
                    await env.cleanup()
                except Exception as e:
                    logger.warning(f"[SWEAgentFlow.reward] cleanup failed: {e}")
 

随后来看swe-agent中定义的RLTokenAgent的具体实现,其中核心看run方法,其中run方法在改请求正式完成前 (step_output.done为True前),不断的在while循环内,调用step_output = await self.step()函数进行每轮的生成与交互,在step函数中,核心调用forward_with_handling方法,该方法也是一步step的核心,其中核心分为两步,首先调用self.forward(history)进行生成,其中核心调用self.model.query(history)进行生成,其中self.model即为SWESglangModel,随后,调用self.tools.parse_actions解析output,获取其中的tool_calls,拆分到step.throught和step.action中,随后调用self.handle_action(step)进行实际的环境交互操作;

class RLTokenAgent(DefaultAgent):
    def __init__(
        self,
        *,
        templates: TemplateConfig,
        tools: ToolHandler,
        history_processors: list[HistoryProcessor],
        model: AbstractModel,
        max_requeries: int = 3,
        name: str = "main",
        _catch_errors: bool = True,
        _always_require_zero_exit_code: bool = False,
        action_sampler_config: ActionSamplerConfig | None = None,
        state: Any | None = None,
    ):
        super().__init__(
            templates=templates,
            tools=tools,
            history_processors=history_processors,
            model=model,
            max_requeries=max_requeries,
            name=name,
            _catch_errors=_catch_errors,
            _always_require_zero_exit_code=_always_require_zero_exit_code,
            action_sampler_config=action_sampler_config,
        )
        self.state = state
        self.token_manager = getattr(model, "token_manager", TokenManager())
        self.input_ids: list[int] = []
        self.loss_mask: list[int] = []
        self.rollout_log_probs: list[float] = []
        self.init_input_ids: list[int] = []
        self.system_prompt_prefix: list[int] = []
        self.generate_prompt_suffix: list[int] = []
        self.user_input_flag: bool = True
        self._routed_experts_raw: str = ""
        self._error_logs: list[dict[str, Any]] = []
 
    def _require_tokenizer(self):
        if self.state is None or not hasattr(self.state, "tokenizer"):
            raise ValueError("RLTokenAgent requires state.tokenizer for token tracking.")
        return self.state.tokenizer
 
    @property
    def routed_experts_raw(self) -> str:
        """Raw base64-encoded routed_experts from the last SGLang query.
 
        Each query sends the full accumulated input_ids, so the last query's
        routed_experts covers the entire token sequence. slime's
        fill_routing_replay expects shape (seq_len-1, num_layers, top_k).
        """
        return self._routed_experts_raw
 
    def _normalize_logprobs(self, token_ids: list[int], logprobs: list[float] | None) -> list[float]:
        logprobs = list(logprobs or [])
        if len(logprobs) < len(token_ids):
            logprobs.extend([1.0] * (len(token_ids) - len(logprobs)))
        elif len(logprobs) > len(token_ids):
            logprobs = logprobs[: len(token_ids)]
        return logprobs
 
    def _add_prompt_tokens(self, token_ids: list[int], logprobs: list[float] | None = None) -> None:
        if not token_ids:
            return
        token_ids = list(token_ids)
        logprobs = self._normalize_logprobs(token_ids, logprobs)
        self.input_ids.extend(token_ids)
        self.loss_mask.extend([0] * len(token_ids))
        self.rollout_log_probs.extend(logprobs)
        self.token_manager.add_prompt(token_ids, logprobs)
 
    def _add_response_tokens(self, token_ids: list[int], logprobs: list[float] | None = None) -> None:
        if not token_ids:
            return
        token_ids = list(token_ids)
        logprobs = self._normalize_logprobs(token_ids, logprobs)
        self.input_ids.extend(token_ids)
        self.loss_mask.extend([1] * len(token_ids))
        self.rollout_log_probs.extend(logprobs)
        self.token_manager.add_response(list(token_ids), logprobs)
 
    def _sync_token_manager_from_lists(self) -> None:
        self.token_manager.reset()
        if not self.input_ids:
            return
        if not (len(self.input_ids) == len(self.loss_mask) == len(self.rollout_log_probs)):
            raise ValueError(
                "RLTokenAgent token buffers length mismatch: "
                f"input_ids={len(self.input_ids)}, loss_mask={len(self.loss_mask)}, "
                f"rollout_log_probs={len(self.rollout_log_probs)}"
            )
 
        cur_mask = int(self.loss_mask[0])
        buf_ids: list[int] = []
        buf_logprobs: list[float] = []
 
        def flush() -> None:
            if not buf_ids:
                return
            if cur_mask:
                self.token_manager.add_response(buf_ids, buf_logprobs)
            else:
                self.token_manager.add_prompt(buf_ids, buf_logprobs)
 
        for token_id, mask, logprob in zip(self.input_ids, self.loss_mask, self.rollout_log_probs):
            mask = int(mask)
            if mask != cur_mask:
                flush()
                buf_ids = []
                buf_logprobs = []
                cur_mask = mask
            buf_ids.append(int(token_id))
            buf_logprobs.append(float(logprob))
        flush()
 
    def _append_history(self, item: dict[str, Any]) -> None:
        self._chook.on_query_message_added(**item)
        self.history.append(item)  # type: ignore[arg-type]
 
        # Token tracking is optional but required for token-in/token-out mode.
        if self.state is None or not hasattr(self.state, "tokenizer"):
            return
 
        tokenizer = self.state.tokenizer
        tools_schema = getattr(self.tools.config, "tools", None)
        role = item["role"]
 
        if role == "system":
            try:
                content_token_ids = tokenizer.apply_chat_template(
                    [{"role": "system", "content": item["content"]}],
                    tools=tools_schema,
                    add_generation_prompt=False,
                    tokenize=True,
                    return_dict=False,
                )
            except TypeError:
                content_token_ids = tokenizer.apply_chat_template(
                    [{"role": "system", "content": item["content"]}],
                    add_generation_prompt=False,
                    tokenize=True,
                    return_dict=False,
                )
 
            self.init_input_ids = list(content_token_ids)
            self._add_prompt_tokens(content_token_ids)
 
        elif role == "assistant":
            output_tokens = item.get("output_tokens")
            rollout_log_probs = item.get("rollout_log_probs")
 
            if output_tokens is None or not isinstance(output_tokens, list):
                output_tokens = []
            if rollout_log_probs is None or not isinstance(rollout_log_probs, list):
                rollout_log_probs = []
 
            tokenizer_name = str(getattr(tokenizer, "name_or_path", "") or "").lower()
 
            if "qwen" in tokenizer_name:
                # 和第一版对齐:Qwen 额外补一个换行 token 198,换行不参与 loss。
                self._add_response_tokens(output_tokens, rollout_log_probs)
                self._add_prompt_tokens([198], [1.0])
            else:
                self._add_response_tokens(output_tokens, rollout_log_probs)
 
        elif role == "user":
            content_token_ids = tokenizer.apply_chat_template(
                [{"role": "user", "content": item["content"]}],
                add_generation_prompt=True,
                tokenize=True,
                return_dict=False,
            )
            content_token_ids = content_token_ids[len(self.system_prompt_prefix):]
 
            if self.user_input_flag:
                self.init_input_ids.extend(content_token_ids)
                self.user_input_flag = False
                self.logger.debug("user instance template matched")
 
            self._add_prompt_tokens(content_token_ids)
 
        elif role == "tool":
            tool_msg: dict[str, Any] = {
                "role": "tool",
                "content": item["content"],
            }
            content_token_ids = tokenizer.apply_chat_template(
                [tool_msg],
                add_generation_prompt=True,
                tokenize=True,
                return_dict=False,
            )
            content_token_ids = content_token_ids[len(self.system_prompt_prefix):]
 
            tokenizer_name = str(getattr(tokenizer, "name_or_path", "") or "").lower()
            if "glm" in tokenizer_name and "qwen" not in tokenizer_name and content_token_ids:
                content_token_ids = content_token_ids[1:]
 
            self._add_prompt_tokens(content_token_ids)
 
    def save_trajectory(self) -> None:
        """No-op in RL mode — trajectory data is returned via AgentRunResult,
        persistence is handled by slime's rollout data saver."""
        pass
 
    async def setup(
        self,
        env: SWEEnv,
        problem_statement: ProblemStatement | ProblemStatementConfig,
        output_dir: Path = Path("."),
    ) -> None:
        tokenizer = self._require_tokenizer()
 
        model = self.model
        if hasattr(model, "reset_rollout_state"):
            model.reset_rollout_state()
 
        self.token_manager = getattr(model, "token_manager", self.token_manager)
        self.token_manager.reset()
 
        self.input_ids = []
        self.loss_mask = []
        self.rollout_log_probs = []
        self.init_input_ids = []
        self.system_prompt_prefix = []
        self.generate_prompt_suffix = []
        self.user_input_flag = True
        self._routed_experts_raw = ""
        self._error_logs = []
 
        try:
            self.system_prompt_prefix = tokenizer.apply_chat_template(
                [{}],
                add_generation_prompt=False,
                tokenize=True,
                return_dict=False,
            )
        except Exception:
            self.system_prompt_prefix = []
 
        self.generate_prompt_suffix = tokenizer.apply_chat_template(
            [{"role": "user", "content": ""}],
            add_generation_prompt=True,
            tokenize=True,
            return_dict=False,
        )[len(tokenizer.apply_chat_template(
            [{"role": "user", "content": ""}],
            add_generation_prompt=False,
            tokenize=True,
            return_dict=False,
        )):]
 
        await super().setup(env=env, problem_statement=problem_statement, output_dir=output_dir)
 
    def add_step_to_history(self, step: StepOutput) -> None:
        content = step.output if not step.tool_calls else (step.thought or step.output)
        if content is None:
            content = ""
 
        assistant_message: dict[str, Any] = {
            "role": "assistant",
            "content": content,
            "thought": step.thought,
            "action": step.action,
            "agent": self.name,
            "message_type": "action",
            "thinking_blocks": step.thinking_blocks,
            "reasoning_content": step.reasoning_content,
            "output_tokens": step.output_tokens,
            "rollout_log_probs": step.rollout_log_probs,
            "rollout_routed_experts": step.rollout_routed_experts,
        }
        if step.tool_calls:
            assistant_message["tool_calls"] = step.tool_calls
 
        self._append_history(assistant_message)
 
        elided_chars = 0
        if step.observation.strip() == "":
            templates = [self.templates.next_step_no_output_template]
        elif len(step.observation) > self.templates.max_observation_length:
            templates = [self.templates.next_step_truncated_observation_template]
            elided_chars = len(step.observation) - self.templates.max_observation_length
        else:
            templates = [self.templates.next_step_template]
 
        self._add_templated_messages_to_history(
            templates,
            observation=step.observation,
            elided_chars=elided_chars,
            max_observation_length=self.templates.max_observation_length,
            tool_call_ids=step.tool_call_ids,
            tool_calls=step.tool_calls,
            **step.state,
        )
 
    def get_model_requery_history(
        self,
        error_template: str,
        *,
        output: str,
        **kwargs: str | int | float | bool | None,
    ) -> list[int]:
        format_dict = {**kwargs, **self._get_format_dict()}
        error_template = Template(error_template).render(**format_dict)
        self.logger.warning(f"{error_template}")
 
        input_ids = copy.deepcopy(self.input_ids)
        return input_ids
 
    async def forward(self, history: list[int]) -> StepOutput:
        if self._total_execution_time > self.tools.config.total_execution_timeout:
            raise _TotalExecutionTimeExceeded()
 
        step = StepOutput()
        step.query = copy.deepcopy(history)
 
        try:
            # Hooks / viewers 仍然看 message history;真正喂给 model 的是 input_ids。
            self._chook.on_model_query(messages=self.messages, agent=self.name)
 
            if self._action_sampler is not None:
                assert self._problem_statement is not None
                best = self._action_sampler.get_action(
                    problem_statement=self._problem_statement,
                    trajectory=self.trajectory,
                    history=self.messages,
                )
                output = best.completion
                step.extra_info.update(best.extra_info)
            else:
                output = await self.model.query(history)  # type: ignore[arg-type]
 
            step.output = output["message"]
 
            if not self.init_input_ids:
                new_prompt_token_ids = output.get("new_prompt_token_ids", []) or []
                if isinstance(new_prompt_token_ids, list):
                    self.init_input_ids = list(new_prompt_token_ids)
 
            step.output_tokens = output.get("output_tokens", []) or []
            step.thinking_blocks = output.get("thinking_blocks", [])
            step.reasoning_content = output.get("reasoning_content", None)
            step.rollout_log_probs = output.get("rollout_log_probs", []) or []
 
            raw_experts = output.get("rollout_routed_experts", "") or ""
            if raw_experts:
                self._routed_experts_raw = raw_experts
 
            step.thought, step.action = self.tools.parse_actions(output)
 
            if output.get("tool_calls") is not None:
                step.tool_call_ids = [call["id"] for call in output["tool_calls"]]
                step.tool_calls = output["tool_calls"]
 
            self._chook.on_actions_generated(step=step)
            return await self.handle_action(step)
 
        except Exception as error:
            if step.action == step.thought == "":
                step.thought = step.output
            error.step = step  # type: ignore[attr-defined]
            raise
 
    async def forward_with_handling(self, history: list[int]) -> StepOutput:
        async def handle_error_with_autosubmission(
            exception: Exception,
            exit_status: str,
            message: str,
        ) -> StepOutput:
            full_traceback = traceback.format_exc()
            current_step = len(self.trajectory) + 1
            self._error_logs.append(
                {
                    "step": current_step,
                    "error_type": type(exception).__name__,
                    "exit_status": exit_status,
                    "message": str(exception),
                    "n_requeries": None,
                    "traceback": full_traceback,
                }
            )
            self.logger.warning(message)
            step = getattr(exception, "step", StepOutput())
            step.thought = message
            step.exit_status = exit_status
            step.output = message
            step.done = True
            return await self.attempt_autosubmission_after_error(step)
 
        def handle_error_with_retry(
            exception: Exception,
            template: str,
            n_requeries: int,
        ) -> list[int]:
            full_traceback = traceback.format_exc()
            current_step = len(self.trajectory) + 1
            exception_message = getattr(exception, "message", "")
            if not exception_message:
                try:
                    exception_message = exception.args[0]
                except (IndexError, AttributeError):
                    pass
 
            self._error_logs.append(
                {
                    "step": current_step,
                    "error_type": "retry",
                    "exception_type": type(exception).__name__,
                    "message": str(exception_message),
                    "n_requeries": n_requeries,
                    "traceback": full_traceback,
                }
            )
 
            self.logger.warning(
                "Requerying model after %s (%dth requery)",
                type(exception).__name__,
                n_requeries,
            )
 
            step: StepOutput = getattr(exception, "step", StepOutput())
            self.add_step_to_trajectory(step)
 
            return self.get_model_requery_history(
                error_template=template,
                **step.to_template_format_dict(),
                **getattr(exception, "extra_info", {}),
                exception_message=exception_message,
            )
 
        last_requery_exception: Exception | None = None
        n_format_fails = 0
 
        while n_format_fails < self.max_requeries:
            try:
                return await self.forward(history)
 
            except KeyboardInterrupt:
                raise
            except EOFError:
                raise
 
            except FormatError as error:
                last_requery_exception = error
                n_format_fails += 1
                history = handle_error_with_retry(
                    error,
                    self.tools.config.format_error_template,
                    n_format_fails,
                )
            except _BlockedActionError as error:
                last_requery_exception = error
                n_format_fails += 1
                history = handle_error_with_retry(
                    error,
                    self.tools.config.filter.blocklist_error_template,
                    n_format_fails,
                )
            except ContentPolicyViolationError as error:
                last_requery_exception = error
                self.logger.warning("Content policy violation, trying to resample")
                n_format_fails += 1
            except BashIncorrectSyntaxError as error:
                last_requery_exception = error
                n_format_fails += 1
                history = handle_error_with_retry(
                    error,
                    self.templates.shell_check_error_template,
                    n_format_fails,
                )
            except _RetryWithOutput as error:
                last_requery_exception = error
                n_format_fails += 1
                history = handle_error_with_retry(
                    error,
                    self.templates.next_step_template,
                    n_format_fails,
                )
            except _RetryWithoutOutput as error:
                last_requery_exception = error
                self.logger.warning("Retry without output, trying to resample")
                n_format_fails += 1
 
            except _ExitForfeit as error:
                return await handle_error_with_autosubmission(
                    error,
                    "exit_forfeit",
                    "Exiting due to forfeit",
                )
            except _TotalExecutionTimeExceeded as error:
                self.logger.exception("Exiting due to total execution time exceeded", exc_info=True)
                return await handle_error_with_autosubmission(
                    error,
                    "exit_total_execution_time",
                    "Exit due to total execution time exceeded",
                )
            except CommandTimeoutError as error:
                self.logger.exception("Exiting due to multiple consecutive command timeouts", exc_info=True)
                return await handle_error_with_autosubmission(
                    error,
                    "exit_command_timeout",
                    "Exit due to multiple consecutive command timeouts",
                )
            except ContextWindowExceededError as error:
                return await handle_error_with_autosubmission(
                    error,
                    "exit_context",
                    "Exit due to context window",
                )
            except InstanceCallLimitExceededError as error:
                return await handle_error_with_autosubmission(
                    error,
                    "exit_max_iter_out",
                    "Exit due to max iteration out of limit",
                )
            except TotalCostLimitExceededError:
                raise
            except CostLimitExceededError as error:
                return await handle_error_with_autosubmission(
                    error,
                    "exit_cost",
                    "Exit due to cost limit",
                )
            except RetryError as error:
                self.logger.exception("Exiting due to retry error: %s", error, exc_info=True)
                return await handle_error_with_autosubmission(
                    error,
                    "exit_api",
                    f"Exit due to retry error: {error}",
                )
            except SwerexException as error:
                self.logger.exception("Exiting due to environment error: %s", error, exc_info=True)
                return await handle_error_with_autosubmission(
                    error,
                    "exit_environment_error",
                    f"Exit due to environment error: {error}",
                )
            except RuntimeError as error:
                self.logger.exception("Exiting due to runtime error: %s", error, exc_info=True)
                return await handle_error_with_autosubmission(
                    error,
                    "exit_error",
                    f"Exit due to runtime error: {error}",
                )
            except Exception as error:
                self.logger.exception("Exiting due to unknown error: %s", error, exc_info=True)
                return await handle_error_with_autosubmission(
                    error,
                    "exit_error",
                    f"Exit due to unknown error: {error}",
                )
 
        self.logger.exception("Exit due to repeated format/blocklist/bash syntax errors", exc_info=True)
 
        exception_to_use = last_requery_exception
        if exception_to_use is None:
            exception_to_use = RuntimeError("Exit due to repeated format/blocklist/bash syntax errors")
            exception_to_use.step = StepOutput()  # type: ignore[attr-defined]
 
        return await handle_error_with_autosubmission(
            exception_to_use,
            "exit_format",
            "Exit due to repeated format/blocklist/bash syntax errors",
        )
 
    def add_step_to_trajectory(self, step: StepOutput) -> None:
        trajectory_step = TrajectoryStep(
            {
                "action": step.action,
                "observation": step.observation,
                "response": step.output,
                "thought": step.thought,
                "execution_time": step.execution_time,
                "state": step.state,
                "query": step.query,
                "extra_info": step.extra_info,
                "reasoning_content": step.reasoning_content,
                "output_tokens": step.output_tokens,
                "rollout_log_probs": step.rollout_log_probs,
                "rollout_routed_experts": step.rollout_routed_experts,
            }
        )
        self.trajectory.append(trajectory_step)
 
    async def step(self) -> StepOutput:
        assert self._env is not None
        self._chook.on_step_start()
 
        n_step = len(self.trajectory) + 1
        self.logger.info("=" * 25 + f" STEP {n_step} " + "=" * 25)
 
        # 关键改动:这里传 input_ids,不再传 self.messages。
        step_output = await self.forward_with_handling(self.input_ids)
 
        self.add_step_to_history(step_output)
 
        self.info["submission"] = step_output.submission
        self.info["exit_status"] = step_output.exit_status  # type: ignore[index]
        self.info.update(
            await self._get_edited_files_with_context(patch=step_output.submission or "")
        )  # type: ignore[arg-type]
        self.info["model_stats"] = self.model.stats.model_dump()
 
        self.add_step_to_trajectory(step_output)
 
        self._chook.on_step_done(step=step_output, info=self.info)
        return step_output
 
    async def run(
        self,
        env: SWEEnv,
        problem_statement: ProblemStatement | ProblemStatementConfig,
        output_dir: Path = Path("."),
    ) -> AgentRunResult:
        await self.setup(env=env, problem_statement=problem_statement, output_dir=output_dir)
 
        self._chook.on_run_start()
        step_output = StepOutput()
        while not step_output.done:
            step_output = await self.step()
            self.save_trajectory()
        self._chook.on_run_done(trajectory=self.trajectory, info=self.info)
 
        data = self.get_trajectory_data()
        data["info"]["error_logs"] = self._error_logs
        data["info"]["response_turn"] = len(self.trajectory)
        return AgentRunResult(info=data["info"], trajectory=data["trajectory"])
 
    def dump_state(self) -> dict[str, Any]:
        """Snapshot mutable state accumulated across steps.
 
        Fields here are the ones that ``setup()`` clears or that the step
        loop mutates. External refs (env/model/tools/hooks) are rebound
        per-run by ``setup``, so they're intentionally left out.
        """
        return {
            "history": copy.deepcopy(self.history),
            "trajectory": copy.deepcopy(self._trajectory),
            "info": copy.deepcopy(dict(self.info)),
            "input_ids": list(self.input_ids or []),
            "loss_mask": list(self.loss_mask or []),
            "rollout_log_probs": list(self.rollout_log_probs or []),
            "init_input_ids": list(self.init_input_ids or []),
            "system_prompt_prefix": list(self.system_prompt_prefix or []),
            "generate_prompt_suffix": list(self.generate_prompt_suffix or []),
            "user_input_flag": bool(self.user_input_flag),
            "error_logs": copy.deepcopy(self._error_logs),
            "total_execution_time": float(self._total_execution_time),
            "n_consecutive_timeouts": int(self._n_consecutive_timeouts),
            "routed_experts_raw": self._routed_experts_raw,
        }
 
    def restore_state(self, state: dict[str, Any]) -> None:
        """Inverse of ``dump_state``.
 
        Call order on the resuming worker: construct agent → ``await
        self.setup(env, problem_statement, output_dir)`` → ``restore_state(state)``.
        ``setup`` would otherwise clear ``init_input_ids / _error_logs /
        info / _routed_experts_raw``; we overwrite its fresh values with
        the snapshot.
        """
        self.history = list(state["history"])
        self._trajectory = list(state["trajectory"])
 
        self.info.clear()
        self.info.update(state["info"])
 
        self.input_ids = list(state.get("input_ids", self.token_manager.token_ids))
        self.loss_mask = list(state.get("loss_mask", self.token_manager.loss_mask))
        self.rollout_log_probs = list(state.get("rollout_log_probs", self.token_manager.logprobs))
        self.init_input_ids = list(state.get("init_input_ids", []))
        self.system_prompt_prefix = list(state.get("system_prompt_prefix", []))
        self.generate_prompt_suffix = list(state.get("generate_prompt_suffix", []))
        self.user_input_flag = bool(state.get("user_input_flag", False))
        self._sync_token_manager_from_lists()
 
        self._error_logs = list(state["error_logs"])
        self._total_execution_time = float(state["total_execution_time"])
        self._n_consecutive_timeouts = int(state["n_consecutive_timeouts"])
        self._routed_experts_raw = state["routed_experts_raw"]
 
    async def resume(
        self,
        env: SWEEnv,
        problem_statement: ProblemStatement | ProblemStatementConfig,
        output_dir: Path = Path("."),
    ) -> AgentRunResult:
        """Partial-rollout entry point. Mirrors ``run`` minus setup / on_run_start.
 
        The caller must have called ``setup`` AND ``restore_state`` before
        this. We skip ``on_run_start`` so hooks don't double-initialise
        their own per-run state (e.g. timers, log files) — this is a
        continuation, not a new run.
        """
        step_output = StepOutput()
        while not step_output.done:
            step_output = await self.step()
            self.save_trajectory()
        self._chook.on_run_done(trajectory=self.trajectory, info=self.info)
 
        data = self.get_trajectory_data()
        data["info"]["error_logs"] = self._error_logs
        data["info"]["response_turn"] = len(self.trajectory)
        return AgentRunResult(info=data["info"], trajectory=data["trajectory"])
 

首先看具体的SWESglangModel的query函数,其中,强制要求history为list[dict],不然直接退出报error,随后调用self._query函数进行生成,而_query函数则调用single_query函数进行生成,其中,