【免费下载链接】agent-runtime
openJiuwen Agent Runtime 是 openJiuwen 体系中的智能体运行与部署管理平台,致力于将 Agent 从“开发态”稳定带到“生产态”,让发布、运行、管理更加标准与可控。
本文以 session 软件设计说明 为骨架,结合 openjiuwen_runtime.management.session 的真实源码与系统测试,完整解析 openJiuwen Agent Runtime 如何在「入口请求」与「多实例、多 Session、可伸缩的后端工作负载(K8s Pod)」之间搭建编排层。读完本文,你将掌握双队列系统优先调度、两级并发控制、Session 亲和路由、min_idle 预热与 service_ttl 缩容、WebSocket 多路复用下行、以及主备切换时的 Pod 批量清理等整套生产级设计,并能对照源码定位每一步实现。
1. 定位与目标:编排层要解决的五个问题
Session 包位于「入口请求 → 多实例、多 Session、可伸缩的后端工作负载(如 K8s Pod)」之间,核心提供一层编排能力,设计文档归纳为五个要点:
| 能力 | 说明 |
|---|---|
| 双队列调度 | 用户业务请求与系统内部事件分队列,系统侧优先,保证缩容等运维动作不被用户流量饿死 |
| 单实例 = 部署单元 + 下行通道 | 一个服务实例对应一个部署单元(如一个 Pod)+ 一条下行通道,典型形态是 WebSocket 上按 request_id 多路复用 |
| 两级并发控制 | 服务级(实例内总并行度)与 Session 内(同一会话内并行度)相互独立,通过信号量 acquire 排队配合 |
| Session 亲和 | 同一 session_id 路由到同一 service_id,在 TTL 内可滑动续期;可选策略(如 chat_id + bot_id 生成稳定 session 键) |
| 弹性伸缩 | min_idle 预热、max_services 上限、空闲 service_ttl 后缩容;部署抽象支持 K8s / Docker / 无部署调试用 NoOp |
这些能力全部落在 management/openjiuwen_runtime/management/session 这一个包内,对外通过 __init__.py 导出 Access、ServiceManager、ServiceHandler、WSServiceMessageChannel、PerChatBotStrategy、AccessConfig、SessionConfig 等公共符号。
2. 整体架构:五层职责与消息流向
设计文档给出如下架构图(mermaid):
各层职责可以浓缩为一张表:
| 层 | 职责 |
|---|---|
| Access | 将 IRequest 经策略变成 ISessionRequest,封 SessionRequestWrapper,只负责入队与消费 response_queue 的异步迭代 |
| ServiceManager | 双队列、路由、多实例池(in_use / idle)、bootstrap min_idle、autoscale、session TTL 与 service idle 回收 |
| ServiceHandler | 单实例:服务级信号量、SessionRouter(子 SessionHandler)、deploy / delete、经 invoke_channel 调通道 send |
| SessionHandler | 同 session_id 的会话内信号量、调用父级 invoke_channel |
| IServiceMessageChannel | 上行业务、下行多路分片、完成时 on_request_complete 归还服务级并发 |
| IDeployController | 创建/删除后端资源,返回 PodDeployInfo 等,供通道 on_pod_ready 建链 |
注意一个关键设计:Access 只入队、不路由。handle_message 是异步队列写入,真正路由发生在 _message_loop 独立 task 中,避免单条长请求阻塞下一条入队(多 session 可并行进入不同实例)。这一"入队与消费解耦"的原则在 access.py 与 service_manager.py 中均有体现:send_message 末尾 await self._service_manager.handle_message(wrapper) 立即返回,而 _message_loop 为每条用户消息 asyncio.create_task(self._handle_user_request(item))。
3. 核心类型与「接口 I/O」
3.1 请求链上的三种类型
从业务入口到编排层,请求经过三层包装:
| 类型 | 说明 | 主要输入 / 输出 |
|---|---|---|
IRequest | 业务入口行协议(request_id / chat_id / bot_id / user_id / session_id),定义在 interfaces.py | 入:由调用方提供实现;Access 在缺省 request_id 时包一层 _AutoIdRequest 并写回 wire_dict(若存在) |
ISessionRequest | 策略产出的会话化请求,含 session_id、session_concurrency、session_ttl、raw(原 IRequest),定义见 interfaces.py | 出:ISessionStrategy.handle_session(IRequest) -> ISessionRequest;标准实现是 session_request.py 中的 SessionRequest |
SessionRequestWrapper | 一次用户调用对应一个 wrapper:session_request + response_queue + cancel Future,定义见 interfaces.py | 入:Access 创建;ServiceManager 入队;通道下行 put 到 response_queue;Access 以 is_completed 判终态 |
其中 _AutoIdRequest 是个重要细节(access.py):当用户 IRequest.request_id 为空时,Access 自动生成 UUID 并覆盖 request_id(同时若 wire_dict 存在则 inplace 注入),因为 request_id 非空是 WSS 多路复用硬条件,下游通道会拒收空 request_id。真实代码路径见 send_message 中 rid = uuid.uuid4().hex 的分支。
3.2 控制面接口一览
设计文档摘录了六组核心接口,其完整抽象定义在 interfaces.py:
| 接口 | 方法 | 含义 |
|---|---|---|
IAccess | init(...) / send_message(IRequest) -> AsyncIterator / shutdown() / update_config() / cleanup_all_pods(namespace, ...) | 初始化并 start 服务管理;流式产出来自下游的解析后结果;运行时热更新配置;主备切换时按 K8s label 清理 Pod |
IServiceManager | init / start / stop / handle_message / enqueue_system / mark_deprecated / try_cleanup_if_idle | 对 wrapper 入用户队列;系统事件(如缩容)入系统队列;热更新后支持旧实例老化回收 |
IServiceHandler | handle_message / deploy / delete / remove_session / 并发与 session 只读属性 | 单实例生命周期与消息处理 |
IServiceMessageChannel | send(service_id, wrapper, *, response_parser, on_request_complete) | 上行一帧 + 下行在独立接收循环中 dispatch + 完成时 await on_request_complete(rid) |
IResponseParser | request_id / is_completed / response 作用于 dict 分片 | 多路流式与终态判断 |
IDeployController | deploy() -> info / delete() / resource_id | 与具体运行时解耦,协议见 runtime.py |
值得注意的实现约定:IServiceMessageChannel 是 @runtime_checkable 的 Protocol,设计文档与源码注释(interfaces.py)均强调:用结构子类型实现该 Protocol,不要让具体子类继承 Protocol,以免与类型检查器对 Protocol 子类化的限制冲突。WSServiceMessageChannel 正是这一约定的落地范例。
回调约定:on_request_complete(Optional[str]) 为可 await 的异步回调(别名 OnRequestCompleteCallback),必须在单条用户请求在通道侧视为结束后调用,以释放 ServiceHandler 的服务级信号量。在 service_handler.py 中,invoke_channel 构造 _complete 闭包:先 self._inflight += 1 登记在途,回调时减一并在 inflight == 0 时通过 _idle_pool_hook 通知 Manager 检查到期 session 与 service TTL。
3.3 数据模型:AccessConfig 与 SessionConfig
两个关键配置类定义在 models.py,设计文档列举的字段与实际默认值如下:
AccessConfig(入口与 ServiceManager 共享运行配置)
| 字段 | 默认值 | 语义 |
|---|---|---|
user_queue_size / system_queue_size | 1000 / 100 | 双队列容量 |
image | "app:latest" | 业务镜像 |
service_concurrency | 200 | 单实例总并发(服务级信号量上限) |
min_idle_services | 1 | 池中至少保持的空闲实例数;仅由 autoscale 补位,不会因 service_ttl 从 idle 删 Pod;设为 0 表示不维护热备 |
max_services | 10 | 实例池上限,达到后新 session 将 _fail(100001) |
target_port / invoke_path / ws_use_tls | 8000 / "" / False | 与 pod_ip 拼 WS URI;空路径仅 ws://host:port;True 为 wss:// |
service_ttl | 300 | ① in_use 实例在所有 session 归还 + 无 inflight 后等待本秒数再转入 _idle;② 当 len(idle) > min_idle_services 时多余 idle 立即被回收(从 in_use 转入的由于已等过一次不再叠二次等待)。底数 min 台不受影响;0 表示①阶段无等待;负值未定义勿用 |
message_timeout / max_retries | 600 / 3 | 响应等待超时与重试次数 |
autoscale_interval | 0.2 | autoscale 巡检周期(秒) |
db_handler | None | 预留存储入口,当前核心路径可不落库 |
SessionConfig(单 session 维度,随策略写入 ISessionRequest)
concurrency:默认1,同一 session 内最大并行处理中的请求数(会话级限流);ttl:默认300秒,session 亲和保活窗口:请求结束起计时,期间收到该 session 的新消息会取消计时;超时则移除 session handler、归还其在 service_handler 上占用的并发位。设为0表示请求一结束、且该 session 无 inflight 时立刻释放,不保留亲和。
3.4 内部事件与错误码
- 内部事件:
ServiceReclaimEvent(service_id: str)(internal_events.py)由Timer与service_idle_ttl触发,经 系统队列 优先消费,执行缩容delete。ServiceManager中_message_loop对isinstance(item, ServiceReclaimEvent)分支直接调用_on_service_reclaim(service_manager.py)。 - 错误码:
ERROR_CODE_MAPPING(exception.py)与ServiceManager._fail配合,向response_queue写入error_code+message+completed的 legacy chunk 形状。常见:100001(服务并发度超过上限,资源满)、100002(服务启动失败/路由处理异常)、-1(内部错误)。
_fail 的实现细节(service_manager.py)非常讲究:错误响应必须符合上游 wire_codec.parse_agent_server_wire_chunk 的 legacy chunk 形状(request_id + channel_id + is_complete + payload,且无 ok 字段),否则上游会抛 ValueError: unrecognized wire shape,前端收不到错误信息。
4. 模块与文件功能一览
| 文件 | 功能 |
|---|---|
| access.py | Access:策略生成 ISessionRequest、自动补 request_id(多路复用必须)、handle_message 入队、从 response_queue 流式 yield;init 时拉 ServiceManager.start();cleanup_all_pods 直接调 K8sServiceHandler 按 label 清理 Pod(主备切换用) |
| service_manager.py | 双队列消费循环、用户消息独立 task 路由、_pick_or_create 亲和/选实例/新 deploy、_bootstrap_min_idle、autoscale、session 与 service 空闲计时器、_fail 写错误、get_stats 统计、Pod 监控/Watch |
| service_handler.py | ServiceHandler:deploy→on_pod_ready、invoke_channel→channel.send、dispatch_inbound_chunk 按 request_id 写回、delete 先关通道再删资源 |
| session_handler.py | SessionHandler:会话内 BoundedSemaphore + invoke_channel |
| dual_queue.py | 系统优先的 get():先 drain 系统队列,再与阻塞用户队列用 asyncio.wait(FIRST_COMPLETED) |
| router.py | ServiceRouter:session_id -> service_id;SessionRouter:request_id -> session_id(在 ServiceHandler 上用于下行匹配) |
| ws_client_channel.py | WSServiceMessageChannel:serialize_request_payload / wire_dict 上行业务、_ensure_connected、on_pod_ready 中预建链、send 内等待 is_completed 与 cancel、close |
| k8s_service_handler.py | K8sServiceHandler 创建/等待 Pod Ready、cleanup_all_agentserver_pods 按 label 批量删 Pod;K8sDeployController 适配 IDeployController、PodDeployInfo |
| docker_service_handler.py | Docker 侧部署信息与实现 |
| runtime.py | IDeployController 协议、NoOpDeployController 不调真实部署(调试用) |
| strategies/_base.py + per_chat_bot.py | BaseSessionStrategy 提供 configure 与 handle_session 骨架;PerChatBotStrategy 以 f"{chat_id}::{bot_id}" 为 session 键 |
| session_request.py | SessionRequest 实现 ISessionRequest(优先级为 MessagePriority.MEDIUM) |
| timer.py | 抽象 ITimer 的 asyncio 调度实现,供 ServiceManager arm session/service 空闲计时 |
| interfaces.py | 各类 Protocol/ABC/别名如 OnRequestCompleteCallback、IServiceMessageChannel |
| init.py | 对外的公开导出 |
5. 全链路业务流程
5.1 启动(含 min_idle 预热)
启动序列与文档一致,源码对应关系如下:
- 构造
ServiceManager(注入IServiceInstanceFactory、PriorityDualAsyncQueues、Timer等),见 service_manager.py; Access.init→ServiceManager.init(response_parser)→ServiceManager.start(),见 access.py;start()内部(service_manager.py):- 启动
_message_loop(从双队列取项)、_autoscale_loop; - 若
_pod_monitor_enabled and deploy_mode == "k8s",额外启动_pod_monitor_loop与_pod_watch_loop; await _bootstrap_min_idle():在lock内循环min_idle与max_services,对每个缺口调用_new_deployed()。
- 启动
_new_deployed()(service_manager.py):factory.new_service(response_parser)得到ServiceHandler,await h.deploy()(K8s 等创建 Pod/容器并等 Ready;WSS在on_pod_ready里预建 WebSocket),成功后放入idle池。
_bootstrap_min_idle 还支持按模板分组预热(service_templates 列表):每个 template_id 可独立配置 min_idle_services、max_services、service_concurrency 等,池结构按 template_id 分组为 {template_id: {service_id: handler}}(None 为默认组)。预拉热入 idle 的实例不启动「in_use→idle / 删 Pod」的 service_ttl 计时——它们只由 autoscale 维护数量。
5.2 单次用户请求
设计文档给出的时序图(mermaid):
对照源码的关键点:
- 入队与消费解耦:
handle_message只是把RawMessage(MessageType.USER_REQUEST, msg, priority=sreq.priority)写入用户队列(service_manager.py);真正的路由在_handle_user_request(service_manager.py)中执行:先await self._timer.cancel_timer(f"sess:{session_id}")取消待生效的 session TTL(满足"TTL 内有新消息则删除计时器"规则),再在lock内完成亲和查找/额度预留/路由绑定。 - 下行写回:
WSServiceMessageChannel._recv_loop收到 JSON 后调用h.dispatch_inbound_chunk(data, p)(ws_client_channel.py),ServiceHandler.dispatch_inbound_chunk按request_id找到_by_request中等待的 wrapper,await w.response_queue.put(data)(service_handler.py)。 - 超时兜底:
Access消费循环用asyncio.wait_for(response_queue.get(), timeout=message_timeout),超时结束迭代并视为挂起(access.py)。
5.3 亲和路由的四步决策(_pick_or_create)
_pick_or_create(service_manager.py)按优先级决策:
- 亲和命中:
ServiceRouter已有session_id -> service_id且实例仍在池 → 复用;若在idle则唤醒到in_use。 - 容量选择:否则在相同
template_id组的in_use/idle中找available_concurrency >= need的实例(need = max(1, int(sreq.session_concurrency)))。 - 新部署:否则若该组未达
max_services,await _new_deployed()并入in_use。 - 拒绝:达上限仍无位 →
_fail(100001)。
另外有两处防御性设计值得注意:
- 预检拦截:若
need > service_concurrency(单 session 声明的并发超过单实例总并发),直接拒绝扩容,避免"新 Pod 一创建即成孤儿"的无效扩容; - 孤儿实例兜底:
try_reserve_session_quota预留失败时,锁外推动该实例走in_use→idle回收流程(_arm_in_use_to_idle_pool),防止_pick_or_create刚放入_in_use的实例因无 session/inflight 而永不触发 idle 钩子,导致 Pod 永久驻留。
5.4 两级并发控制
- 服务级:
ServiceHandler维护_session_reserved: Dict[str, int](session → 预留额度),available_concurrency = total - sum(reserved)(service_handler.py)。新 session 首次占用时通过try_reserve_session_quota预留session_concurrency额度;同 session 的后续消息不再增加预留,仅受 SessionHandler 内并发限制。单条请求 = 1 点消息级并发(_inflight计数,用于 TTL/idle 钩子,与服务额度无关)。 - Session 级:
SessionHandler持有一个asyncio.BoundedSemaphore(max_parallel)(session_handler.py),由SessionConfig→策略→ISessionRequest.session_concurrency提供上限。handle_message先await self._sem.acquire(),finally中释放,并维护_active_rids与request_id -> session_id映射(SessionRouter)。
两级信号量配合方式:服务级额度在路由时预留(持锁、幂等),消息级在 invoke_channel 中计数(service_handler.py);通道完成回调 on_request_complete 释放 _inflight,与"服务额度占用"解耦。
5.5 Session TTL 与实例空闲回收
- 若
session_ttl > 0:成功处理完一条用户请求后_arm_session_timer对sess:{session_id}arm(service_manager.py);到期_on_session_expired移除 session 并清ServiceRouter映射,若该 session 仍有 inflight 则进入_pending_expired_sessions延期清理队列,由_complete钩子触发_flush_pending_expired_for_service兜底(service_manager.py)。 - 若无其它 session 且实例在
in_use,_arm_in_use_to_idle_pool对to_idle:svc:{service_id}armservice_ttl(service_manager.py)。 service_idle_ttl到期 →ServiceReclaimEvent入系统队列,_on_service_reclaim确认无活跃 session / inflight 且 idle 数量超过 min 后h.delete()(先关 WSS 再删 Pod 等),见 service_manager.py。
5.6 部署与 WebSocket 预建链
ServiceHandler.deploy()(service_handler.py):pod_info = await deploy_controller.deploy(),若非空且通道有on_pod_ready,await on_pod_ready(service_id, pod_info)。WSServiceMessageChannel.on_pod_ready(ws_client_channel.py):用PodDeployInfo.pod_ip(或 Docker 的host)与部署声明的port拼ws:///wss://URL——端口选择优先级为pod_info.containers[target_container].port→pod_info.port→ 构造时target_port;随后await _ensure_connected()完成与业务进程握手,避免「K8s Ready 但首包发不出去」的纯懒连问题。- 上行体:
serialize_request_payload(ws_client_channel.py)对IRequest/wire_dict序列化:优先wire_dict,其次 dataclassasdict、model_dump(),最后退回字段组装;request_id非空是 WSS 多路复用硬条件,Access缺失时自动生成并补wire_dict。 - 链路鉴权扩展:通道支持
additional_headers(静态 dict 或无参回调,用于令牌逐次刷新)与verify_peer(消费首帧connection.ack反向核验对端,失败即断开)。
5.7 缩容与优雅停止
- 空闲回收见 5.5;
ServiceManager.stop()(service_manager.py)标记队列关闭、取消_message_loop/_autoscale_loop/ Pod 监控任务与用户路由子 task、timer.stop_all(),遍历in_use/idle/deploying全部实例delete,最后清空ServiceRouter亲和表;只有全部成功才标记_stop_completed = True,否则保持可重试。 Access.shutdown()幂等封装(access.py),重复调用直接忽略。
6. 主备切换与 Pod 批量清理
Gateway 部署为 K8s 主备模式(LeaderElection),主备切换时需清理旧 PRIMARY 遗留的 AgentServer Pod。设计文档给出的调用链:
app_gateway._session_on_role_change(PRIMARY)
→ RuntimeManagementAgentClient.cleanup_all_pods()
→ Access.cleanup_all_pods(namespace, kubeconfig, label_selector)
→ K8sServiceHandler.cleanup_all_agentserver_pods()
标签设计:
| Pod 类型 | 标签 | 说明 |
|---|---|---|
| AgentServer | jiuwenclaw-component=agentserver | 由 RuntimeManagementAgentClient 通过 extra_labels 传入 K8sServiceHandler,在 Pod 创建时打上 |
| Gateway | jiuwenclaw-component=gateway | 在 K8s YAML 中配置,不在代码中管理 |
对应常量见 k8s_service_handler.py:POD_LABEL_KEY = "jiuwenclaw-component"、POD_LABEL_VALUE = "agentserver"、POD_LABEL_SELECTOR = "jiuwenclaw-component=agentserver"。
流程:
- STANDBY 升为 PRIMARY 时,LeaderElection 回调触发
cleanup_all_pods; cleanup_all_pods(access.py)通过 K8s label selector 批量删除所有jiuwenclaw-component=agentserverPod(cleanup_all_agentserver_pods,k8s_service_handler.py,不依赖内存池状态——STANDBY 的池为空);- 清理完成后,
ServiceManager的 autoscale 机制自动重建min_idle数量的新 Pod。
为什么不在 ServiceManager 内实现:cleanup_all_pods 不操作 ServiceManager 的内存状态(STANDBY 的 in_use/idle 池为空),只做 K8s API 调用,因此放在 Access 层直接调 K8sServiceHandler 的静态方法。
此外,K8s 部署还内置了 Pod 自愈监控:ServiceManager 在 k8s 模式下启动 _pod_monitor_loop(轮询)与 _pod_watch_loop(长连接 Watch,timeout_seconds=300),检测 FAILED_POD_STATUSES(Terminating、Failed、CrashLoopBackOff、ImagePullBackOff 等)或从 K8s list 消失的 Pod,通过 _cleanup_dead_pods 同步清理服务池、session 路由与计时器(service_manager.py)。
7. 设计取舍与扩展点
| 点 | 说明 |
|---|---|
| 策略 | 新增 ISessionStrategy 可换「按 user 维 session」「仅 request_id 维」等。现有 PerChatBotStrategy 以 f"{chat_id}::{bot_id}" 为 session 键(per_chat_bot.py),BaseSessionStrategy 只需子类实现 _build_key |
| 通道 | 实现 IServiceMessageChannel:除 send 外可实现 bind_handler / on_pod_ready / close 以配合 ServiceHandler。MockMessageChannel(测试用)与 WSServiceMessageChannel(生产用)是两种参考实现 |
| 部署 | 实现 IDeployController 即可接 VM、Nomad 等。现有 NoOpDeployController(调试)、K8sDeployController(生产)、docker_service_handler.py(Docker) |
| 存储 | AccessConfig.db_handler 预留,当前核心路径可不落库 |
| 热更新 | Access.update_config()(access.py)在锁外并行创建并启动新 ServiceManager,持锁原子切换引用,再异步 mark_deprecated() + try_cleanup_if_idle() 优雅回收旧实例;无在途任务即可清理,否则 _periodic_cleanup_deprecated_sm 每 30 秒重试(最多 20 次)。旧实例标记 deprecated 后自然回收,新请求路由到新一代实例 |
8. 与测试 / 可执行样例的对应关系
- 系统级 Mock:tests/system_tests/management_session/test_session_sdk.py 用
NoOpDeployController+MockMessageChannel覆盖了:session 亲和复用(test_session_affinity_same_service,断言同一 chat+bot 两条消息打到同一 service_id)、min_idle预热建实例、无可用实例返回100001、不同 session 在容量足够时共享单实例、deploy/delete生命周期等。 - 真 K8s + WSS:tests/system_tests/management_session/main_k8s_access.py 用
K8sServiceHandler+WSServiceMessageChannel跑通全链路,业务 JSON 可经WireIRequest等IRequest实现上送;其E2aEnvelopResponseParser展示了 jiuwenclaw 网关归一化下行帧的解析方式(request_id优先取request_id,否则response_id/id;终态判定is_final/provenance.details.is_complete/ 历史兼容字段)。可执行命令示例:
# 在 management 包根目录、已 uv sync 安装依赖后
uv run python tests/system_tests/management_session/main_k8s_access.py \
--image your-registry/svc:tag \
--namespace default \
--name-prefix jiuwenclaw \
--container-port 18092 \
--ws-tls 0 \
--min-idle 1 --max-services 2 --service-concurrency 10 \
--message-json '{"text":"hi","request_id":"r1","chat_id":"c1","bot_id":"b1"}'
- 多 session 并发样例:tests/system_tests/management_session/main_k8s_multi_session.py 与配套 conftest.py 提供了多会话并行验证入口。
文档版本与代码包同步于
management/openjiuwen_runtime/management/session/;若行为与实现不一致,以源码为准。设计文档全文见 DESIGN.md。
【免费下载链接】agent-runtime
openJiuwen Agent Runtime 是 openJiuwen 体系中的智能体运行与部署管理平台,致力于将 Agent 从“开发态”稳定带到“生产态”,让发布、运行、管理更加标准与可控。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



