init commit for external agent framework+gateway#25
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive agent framework and gateway system designed to facilitate agentic workflows within a training environment. Key components include a factory for constructing frameworks, an OpenAI-compatible framework implementation that manages sequence generation and trajectory logging, and a gateway system that provides an OpenAI-compatible API for agent interactions. The gateway handles session lifecycle, trajectory buffering, and multimodal data processing. Feedback identifies a critical issue with the incremental token encoding logic in the gateway, which may produce malformed sequences due to assumptions about tokenizer stability and turn separators. Further recommendations include parallelizing reward calculations to improve performance and replacing blocking ray.get calls with asynchronous operations to avoid event loop starvation.
| def _encode_incremental( | ||
| self, | ||
| messages: list[dict[str, Any]], | ||
| image_data: list[Any] | None = None, | ||
| video_data: list[Any] | None = None, | ||
| ) -> list[int]: | ||
| """Encode incremental messages (tool results, user follow-ups) for a continuation turn. | ||
|
|
||
| Uses the remove_system_prompt pattern from ToolAgentLoop: encode the new messages | ||
| alone (which prepends a system prompt), then strip the known system_prompt prefix. | ||
| No tools parameter — tool schema is already in the initial prompt_ids. | ||
| """ | ||
| if self._processor is not None: | ||
| raw_prompt = _apply_chat_template( | ||
| self._processor, | ||
| messages, | ||
| add_generation_prompt=True, | ||
| tokenize=False, | ||
| **self._apply_chat_template_kwargs, | ||
| ) | ||
| videos = video_data | ||
| video_metadata = None | ||
| if videos is not None: | ||
| videos, video_metadata = zip(*videos, strict=False) | ||
| videos, video_metadata = list(videos), list(video_metadata) | ||
| model_inputs = self._processor( | ||
| text=[raw_prompt], | ||
| images=image_data, | ||
| videos=videos, | ||
| video_metadata=video_metadata, | ||
| return_tensors="pt", | ||
| do_sample_frames=False, | ||
| ) | ||
| ids = normalize_token_ids(model_inputs["input_ids"]) | ||
| else: | ||
| ids = normalize_token_ids( | ||
| _apply_chat_template( | ||
| self._tokenizer, messages, add_generation_prompt=True, | ||
| **self._apply_chat_template_kwargs, | ||
| ) | ||
| ) | ||
| return ids[len(self._system_prompt):] |
There was a problem hiding this comment.
The incremental encoding logic is fragile and likely to produce malformed token sequences. Slicing tokens based on the length of a pre-encoded system prompt assumes that the tokenizer is prefix-stable and that the chat template doesn't insert turn separators or special tokens between the system prompt and the first message. Furthermore, concatenating these incremental IDs to the previous turn's response IDs (at line 542) will miss the necessary turn separators (e.g., <|im_end|> and <|im_start|>user) required by most chat templates. It is safer to re-encode the full message history and identify the delta, or simply rely on the backend's prefix caching by sending the full prompt.
| gateway_actor_kwargs["backend"] = self | ||
|
|
||
| self.owned_gateway_actors = [GatewayActor.remote(**gateway_actor_kwargs) for _ in range(gateway_count)] | ||
| ray.get([gateway.start.remote() for gateway in self.owned_gateway_actors]) |
There was a problem hiding this comment.
Using ray.get inside an async context (called via build_agent_framework) will block the event loop, preventing other concurrent tasks from making progress. Since a helper _await_ray_ref is already defined in this file, you should consider moving the gateway startup logic to an async initialization method that can be awaited, rather than performing blocking calls in the constructor.
|
为什么放在trainer目录下?我觉得这是黑盒调用训推通用的流程。我偏向往上提一级,直接放uni_agent/framework和uni_agent/gateway. |
Overwrite stale init-commit code with latest verl gateway-framework-pr-source
(4605deb4). Key changes since init:
- Inline reward scoring (remove callable abstraction)
- Gateway round-robin placement, finish_reason map, tool parse tolerance
- Zero-fill rollout_log_probs/rm_scores for trainer compatibility
- Session concurrency cap (max_concurrent_sessions)
- Delete helpers.py (inlined into framework.py)
- Entry.py now includes AgentFrameworkRolloutAdapter
Import paths rewritten: verl.agent.{framework,gateway} -> uni_agent.trainer.{framework,gateway}
External verl imports (verl.utils.*, verl.workers.*) kept as-is (accessed via submodule).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
bee0d08 to
ef46265
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| return DataProto(batch=batch, non_tensor_batch=non_tensor_batch) | ||
|
|
||
|
|
||
| class OpenAICompatibleAgentFramework(AgentFramework): |
There was a problem hiding this comment.
Move OpenAICompatibleAgentFramework into a separate file, keep abstract interface only.
There was a problem hiding this comment.
moved the abstract interface to base.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2da5be1 to
825b7f3
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
825b7f3 to
9c7c97a
Compare
2dc9a0f to
aa08c58
Compare
…fixes from PR verl-project#5 Three independent fixes carried over from PR verl-project#5 (zqz/main d700136) so this PR is self-contained: A. _normalize_message now parses tool_calls[i].function.arguments from JSON string to dict. The OpenAI spec defines arguments as a JSON string, but Qwen-style chat templates iterate via |items and require a dict; without this conversion multi-turn tool calling breaks for those templates. B. compute_position_ids returns the text-only path when multi_modal_inputs is empty, avoiding a multimodal codepath crash on pure-text samples in mixed-modality runs. C. OpenAICompatibleAgentFramework injects session_runtime as a kwarg into agent_runner so blackbox runners (mini-swe-agent) can drive session lifecycle directly. deepeyes_agent_runner already accepts **kwargs and is unaffected.
Plumb actor_rollout_ref.rollout.prompt_length and response_length from hydra config through entry.py into _GatewayActor as runtime constants. No enforcement yet; wave2 follow-up commit enforces continuation budget and clamps request max_tokens against the response budget.
…est max_tokens Mirrors ToolAgentLoop:358-359 budget semantics. When continuation incremental tokens would push the trajectory past response_length, the gateway materializes the current state as a length-stop trajectory (extra_fields.finish_reason='length') and returns an OpenAI-compatible finish_reason='length' response without invoking the backend. Also clamps request-level max_tokens to (response_length - current_response_mask_len) so request overrides cannot exceed training response budget. The framework TQ writer copies finish_reason from extra_fields into the TQ tag for downstream filtering. Length-stop trajectories keep status='success' to align with VERL native ToolAgentLoop semantics; mask/drop policy is deferred to framework layer.
All gateway error responses now use the OpenAI/vLLM-compatible top-level body shape {error: {message, type, code, param}}. A FastAPI exception handler installed in _register_routes rewrites HTTPException raises into this shape; business code uses idiomatic raise HTTPException(...) and relies on the handler for body shape.
Backend ValueError raises 400 invalid_request_error; other backend exceptions raise 500 internal_server_error. ContextWindow keyword detection (litellm whitelist alignment) is deferred to verl source PR V4.
…ntent Per-request chat_template_kwargs in payload are now merged with the actor-init self._apply_chat_template_kwargs and passed to verl.utils.chat_template.apply_chat_template. Per-request values override actor-init defaults so reasoning models can switch enable_thinking / reasoning_effort etc. on a per-call basis. _normalize_message now allow-lists reasoning_content so multi-turn reasoning history (Qwen3.5 chat_template.jinja:90-99 reads message.reasoning_content) survives gateway-level message normalization. _canonicalize_message_for_prefix_comparison automatically picks up the new field. Note: this is input-side reasoning support only. Output-side reasoning parsing (gateway _decode_response splitting reasoning_content from content) and verl finish_reason raw-preservation (V2) are deferred to follow-up work; reasoning models still need agent-side parsing of content -> reasoning_content for now.
…hoice=none Per Q5c (AReaL-equivalent boundary): stream=true falls back to non-streaming with a warning so existing OpenAI SDK clients with unconditional stream=False/True flags do not break. n!=1 / response_format / tool_choice="required" / tool_choice with a specific function return 400 invalid_request_error because the gateway cannot honor those semantics without verl source PR adding grammar / multi-sample passthrough. tool_choice="none" is now supported: tools are not injected into the chat template, and the existing tool-parser branch in _decode_response naturally skips parsing when tools is None. Defaults / unset tool_choice continue to behave as before.
Trim 6 low-signal tests, merge 2 parametrize groups: - Delete: test_gateway_actor_accepts_and_stores_rollout_budget (e2e covers) - Delete: test_normalize_request_context_preserves_multimodal_blocks (identity pass-through) - Delete: test_decode_response_preserves_unknown_stop_reasons (hypothetical backend value) - Delete: test_gateway_serving_runtime_complete_session_forwards_reward_info (duplicate) - Delete: test_canonicalize_for_prefix_comparison_includes_reasoning_content (private impl detail) - Delete: test_run_session_passes_session_runtime_to_agent_runner (private impl detail) - Merge sampling-params allowlist tests into parametrized test_gateway_actor_allowlist_filters_sampling_params - Merge trajectory-split trigger tests into parametrized test_gateway_actor_context_change_splits_trajectory 64 tests pass.
Auto-fixed by ruff: import sort (I001), quoted annotations (UP037), line length (E501), and ruff-format reformatting across changed files. mypy and compileall pass.
… config Remove hardware-specific and experiment-specific fields that don't belong in a community recipe: model path, batch sizes, fsdp offload flags, sglang tuning knobs, checkpoint config, and trainer experiment names. Keeps only recipe-relevant fields, matching the style of verl/recipe/deepeyes/configs/deepeyes_multiturn_grpo.yaml.
a5e6007 to
a7e392b
Compare
a7e392b to
9677db1
Compare
|
The current entry point binds a single runner via We may introduce an AgentRunner abstract base with a minimal run() contract: Each sample carries the runner name; config mounts a name → runner map. The config could be in the following format: Then the framework resolves the runner per-session by sample["agent_runner_name"], like: This could be similar to verl's existing |
…ison after _is_message_prefix inline
|
|
||
| @self._app.post("/sessions/{session_id}/v1/chat/completions") | ||
| async def _chat_completions(session_id: str, request: Request): | ||
| payload = await request.json() |
There was a problem hiding this comment.
We should use openai sdk for typed request/response class.
For reference, see vllm: https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/chat_completion/api_router.py#L40-L74
| finish_reason = _FINISH_REASON_MAP.get(stop_reason, stop_reason) if stop_reason else "stop" | ||
| return {"role": "assistant", "content": response_text}, finish_reason | ||
|
|
||
| async def _handle_chat_completions(self, session_id: str, payload: dict[str, Any]) -> JSONResponse: |
There was a problem hiding this comment.
Separate gateway functionality: http request -> SessionState.encode -> LLMServerClient.generate -> SessionState.decode -> http response. Gateway should only handle http related request/response.
|
Hi, I would like to propose using Prefix Trie for multi-trajectory storage for Agentgateway. My RFC is here:#51
For detailed explanation, please also refer to this comment: verl-project/verl#6299 (comment) |
What does this PR do?
This PR adds a trainer-side agent framework and gateway runtime for multi-turn agent-style rollout in uni-agent, as a downstream integration of verl RFC #5790 and the upstream agent framework PR verl#6299.
Specifically, it:
uni_agent.trainer.framework—AgentFrameworkabstract base,OpenAICompatibleAgentFrameworkconcrete implementation, andAgentFrameworkRolloutAdapter(satisfies the trainer'sagent_loop_manager_classextension point; recipes wire it in via YAML with no per-recipe glue),uni_agent.trainer.gateway—_GatewayActor/GatewayManager/GatewayServingRuntimefor OpenAI-compatible session serving, sticky session routing, tool-parser wiring, and multimodal media accumulation; backend routing delegates toLLMServerClient,examples/deepeyes/,Wave 2 additions (length budget enforcement + OpenAI parity):
prompt_length/response_lengthbudget injected into_GatewayActor; continuation turns clamped to remaining budget; budget-exhausted turns materialise a syntheticfinish_reason=lengthresponse without hitting the backend.{"error": {"message": …, "type": …, "code": …}}); encode/decode failures caught and surfaced as 400.chat_template_kwargsforwarded toapply_chat_template;reasoning_contentpreserved through_normalize_messageand prefix comparison.n>1,response_format,tool_choice=required/function) rejected with 400;tool_choice="none"supported (skips tool injection and parser).3c5f6e04(verl PR #6129: moveLLMServerManagerout ofAgentLoopManager) so reviewers cangit submodule update --initwithout access to a private fork.Checklist Before Starting
https://github.com/verl-project/uni-agent/pulls?q=is%3Apr+gateway+framework
[{modules}] {type}: {description}[trainer] feat: add agent framework and gateway runtimeTest
PYTHONPATH=$(pwd) pytest tests/uni_agent/trainer/ -qResult: 64 passed, 6 warnings (framework, gateway, runtime, multimodal postprocess).
Real-rollout evidence from the deepeyes gateway recipe: a 50-step GRPO run on multi-turn multimodal data (Qwen3.5-4B, 7× RTX 3090 train + 1× local judge) produced a real learning curve —
critic/rewards/meanmoved from ~0.21 at step 1 to ~1.86 by step 50.API and Usage Example
Public APIs added:
uni_agent.trainer.framework—AgentFramework,OpenAICompatibleAgentFramework,AgentFrameworkRolloutAdapter,build_agent_frameworkuni_agent.trainer.gateway—GatewayServingRuntime,GatewayManager,GatewayActorMinimum viable wiring via YAML config:
The adapter calls
build_agent_framework()which wiresGatewayServingRuntimeand the framework subclass from config. The agent runner only needs the gateway base URL:generate_sequences()writes finalized trajectories directly to TransferQueue with key"{uid}_{session_id}_{index}", matchingAgentLoopWorkerTQ._agent_loop_postprocess()'s field / tag layout.Design & Code Changes
High-level changes:
AgentFrameworkbase class +OpenAICompatibleAgentFrameworkown session orchestration (create_session→agent_runner→finalize_session), trajectory assembly, multimodal post-processing, reward scoring, and TransferQueue writes. Per-session failures are isolated viaasyncio.gather(..., return_exceptions=True)so one bad session does not cancel the rest of the batch._GatewayActorprovides OpenAI Chat Completions over sticky sessions with prefix-consistency checks, tool-parser decoding, multimodal media accumulation, and rollout budget enforcement.GatewayManagerroutes new sessions by least-active count.GatewayServingRuntimeowns gateway actor lifecycle and delegates backend routing toLLMServerClient.multi_modal_inputsand(4, seq_len)position ids inside the framework, so VLM sessions do not need per-recipe glue.AgentFrameworkRolloutAdaptersatisfies the trainer'sagent_loop_manager_classcontract; every recipe wires the same class in YAML — no per-recipe adapter code.WIP / Follow-up
GatewayActordefault placement strategy (at least one per node) once multi-node validation is inChecklist Before Submitting
*_on_cpu.pynaming convention.pre-commit install && pre-commit run --all-files