From 9ca55d7d78c106481ffee1b19e110aa4dddbf5e3 Mon Sep 17 00:00:00 2001 From: tzhouam Date: Thu, 1 Oct 2026 16:31:20 +0800 Subject: [PATCH] docs(knowledge): enrich JiuwenSwarm behavior from pinned source --- .../components/agent-runtime/_index.md | 29 +++++- .../jiuwenswarm-runtime-control.md | 49 ++++++++++ .../jiuwenswarm-runtime-execution.md | 38 ++++++++ .../jiuwenswarm-runtime-session.md | 89 ++++++++++++++++--- .../components/common-core/_index.md | 12 ++- .../jiuwenswarm-common-config-panel.md | 21 ++++- .../jiuwenswarm-common-model-catalog.md | 42 +++++++++ .../common-core/jiuwenswarm-common.md | 43 ++++++++- .../components/cron-scheduling/_index.md | 21 ++++- .../cron-scheduling/jiuwenswarm-cron-store.md | 48 ++++++++++ .../jiuwenswarm-runtime-cron.md | 55 +++++++++++- .../components/extensions-plugins/_index.md | 12 ++- .../jiuwenswarm-application-plugins.md | 36 ++++++++ .../jiuwenswarm-extensions.md | 54 ++++++++++- .../components/gateway-channels/_index.md | 12 ++- .../jiuwenswarm-channels-cli.md | 53 ++++++++++- .../jiuwenswarm-channels-process-cli.md | 45 +++++++++- .../jiuwenswarm-process-cli-machine.md | 37 ++++++++ .../jiuwenswarm/components/launch/_index.md | 8 +- .../launch/jiuwenswarm-instance-manager.md | 40 ++++++++- .../components/launch/jiuwenswarm.md | 39 +++++++- .../components/login-auth/_index.md | 13 ++- .../login-auth/jiuwenswarm-common-auth.md | 46 +++++++++- .../login-auth/jiuwenswarm-login-models.md | 45 ++++++++++ .../components/observability/_index.md | 8 +- .../jiuwenswarm-observability.md | 58 +++++++++++- .../jiuwenswarm-trajectory-retention.md | 44 +++++++++ .../symphony-orchestration/_index.md | 31 ++++++- .../jiuwenswarm-symphony-evolution.md | 53 ++++++++++- .../jiuwenswarm-symphony-service.md | 52 +++++++++++ .../jiuwenswarm-symphony.md | 65 +++++++++++++- 31 files changed, 1130 insertions(+), 68 deletions(-) create mode 100644 knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-control.md create mode 100644 knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-execution.md create mode 100644 knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common-model-catalog.md create mode 100644 knowledge/repos/jiuwenswarm/components/cron-scheduling/jiuwenswarm-cron-store.md create mode 100644 knowledge/repos/jiuwenswarm/components/extensions-plugins/jiuwenswarm-application-plugins.md create mode 100644 knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-process-cli-machine.md create mode 100644 knowledge/repos/jiuwenswarm/components/login-auth/jiuwenswarm-login-models.md create mode 100644 knowledge/repos/jiuwenswarm/components/observability/jiuwenswarm-trajectory-retention.md create mode 100644 knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony-service.md diff --git a/knowledge/repos/jiuwenswarm/components/agent-runtime/_index.md b/knowledge/repos/jiuwenswarm/components/agent-runtime/_index.md index f9b89d66..7564b936 100644 --- a/knowledge/repos/jiuwenswarm/components/agent-runtime/_index.md +++ b/knowledge/repos/jiuwenswarm/components/agent-runtime/_index.md @@ -1,7 +1,7 @@ --- title: "Agent 运行时与会话" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: index tags: [jiuwenswarm] sources: [] @@ -9,14 +9,35 @@ sources: [] # Agent 运行时与会话 -- [共享 Agent Runtime(jiuwenswarm/runtime)](jiuwenswarm-runtime.md) -- [Runtime Session 协调器(jiuwenswarm/runtime/session/)](jiuwenswarm-runtime-session.md) +## 什么时候查这里 + +请求规范化、会话 provision,以及 generation、lane 排队、交互输入、取消与关闭协调。 + +## 不放什么 + +持久会话元数据由服务端 session 模块拥有;cron 产生定时工作、通道处理传输,分别查对应 component。 + +## 目录内容 + +- [共享 Agent Runtime(jiuwenswarm/runtime)](jiuwenswarm-runtime.md) — 传输层无关的 Runtime:请求规范化、会话 create/switch/fork/delete、Plan 模式、模型/权限/MCP 目录 +- [Runtime Session 协调器(jiuwenswarm/runtime/session/)](jiuwenswarm-runtime-session.md) — 会话执行协调:准入与 generation、lane 排队、unary/流式执行、control 投递与心跳链、取消与关闭 ## 代码快速入口(Direct) -触发词:runtime、AgentRuntime、session create/switch/fork、session.delete、plan mode、model catalog、permission snapshot、MCP reference、push handler、process CLI、agent definition、运行时。PR 描述或改动命中下表一行时,先读该行的规则,再读页面其余部分。 +触发词:runtime、AgentRuntime、session create/switch/fork、session.delete、plan mode、model catalog、permission snapshot、MCP reference、push handler、process CLI、agent definition、运行时、session lane、execution registry、work scheduler、deliver_control、cancel_execution、close_session、QUIESCING、control input、心跳链。PR 描述或改动命中下表一行时,先读该行的规则,再读页面其余部分。 | PR 描述在做什么 | 精确规则 | 第一批 live 源码 | |---|---|---| | runtime、AgentRuntime、session create/switch/fork、session.delete、plan mode、model… | 入口 | `jiuwenswarm/runtime/agent_definition.py`、`jiuwenswarm/runtime/context.py`、`jiuwenswarm/runtime/events.py` | +| session 执行协调、lane 排队、deliver_control、cancel_execution、close_session | Runtime Session 协调器(见目录内容) | `jiuwenswarm/runtime/session/coordinator.py`、`jiuwenswarm/runtime/session/execution_registry.py`、`jiuwenswarm/runtime/session/work_scheduler.py` | + +## 验证入口 + +`tests/unit_tests/runtime/test_runtime_session_coordinator.py` 覆盖进程内排序、控制输入、取消与关闭;Runtime 上层契约见共享 Runtime 页。 + +## 专题入口 + +- [Runtime 交互输入、执行取代与取消](jiuwenswarm-runtime-control.md) — 协调等待交互的执行、补充输入、工作取代和子执行取消;会话注册、lane 调度与关闭由 Runtime Session 主页面说明。 + +- [Runtime unary 与流式执行](jiuwenswarm-runtime-execution.md) — 提交去重、超时与取消、终态 owner 和流消费者提前退出。 diff --git a/knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-control.md b/knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-control.md new file mode 100644 index 00000000..0a25f34e --- /dev/null +++ b/knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-control.md @@ -0,0 +1,49 @@ +--- +title: "Runtime 交互输入、执行取代与取消" +created: 2026-10-01 +updated: 2026-10-01 +type: architecture +tags: [jiuwenswarm] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/session/coordinator.py +--- + +# Runtime 交互输入、执行取代与取消 + +## 职责和边界 + +协调等待交互的执行、补充输入、工作取代和子执行取消;会话注册、lane 调度与关闭由 Runtime Session 主页面说明。 + +### control 投递与心跳链 + +- `deliver_control` 的父执行 = 当前 generation 里活跃、`waiting_control_id == request_id` 且状态 RUNNING/WAITING_FOR_CONTROL 的最新者(按 started_at/created_at 取 max);找不到抛 "session has no active execution"([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L881-L901))。 +- claim 是四元组 (session_id, generation, parent_execution_id, request_id),重复投递抛 "control input is already being delivered"。claim 先加、子执行创建仍可能失败:失败时 finally 释放 claim,父执行保持可重试——泄漏的 claim 或被恢复却无人接手的父都会把问题永久卡死(源码注释;测试 `test_failed_control_setup_releases_claim_and_parent`)([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L293-L316))。 +- 投递完成时若父已终态/记录被替换/会话 QUIESCING 或 CLOSED:子记 CANCELLED 并抛 `SessionExecutionEndedError`("control input arrived after its execution ended: ...")([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L338-L352);测试 `test_control_result_after_parent_ended_raises_typed_error`)。 +- control 子执行失败/被取消:子记 CANCELLED/FAILED;父若原本在等且未终态,重新 mark_awaiting_control + mark_waiting,答案可以重试;取消 control 子执行不会取消父工作([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L320-L336);测试 `test_control_input_cancellation_does_not_cancel_parent_work`)。 +- 链式追问:control 操作自己的 `suspension_key` 又给出 control_id 时,若有 HEARTBEAT 祖先(`_heartbeat_root` 沿 parent 链、限同 session+generation 查找),则把心跳根重新挂起(waiting)、control 子记 SUCCEEDED;无心跳根时挂起 control 子自身。普通父等到答案后 SUCCEEDED;若父已发布更新的 waiting_control_id 则不覆盖([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L354-L377)、[L934-L953](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L934-L953);测试 `test_control_input_resumes_work_after_output_stream_ends`)。 +- `deliver_control_stream` 是流式版:观察在操作结束前就可见(包括第二个问题),claim 用 per-record 的 `stream_control_claims`(按 request_id),重复抛 "interaction answer is already in progress";父只认已存在的匹配交互,绝不认一次新对话轮。消费者拥有该 generator,退出时必须 close([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L404-L495))。 +- `record_interaction` 供被拥有的回调执行中登记交互:仅当会话 ACTIVE 且找到 RUNNING 的匹配 request_id 才写 waiting_control_id 并刷新 gate,返回 bool([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L265-L282))。`has_control_target` / `control_target_work_kind` 供宿主预判 control 能否送达及其根工作类型。 + +### 补充输入(stream_session_input) + +- 语义:输入不恢复、不完成、也不替换等待中的交互。会话里任一活跃执行在等答案 → `SessionInputRejectedError`("session is waiting for an interaction answer; supplemental input was not sent");父执行正在取消 → 同样拒绝;`expected_execution_id` 与当前父不符 → `SessionInputTargetError`([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L497-L546);两个异常定义在 `jiuwenswarm/runtime/session_input.py`)。 +- 有父(RUNNING 且非 SESSION_INPUT/GOAL_CONTROL/GOAL_ATTACH)时以 SESSION_INPUT 挂到父执行并传 `operation(record.channel_id)`;无父时作为新 CHAT_STREAM 走 `idle_operation` 兜底。 + +### 新工作取代等待中的执行 + +- 新 CHAT_UNARY/CHAT_STREAM 会把同 generation 中 WAITING_FOR_CONTROL 的 CHAT_UNARY/CHAT_STREAM/HEARTBEAT 记 `cancellation_requested=True` 并置 CANCELLED;新 GOAL_STREAM 同理取代 WAITING_FOR_CONTROL 的 GOAL_STREAM/GOAL_ATTACH([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L811-L836);测试 `test_new_work_supersedes_waiting_control`)。 + +### 取消(cancel_execution) + +- 选择器:request_id / execution_id / 整个 session(均 active_only),先经 `_with_descendants` BFS 扩展子执行,范围限同 (session_id, generation)([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L648-L679)、[L955-L979](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L955-L979);测试 `test_cancel_by_request_execution_and_session`)。 +- 两条取消路径:scheduled 类走 `SessionWorkScheduler.cancel_handles`;SESSION_MESSAGE(虽属 scheduled)和非 scheduled 类按 `_requires_direct_cancel` 直接 cancel 其 asyncio 任务([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L852-L856))。等待默认 `cancel_timeout=5.0` 秒,调用可用 `wait_timeout` 覆盖。 +- 结果 `CancelExecutionResult(matched, cancelled, timed_out)`:超时未退出的执行保持原状态(如 RUNNING)且 `cancellation_requested=True`,仍被登记表跟踪;未超时的非终态一律强制记 CANCELLED([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L680-L692);测试 `test_cancel_timeout_keeps_resistant_execution_tracked`)。 + +## 怎样验证 + +源码链接固定在 f0a69728c96b5961d993449f1a901cbd2f4dac5b。聚焦单测选择器和验证范围见[原模块页面](jiuwenswarm-runtime-session.md#怎样验证);单元契约不能替代真实服务、模型端点或跨平台集成验证。 + +## 相关文档 + +- [原模块架构与入口](jiuwenswarm-runtime-session.md) +- [相邻模块](../cron-scheduling/jiuwenswarm-runtime-cron.md) diff --git a/knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-execution.md b/knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-execution.md new file mode 100644 index 00000000..fde273db --- /dev/null +++ b/knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-execution.md @@ -0,0 +1,38 @@ +--- +title: "Runtime unary 与流式执行" +created: 2026-10-01 +updated: 2026-10-01 +type: architecture +tags: [jiuwenswarm] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/session/coordinator.py +--- + +# Runtime unary 与流式执行 + +## 职责和边界 + +说明已准入会话工作如何提交 unary 操作、处理 deadline、保留终态 owner 并解耦流生产者与消费者;会话注册、lane 调度与关闭见[协调器主页面](jiuwenswarm-runtime-session.md)。 + +### unary 执行 + +- `run_unary` 在调用方任务里 await 完成并返回值;`submit_unary` 排后台任务、立刻返回 QUEUED 快照。`submit_unary` 在对应记录仍留在 registry 时,按 (session_id, request_id, generation) 去重:重复提交返回同一 execution 快照;终态记录被 TTL/容量淘汰后,同一 request_id 可创建新执行,work_kind 不一致抛 `ValueError("request_id already belongs to ...")`([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L127-L159))。 +- `submit_unary` 的 SESSION_MESSAGE 先 `await record.control_ready.wait()`:会话里有执行在等交互答案时,排队消息不会被调度([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L151-L154);测试 `test_session_message_waits_for_heartbeat_interaction`)。 +- 超时与取消的区分:`timeout_scope`(`asyncio.Timeout`)expired 时,CancelledError 记 FAILED(error=timeout_error,默认 "execution deadline exceeded")而非 CANCELLED;操作吞掉取消返回"迟到成功"也照样记 FAILED 并向调用方抛 `TimeoutError`([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L177-L201);测试 `test_swallowed_deadline_cancellation_is_still_failed`)。 +- `wait_for_terminal=True` 置 `retain_owner_task`;非 scheduled 工作(例如 HEARTBEAT)拿到值后等 `terminal_event`,终态 CANCELLED 抛 CancelledError、FAILED 抛 RuntimeError;owner task 退出前该终态执行不被登记表淘汰([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L226-L232);测试 `test_retained_terminal_owner_is_not_evicted_before_task_exit`)。 +- 调用方任务被取消时:scheduled 的先经 scheduler 取消,未到终态则记 CANCELLED,随后级联取消子执行(`_cancel_descendants`)再刷新会话状态;其他异常同样取消子执行([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L234-L256);测试 `test_cancelling_unary_caller_also_stops_owned_operation`)。子执行取消被再次打断时,会不可中断地等它们退出([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L1044-L1057);测试 `test_interrupted_descendant_cancel_keeps_child_tracked`)。 + +### 流式执行(run_stream) + +- 生产者/消费者经有界 `asyncio.Queue`(默认 64)解耦;生产者对每个条目跑 `suspension_key`,产出 control_id 就 mark_awaiting_control 并刷新 gate;终止标记(done/error)即使缓冲满也必须投递,否则消费者在最后一个数据项之后会永远等待(源码注释明说)([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L565-L614))。 +- 消费者提前退出(aclose/异常)且生产任务未完成时,协调器对该 handle 调 `cancel_execution`,在取消等待期限内等待生产者;抵抗取消并超时的任务仍由 registry 跟踪([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L625-L646);测试 `test_stream_early_close_cancels_producer_and_waits_for_exit`、`test_external_stream_cancel_unblocks_consumer`)。 +- lane 使用规则:`scheduled` 且不是 SESSION_MESSAGE 才进 lane——流式 mailbox 消息已持有宿主准入,排进聊天 lane 会死锁在等待该准入的用户轮次后面(源码注释);GOAL_STREAM 非 scheduled 不占 lane,聊天占用 lane 时仍可运行([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L616-L624);测试 `test_admitted_session_message_stream_bypasses_user_waiting_in_lane`、`test_goal_stream_runs_while_chat_lane_is_busy`)。 + +## 怎样验证 + +焦点用例与验证范围见[协调器验证入口](jiuwenswarm-runtime-session.md#怎样验证)。这里只记录固定源码 f0a69728c96b5961d993449f1a901cbd2f4dac5b 的进程内契约,不代替真实生产生命周期验证。 + +## 相关文档 + +- [Runtime Session 的准入、状态与关闭](jiuwenswarm-runtime-session.md) +- [Runtime 交互输入、执行取代与取消](jiuwenswarm-runtime-control.md) diff --git a/knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-session.md b/knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-session.md index 8e342f90..fbf2b3df 100644 --- a/knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-session.md +++ b/knowledge/repos/jiuwenswarm/components/agent-runtime/jiuwenswarm-runtime-session.md @@ -1,30 +1,97 @@ --- title: "Runtime Session 协调器(jiuwenswarm/runtime/session/)" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/session/coordinator.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/session/execution_registry.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/session/work_scheduler.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/session/model.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/session/__init__.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/runtime/test_runtime_session_coordinator.py --- # Runtime Session 协调器(jiuwenswarm/runtime/session/) -负责产品层 Session 的生命周期和执行协调,包括注册与关闭会话、调度单次(unary)和流式执行、投递 control 请求,以及取消执行及其子执行。另外维护一个有容量上限的执行登记表,记录排队中、运行中和最近结束的执行,并为每个 Session 提供一个工作调度器。 +负责产品层 Session 的生命周期和执行协调,包括注册与关闭会话、调度单次(unary)和流式执行、投递 control 请求,以及取消执行及其子执行。另外维护一个有容量上限的执行登记表,记录排队中、运行中和最近结束的执行,并为每个 Session 提供一个工作调度器。它只拥有临时(ephemeral)执行状态,不是持久 Session 元数据/历史的仓库——coordinator 与 registry 的类 docstring 都这样声明。 -**从哪里开始读** +本页源码结论固定在 upstream commit `f0a6972`(f0a69728c96b5961d993449f1a901cbd2f4dac5b),行号见各 GitHub blob 链接。 -- `jiuwenswarm/runtime/session/coordinator.py` — RuntimeSessionCoordinator 是对外入口,提供 register_session、run_unary/submit_unary、run_stream、deliver_control、cancel_execution、c +## 职责和边界 -**关键文件** +- 包级公开导出只有六个名字:`RuntimeSessionCoordinator`、`RuntimeSessionState`、`SessionCloseTimeoutError`、`SessionExecutionEndedError`、`SessionPersistencePolicy`、`SessionWorkKind`([session/\_\_init\_\_.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/__init__.py#L3-L19));registry、scheduler、handle/snapshot 类型需从子模块导入。 +- 新执行与输入准入入口先过 `_require_open_session`:先调用 `jiuwenswarm/server/runtime/session/lifecycle.py` 的 `guard(session_id)` 检查会话归档、删除和进行中的生命周期操作,以及所属项目的生命周期门(函数内 import;[lifecycle.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/server/runtime/session/lifecycle.py#L389-L427)),再拒绝未注册("session is not registered")与 QUIESCING/CLOSED("session is not accepting work")([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L791-L801))。 +- 协调器整体 `close()` 后 `_accepting=False`,此后 `register_session` 抛 "session coordinator is closed"。 +- 两个专职异常都在 `model.py`:`SessionExecutionEndedError` 刻意不是 `asyncio.CancelledError`(投递任务从未被取消,报成取消会让上游流处理器把真实失败当成静默中止);`SessionCloseTimeoutError` 携带 session_id 与仍存活的 execution_ids([model.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/model.py#L12-L30))。 -- `jiuwenswarm/runtime/session/coordinator.py` — 管理会话记录、执行句柄的创建和取消(含子执行和心跳根)、control 请求的认领与转发,以及会话状态刷新 -- `jiuwenswarm/runtime/session/execution_registry.py` — SessionExecutionRegistry 负责执行状态流转(running/awaiting_control/waiting/terminal),终态记录按容量和 TTL 淘汰 -- `jiuwenswarm/runtime/session/work_scheduler.py` — SessionWorkScheduler 为每个 Session 分配一条 lane(按 generation 区分),负责排队执行、取消排队项、关闭时清空队列 -- `jiuwenswarm/runtime/session/model.py` — 共享模型:会话/执行状态枚举、SessionWorkKind、执行句柄与快照、取消和关闭的结果,以及超时和已结束异常 +## 主要源码和调用入口 + +- `jiuwenswarm/runtime/session/coordinator.py` — `RuntimeSessionCoordinator` 是对外入口:`register_session`、`run_unary`/`submit_unary`、`run_stream`/`stream_session_input`、`deliver_control`/`deliver_control_stream`/`record_interaction`/`has_control_target`/`control_target_work_kind`、`cancel_execution`、`close_session`/`close`、`get_execution`/`snapshot_session`。构造默认 `cancel_timeout=5.0`、`stream_buffer_size=64`(最小 1)([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L56-L72))。 +- `jiuwenswarm/runtime/session/execution_registry.py` — `SessionExecutionRegistry` 负责执行状态流转(QUEUED/RUNNING/WAITING_FOR_CONTROL/terminal),终态记录按容量和 TTL 淘汰。 +- `jiuwenswarm/runtime/session/work_scheduler.py` — `SessionWorkScheduler` 为每个 Session 分配一条 lane(按 generation 区分),负责排队执行、取消排队项、关闭时清空队列。 +- `jiuwenswarm/runtime/session/model.py` — 共享模型:会话/执行状态枚举、`SessionWorkKind`、执行句柄与快照、取消和关闭的结果,以及超时和已结束异常。 + +## 数据怎样流动 + +### 会话准入与 generation + +- `register_session`:session_id 去空白后为空抛 `ValueError`;新记录(不存在或已 CLOSED)的 generation = `_generations` 计数 +1,跨关闭单调递增;已存在且未 CLOSED 时只更新 channel_id(仅传入真值时)和 persistence_policy,不换 generation;channel_id 缺省 "default"([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L74-L100))。 +- QUIESCING 期间重复 register 返回同一 generation 且状态仍为 QUIESCING;只有 `close_session` 真正置 CLOSED 后,下一次 register 才分配 generation+1(焦点测试 `test_old_generation_completion_does_not_clear_new_generation`)。 + +### 执行登记表:状态机与终态淘汰 + +- 通常从 QUEUED 开始并进入 RUNNING;等待交互时可进入 WAITING_FOR_CONTROL 再恢复 RUNNING,也可不经等待直接结束。SUCCEEDED/FAILED/CANCELLED 为终态,排队取消也可直接进入 CANCELLED([model.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/model.py#L71-L85))。`mark_running` 只在 QUEUED 时生效并写 `started_at`;`mark_awaiting_control` 只对非终态写 `waiting_control_id`;`mark_waiting` 进入 WAITING_FOR_CONTROL 并清 `handle.task`(`retain_owner_task` 除外);`resume_waiting` 只从 WAITING_FOR_CONTROL 回 RUNNING([execution_registry.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/execution_registry.py#L39-L64))。 +- `mark_terminal` 必须给终态(否则 `ValueError`),对已终态幂等(只补 `terminal_event`);落终态时清 `waiting_control_id`、写 `finished_at`、error 字符串化,task 未保留时清空并把执行放进淘汰队列([execution_registry.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/execution_registry.py#L66-L88))。 +- 终态历史有界:默认 `terminal_capacity=512`、`terminal_ttl=900.0` 秒(capacity<0 报错)。淘汰按 TTL 过期或超容量触发,且只有 `handle.task is None or task.done()` 才真正删索引;`retain_owner_task` 的终态执行要等 owner task 退出(`release_owner_task`)后才可被淘汰([execution_registry.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/execution_registry.py#L19-L27)、[L147-L164](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/execution_registry.py#L147-L164))。 +- `select` 支持 execution_id / (session_id, request_id) / session_id 三种键,可过滤 generation 与 active_only,结果按 `created_at` 排序,快照顺序稳定([execution_registry.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/execution_registry.py#L105-L131))。 + +### lane 调度 + +- `SessionWorkKind` 九种;`scheduled` 只含 CHAT_UNARY/CHAT_STREAM/SESSION_MESSAGE(进 lane),`latest_first` 只含两种 CHAT([model.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/model.py#L44-L68))。 +- 每个 Session 一条 lane,lane 绑定 generation;`_ensure_lane` 遇 generation 不匹配抛 "session generation mismatch"。lane 内一次只跑一个 operation,Session 之间并行([work_scheduler.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/work_scheduler.py#L154-L166))。 +- 优先级分组:`latest_first` 的 CHAT 进 group 0(priority 随提交递减,越新越先跑),其余 scheduled(SESSION_MESSAGE)进 group 1 按 sequence FIFO——新聊天会插到排队消息前面([work_scheduler.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/work_scheduler.py#L55-L63);测试 `test_latest_first_within_session_and_parallel_across_sessions`、`test_session_messages_wait_for_control_and_keep_fifo_order`)。 +- 提交时 `contextvars.copy_context()`,processor 用 `asyncio.create_task(op, context=...)` 在快照上下文执行:排队项看到的是提交时的上下文,调用方之后改 ContextVar 不影响它([work_scheduler.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/work_scheduler.py#L181-L183);测试 `test_contextvars_are_captured_for_each_queued_operation`)。 +- 取消排队项不动 PriorityQueue(没有删除操作):cancel 它的 result future,processor 取出时发现 result 已取消就跳过、不执行该操作([work_scheduler.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/work_scheduler.py#L217-L223);测试 `test_cancel_queued_request_never_runs_its_operation`)。 + +### unary 与流式执行 + +提交、deadline、终态 owner 保留与生产者/消费者清理见[Runtime 执行专题](jiuwenswarm-runtime-execution.md)。 + +## Runtime 交互输入、执行取代与取消 + +该工作流的职责、顺序、错误边界与源码入口见[Runtime 交互输入、执行取代与取消](jiuwenswarm-runtime-control.md)。 + +## 关闭与停机 + +- `close_session(session_id, generation=None)`:记录不存在或 generation 不符 → `CloseSessionResult(existed=False)`。流程:置 QUIESCING → 直接取消 active 的 direct 类 → 关闭该 generation 的 lane(清队列、取消 processor)→ 等待 settling(retain_owner_task 的终态执行,或无 lane 时仍有活任务的 scheduled 执行)→ 未超时的残留 active 强制 CANCELLED → 无超时才置 CLOSED([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L694-L753);测试 `test_close_session_cancels_goal_stream`)。 +- 有 timed_out 时会话停留在 QUIESCING:新工作被 "not accepting work" 拒绝,同 generation 重试 close 时,仍未退出的执行可继续被报告为超时;旧执行全部退出后再次 close 才 CLOSED,此后 register 分配 generation+1(测试 `test_old_generation_completion_does_not_clear_new_generation`)。 +- `close()`:锁内置 `_accepting=False`,逐会话 close_session;任何超时汇总为 `SessionCloseTimeoutError("runtime", ids)` 并先抛出;没有该超时才继续关 scheduler,其余异常抛第一个([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L763-L789);测试 `test_close_stops_new_work_and_cancels_active_execution`)。 + +## 会话状态与 control gate + +- `_refresh_session_state`:非 QUIESCING/CLOSED 时,有活跃执行 → ACTIVE,否则 READY;记录已被替换则不动([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L1059-L1070))。 +- `_refresh_control_gate`:任一活跃执行带 waiting_control_id 就清 `control_ready`,否则置位——这是 SESSION_MESSAGE 排队等待的闸门([coordinator.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session/coordinator.py#L1072-L1086))。 + +## 怎样验证 + +焦点测试文件:`tests/unit_tests/runtime/test_runtime_session_coordinator.py`(`pytestmark = pytest.mark.unit`,asyncio 用例),[upstream 链接](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/runtime/test_runtime_session_coordinator.py)。按主题的精确选择器: + +- 排序/并行/上下文:`test_latest_first_within_session_and_parallel_across_sessions`、`test_contextvars_are_captured_for_each_queued_operation`、`test_session_messages_wait_for_control_and_keep_fifo_order` +- 消息闸门与幂等:`test_submitted_session_message_starts_without_active_execution`、`test_session_message_waits_for_heartbeat_interaction`、`test_submitted_session_message_is_idempotent_by_request_id`、`test_admitted_session_message_stream_bypasses_user_waiting_in_lane`、`test_cancel_session_message_while_it_waits_for_control` +- 取消:`test_cancel_by_request_execution_and_session`、`test_cancelling_unary_caller_also_stops_owned_operation`、`test_cancel_queued_request_never_runs_its_operation`、`test_cancel_timeout_keeps_resistant_execution_tracked`、`test_external_stream_cancel_unblocks_consumer`、`test_interrupted_descendant_cancel_keeps_child_tracked` +- 流式:`test_stream_early_close_cancels_producer_and_waits_for_exit`、`test_goal_stream_runs_while_chat_lane_is_busy`、`test_close_session_cancels_goal_stream` +- control:`test_control_input_bypasses_running_session_work_lane`、`test_control_input_resumes_work_after_output_stream_ends`、`test_control_input_matches_goal_interaction_id`、`test_control_input_requires_running_session_work`、`test_control_input_cancellation_does_not_cancel_parent_work`、`test_failed_control_setup_releases_claim_and_parent`、`test_control_result_after_parent_ended_raises_typed_error`、`test_new_work_supersedes_waiting_control` +- 登记/关闭/generation:`test_registry_retains_only_bounded_terminal_history`、`test_retained_terminal_owner_is_not_evicted_before_task_exit`、`test_swallowed_deadline_cancellation_is_still_failed`、`test_close_stops_new_work_and_cancels_active_execution`、`test_old_generation_completion_does_not_clear_new_generation` + +验证边界:这些选择器来自固定版本的源码与既有断言,覆盖进程内异步协调行为;不能单独证明生产生命周期接线、集成或硬件行为。`SessionInputTargetError` 继承 `SessionInputRejectedError`,两者分别带稳定的错误码 `SESSION_INPUT_TARGET_CHANGED` / `SESSION_INPUT_REJECTED`([session_input.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/session_input.py#L15-L24))。 **相关文档** -- `TESTING.md` — 为本模块新增或修改测试时参考。其中部分路径可能已过期,先对照 tests/ 下已有的用例 +- [共享 Agent Runtime(jiuwenswarm/runtime)](jiuwenswarm-runtime.md) — 上一层入口:AgentRuntime、请求规范化与会话 provision 契约;本页只覆盖 `runtime/session/` 子包。 +- [定时任务(Cron)运行时](../cron-scheduling/jiuwenswarm-runtime-cron.md) — HEARTBEAT 执行的产生方在 cron 侧;心跳链在本页的挂起/恢复语义里。 +- `TESTING.md` — 为本模块新增或修改测试时参考。其中部分路径可能已过期,先对照 tests/ 下已有的用例。 **改动路由** diff --git a/knowledge/repos/jiuwenswarm/components/common-core/_index.md b/knowledge/repos/jiuwenswarm/components/common-core/_index.md index d0471913..6bf58292 100644 --- a/knowledge/repos/jiuwenswarm/components/common-core/_index.md +++ b/knowledge/repos/jiuwenswarm/components/common-core/_index.md @@ -1,7 +1,7 @@ --- title: "公共基础模块与配置" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: index tags: [jiuwenswarm] sources: [] @@ -9,9 +9,9 @@ sources: [] # 公共基础模块与配置 -- [jiuwenswarm/common/:公共基础模块](jiuwenswarm-common.md) +- [jiuwenswarm/common/:公共基础模块](jiuwenswarm-common.md) — config.yaml 读写、模型目录与选择持久化、目录校验/连通性探测、permissions 与 MCP 配置面 - [ACP stdio 客户端(common/acp)](jiuwenswarm-common-acp.md) -- [配置面板 handler(config_panel)](jiuwenswarm-common-config-panel.md) +- [配置面板 handler(config_panel)](jiuwenswarm-common-config-panel.md) — Web/TUI config/models handler;provider 归一化与 reasoning 档位的序列化/校验 - [公共 Schema:Agent 请求响应、统一消息与参数契约](jiuwenswarm-common-schema.md) ## 代码快速入口(Direct) @@ -21,6 +21,10 @@ sources: [] | PR 描述在做什么 | 精确规则 | 第一批 live 源码 | |---|---|---| -| common、config、配置、模型选择、model catalog、mode、工作区、cron、updater、doctor、hooks、kv cache… | 入口 | `jiuwenswarm/common/_build_config.py`、`jiuwenswarm/common/chat_final.py`、`jiuwenswarm/common/cleanup.py` | +| common、config、配置、模型选择、model catalog、mode、工作区、cron、updater、doctor、hooks、kv cache… | 入口 | `jiuwenswarm/common/config.py`、`jiuwenswarm/common/model_catalog.py`、`jiuwenswarm/common/mcp_config.py`、`jiuwenswarm/common/_build_config.py` | - [jiuwenswarm/common 审查规则:配置读写事务、跨仓契约与持久化键稳定性](rules.md) + +## 专题入口 + +- [模型目录、稳定选择与配置校验](jiuwenswarm-common-model-catalog.md) — 说明共享配置模型的稳定业务 ID、目录视图、选择 DTO、候选写入与校验;登录凭据的产生和续期由 login-auth 拥有。 diff --git a/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common-config-panel.md b/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common-config-panel.md index 8e6ea209..07283b7e 100644 --- a/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common-config-panel.md +++ b/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common-config-panel.md @@ -1,10 +1,13 @@ --- title: "配置面板 handler(config_panel)" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/config_panel/models_handlers.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/reasoning_config.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/test_reasoning_config.py --- # 配置面板 handler(config_panel) @@ -19,7 +22,7 @@ sources: [] **关键文件** -- `jiuwenswarm/common/config_panel/config_set_handlers.py` — ConfigChangeSet 和 ConfigApplyResult 表示一次改动的结果(改了哪些键、要重载哪些范围);flatten_* / build_*_config_update 负责 symphony、skill retriev +- `jiuwenswarm/common/config_panel/config_set_handlers.py` — ConfigChangeSet 和 ConfigApplyResult 表示一次改动的结果(改了哪些键、要重载哪些范围);flatten_* / build_*_config_update 负责 symphony、skill retrieval - `jiuwenswarm/common/config_panel/models_handlers.py` — 处理模型列表:前端传入的模型转成默认配置、replace_all 时的合并、环境变量占位符比较、provider 规范化、reasoning level 序列化;ConfigPanelBadRequest 是请求错误 - `jiuwenswarm/common/config_panel/tui_models_handlers.py` — TUI 契约与 Web 侧不一致,文件头明确禁止合并;包含 build_config_schema、Auto-Harness 的 git 用户与 gitcode token 设置、模型增删改和切换,以及后台重载配置 @@ -33,3 +36,15 @@ sources: [] **路由** - `jiuwenswarm/common/config_panel/` + +## 模型规范化与 reasoning 档位 + +- provider 归一化在 `normalize_provider_value`:任意大小写按 `ProviderType` 枚举值映射为规范大小写,查不到就原样返回;这是为了与 TUI 侧 `tui_connect._normalize_provider_value` 对齐,避免同一份历史配置在 TUI 能识别、Web 的"测试/保存"因大小写敏感被误判非法([models_handlers.py L127-L141](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config_panel/models_handlers.py#L127-L141))。 +- reasoning 档位的持久化形态有两层防御:读取侧 `reasoning_level_display` 把 YAML 1.1 加载器解析成布尔的裸 `on`/`off` 标量映射回字符串;写入侧 `_serialize_reasoning_level` 对非空值输出 DoubleQuotedScalarString,None/空白返回 None,同一字段绝不往返成"有时裸标量、有时带引号"的混合形态([models_handlers.py L95-L118](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config_panel/models_handlers.py#L95-L118))。`build_models_defaults_from_frontend` 保存前统一走共享的 `validate_reasoning_level_for_model`,此转换入口调用共享的 core 能力校验;其它保存入口仍需检查是否调用了该 helper([L53](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config_panel/models_handlers.py#L53)、[L440](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config_panel/models_handlers.py#L440))。 +- `jiuwenswarm/common/reasoning_config.py` 提供档位语义:`normalize_reasoning_level` 把别名(`none/false/disabled→off`、`true/enabled→on`、`minimum→minimal`、`med/mid→medium`、`extra_high→xhigh`、`maximum/ultra→max`)归一到 8 个 canonical 值;`reasoning_config_for_level` 转成 core 的 provider-neutral 配置(off→`{"mode":"disabled"}`,on→`{"mode":"enabled"}`,其余→`{"mode":"enabled","effort":}`)。`validate_reasoning_level_for_model` 只把 None/空白视为未设置(裸 `off` 被 YAML 1.1 读成 False 时会走归一化变成 `"off"` 而不是被清空),能力查询异常统一转 `ValueError` 让保存路径返回可读的 BAD_REQUEST 而非 500;`effective_endpoint_profile` 显式 `endpoint_profile` 永远优先,否则查用户配置的 host 覆盖表,源码不内置任何 host([reasoning_config.py L80-L112](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/reasoning_config.py#L80-L112)、[L120-L171](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/reasoning_config.py#L120-L171);选择器 `test_reasoning_level_aliases_are_canonicalized`、`test_validate_reasoning_level_normalizes_legacy_yaml_booleans`,[test_reasoning_config.py L35-L44](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/test_reasoning_config.py#L35-L44)、[L176-L210](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/test_reasoning_config.py#L176-L210))。 + +## 相关组件知识 + +- [jiuwenswarm/common/:公共基础模块](jiuwenswarm-common.md) — 本页 handler 最终落盘所用的 `update_config` / `save_models_candidate` 等持久化入口 +- [../gateway-channels/jiuwenswarm-channels-cli.md](../gateway-channels/jiuwenswarm-channels-cli.md) — TUI 侧 channel 与本页 TUI 契约的边界(TUI 契约文件头明确禁止与 Web 侧合并) +- [common 审查规则](rules.md) — handler 改动时的 config.yaml 写事务门禁 diff --git a/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common-model-catalog.md b/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common-model-catalog.md new file mode 100644 index 00000000..dc8796e8 --- /dev/null +++ b/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common-model-catalog.md @@ -0,0 +1,42 @@ +--- +title: "模型目录、稳定选择与配置校验" +created: 2026-10-01 +updated: 2026-10-01 +type: architecture +tags: [jiuwenswarm] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/config.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/model_catalog.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/model_config_validation.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/model_selection.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/test_model_selection_migration.py +--- + +# 模型目录、稳定选择与配置校验 + +## 职责和边界 + +说明共享配置模型的稳定业务 ID、目录视图、选择 DTO、候选写入与校验;登录凭据的产生和续期由 login-auth 拥有。 + +## 模型目录:快照、业务 ID 与写路径 + +- 持久化标识由 `_ensure_model_business_ids` 补齐:`defaults`/`agentos` 条目缺 `model_id` 时补 `mdl_`,groups 补 `mgp_` 前缀、routes 补 `rte_` 前缀。`save_models_candidate` 校验候选后原子替换 models 段:deepcopy → 补 ID → `raise_if_invalid` → `update_config` 内只替换 `data["models"]`,校验失败时完全不写盘([config.py L1547-L1572](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1547-L1572)、[L1609-L1621](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1609-L1621))。`migrate_model_business_ids` 幂等:无变化时 mutator 返回 None 直接跳过写盘([L1575-L1590](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1575-L1590);选择器 `test_legacy_migration_only_adds_stable_ids`,[test_model_selection_migration.py L62-L99](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/test_model_selection_migration.py#L62-L99))。 +- `upsert_model_resource` 从 `get_config_raw().models` 出发(未做 env 解析的原文),按 `model_id` 合并或追加;`_normalize_model_entry` 把前端扁平字段提升为嵌套结构(`model_provider`/`client_provider` 统一写 `client_provider`,`endpoint_profile`/`vendor_key`/`plan` 仅真值保留,`temperature` 转 float,`reasoning_level` 去空白);`_merge_model_update` 跳过 `source`/`is_agentos`/`read_only`/`write_only_fields` 展示字段并递归合并 dict,因此省略的 write-only 字段(`api_key`、`custom_headers`)保持原值([config.py L1624-L1733](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1624-L1733);选择器 `test_model_update_preserves_omitted_write_only_values`)。组 upsert 会剥掉 legacy `routing` 键(model-pool 模式下不生效)并给 routes 补默认 `route_id`。 +- `get_default_models` 读取优先级:`models.defaults`(列表)→ 旧格式 `models.default`(单对象包装成列表)→ 环境变量兜底条目(`API_BASE`/`API_KEY`/`MODEL_NAME`/`MODEL_PROVIDER`/`ENDPOINT_PROFILE`/`CUSTOM_HEADERS`,固定 `timeout: 1800`、`verify_ssl: False`)。所有分支最后都追加 `get_agentos_models` 的条目,且 agentos 条目强制 `is_default=False`、在 `model_config_obj._source` 打 `agentos` 标(前端置灰只读展示),`model_name` 为空视为未配置直接跳过([config.py L1736-L1781](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1736-L1781)、[L1496-L1540](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1496-L1540))。`_infer_is_default` 按 `model_name` 分组推断默认标记,不是全目录范围([L1416-L1459](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1416-L1459))。 +- `get_available_models` 组合配置与登录来源的模型清单:登录送的模型永远叠加在配置模型**之后**(`models.list` 的 origin_index 就是此处下标,保存设置按它回写 `models.defaults`,排到前面会把下标写串),同名以用户自配为准且按原样比较、不做大小写归一(`GLM-5.2` 与 `glm-5.2` 是两个模型);登录模型不落 config.yaml,必须传 `session_id` 才会叠加,登录模块失败只降级为已配置列表([config.py L1784-L1830](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1784-L1830))。`update_default_model_provider_in_config` 只改默认条目(首个 `is_default: true`,无标记则取 defaults 第一条),且是裸 `load_yaml_round_trip`/`dump_yaml_round_trip` 写法,不走写锁——与 [rules.md](rules.md) JIUWENSW-I4 列的旧写法同类([L1877-L1916](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1877-L1916))。 + +## 模型选择 DTO、目录视图与校验 + +- `ModelSelection` DTO `extra="forbid"`:`id`/`route_id` 去空白后非空,`route_id` 只在 `type == "model_group"` 时合法([model_selection.py L10-L38](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/model_selection.py#L10-L38);选择器 `test_single_model_selection_rejects_route_id`)。`ModelCatalog` 构造时经 `load_models_config` 建快照(解密后的 defaults/agentos、groups、按 `model_id` 索引的 `by_id`),`get_model`/`get_group` 查不到抛 `ModelSelectionError(MODEL_SELECTION_NOT_FOUND)`([model_catalog.py L80-L95](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/model_catalog.py#L80-L95)、[config.py L1593-L1606](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1593-L1606))。 +- 目录对外输出是脱敏视图:`list_public_models` 只暴露 `model_id`/`alias`/`model_name`/`provider`/`source`/`is_agentos`/`is_default`/`enabled`/`context_window`,不含 `api_key`/`api_base`;`get_public_model_detail` 额外剥掉 write-only 的 `model_client_config.api_key`/`custom_headers` 并在 `write_only_fields` 里声明,agentos 条目 `read_only=True`([model_catalog.py L97-L137](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/model_catalog.py#L97-L137);选择器 `test_catalog_is_desensitized`、`test_model_detail_is_editable_without_exposing_write_only_values`)。`find_references` 按 type==model 扫组内 routes,另扫会话 `metadata.json`、cron jobs 和 `agents`/`team` 配置树里内嵌的 `model_selection`,结果去重保序([model_catalog.py L153-L188](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/model_catalog.py#L153-L188);选择器 `test_catalog_finds_agent_and_team_references`)。 +- `validate_models_config` 的错误分类:三段必须是数组;`model_id` 在 defaults+agentos 合并范围内查重;agentos 条目 `is_default: true` 直接报错;默认组最多一个且默认组不允许 `enabled: false`;每组至少一条 route,route 必须引用存在的 `model_id`,组内 `route_id` 查重([model_config_validation.py L203-L272](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/model_config_validation.py#L203-L272))。`request_overrides`/`request_config` 拒绝 `FORBIDDEN_REQUEST_KEYS`(`model`/`messages`/`tools`/`stream`/`api_key`/`provider`/`client_provider`/`timeout_seconds`/`context_window`/`_source` 等 17 键),防止 patch 覆盖接线字段;`routing.strategy` 只接受 None/`ordered-failover`/`tag-filtered`,legacy strategy 只为升级兼容保留、不执行也不转发([L150-L198](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/model_config_validation.py#L150-L198))。 +- 占位凭证识别 `is_placeholder_model_entry`:`example.com/.org/.net` 域(含子域)、`https://example.com/compatible-mode/v1`、`your-model-name`、`sk-xxxxxxxxx`([model_config_validation.py L16-L19](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/model_config_validation.py#L16-L19)、[L122-L141](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/model_config_validation.py#L122-L141);选择器 `tests/unit_tests/common/test_model_config_validation.py` 前 4 个用例)。连通性探测 `probe_model_connection` 按默认 `token_limits=(3, 16)` 逐档放大 `max_tokens` 重试;content、`reasoning_content` 或 `usage_metadata.output_tokens > 0` 任一非空才算有输出(推理模型可能把小预算全花在 reasoning 上);最后一次失败原样抛给调用方,`timeout_seconds` 用 `asyncio.wait_for` 包住每次完整调用([L30-L119](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/model_config_validation.py#L30-L119))。 + +## 怎样验证 + +源码链接固定在 f0a69728c96b5961d993449f1a901cbd2f4dac5b。聚焦单测选择器和验证范围见[原模块页面](jiuwenswarm-common.md#怎样验证);单元契约不能替代真实服务、模型端点或跨平台集成验证。 + +## 相关文档 + +- [原模块架构与入口](jiuwenswarm-common.md) +- [相邻模块](../login-auth/jiuwenswarm-common-auth.md) diff --git a/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common.md b/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common.md index e098c4a4..90dbd9ba 100644 --- a/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common.md +++ b/knowledge/repos/jiuwenswarm/components/common-core/jiuwenswarm-common.md @@ -1,10 +1,20 @@ --- title: "jiuwenswarm/common/:公共基础模块" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/config.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/model_selection.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/model_catalog.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/model_config_validation.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/permission_tools.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/mcp_config.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/test_model_selection_migration.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/test_config.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/common/test_model_config_validation.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/common/test_config_concurrent_models.py --- # jiuwenswarm/common/:公共基础模块 @@ -45,6 +55,31 @@ Gateway 和 AgentServer 共用的基础层,负责 config.yaml 的读写与迁 - `docs/zh/Harness.md` — 改 cron_team_completion.py 里 harness 轮次结束条件时读 - `TESTING.md` — 给本模块补测试时参考;其中有过期路径,要先核对 tests/ 下已有的用例 +## 配置读取与环境变量解析 + +- `get_config()` 的流水线固定为 `_read_with_retry` → `resolve_env_vars` → `_normalize_config`,env 解析和归一化在锁外执行以缩短锁持有时间。只有 YAML parse 结果进缓存:键是文件身份 `(mtime_ns, size)`,命中与写入都深拷贝,文件 mtime_ns 或 size 改变时会使缓存失效;不能检测保持两者不变的内容改写,而 `load_dotenv_runtime(override=True)` 后的新环境变量下一次读取立即生效,无需显式失效钩子([config.py L171-L244](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L171-L244))。`_read_with_retry` 遇 `YAMLError` 默认最多尝试 3 次(最多重试 2 次)(间隔 0.05s 递增)以跨过另一进程写一半的文件;`get_config_raw()` 跳过 env 解析,专供局部读改写回([L199-L234](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L199-L234)、[L262-L264](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L262-L264))。 +- `resolve_env_vars` 的 `${VAR:-default}` 按 bash 语义:unset 或空串都用默认值;无 `:-` 的 `${VAR}` 在 unset 时得到空串。变量名含 `api_key`/`token`(不区分大小写)且有值时先经 crypto provider 解密。`mcp.servers` 条目的 `headers`/`env`/`staticHeaders`/`static_headers` 子树原样保留 `${VAR}` 占位符——它们属于 CredentialStore,在构建 `McpServerConfig` 时才解析;提前塌缩成空串会让 httpx 报 `Illegal header value b'Bearer '`([config.py L62-L125](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L62-L125);选择器 `TestResolveEnvVars::test_mcp_server_headers_env_placeholders_preserved`,[test_config.py L623-L712](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/test_config.py#L623-L712))。 +- `_normalize_config` 对 KV cache 亲和 fail-closed:开关已开但 `validate_affinity_invariant` 不通过时只在内存里关掉开关(保留用户文件供诊断),绝不激活不一致配置;同时把 JSON 字符串形式的 `custom_headers` 解析成 dict,并为 `channels.{web,feishu,xiaoyi}` 顶层缺省的 `send_file_allowed` 兜底 True([config.py L128-L168](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L128-L168))。 + +## 模型目录、稳定选择与配置校验 + +该工作流的职责、顺序、错误边界与源码入口见[模型目录、稳定选择与配置校验](jiuwenswarm-common-model-catalog.md)。 + +## permissions 与 MCP 配置面 + +- `permissions.tools` 的值只允许小写 `allow|ask|deny` 或 legacy `{"*": level}`,写入前经 `_validate_tools_map` 规范化;rules 必须有非空 `tools`+`pattern`,`id` 缺省自动生成 `ui_rule_`,可变键白名单为 `tools/pattern/severity/action/description/match_type`,`severity` 归一为大写 `LOW|MEDIUM|HIGH|CRITICAL`、`action` 归一为小写三级;重复 rule id 在 mutator 内抛 `ValueError`,整个写入中止([config.py L1097-L1204](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1097-L1204)、[L1225-L1261](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1225-L1261)、[L1368-L1387](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/config.py#L1368-L1387))。 +- `permission_tools.py` 的别名表只有四条:`exec_command→mcp_exec_command`、`fetch_webpage→mcp_fetch_webpage`、`free_search→mcp_free_search`、`paid_search→mcp_paid_search`。`resolve_permission_tool_name` 对歧义别名图 fail-closed(conflict 时 `canonical_name` 置空,不猜);`stricter_permission_level` 按 `allow(0) @@ -19,3 +32,7 @@ sources: [] | PR 描述在做什么 | 精确规则 | 第一批 live 源码 | |---|---|---| | cron、定时任务、cron job、cron 表达式、etcd、主备、HA、cron store、work_mode、proactive、钉钉推送、time… | 入口 | `jiuwenswarm/runtime/cron/` | + +## 专题入口 + +- [Cron 文件与 etcd 存储后端](jiuwenswarm-cron-store.md) — 说明 CronJob 持久化、后端选择、文件锁、变更修订与 etcd CAS/watch;任务 schema、表达式和执行路由见 Cron 主页面。 diff --git a/knowledge/repos/jiuwenswarm/components/cron-scheduling/jiuwenswarm-cron-store.md b/knowledge/repos/jiuwenswarm/components/cron-scheduling/jiuwenswarm-cron-store.md new file mode 100644 index 00000000..c4955cc4 --- /dev/null +++ b/knowledge/repos/jiuwenswarm/components/cron-scheduling/jiuwenswarm-cron-store.md @@ -0,0 +1,48 @@ +--- +title: "Cron 文件与 etcd 存储后端" +created: 2026-10-01 +updated: 2026-10-01 +type: architecture +tags: [jiuwenswarm] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/cron_job_mutations.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/etcd_client.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/etcd_store.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/factory.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/store.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/gateway/test_cron_etcd_store.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/gateway/test_cron_factory.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/gateway/test_cron_job_mode.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/gateway/test_cron_store_path_resolution.py +--- + +# Cron 文件与 etcd 存储后端 + +## 职责和边界 + +说明 CronJob 持久化、后端选择、文件锁、变更修订与 etcd CAS/watch;任务 schema、表达式和执行路由见 Cron 主页面。 + +## 文件存储(store.py) + +- JSON 读、upsert 和删除各自通过双层锁保护:进程内 asyncio.Lock + 跨进程 portalocker 伴生 `cron_jobs.json.lock`(默认 10s 超时);文件锁在 to_thread 里获取,避免阻塞事件循环。但 update_job 先 get_job、在锁外合并 patch、再单独锁定 upsert;不能据此宣称同一任务的并发 patch 是完整原子读改写([store.py:222-230](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/store.py#L222-L230))([store.py:28](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/store.py#L28)、[store.py:56-69](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/store.py#L56-L69))。 +- 原子写(.tmp + replace);读侧任何异常(缺文件/坏 JSON/非 dict)都静默返回 {"version":1,"jobs":[]}——文件损坏的表现是"清空"而非报错([store.py:301-324](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/store.py#L301-L324))。 +- get_revision 是 stat 派生哈希 (mtime_ns<<40)^(ctime_ns<<16)^size,不是递增序号;supports_watch=False、watch() 直接 NotImplementedError,该后端不提供事件 watch,上层可轮询 revision 发现变更([store.py:40](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/store.py#L40)、[store.py:71-83](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/store.py#L71-L83))。 +- work_mode 惰性迁移发生在 list_jobs 的同一把锁内:project_id 反查(include_hidden=True)优先,查不到按 targets 含 tui → "code"、否则 "work";只写回缺/非法 work_mode 的 job,全部合法时跳过项目查询与迁移写回(仍有读取、加锁和解析开销)([cron_job_mutations.py:55-117](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_job_mutations.py#L55-L117)、[store.py:85-105](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/store.py#L85-L105),TestCronJobLazyMigration [test_cron_job_mode.py:324-544](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/gateway/test_cron_job_mode.py#L324-L544))。 +- 默认存储路径为 workspace 的 `agent` 目录下的 `home/cron_jobs.json`(FileCronJobStore 构造器可指定 path)(get_cron_jobs_path):遗留 gateway/cron_jobs.json 不读、不迁移、不删除([test_cron_store_path_resolution.py:28-38](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/gateway/test_cron_store_path_resolution.py#L28-L38)、[test_cron_store_path_resolution.py:182-196](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/gateway/test_cron_store_path_resolution.py#L182-L196))。 +- 错误面:update_job 任务不存在抛 KeyError("job not found");delete_job 对不存在 id 返回 False;build_job 静态方法供 runtime 侧 cron 工具先构造同校验的 wire 表示再经常驻 host 提交 mutation,该工具接线把持久化请求交给常驻 Gateway host,store 本身不限制调用进程;另有 disable_project_jobs 批量停用项目下 enabled 任务([store.py:116-169](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/store.py#L116-L169)、[store.py:222-272](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/store.py#L222-L272))。 + +## etcd 存储(etcd_store.py)与后端选择(factory.py) + +- 每 job 一个 key(前缀默认 /jiuwenswarm/cron/jobs/);连接失败不阻塞 Gateway:list_jobs 返回 []、get_job 返回 None;store_backend=etcd 而 endpoints 为空时记一次 error 且仍返回 etcd store,绝不回落本地文件([etcd_store.py:34-74](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/etcd_store.py#L34-L74)、[test_cron_factory.py:33-41](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/gateway/test_cron_factory.py#L33-L41))。 +- update_job 的 CAS:range 取 mod_revision → put_if_mod_revision 写入;EtcdCasError 冲突后重读最新值、对最新值重放同一 patch、再 CAS 一次(恰好一次重试);二次冲突(EtcdCasError 是 EtcdError 子类)或网络错统一转 EtcdError,任务消失转 KeyError;delete_job 是先 get 校验再普通 delete,非 CAS([etcd_store.py:208-261](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/etcd_store.py#L208-L261)、[etcd_client.py:17-22](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/etcd_client.py#L17-L22)、[test_cron_etcd_store.py:163-209](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/gateway/test_cron_etcd_store.py#L163-L209))。 +- watch 循环:成功连接先回调一次(全量 reload),之后每个前缀事件批回调一次;任何异常按 1s→30s 指数退避重连;list_jobs 跳过 JSON 解码失败的 value,work_mode 迁移写回是逐 key put、无整体事务([etcd_store.py:30-31](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/etcd_store.py#L30-L31)、[etcd_store.py:263-276](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/etcd_store.py#L263-L276)、[etcd_store.py:89-111](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/etcd_store.py#L89-L111))。 +- create_gateway_cron_store 只认 gateway.cron.store_backend(默认 file),etcd 必须显式 opt-in,绝不因配置了 endpoints 或 agentos router 而隐式切换;扁平键 gateway.cron_store_backend 与 agentos.cron_store_backend 被忽略;endpoints 支持列表或逗号分隔字符串([factory.py:41-84](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/factory.py#L41-L84)、[test_cron_factory.py:43-76](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/gateway/test_cron_factory.py#L43-L76))。 + +## 怎样验证 + +源码链接固定在 f0a69728c96b5961d993449f1a901cbd2f4dac5b。聚焦单测选择器和验证范围见[原模块页面](jiuwenswarm-runtime-cron.md#怎样验证);单元契约不能替代真实服务、模型端点或跨平台集成验证。 + +## 相关文档 + +- [原模块架构与入口](jiuwenswarm-runtime-cron.md) +- [相邻模块](../launch/jiuwenswarm-instance-manager.md) diff --git a/knowledge/repos/jiuwenswarm/components/cron-scheduling/jiuwenswarm-runtime-cron.md b/knowledge/repos/jiuwenswarm/components/cron-scheduling/jiuwenswarm-runtime-cron.md index 969430f8..6575b6a0 100644 --- a/knowledge/repos/jiuwenswarm/components/cron-scheduling/jiuwenswarm-runtime-cron.md +++ b/knowledge/repos/jiuwenswarm/components/cron-scheduling/jiuwenswarm-runtime-cron.md @@ -1,10 +1,16 @@ --- title: "定时任务(Cron)运行时:任务模型、存储后端与投递路由" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/models.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/cron_job_mutations.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/store.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/etcd_store.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/factory.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/cron/cron_expr.py" --- # 定时任务(Cron)运行时:任务模型、存储后端与投递路由 @@ -20,12 +26,55 @@ sources: [] **关键文件** - `jiuwenswarm/runtime/cron/cron_job_mutations.py` — 文件和 etcd 两种存储共用的任务构建、patch 合并、work_mode 推断与迁移逻辑,以及 proactive 任务的改删保护 -- `jiuwenswarm/runtime/cron/store.py` — FileCronJobStore:在文件锁下读写 JSON,用文件修订号实现 watch,另有 disable_project_jobs +- `jiuwenswarm/runtime/cron/store.py` — FileCronJobStore:在双层锁下读写 JSON,文件修订号供上层轮询变更(不支持 etcd 式 watch),另有 disable_project_jobs - `jiuwenswarm/runtime/cron/etcd_store.py` — EtcdCronJobStore:主备 Gateway 用的 etcd 存储,更新时带 mod_revision 做 CAS,并监听前缀变化 - `jiuwenswarm/runtime/cron/etcd_client.py` — 基于 httpx 的精简 etcd v3 gRPC-gateway JSON 客户端(range/put/delete/CAS/watch),不依赖 grpc - `jiuwenswarm/runtime/cron/cron_expr.py` — cron 表达式规范化、按时区校验、把 ISO 时间转成 7 段 cron、计算下次触发时间 - `jiuwenswarm/runtime/cron/dingtalk_routing.py` — 钉钉定时任务的会话 ID 编码与解析,以及推送元数据解析(Issue #2449) +## 数据模型与目标路由 + +- `CronJob.from_dict` 的准入门槛:id/name/cron_expr/timezone/targets/description 缺一抛 ValueError;name ≤ 64、description ≤ 500 字符;wake_offset_seconds 必须可转 int 且负值钳为 0;cron_expr 连同 timezone 一起走 validate_cron_expression([models.py:476-538](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/models.py#L476-L538))。 +- 序列化不对称:to_dict 始终输出 project_id 与 work_mode(空串=默认项目),而 session_id/chat_type/last_session_id/model_name/model_selection/mcp/app_id/user_id/credential_ref 只在非空时写入([models.py:431-474](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/models.py#L431-L474))。 +- targets 只存一个频道 ID 字符串;旧 list[dict] 形态取第一个非空 channel_id 兼容;规范化走 CronTargetChannel 枚举(大小写不敏感),非空但非法的值回落默认 "web";`feishu_enterprise:` 是唯一带参键,忽略 `:chat:` 等后续后缀([models.py:36-77](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/models.py#L36-L77)、[models.py:501-513](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/models.py#L501-L513))。 +- CronRunState 是内存执行态,当前 CronJob 存储协议不序列化它;其中 exec_mode/exec_channel_id/exec_session_id/exec_user_id/exec_work_mode/exec_project_* 在任务被删后仍保留,让 ghost/超时取消仍能路由到持有该请求的同一个 Runtime agent,而不是回落默认用户([models.py:644-683](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/models.py#L644-L683)、[store_base.py:11-18](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/store_base.py#L11-L18))。 + +## 执行模式与 proactive 保护 + +- 两套 mode 归一入口:normalize_cron_job_mode(create/update 严格路径)对未知值抛 ValueError("Invalid cron job mode"),已知值先过 _CRON_JOB_MODE_ALIASES(plan→agent、agent.fast→agent、team.plan→team.plan.normal)再经 deprecate_mode 落到新三段 canonical(agent→agent.work.normal);coerce_cron_job_mode(读盘路径)同样叠加 deprecate_mode,但未知值小写直通不抛错,存量数据不会因未知 mode 起不来。默认 mode 为 "agent";cron_job_metadata 把 modes/default_mode/超时常量作为 TUI/Web 客户端的单一 schema 来源([models.py:113-168](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/models.py#L113-L168),两步归一期望表见 [test_cron_job_mode.py:35-58](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/gateway/test_cron_job_mode.py#L35-L58))。 +- 超时三档解析:显式 timeout_seconds(60 ≤ 值 ≤ 72h,否则 ValueError)→ team 模式默认 1h → 普通默认 1h;两档默认当前同为 3600s([models.py:186-205](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/models.py#L186-L205)、[models.py:335-342](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/models.py#L335-L342))。 +- proactive.tick 任务有独立的 store 层保护;注册与实际触发路由需沿 proactive_cron_sync 和 Gateway scheduler 核对:update 只放行 {cron_expr, timezone, expired, updated_at} 四个键,越权键丢弃并 warning;非 force 删除抛 _ProactiveJobProtected(RuntimeError 子类),直接调用这些 store 删除入口时会经过这一拦截([cron_job_mutations.py:26-46](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_job_mutations.py#L26-L46)、[cron_job_mutations.py:225-246](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_job_mutations.py#L225-L246))。 + +## 模型解析与登录凭据绑定 + +- resolve_cron_model 命中顺序 config → zen → login,同名自配优先;返回 (canonical, source),source="login" 向调用方表明需要从创建连接的登录会话捕获 CronJob.credential_ref;controller 与执行注入的实际接线需另沿调用方核对([models.py:239-263](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/models.py#L239-L263))。 +- 别名解析到 model_client_config.model_name 并展开环境变量占位符;显式配置却解析为空 → ValueError,不持久化空名;zen/登录目录是内存缓存,查找异常只 debug 日志、不阻塞主校验路径;未知模型 ValueError 附最多 20 个可用模型提示;登录模型名可带 "#index" 后缀,取前半段匹配([models.py:282-322](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/models.py#L282-L322);测试用 autouse fixture 把两个免费来源置空作为干净基线 [test_cron_model_validation.py:28-42](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/gateway/test_cron_model_validation.py#L28-L42))。 + +## 共享创建/更新规则(cron_job_mutations.py) + +- build_new_cron_job 不落盘:省略 id 时生成 uuid4().hex,构造后经 from_dict(to_dict()) 往返自校验再返回;parse_cron_jobs 逐条容错,单条损坏只 warning("Ignoring invalid cron job id=…") 跳过,不拖垮整个调度器,结果按 updated_at 降序([cron_job_mutations.py:120-136](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_job_mutations.py#L120-L136)、[cron_job_mutations.py:196-222](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_job_mutations.py#L196-L222))。 +- apply_cron_job_patch 的隐式状态机:patch enabled=true 或修改 cron_expr 时自动清 expired(除非显式 patch expired);cron_expr 实际变化且未显式传 delete_after_run 时重置该标记(防止一次性提醒任务换排程后被调度器判过期);patch model_name 而不带 model_selection 时清空旧 model_selection(否则执行侧优先旧 ID 选择);model_selection 本身经 ModelSelectionResolver 校验;结尾刷新 updated_at 并整体重新校验([cron_job_mutations.py:249-368](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_job_mutations.py#L249-L368),回归见 [test_cron_store.py:11-48](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/runtime/test_cron_store.py#L11-L48))。 + +## Cron 文件与 etcd 存储后端 + +该工作流的职责、顺序、错误边界与源码入口见[Cron 文件与 etcd 存储后端](jiuwenswarm-cron-store.md)。 + +## cron 表达式(cron_expr.py) + +- 只收 5 段或 7 段:5 段自动前补秒 "0"、后补年 "*",其余段数抛 ValueError;validate_cron_expression 仅做语法校验(croniter.is_valid + ZoneInfo 时区 + 从当前时刻构造一次),固定过去年份的一次性表达式仍可能在求下次触发时失败([cron_expr.py:15-32](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_expr.py#L15-L32)、[cron_expr.py:55-76](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_expr.py#L55-L76))。 +- next_cron_datetime 以 max_years_between_matches=130 支持远期固定年份(如 2099),naive 结果继承 base 的时区;iso_to_seven_field_cron 对 naive 输入按给定 timezone 解释、aware 输入先转换,dow 位固定 "?"([cron_expr.py:8](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_expr.py#L8)、[cron_expr.py:35-52](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_expr.py#L35-L52)、[cron_expr.py:79-94](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/cron/cron_expr.py#L79-L94))。 + **路由** - `jiuwenswarm/runtime/cron/` + +## 怎样验证 + +- 聚焦测试(本轮仅静态核对源码与用例,未执行):tests/unit_tests/runtime/{test_cron_expr,test_cron_store}.py 与 tests/unit_tests/gateway/{test_cron_etcd_store,test_cron_factory,test_cron_job_mode,test_cron_model_validation,test_cron_store_path_resolution}.py。test_cron_etcd_store.py 用内存 FakeEtcdJsonClient 模拟 CAS/watch,不代表真实 etcd 集群行为;scheduler/controller 的 HA 行为只有 test_watch_triggers_scheduler_reload 触及,本页未展开。 +- 旧 import 路径 jiuwenswarm.gateway.cron.models 是 jiuwenswarm.runtime.cron 的再导出 shim([gateway/cron/models.py:3-5](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/gateway/cron/models.py#L3-L5)),既有测试大多仍从 gateway.cron 旧路径 import。 + +## 相关文档 + +- [华为账号登录与免费模型凭据(common/auth)](../login-auth/jiuwenswarm-common-auth.md) — resolve_cron_model 的 login 来源与 credential_ref 句柄的来源侧。 +- [共享 Agent Runtime(jiuwenswarm/runtime)](../agent-runtime/jiuwenswarm-runtime.md) — mode/mcp/timeout 在 AgentServer 执行侧的消费。 +- [jiuwenswarm/common/:公共基础模块](../common-core/jiuwenswarm-common.md) — work_mode 与 mode_matrix/deprecate_mode 的共享定义。 diff --git a/knowledge/repos/jiuwenswarm/components/extensions-plugins/_index.md b/knowledge/repos/jiuwenswarm/components/extensions-plugins/_index.md index bcc7884d..6c0b8d25 100644 --- a/knowledge/repos/jiuwenswarm/components/extensions-plugins/_index.md +++ b/knowledge/repos/jiuwenswarm/components/extensions-plugins/_index.md @@ -1,7 +1,7 @@ --- title: "扩展与插件机制" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: index tags: [jiuwenswarm] sources: [] @@ -9,7 +9,7 @@ sources: [] # 扩展与插件机制 -- [扩展机制与 openYuanRong 集成(jiuwenswarm/extensions/)](jiuwenswarm-extensions.md) +- [扩展机制与 openYuanRong 集成(jiuwenswarm/extensions/)](jiuwenswarm-extensions.md) — 扩展搜索/发现/加载顺序、registry 单例与槽位替换、hook 分发、应用插件绑定禁用门禁与 HTTP/WS/静态资源挂载约束 - [AgentOS 扩展:AgentOS Router 与 SSH/Token 鉴权](jiuwenswarm-extensions-agentos.md) - [视频全双工扩展(video_duplex)](jiuwenswarm-extensions-video-duplex.md) @@ -23,3 +23,11 @@ sources: [] | extension、扩展、plugin、应用插件、hook、registry、loader、manifest、yuanrong、openYuanRong、cl… | 入口 | `jiuwenswarm/extensions/agent_client/`、`jiuwenswarm/extensions/application_host.py`、`jiuwenswarm/extensions/callback_compat.py` | - [video_duplex 全双工扩展审查规则:任务检查点、授权投递与 Provider 协议](rules.md) + +## 验证入口 + +扩展框架行为的单元测试在 `tests/unit_tests/test_extension_manager.py`(搜索路径、transport 标志)和 `tests/unit_tests/test_application_plugins.py`(禁用门禁、manifest-only 插件、资产路由);`tests/unit_tests/extensions/` 下是 AgentOS 专属测试。 + +## 专题入口 + +- [应用插件绑定、前端贡献与资源挂载](jiuwenswarm-application-plugins.md) — 说明 ApplicationPlugin 的 manifest-only 构造、宿主绑定、禁用门、HTTP 资产与 WebSocket 路由;扩展发现和进程生命周期见扩展主页面。 diff --git a/knowledge/repos/jiuwenswarm/components/extensions-plugins/jiuwenswarm-application-plugins.md b/knowledge/repos/jiuwenswarm/components/extensions-plugins/jiuwenswarm-application-plugins.md new file mode 100644 index 00000000..cdcec956 --- /dev/null +++ b/knowledge/repos/jiuwenswarm/components/extensions-plugins/jiuwenswarm-application-plugins.md @@ -0,0 +1,36 @@ +--- +title: "应用插件绑定、前端贡献与资源挂载" +created: 2026-10-01 +updated: 2026-10-01 +type: architecture +tags: [jiuwenswarm] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/application_host.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/loader.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/registry.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/sdk/application_plugin.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/gateway/app_gateway.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/gateway/channel_manager/web/web_channel_app.py +--- + +# 应用插件绑定、前端贡献与资源挂载 + +## 职责和边界 + +说明 ApplicationPlugin 的 manifest-only 构造、宿主绑定、禁用门、HTTP 资产与 WebSocket 路由;扩展发现和进程生命周期见扩展主页面。 + +## 应用插件:manifest-only、绑定与挂载 + +- manifest-only 插件:无入口脚本且 `package_type == "application"` 时 loader 直接构造 `ManifestApplicationPlugin` 并注册,要求 `metadata.id` 非空、至少一个 frontend 贡献;frontend 仅允许 `render_mode == "iframe"`(其余抛 `ValueError`),`entrypoint` 缺省 `index.html`,id/nav_key/position 缺省分别为 `-page`、`app:`、`100+序号`;initialize/shutdown 均为 no-op。无入口且非 application 的根会在 `_import_module` 抛 `FileNotFoundError`。见 [sdk/application_plugin.py L107-153](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/sdk/application_plugin.py#L107-L153)、[loader.py L70-81](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/loader.py#L70-L81)。 +- 绑定与禁用门禁:Gateway 在 `_register_web_handlers` 之后调 `bind_application_plugins(web_channel, agent_client=..., media_attachment_normalizer=...)`,每个插件拿到包了 `_ApplicationPluginChannel` 的 channel 与 `ApplicationPluginServices`。wrapper 拦截所有 `register_method`:除非声明 `available_when_disabled=True`,禁用插件的 RPC 直接回 `code="APPLICATION_PLUGIN_DISABLED"` 错误响应(设置类方法可声明该标志让用户从管理页重新启用);同时设 `channel.application_plugin_registry = registry`,Web app 构建时从该属性取插件 registry。`ApplicationPluginServices.require_agent_client` / `normalize_media_attachments` 在宿主未注入对应服务时抛 `RuntimeError`。见 [registry.py L54-82](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/registry.py#L54-L82)、[L162-180](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/registry.py#L162-L180)、[app_gateway.py L2215-2219](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/gateway/app_gateway.py#L2215-L2219)。 +- HTTP 清单与资产:`GET /api/application-plugins` 返回 `{"api_version": 1, "plugins": [...]}`,每个前端贡献一项(含 plugin_id/enabled/permissions/entry_url),按 `(position, nav_key)` 排序;无贡献的插件合成 `:management` 条目(`render_mode="none"`、position 1000)。资产路由 `GET /api/application-plugins/{plugin_id}/assets/{asset_path:path}` 的 404 条件:插件不存在、frontend_asset_root()(SDK 默认实现为已存在的 <扩展目录>/frontend/dist,插件可覆盖该方法)为 None、`resolve()` 后落在根外(`relative_to` 抛 `ValueError`)、目标不是文件;命中时以 `Cache-Control: no-cache` 返回 FileResponse。该检查使用解析后的 root 与 target 比较路径包含关系;它本身不构成整个插件的安全保证。见 [application_host.py L24-110](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/application_host.py#L24-L110)。 +- WebSocket 路由:`iter_websocket_routes` 产出 `(plugin_id or metadata.id, route)`,运行时 `plugin_id` 优先于清单 id(`test_websocket_routes_use_the_registered_runtime_plugin_id` 固定)。Web app 构建时保留主 WS 路径与 `/ws/git`,插件路由与之冲突或彼此重复都在**构建期**抛 `ValueError`;连接时先做可选 origin 检查(`check_origin` 默认 True),插件禁用且未声明 `available_when_disabled` 时 `close(1008, reason="application plugin is disabled")`。见 [web_channel_app.py L59-95](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/gateway/channel_manager/web/web_channel_app.py#L59-L95)、[application_host.py L80-85](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/application_host.py#L80-L85)。 + +## 怎样验证 + +源码链接固定在 f0a69728c96b5961d993449f1a901cbd2f4dac5b。聚焦单测选择器和验证范围见[原模块页面](jiuwenswarm-extensions.md#怎样验证);单元契约不能替代真实服务、模型端点或跨平台集成验证。 + +## 相关文档 + +- [原模块架构与入口](jiuwenswarm-extensions.md) +- [相邻模块](jiuwenswarm-extensions-video-duplex.md) diff --git a/knowledge/repos/jiuwenswarm/components/extensions-plugins/jiuwenswarm-extensions.md b/knowledge/repos/jiuwenswarm/components/extensions-plugins/jiuwenswarm-extensions.md index dab6c13b..040e53b1 100644 --- a/knowledge/repos/jiuwenswarm/components/extensions-plugins/jiuwenswarm-extensions.md +++ b/knowledge/repos/jiuwenswarm/components/extensions-plugins/jiuwenswarm-extensions.md @@ -1,10 +1,22 @@ --- title: "扩展机制与 openYuanRong 集成(jiuwenswarm/extensions/)" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/manager.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/loader.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/registry.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/application_host.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/sdk/base.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/sdk/application_plugin.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/extensions/callback_compat.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/gateway/channel_manager/web/web_channel_app.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/gateway/app_gateway.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/runtime/service.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/test_extension_manager.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/test_application_plugins.py --- # 扩展机制与 openYuanRong 集成(jiuwenswarm/extensions/) @@ -33,6 +45,44 @@ sources: [] - `jiuwenswarm/extensions/callback_compat.py` — 兼容 openjiuwen<0.1.9 的 AsyncCallbackFramework:补上缺失的 unregister_sync - `jiuwenswarm/extensions/yuanrong_frontend_client.py` — YuanrongFrontendAgentClient:调用 openYuanRong Frontend 函数,管理常驻 agent 沙箱,处理 trace id 和 TCP 探针配置 +## 扩展发现与加载(manager / loader) + +以下行为按源码 f0a6972 静态核对,未运行测试。 + +- 搜索路径按固定顺序拼接:最前是 `/application_plugins`(不存在会自动创建),随后是配置 `extensions.extension_dirs`(只接受字符串、按 `;` 分割),末尾总是追加内置目录 `jiuwenswarm/extensions`;去重按小写归一的路径 parts 保留首个出现者,因此配置里显式写内置路径不会重复搜索。相对路径 `jiuwenswarm/extensions` 会同时尝试 CWD 解析和 extensions 包目录两个候选。见 [manager.py L31-80](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/manager.py#L31-L80)。 +- 扩展根目录的判定只看搜索路径的**一级子目录**:目录内含 `extension.yaml` **或** `extension.py` 即算,不递归、普通文件跳过。清单缺失时按 `{}` 处理,`extension.py`-only 的根仍可加载。见 [loader.py L29-58](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/loader.py#L29-L58)、[L167-177](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/loader.py#L167-L176)。 +- `load_all_extensions` 逐根处理:先 `load_manifest`(只读 YAML,不导入入口模块),`include_transport_extensions=False` 时跳过 `requires_transport is True` 的扩展——判断是严格布尔恒等,字符串 `"false"` 也会被加载(`tests/unit_tests/test_extension_manager.py::test_transport_manifest_flag_requires_a_real_boolean` 固定该行为);单个根加载抛任何异常捕获 Exception 后记 error 日志并继续其余根;取消等 BaseException 不在该捕获范围。Runtime 直连与 AgentServer 启动都传 False,Gateway 默认加载全部。见 [manager.py L82-109](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/manager.py#L82-L109)、[app_agentserver.py L263-265](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/server/app_agentserver.py#L263-L265)。 +- `load_extension` 顺序:读清单 → 安装依赖 → 无入口脚本且 `package_type == "application"` 时走 manifest-only 分支,否则按 `jiuwenswarm.loaded_extension.<目录名>` 导入入口并调用模块级异步 `register_extensions(registry)`(可返回单个对象或列表;未定义或返回空时 loader 返回 None)。返回对象有 set_extension_dir 方法时才调用 set_extension_dir(root),但只有 `ApplicationPluginExtension` 实例由 loader 追加 `await ext.initialize(registry.config)`,其他扩展点的初始化由各自 `register_extensions` 内部负责。依赖安装:清单 `dependencies` 为 包名→版本约束 映射,先 `importlib.metadata.version` 探测,未装时优先 `uv pip install`(PATH 有 uv 时)否则 `python -m pip install`,单包 120 秒超时;超时与失败**只记日志、继续加载**,不阻断导入。见 [loader.py L65-144](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/loader.py#L65-L144)。 +- `BaseExtension.metadata` 首次访问从扩展目录 `extension.yaml` 解析并缓存;目录优先取 `set_extension_dir` 的显式值,否则从子类模块文件位置推断(仅当旁边恰有 extension.yaml),都取不到抛 `ValueError`,清单文件缺失抛 `FileNotFoundError`;`set_extension_dir` 同时清空 metadata 与 config.yaml 两个缓存。见 [sdk/base.py L38-114](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/sdk/base.py#L38-L114)。 + +## Registry:单例、槽位替换与 hook 分发 + +- `ExtensionRegistry` 是单槽单例:初始化前 `get_instance`、重复 `create_instance` 都抛 `RuntimeError`。AgentServerClient / CryptoUtility / ThirdAgent 三个扩展点是单槽属性,**后注册直接覆盖、不报错**;应用插件按 `plugin_id`(空值回退 `metadata.id`)存字典,id 为空或重复注册抛 `ValueError`。`reset_instance` 同时清掉注入到 `common.security.base_crypto` 的 crypto 桥。见 [registry.py L101-151](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/registry.py#L101-L151)、[L125-129](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/registry.py#L125-L129)。 +- `create_instance` 把 `_RegistryCryptoBridge` 装进 `base_crypto` 全局钩子(common/config 的 api_key 解密入口)。桥按调用时点解析 crypto 扩展;未注册时 encrypt/decrypt **原样返回输入**(回退明文,不抛错),供调用方继续处理原文。见 [registry.py L33-51](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/registry.py#L33-L51)、[L121-122](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/registry.py#L121-L122)。 +- hook 分发委托 openjiuwen 的 `AsyncCallbackFramework`:`register` = `register_sync(event, handler, priority=100)`;`unregister` 走兼容 shim(openjiuwen<0.1.9 没有 `unregister_sync`,shim 还按 `__wrapped__` 解装饰器匹配,callback 为 None 时直接返回);`trigger` 区分三种载荷——context+kwargs、仅 context、仅 kwargs(context 为 None 且有 kwargs 时只传 kwargs)。见 [registry.py L204-223](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/registry.py#L204-L223)、[callback_compat.py L9-52](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/callback_compat.py#L9-L52)。 + +## 应用插件绑定、前端贡献与资源挂载 + +该工作流的职责、顺序、错误边界与源码入口见[应用插件绑定、前端贡献与资源挂载](jiuwenswarm-application-plugins.md)。 + +## 进程生命周期与关闭 + +- Gateway `run()`:`create_instance` → `ExtensionManager` → `load_all_extensions()`(含 transport 扩展)→ 从 registry 取 AgentServerClient 扩展,取不到回退内置 WebSocket 客户端;clawee FaaS 入口同样 create + 全量加载。见 [app_gateway.py L1843-1868](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/gateway/app_gateway.py#L1843-L1868)。 +- Runtime 直连在进程锁内做借用判定:AgentServer/Gateway 已预加载 registry 时 Runtime 只借用、不参与其生命周期(直接返回);否则自建并 `load_all_extensions(include_transport_extensions=False)`。加载失败时先 `shutdown_all_extensions`,再在"当前单例仍是自己"时 `reset_instance`;清理自身失败只记 warning,原始加载异常保留并重抛。释放按 `_PROCESS_RUNTIME_EXTENSION_USERS` 引用计数,最后一个使用者才关闭并重置。见 [runtime/service.py L215-293](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/runtime/service.py#L215-L293)。 +- `shutdown_all_extensions` 按加载顺序逐个 `await ext.shutdown()`(仅当对象有该方法),单个失败记 warning 后继续,最后清空列表——一个扩展关闭抛错不会阻断其余扩展。见 [manager.py L111-118](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/extensions/manager.py#L111-L118)。 + +## 怎样验证 + +- `tests/unit_tests/test_extension_manager.py` — 搜索路径拼接与去重(`test_extension_dirs_*`)、transport 标志严格布尔、registry 类型注解无 transport 导入(`test_registry_type_hints_resolve_without_transport_imports`)。 +- `tests/unit_tests/test_application_plugins.py` — 禁用门禁与 `APPLICATION_PLUGIN_DISABLED`、WS 路由用运行时 plugin_id、manifest-only iframe 插件加载→清单→资产 200 的进程内链路(`test_manifest_only_iframe_plugin_requires_no_python_entry`)。 +- 本页结论来自 pinned 源码静态核对;未运行上述测试,也未做浏览器或 FaaS 环境验证。`tests/unit_tests/server/extensions/` 是服务端设备包(equipment)测试,不属于本 owner。 + +**相关页面** + +- [AgentOS 扩展](jiuwenswarm-extensions-agentos.md)、[视频全双工扩展](jiuwenswarm-extensions-video-duplex.md) — 两个真实扩展/应用插件实现 +- [共享 Agent Runtime](../agent-runtime/jiuwenswarm-runtime.md) — Runtime 直连借用/自建 registry 的调用方 +- [video_duplex 审查规则](rules.md) + **路由** - `jiuwenswarm/extensions/agent_client/` diff --git a/knowledge/repos/jiuwenswarm/components/gateway-channels/_index.md b/knowledge/repos/jiuwenswarm/components/gateway-channels/_index.md index 8a7b39c3..5d3a42ee 100644 --- a/knowledge/repos/jiuwenswarm/components/gateway-channels/_index.md +++ b/knowledge/repos/jiuwenswarm/components/gateway-channels/_index.md @@ -1,7 +1,7 @@ --- title: "Gateway 与频道" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: index tags: [jiuwenswarm] sources: [] @@ -9,8 +9,10 @@ sources: [] # Gateway 与频道 -- [CLI 频道(jiuwenswarm/channels/cli)](jiuwenswarm-channels-cli.md) -- [进程式 CLI 频道(process_cli)](jiuwenswarm-channels-process-cli.md) +- [CLI 频道(jiuwenswarm/channels/cli)](jiuwenswarm-channels-cli.md) — `jiuwenswarm chat` 的连接握手(connection.ack)、chat.send 请求与 agent_ref、SIGINT 中断、team/plan 终止判定和 human/JSON/JSONL 三种渲染。 +- [进程式 CLI 频道(process_cli)](jiuwenswarm-channels-process-cli.md) — REPL 每命令一个 worker 与斜杠命令、app.run 会话操作提交时机、机器模式的 SessionLease/Agent 绑定与停机顺序。 + +两页都覆盖 `jiuwenswarm/channels/` 下的频道实现;Gateway 自身的网络协议与路由不在这里。 ## 代码快速入口(Direct) @@ -22,3 +24,7 @@ sources: [] | cli、chat、命令行、CLI 频道、gateway client、websocket、tui、renderer、jsonl、spinner、SIGINT、… | 入口 | `jiuwenswarm/channels/cli/`、`jiuwenswarm/channels/process_cli/` | - [Process CLI 频道(jiuwenswarm/channels/process_cli)审查规则](rules.md) + +## 专题入口 + +- [进程式 CLI 机器执行、会话租约与停机](jiuwenswarm-process-cli-machine.md) — 一次性执行的跨进程所有权、完成证据与清理边界。 diff --git a/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-channels-cli.md b/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-channels-cli.md index e43c3eca..0454e009 100644 --- a/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-channels-cli.md +++ b/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-channels-cli.md @@ -1,16 +1,23 @@ --- title: "CLI 频道(jiuwenswarm/channels/cli)" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/channels/cli/chat.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/channels/cli/gateway_client.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/channels/cli/events.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/channels/cli/render.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/channels/cli/test_chat.py --- # CLI 频道(jiuwenswarm/channels/cli) 提供根命令 `jiuwenswarm` 的 CLI 入口和命令行聊天频道:解析参数,通过兼容 `/tui` 的 WebSocket 连接 Gateway,再把流式事件渲染成人类可读、JSON 或 JSONL 输出。交互模式下还负责权限确认和用户输入、Ctrl+C(SIGINT)中断,以及受信目录的持久化和清理。 +本页源码结论固定在 upstream commit `f0a6972`(f0a69728c96b5961d993449f1a901cbd2f4dac5b),行号见各 GitHub blob 链接。 + **从这里读起** - `jiuwenswarm/channels/cli/main.py` — 根命令 `jiuwenswarm` 的 CLI 入口 `main()` @@ -24,8 +31,50 @@ sources: [] - `jiuwenswarm/channels/cli/events.py` — 事件判定:内容是否结束、流是否终止、是否需要用户输入,以及事件归类 - `jiuwenswarm/channels/cli/_terminal.py` — 用 `os.write` 实现的终端输出原语;按 docstring 所说,这是终端 UI,不是日志,所以没有走 `logging`(G.LOG.02) +## 连接握手与请求构造 + +- 默认 Gateway URL 由环境变量拼出 `ws://{GATEWAY_HOST=127.0.0.1}:{GATEWAY_PORT=19001}/tui`;未指定 `--session` 时本地生成 `cli-YYYYMMDD-HHMMSS-` 形式的会话 ID([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L325-L334);测试 `TestSessionId::test_generated_id_format`)。 +- `GatewayClient.connect` 要求服务端第一帧必须是 `{"type":"event","event":"connection.ack"}`:连接后提前关闭、非 JSON 或非 ack 帧都收敛为 `ConnectionError`,随后 `_run_chat` 的 10 秒连接超时/`ConnectionError`/`OSError` 统一打印 `jiuwenswarm-start app` 启动提示并返回退出码 3([gateway_client.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/gateway_client.py#L49-L77)、[chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L1093-L1105);测试 `TestGatewayClient::test_connect_rejects_non_ack`)。WebSocket 参数:优先 `websockets.legacy` 客户端,`close_timeout=2.0`、`max_size=8MiB`、`ping_interval=20`、`ping_timeout=60`([gateway_client.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/gateway_client.py#L14-L32))。 +- `_build_request` 发送 `method="chat.send"`、`is_stream=True`,params 携带 session_id、content+query(同值)、mode、cwd、project_dir、trusted_dirs 和 `agent_ref={"mode": , "id": "default"}`:V2 显式 agent_ref 让 Gateway 按 (channel, scope, agent_ref) 注册,同 session 切 mode 后旧 agent 的延迟 chunk 不会错路由(源码注释;[chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L422-L463);测试 `TestBuildRequest::test_minimal_request`、`test_with_session`)。trusted_dirs 缺省取 project_dir,并把 config.yaml 中已持久化的信任目录合并进请求。 + +## 中断、超时与退出码 + +- 交互循环里 SIGINT 分两段:第一次设标志并提示,第二次在 POSIX 上直接 `os.kill(SIGTERM)`;Windows 没有 `loop.add_signal_handler`,退回 `signal.signal` 处理器,第二次 Ctrl+C 只置 force-exit 标志([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L547-L583))。触发中断时发送 `chat.interrupt`(`intent:"cancel"`、带 session_id 和 mode,3 秒 `wait_for`,异常吞掉)并返回 130([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L596-L618))。 +- human 输出循环中的 `--timeout` 是总响应预算:循环用 `time.monotonic()` 维护 deadline,每次 recv 与 sleep 竞速的时长为 `max(min(remaining, 0.3), 0.01)`,无 deadline 时用 1 秒空闲超时,并把 recv 与短 sleep 竞速,让 Windows ProactorEventLoop 上也能约 1 秒内响应 Ctrl+C([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L488-L501)、[L638-L654](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L638-L654))。 +- human 循环的退出码约定:2 = 参数错误(非法 mode、`--json`+`--jsonl`、timeout≤0、空 prompt);3 = 连不上 Gateway;4 = recv `OSError`(提示用 `--session` 重连)或收到交互事件但本调用不支持交互;5 = 其他 recv 异常(指向 full.log/gateway.log);1 = `chat.error`、超时或 team.error;130 = 用户中断([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L405-L419)、[L698-L711](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L698-L711))。`--json`/`--jsonl` 模式下请求永远带 `supports_user_interaction=False`,所以交互事件在这两种模式必然以 4 退出([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L1107-L1113))。JSON/JSONL 的循环没有接收 `args.timeout`,不能把 human 的总预算、recv 异常映射和 SIGINT 处理推广到这两种输出([L1000-L1043](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L1000-L1043)、[L1121-L1135](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L1121-L1135))。 + +## 交互应答分流 + +- 四种中断恢复源(confirm/permission/ask_user/evolution `_interrupt`)且 `request_id` 非空时用新的流式 `chat.send` 应答(params 带 request_id、answers、source,query 为空)才能恢复被暂停的任务;其他交互事件走非流式 `chat.user_answer`([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L277-L282)、[L834-L868](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L834-L868))。 +- 权限卡片答案只绑定唯一一张不透明卡片:questions 必须恰好一条且带非空 `card_id`(≤128 字符),答案形如 `{"selected_options":[..],"custom_input":..,"card_id":..}`([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L285-L309);测试类 `TestPermissionCardAnswer`)。选项支持序号或标签(大小写不敏感)映射到 option value。 + +## 终止判定与 team/plan 语义 + +- `is_terminal_event`:`chat.error` 恒终止;`chat.final` 仅在内层 `event_type` 为空/同为 `chat.final`(真内容)或 `team.error` 时终止;`chat.processing_status` 仅在 `is_processing=False` 时终止。Gateway 把没有 EventType 映射的控制事件(keepalive、chat.llm_usage、team.runtime_ready 等)装进 chat.final 信封传输,它们不能当终止([events.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/events.py#L10-L30);测试 `test_is_terminal_chat_final_keepalive`)。 +- Team 模式里 leader 的 chat.final 只是轮次边界:真正结束流的是 `processing_status(is_processing=False)` 或 team.error;若 leader 回复后没建任务、服务端 team-completion 不触发,客户端用 3 秒空闲窗口检测停顿并在 TTY 上直接提示下一轮输入([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L506-L517)、[L882-L936](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L882-L936))。 +- Plan 模式里 chat.final 不一定是结束:没收到 `plan.mode_exited`、也不是审批恢复(source=confirm_interrupt 的应答)时,视为 agent 抛出文本问题,TTY 上提示 follow-up 并继续同一条连接([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L519-L543)、[L940-L975](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L940-L975))。发出恢复/追问后,`awaiting_resume` 只跳过遇到的第一个终止事件,然后立即清零;它不通过请求 ID 判定该事件属于旧流还是新流。 +- REPL(无 prompt 且 TTY)每轮 `asyncio.run` 新建一条连接;退出码 130 时留在 REPL 继续下一轮而不是退出([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L1210-L1264))。Team 模式无 prompt 时例外:先读一行进入同一个持久交互会话,避免每轮重连丢 team 状态([chat.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/chat.py#L1158-L1177))。 + +## 渲染输出 + +- `JsonlRenderer` 每个事件一行 `{"type":"event","event":,"payload":}`;`JsonRenderer` 攒完所有事件后输出单个对象,content 取最后一个非空 content,任一事件带 error 则 `ok=false` 并报最后一个错误([render.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/render.py#L204-L250))。 +- `HumanRenderer` 流式打印 delta,相邻 delta 间隔超过 0.5 秒插入空行;spinner 每 0.2 秒 tick 一次、动词每 5 秒轮换、空闲超过 3 秒变红。final 只打印一次并复用 TUI 的 chooseFinalAssistantContent 策略:final 是 streamed 的延续时只补后缀,是子集时不再打印,完全不同时只在内部保留更长版本,绝不重刷已流出的终端文本([render.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/render.py#L21-L29)、[L167-L196](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/cli/render.py#L167-L196);测试类 `TestHumanRenderer`)。 + +## 怎样验证 + +焦点测试文件:`tests/unit_tests/channels/cli/test_chat.py`([upstream 链接](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/channels/cli/test_chat.py)),另有 test_gateway_client.py、test_render.py、test_events.py。按主题的精确选择器: + +- 请求与会话:`TestSessionId::test_generated_id_format`、`TestBuildRequest::test_minimal_request`、`test_with_session` +- 握手:`TestGatewayClient::test_connect_waits_for_ack`、`test_connect_rejects_non_ack`、`test_connect_rejects_invalid_json` +- 终止判定:`test_is_terminal_chat_final`、`test_is_terminal_chat_final_keepalive`、`test_is_terminal_processing_done` +- 渲染:`TestHumanRenderer`、`TestJsonRenderer`、`TestJsonlRenderer`、`TestPermissionCardAnswer` + +验证边界:以上是固定 commit 下源码与单测断言的结论,未实际运行;gateway 侧对 `agent_ref` 三元组的注册行为、team/plan 服务端配合属于 Gateway/Runtime 实现,本页只描述客户端假设。 + **相关文档** +- [Runtime Session 协调器](../agent-runtime/jiuwenswarm-runtime-session.md) — chat.send/chat.interrupt 在服务端的准入、lane 调度与取消语义。 +- [共享 Agent Runtime](../agent-runtime/jiuwenswarm-runtime.md) — 会话 provision 与请求规范化契约。 - `docs/zh/Quickstart.md` — 想了解用户通常怎样启动和使用命令行时读 - `README_CN.md` — 需要项目总体介绍,或核对 CLI 在整体架构里的位置时读 - `docs/zh/FAQ.md` — PR 改动连接提示、报错文案或常见使用问题时读 diff --git a/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-channels-process-cli.md b/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-channels-process-cli.md index 99bfd17d..674a5249 100644 --- a/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-channels-process-cli.md +++ b/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-channels-process-cli.md @@ -1,15 +1,24 @@ --- title: "进程式 CLI 频道(process_cli)" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/channels/process_cli/app.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/channels/process_cli/repl.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/channels/process_cli/machine.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/channels/process_cli/session_guard.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/process_cli/test_agent_session_guard.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/process_cli/test_machine.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/process_cli/test_repl.py --- # 进程式 CLI 频道(process_cli) -进程式 JiuwenSwarm CLI 频道:交互式 REPL 给每条命令启动一个独立的 Runtime worker 进程;另有两类入口,一类是面向机器的一次性执行(NDJSON 输出,可选双工应答/取消控制),另一类是只读查询。所有入口都通过进程内客户端调用共享的 Agent Runtime Public API。 +进程式 JiuwenSwarm CLI 频道:交互式 REPL 给每条命令启动一个独立的 Runtime worker 进程;另有两类入口,一类是面向机器的一次性执行(NDJSON 输出,可选双工应答/取消控制),一类是只读查询。所有入口都通过进程内客户端调用共享的 Agent Runtime Public API。 + +本页源码结论固定在 upstream commit `f0a6972`(f0a69728c96b5961d993449f1a901cbd2f4dac5b),行号见各 GitHub blob 链接。 **入口** @@ -34,8 +43,38 @@ sources: [] - `jiuwenswarm/channels/process_cli/live_input.py` — LiveSessionInputController:TTY 实时补充输入(steer),以及投递回执的分类 - `jiuwenswarm/channels/process_cli/render.py` — EventRenderer:把 Runtime 事件流渲染成 human、JSON 或 JSONL 输出 +## REPL:每命令一个 worker 与斜杠命令 + +- run_repl 只拥有 UI;每条非斜杠输入用 `python -m jiuwenswarm.channels.process_cli.main --_interactive-worker --_prompt-file …` 拉起全新 worker(prompt 写临时文件、绝不进 argv),worker 环境固定 `PYTHONIOENCODING=utf-8`,Windows 上加 `CREATE_NEW_PROCESS_GROUP` 以便投递 CTRL_BREAK_EVENT([repl.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/repl.py#L105-L153);测试 `test_repl_runs_every_instruction_in_a_new_worker_and_reuses_session`)。 +- 斜杠命令分三组:本地控制命令 `/model`(model.list/model.resolve,把 selection_key 记到下一轮 worker)、`/plan`(仅 agent.* 模式,切换 `agent.{work_mode}.{plan|normal}`)、`/status`(session.get)、`/permissions`(permission.get)不启动 worker;`/skills list` 和有状态操作 `/new [--persist|--persist-session]`、`/resume [id]`、`/branch`、`/delete ` 各跑一个带 `--_operation` 的 worker;`/sessions` 打开分页选择器(limit 1..200、搜索词 ≤200 字符)选中后走 switch([repl.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/repl.py#L766-L931);测试 `test_repl_unknown_slash_and_idle_cancel_do_not_start_worker`、`test_repl_session_lifecycle_commands_use_workers_and_update_state`)。 +- 父 REPL 只在 worker-result.json 的 `operation` 与本轮一致时采纳其中的 session_id/mode/work_mode/project_dir;chat 在没有有效 worker-result 时还可从旧 `session-id.txt` 恢复 session_id(见 JIUWENSW-I39);有状态操作退出码 0 但没有结果文件时改为 1 并输出诊断,退出码 130 一律回到输入循环([repl.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/repl.py#L444-L487);测试 `test_successful_stateful_worker_without_result_is_reported_as_failure`、`test_stateful_worker_result_restores_mode_work_mode_and_project`)。 +- 中断升级有界:live `/cancel` 或 Ctrl+C 先发 CTRL_BREAK_EVENT/SIGINT 等 15 秒,再 `terminate()` 等 5 秒,最后 `kill()` 等 5 秒;`cancel_requested` 置位时返回码强制 130,且 130 不附带 worker 诊断([repl.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/repl.py#L297-L330)、[L407-L414](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/repl.py#L407-L414);测试 `test_worker_interrupt_has_bounded_terminate_and_kill_fallback`、`test_live_cancel_returns_interrupt_code_without_worker_diagnostics`)。任务运行中其他斜杠命令被拒绝,提示先 `/cancel`。 + +## worker 生命周期与会话操作(app.run) + +- `app.run` 对每条命令恰好拥有一个 Runtime:chat 操作先 `create_or_resume_session(channel_id="process_cli", …)`,随后立刻写结果文件,再构造 `AgentRequest`(CHAT_SEND、is_stream=True;`supports_user_interaction` 仅在 interactive worker 或 human 输出 + TTY 时为真,`--_model-selection` 透传为 model_name),按需启动 LiveSessionInputController(REPL 转发用 PipeLineReader,本机 TTY 用 TtyLineReader)后进入 `_consume`([app.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/app.py#L798-L853))。 +- 失败映射:`--timeout` 到期(`asyncio.timeout` 包住整个 execute)→ 先发有界 `CHAT_CANCEL` 再渲染超时错误事件、退出码 124;`CancelledError` → 取消 + `renderer.interrupted()` 后重新抛出(父进程显示为中断);其他异常 → 渲染错误事件(`SessionProvisionError` 的 code 进 metadata)、退出码 1。finally 里 chat 操作先 `cleanup_session` 再 `client.close()`,两步各限 5 秒,取消也不能跳过 close([app.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/app.py#L855-L929))。 +- 会话操作提交时机不同:create 用 `AFTER_RESULT_DELIVERY`(终态输出和父结果文件先落盘,延迟 KVC 提交失败只记日志、不改写已交付的成功,finally 只 abort 仍处 PREPARED 的租约);switch/fork 用 `BEFORE_RESULT_DELIVERY`;delete 不走 prepare,直接 `delete_session`。所有目标会话先经 `_owned_session_descriptor` 校验 descriptor 的 channel_id 必须是 `process_cli`,否则按 NOT_FOUND 拒绝——别的频道的会话不能在这里切换/分叉/删除([app.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/app.py#L208-L227)、[L299-L327](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/app.py#L299-L327))。create 刻意传 `project_dir=""`(projectless 工作区,cwd 仍是执行位置),is_swarm/team_hint 由 team 模式推导。 +- 交互应答分流与 Gateway CLI 一致:四种 `_interrupt` 恢复源且交互 request_id 非空时用新的流式 CHAT_SEND(query 置空、带 request_id/answers/source)恢复暂停任务,其余走 CHAT_ANSWER;取消请求复用原 request_id 构造 CHAT_CANCEL,因为取消目标是 Runtime 里在飞的那个请求本身([app.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/app.py#L530-L582))。 + +## 机器执行与停机 + +机器调用的准入、会话租约、根 Agent 指纹、结果判定与清理顺序见[机器执行专题](jiuwenswarm-process-cli-machine.md)。 + +## 怎样验证 + +焦点测试文件:`tests/unit_tests/process_cli/`(test_agent_session_guard.py、test_machine.py、test_repl.py 等,[upstream 链接](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/tests/unit_tests/process_cli/test_machine.py))。按主题的精确选择器: + +- 租约与绑定:`test_session_lease_is_exclusive_and_reusable`、`test_binding_survives_new_process_and_rejects_identity_change`、`test_session_guard_busy_preserves_safe_retry_message`、`test_session_guard_conflict_preserves_safe_error_message` +- 机器执行/清理:`test_one_command_owns_one_runtime_and_returns_result_after_close`、`test_timeout_covers_start_allocation_and_stream_with_precise_cancel`、`test_external_task_cancellation_is_a_terminal_result_after_cleanup`、`test_broken_output_cancels_and_closes_without_retrying_output` +- REPL/worker:`test_worker_command_uses_a_fresh_process_entry_and_runtime_session`、`test_windows_worker_sigbreak_runs_async_cleanup_and_exits_130`、`test_repl_interrupts_only_current_worker_and_continues` + +验证边界:以上选择器来自固定 commit 的源码与既有单测断言,本页未实际运行;`system_tests/test_process_cli_*_live.py` 的 live 行为、双工端到端与 Windows 信号路径未在本轮核对,不据此下结论。审查本模块改动时另见 [审查规则](rules.md)。 + **相关文档** +- [共享 Agent Runtime(jiuwenswarm/runtime)](../agent-runtime/jiuwenswarm-runtime.md) — create_or_resume_session、provision 提交时机与请求规范化契约在 Runtime 侧的实现。 +- [Runtime Session 协调器](../agent-runtime/jiuwenswarm-runtime-session.md) — CHAT_SEND/CHAT_CANCEL 的服务端准入、lane 调度与取消语义。 - `docs/zh/Quickstart.md` — 想了解用户怎样启动和使用 JiuwenSwarm 时读;本模块的机器协议字段以 protocol/ 下的源码为准 - `TESTING.md` — 为 CLI 改动补测试时参考;其中有过期路径,要先对照 tests/ 下现有的用例 diff --git a/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-process-cli-machine.md b/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-process-cli-machine.md new file mode 100644 index 00000000..d32d907c --- /dev/null +++ b/knowledge/repos/jiuwenswarm/components/gateway-channels/jiuwenswarm-process-cli-machine.md @@ -0,0 +1,37 @@ +--- +title: "进程式 CLI 机器执行、会话租约与停机" +created: 2026-10-01 +updated: 2026-10-01 +type: architecture +tags: [jiuwenswarm] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/channels/process_cli/machine.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/channels/process_cli/session_guard.py +--- + +# 进程式 CLI 机器执行、会话租约与停机 + +## 职责和边界 + +说明一次性机器调用如何取得会话租约、绑定根 Agent、判定完成并按顺序清理;REPL worker 和交互会话操作见[进程式 CLI 主页面](jiuwenswarm-channels-process-cli.md)。 + +## 机器模式:会话租约与 Agent 绑定 + +- `_MachineRun.execute` 的准入顺序:调用方带 session_id 时先 `SessionLease(session_id).acquire()` 再 `client.start()`;`describe_session` 找不到报 SESSION_NOT_FOUND;即使调用方给了新 mode 也要 `resolve_mode_capability(descriptor.mode)` 校验既有会话的根;自定义 Agent 在会话创建前先 `validate_agent_definition`;新会话在 `create_or_resume_session` 返回后立刻补租约,随后 `bind_agent` 固定指纹([machine.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/machine.py#L159-L198);测试 `test_custom_agent_is_validated_before_session_and_uses_stream_agent`)。 +- `SessionLease` 用 portalocker 独占锁文件(sessions 目录 `.process_cli_locks/.lock`,`LOCK_EX|LOCK_NB`、timeout=0)实现跨进程互斥:冲突抛 `SESSION_BUSY`(retryable=True),释放后可重用([session_guard.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/session_guard.py#L28-L58);测试 `test_session_lease_is_exclusive_and_reusable`)。 +- `bind_agent` 把会话钉在声明的根 Agent 指纹上:绑定文件 `.process_cli_bindings/.json` 存 `{"schema":1,"fingerprint"}`;指纹不一致或绑定文件损坏分别抛 `AGENT_DEFINITION_SESSION_CONFLICT` / `AGENT_BINDING_INVALID`;已有消息(`message_count>0`)的未绑定会话不能事后采纳自定义 Agent;写入走临时文件 + fsync + `os.replace`([session_guard.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/session_guard.py#L61-L116);测试 `test_binding_survives_new_process_and_rejects_identity_change`,指纹 64 字符)。 +- 恢复(resume)语义:未指定 mode 时继承会话 descriptor 的 mode(全新会话才回退默认 `agent.code.normal`);workspace 字段省略表示继承持久化绑定而不是用调用方当前目录覆盖,只有显式给出的 cwd/project_dir/trusted_dirs 才进 params([machine.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/machine.py#L96-L120)、[L176-L179](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/machine.py#L176-L179);测试 `test_resume_omissions_inherit_runtime_mode_and_do_not_replace_workspace`、`test_missing_or_foreign_resume_never_allocates_or_cleans_session`)。 + +## 机器模式:结果与停机顺序 + +- 结果状态机:流结束但没有任何完成证据(`event.ok` 且 `is_complete`/chat.final)→ `INCOMPLETE_RUN`;无控制通道的运行遇到 `chat.ask_user_question`/`plan.approval_required`/`harness.activate_interaction` → `INTERACTION_REQUIRED`(绝不代答);超时 → TIMEOUT/124;取消 → CANCELLED/130;输出管道断裂 → OUTPUT_CLOSED([machine.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/machine.py#L233-L263)、[L362-L396](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/machine.py#L362-L396);测试 `test_stream_without_completion_evidence_is_not_success`、`test_interaction_never_grants_approval_and_stops_stream`)。 +- 清理按 control_input → cancel → control_streams → stream_close → cleanup_session → runtime_close 的顺序尝试,每个已执行步骤限 5 秒:control 步骤仅在双工时执行;cancel 仅在已有主错误且已构造 request 时执行(双工时 target_request_id 置空,取消整个会话范围);stream_close 和 cleanup_session 也分别要求已有 stream/session。某步失败记名并继续后续步骤;任一步失败且没有主错误时记 `SHUTDOWN_FAILED`,已有主错误则保留并附加 cleanup_errors;清理中首次收到 CancelledError 可设 CANCELLED/130,清理期间命令信号被延迟(`defer_command_signals`),租约在整个清理完成后才释放([machine.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/machine.py#L265-L328)、[L397-L404](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/channels/process_cli/machine.py#L397-L404);测试 `test_every_cleanup_stage_is_bounded_and_later_stages_still_run`、`test_each_cleanup_exception_turns_otherwise_successful_run_into_failure`)。 + +## 怎样验证 + +源码固定在 f0a69728c96b5961d993449f1a901cbd2f4dac5b。租约、绑定、超时与清理的测试选择器见[主页面的验证入口](jiuwenswarm-channels-process-cli.md#怎样验证);静态断言不能替代双工端到端、真实模型端点或 Windows 信号验证。 + +## 相关文档 + +- [进程式 CLI 的 REPL 与 worker 生命周期](jiuwenswarm-channels-process-cli.md) +- [Runtime 交互输入、执行取代与取消](../agent-runtime/jiuwenswarm-runtime-control.md) diff --git a/knowledge/repos/jiuwenswarm/components/launch/_index.md b/knowledge/repos/jiuwenswarm/components/launch/_index.md index 55f07154..3f3dff29 100644 --- a/knowledge/repos/jiuwenswarm/components/launch/_index.md +++ b/knowledge/repos/jiuwenswarm/components/launch/_index.md @@ -1,7 +1,7 @@ --- title: "启动与多实例管理" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: index tags: [jiuwenswarm] sources: [] @@ -9,9 +9,9 @@ sources: [] # 启动与多实例管理 -- [jiuwenswarm 顶层包:启动入口、多实例与运行时补丁](jiuwenswarm.md) +- [jiuwenswarm 顶层包:启动入口、多实例与运行时补丁](jiuwenswarm.md) — start_services/jiuwenswarm-init/dotenv_early 的启动分派、端口回退持久化、子进程环境注入与就绪横幅 - [桌面端应用外壳(channels/desktop)](jiuwenswarm-channels-desktop.md) -- [instance_manager:多实例配置、端口与进程管理](jiuwenswarm-instance-manager.md) +- [instance_manager:多实例配置、端口与进程管理](jiuwenswarm-instance-manager.md) — 端口分配公式与探测语义、instances.yaml 校验、InstanceLock/GatewayLock 与安全停止阶梯 ## 代码快速入口(Direct) @@ -20,4 +20,4 @@ sources: [] | PR 描述在做什么 | 精确规则 | 第一批 live 源码 | |---|---|---| -| start_services、jiuwenswarm-start、debug launcher、multi-instance、--dotenv、--name、… | 入口 | `jiuwenswarm/acp/`、`jiuwenswarm/app.py`、`jiuwenswarm/channels/acp/` | +| start_services、jiuwenswarm-start、debug launcher、multi-instance、--dotenv、--name、… | 入口 | `jiuwenswarm/start_services.py`、`jiuwenswarm/dotenv_early.py`、`jiuwenswarm/init_workspace.py`、`jiuwenswarm/instance_manager/` | diff --git a/knowledge/repos/jiuwenswarm/components/launch/jiuwenswarm-instance-manager.md b/knowledge/repos/jiuwenswarm/components/launch/jiuwenswarm-instance-manager.md index 1aa330f4..6a05ac5d 100644 --- a/knowledge/repos/jiuwenswarm/components/launch/jiuwenswarm-instance-manager.md +++ b/knowledge/repos/jiuwenswarm/components/launch/jiuwenswarm-instance-manager.md @@ -1,10 +1,17 @@ --- title: "instance_manager:多实例配置、端口与进程管理" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/instance_manager/config.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/start_services.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/instance_manager/status.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/instance_manager/lock.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/instance_manager/yaml.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/instance_manager/bootstrap.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/test_instance_manager.py" --- # instance_manager:多实例配置、端口与进程管理 @@ -14,7 +21,7 @@ sources: [] **从这里开始读** - `jiuwenswarm/instance_manager/status.py` — list_all_instances、get_instance_status 查询实例状态,stop_instance_process 停止实例进程 -- `jiuwenswarm/instance_manager/bootstrap.py` — create_bootstrap_env 为实例生成 bootstrap .env;load_instance_bootstrap_by_name 在 argparse 解析后加载它;带有 main() +- `jiuwenswarm/instance_manager/bootstrap.py` — create_bootstrap_env 为实例生成 bootstrap .env;load_instance_bootstrap_by_name 在 argparse 解析后加载它;它是被启动入口调用的模块 - `jiuwenswarm/instance_manager/yaml.py` — load_instances_yaml 和 update_instances_yaml 负责读写并校验 instances.yaml **关键文件** @@ -35,3 +42,30 @@ sources: [] **改动路由** - `jiuwenswarm/instance_manager/` + +## 端口、锁与安全停止的实现细节 + +以下行为在 commit `f0a6972` 上逐条核对,行号链接即证据;未运行任何测试,仅记录用例断言的契约。 + +- **端口分配公式与覆盖**:`BASE_PORTS = {agent_server: 18092, web: 19000, gateway: 19001, frontend: 5173}`,实例端口 = base + index×1000;index 0 固定给默认实例,命名实例按 instances.yaml 声明顺序从 1 开始(声明顺序影响未显式配置端口时的自动计算;已持久化的 ports 以条目值为准)。`JIUWENSWARM__PORT` 环境变量可覆盖 base(供 Docker 等场景),非整数值仅 debug 日志后忽略,未知端口类型回落 10000([config.py:39-51](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/config.py#L39-L51)、[config.py:133-188](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/config.py#L133-L188))。 +- **实例名校验**:1-64 字符、仅字母/数字/下划线/连字符、不能以点开头;保留名按小写比较,集合为 `{default, config, tmp, jiuwenswarm, all}`([config.py:103-122](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/config.py#L103-L122))。注意 dotenv_early 的早期校验只拒绝其中 `default/config/tmp`(见 [jiuwenswarm 顶层包](jiuwenswarm.md) 的 dotenv_early 契约)。 +- **端口占用探测语义**:`is_port_available` 用 `bind()+listen()` 而非 connect(),因此能识别"已死但仍处 LISTENING 的僵尸监听者"(connect 探测会超时误判为空闲);POSIX 上对 EADDRINUSE 再用 SO_REUSEADDR 复探,把只剩 TIME_WAIT/半关闭**连接**套接字的端口判为可用(真实服务自身都设 SO_REUSEADDR),Windows 禁用复探以免掩盖真冲突;host 为回环/wildcard 时同时探测 `::1`(Vite on Windows 常只绑 IPv6 回环);`EADDRNOTAVAIL`/`EAFNOSUPPORT`/`EPROTONOSUPPORT` 视为协议栈不可用而非端口占用([config.py:191-343](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/config.py#L191-L343))。 +- **instances.yaml 读写**:`load_instances_yaml` 校验顶层结构(非空 mapping 缺 `instances` 键报错,空 YAML 归一为 `instances: {}`)、实例名规则、端口类型必须是 4 种之一且为 1-65535 整数、workspace 必须是字符串,错误统一抛带修复建议的 `InstancesYamlError`;`save_instances_yaml` 用同目录临时文件 + `os.replace` 原子替换,失败时清理临时文件——两个实例并发更新时读方永远不会看到半个文件,原子替换保证文件完整,但不保证全局读改写事务:按 workspace 持有的 `InstanceLock` 不能串行化不同实例对同一 instances.yaml 的更新,仍可能丢更新([yaml.py:45-254](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/yaml.py#L45-L254))。条目缺 `ports` 时按声明顺序 index 自动补齐端口([status.py:261-294](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/status.py#L261-L294))。 +- **PID 文件 schema**:`.instance.pid` 是 workspace 下的 JSON(pid/started_at/name),先写临时文件,再删除已有目标并 rename;这避免直接写入半个 JSON,但删除与 rename 之间仍有目标不存在的窗口;schema v2 追加 launcher 的 pid+create_time、pgid/sid、`dedicated_session` 标志和 witnesses(每个子进程的 pid、create_time、`-m` 模块名、`--dotenv` 实路径),用于外置 `setsid` 启动器的归属验证;Windows 上 pgid/sid 为 None、dedicated_session 为 False,launcher 出生时间与可取得的 witnesses 仍保留([lock.py:442-511](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/lock.py#L442-L511)、[start_services.py:460-522](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L460-L522))。 +- **安全停止阶梯**:launcher 终身持有 `InstanceLock`,stop 不抢启动锁;schema v2 记录对存活 PID 还要求 psutil create_time 与记录一致(3 秒容差)才认定未被复用,不一致直接拒绝发信号返回 False;存活且证明专用会话(pgid==sid==pid 且元数据吻合)时 `killpg` 整组;launcher 已死但 witness 证明子进程仍在其原专用组时也 `killpg` 旧组(不做端口扫描);POSIX 存活 launcher 没有专用组证明时只停 launcher PID(旧 schema 也沿用单 PID 行为);launcher 已死且无 witness、或没有 PID 记录时才回退到监听者检查:要求拼接后的 cmdline 包含 `jiuwenswarm` 和 bootstrap 路径字符串,属于子串检查,而非 witness 使用的 `--dotenv` 参数 realpath 精确匹配——检查失败不构成杀进程的许可;成功判定要求端口已释放,最后以 stop 开始时读到的 pid_data 为期望值、在短时 `InstanceLock` 下删除 PID 文件,防止误删并发新启动写入的记录([status.py:591-690](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/status.py#L591-L690)、[status.py:400-526](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/status.py#L400-L526))。 +- **InstanceLock(启动并发锁)**:POSIX 用 `fcntl.flock` 排他锁,Windows 无 fcntl,改用独占创建 + 30 秒 stale 判定(只依据文件 mtime,超龄删除后重试,不核验持有者 PID);launcher 在实例启动全程持有([lock.py:32-34](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/lock.py#L32-L34)、[lock.py:42-198](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/lock.py#L42-L198))。 +- **GatewayLock(每 workspace Gateway 单例)**:动机是防止同一 workspace 起两个 Gateway——两个独立 CronSchedulerService 共享同一 `cron_jobs.json` 可导致重复执行(详见 [定时任务(Cron)运行时](../cron-scheduling/jiuwenswarm-runtime-cron.md))。用 portalocker OS 级锁(POSIX flock / Windows LockFileEx)终身持有:锁文件从不 unlink(杜绝删除竞态),OS 锁放在伴随文件 `.gateway.lock.lock` 上(Windows 强制锁会使被锁文件不可读),元数据 `.gateway.lock` 永不加锁、随时可读;`find_holder` 先确认元数据 PID 存活、再探测 OS 锁确认持有者真存活,避免 PID 复用造成"Gateway 还在跑"误报([lock.py:201-439](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/lock.py#L201-L439))。 +- **默认实例状态判定**:先查 PID 文件;PID 记录不存在或未证明进程仍存活时,对 agent_server/gateway/frontend 端口做占用检测,任一被占即视为运行,并用 netstat(Windows)/lsof(Unix,5 秒超时)反查监听 PID([status.py:89-203](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/status.py#L89-L203))。 +- **bootstrap .env 内容**:整文件重写为 `JIUWENSWARM_DATA_DIR`、`JIUWENSWARM_INSTANCE` 加 4 个端口变量;早期版本的 `_create_basic_bootstrap_env` 按 yaml 声明顺序算 index,不导入任何 jiuwenswarm 模块(供 dotenv_early 在 import 前调用)([bootstrap.py:32-71](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/bootstrap.py#L32-L71)、[bootstrap.py:85-136](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/instance_manager/bootstrap.py#L85-L136))。 + +### 怎样验证 + +- `tests/unit_tests/test_instance_manager.py::TestIssue2788SafeStop` — 复用存活 PID 不发信号、launcher 已死走 witness 而非端口扫描、专用会话整组停、终锁不可得时保留记录。 +- `::TestGatewayLock` — 跨进程互斥、持有者死亡后的接管、`find_holder` 对复用 PID/已释放 OS 锁的判定。 +- `::TestStartServicesFallback`、`::TestPortAvailability`、`::TestUpsertEnvPorts`、`::TestEnvVarOverride`、`::TestInstancesYamlError` — 回退持久化、探测语义、.env 端口 upsert、env 覆盖与 yaml 校验错误。 + +**相关模块** + +- [jiuwenswarm 顶层包:启动入口、多实例与运行时补丁](jiuwenswarm.md) — start_services/jiuwenswarm-init 如何调用本模块做端口检查、回退持久化与停止 +- [定时任务(Cron)运行时:任务模型、存储后端与投递路由](../cron-scheduling/jiuwenswarm-runtime-cron.md) — GatewayLock 防重复调度的对象 cron_jobs.json 的存储实现 +- [jiuwenswarm/common/:公共基础模块](../common-core/jiuwenswarm-common.md) — workspace/home 路径解析(get_user_home)与被拉起进程的监管归属 diff --git a/knowledge/repos/jiuwenswarm/components/launch/jiuwenswarm.md b/knowledge/repos/jiuwenswarm/components/launch/jiuwenswarm.md index 9fa34c17..69e551b5 100644 --- a/knowledge/repos/jiuwenswarm/components/launch/jiuwenswarm.md +++ b/knowledge/repos/jiuwenswarm/components/launch/jiuwenswarm.md @@ -1,10 +1,17 @@ --- title: "jiuwenswarm 顶层包:启动入口、多实例与运行时补丁" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/start_services.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/app.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/dotenv_early.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/init_workspace.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/test_start_services_ready_hint.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/test_start_services_proxy.py" + - "openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/system_tests/test_init_workspace.py" --- # jiuwenswarm 顶层包:启动入口、多实例与运行时补丁 @@ -30,6 +37,34 @@ sources: [] - `jiuwenswarm/acp/subprocess_env.py` — ACP 子进程环境构造的 re-export,按 docstring 计划在阶段 3 随 `acp/` 目录一起删除 - `jiuwenswarm/cli/chat.py` — `channels.cli.chat` 的兼容别名;同目录下的 _terminal/events/gateway_client/render 也是同类别名 +## 启动流程与早期环境加载的实现细节 + +以下行为在 commit `f0a6972` 上逐条核对,行号链接即证据;未运行任何测试,仅记录用例断言的契约。 + +- **参数分派与校验**:`mode` 限定 `all/web/app/dev/debug`(默认 `all`);`--list/--status/--stop/--restart` 是互斥组;`--restart` 只接受 `all/app/web`;`debug` 不能与 `--name` 组合(debug 永远驱动默认实例);`--skip-build` 仅对 `debug` 有意义;后面三种自定义组合校验失败返回 1;非法 mode 或管理选项互斥冲突由 argparse 以 SystemExit(2) 拒绝([start_services.py:1022-1112](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L1022-L1112))。 +- **SIGTERM 与日志初始化**:`main()` 把 SIGTERM 接到 `default_int_handler`,将其转为 KeyboardInterrupt;在 `_run_processes` 或 `_run_instance_with_pid` 的监管循环中触发时清理并返回 130;INFO 级 stdout 日志在 `main()` 内配置而非 import 时,否则 `--list/--status` 的表格会被根 logger 默认 WARNING 级静默吞掉([start_services.py:1158-1173](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L1158-L1173))。 +- **默认实例的端口持久化契约**:默认实例没有 bootstrap .env 也没有 instances.yaml 条目,子进程经 `app.py` 的 `load_dotenv_runtime(get_env_file(), override=True)` 读端口;因此 `_run` 的每次默认实例启动(all/app/web/dev)(无论是否发生回退)都把本次实际端口 upsert 进 `~/.jiuwenswarm/config/.env`,防止上次回退残留(如 `GATEWAY_PORT=20001`)让之后无冲突的启动静默绑定旧端口;写入失败视为致命返回 1([start_services.py:349-382](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L349-L382)、[start_services.py:827-859](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L827-L859)、[app.py:15-40](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/app.py#L15-L40))。 +- **端口冲突回退**:从实例自身 index 向上扫 10 个 index 的完整 4 端口组,并排除其他已配置实例已声明的端口;命中后原地改写内存 config 并持久化(命名实例写 instances.yaml + bootstrap .env,默认实例写 config/.env),持久化失败直接中止启动——否则子进程仍读旧端口并在 bind 时崩溃;区间耗尽返回 1,并按平台给出 `--stop`/netstat/lsof 排查提示([start_services.py:228-346](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L228-L346))。 +- **子进程环境注入**:`_start_process` 为每个子进程注入 `JIUWENSWARM_START_CMD` 与解析后的端口组(`AGENT_SERVER_PORT`、`AGENT_PORT`、`WEB_PORT`、`GATEWAY_PORT`、`FRONTEND_PORT`),设置 `JIUWENSWARM_CLI_PORTS=1` 并删除继承的 `AGENT_SERVER_URL`(Gateway 优先读该 URL,残留值会绕过重映射端口);子进程内 `load_dotenv_runtime(override=True)` 据此保留启动器注入的端口组,不被旧 .env 覆盖([start_services.py:584-615](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L584-L615)、[dotenv_early.py:110-143](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/dotenv_early.py#L110-L143))。 +- **就绪等待与端口横幅**:`_wait_for_services_ready` 对每个已启动进程的目标端口做 TCP connect 探测(`127.0.0.1` 失败再试 `::1`,对应 Vite on Windows 只绑 IPv6 回环);Web UI 一可达就先打印"启动中"部分横幅,全部就绪后刷新为"服务已启动";任一已启动子进程退出立即停止等待;总超时默认 30s,含 `web-dev` 时 45s([start_services.py:687-791](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L687-L791))。 +- **监管与退出路径**:启动器以 0.5s 间隔轮询子进程,任一退出即以其退出码作为启动器退出码;KeyboardInterrupt 返回 130;`finally` 中先对全部存活子进程 `terminate()`,8 秒内未全部退出再 `kill()`([start_services.py:413-457](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L413-L457)、[start_services.py:618-634](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L618-L634))。dev 模式在包安装且缺 `channels/web/frontend/package.json` 时抛 `RuntimeError`;npm 子进程不接收 `--dotenv`,端口靠继承的环境变量传入([start_services.py:525-562](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L525-L562))。 +- **dotenv_early 契约**:每个入口必须在导入其他 jiuwenswarm 模块**之前**调用 `parse_dotenv_early`;它扫描 sys.argv 但不删除参数(argparse 稍后正常解析);`--dotenv` 优先于 `--name`;`--name` 时自行定位 instances.yaml(home 可用 `JIUWENSWARM_HOME` 覆盖),workspace 缺省 `~/.jiuwenswarm-instances/`,bootstrap .env 缺失时现场用按 yaml 顺序和基础端口直接计算的 `_create_basic_bootstrap_env`(函数体不调用共享路径解析;bootstrap 模块本身仍导入 instance_manager 依赖) 生成([dotenv_early.py:146-202](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/dotenv_early.py#L146-L202)、[dotenv_early.py:205-278](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/dotenv_early.py#L205-L278))。导入该模块用 `setdefault` 补入 `GRPC_ENABLE_FORK_SUPPORT=0`、`GRPC_VERBOSITY=ERROR`,已有环境值保留(必须在 grpc 懒初始化前生效,否则 fork 出的工具子进程 stderr 会被 C-core 日志污染)([dotenv_early.py:33-49](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/dotenv_early.py#L33-L49))。注意早期名校验只拒绝空名和 `default/config/tmp`,完整规则(含保留名 `jiuwenswarm`/`all`)留给后续 `validate_instance_name`。 +- **init_workspace 命名实例**:实例运行中拒绝初始化并提示先 `--stop`;初始化后先探测自身 index 端口;仅发现占用时才扫描无冲突端口组(`scan_range=20`,排除其他实例已声明端口)并连同 workspace 写入 instances.yaml 与 bootstrap .env;区间耗尽仅警告并保留原 index 端口,留给启动时回退重试([init_workspace.py:73-143](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/init_workspace.py#L73-L143))。 +- **macOS 系统代理继承**:`_inherit_system_proxy` 仅在 darwin 生效;环境中已存在任何 `http_proxy`/`https_proxy`/`all_proxy`(含空值,显式设置优先)时不覆盖,否则才把 `getproxies_macosx_sysconf` 发现的系统代理写入子进程 env([start_services.py:565-581](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L565-L581))。`_resolve_runtime_ports`(读 `AGENT_SERVER_PORT`/`WEB_PORT`/`GATEWAY_PORT`/`FRONTEND_PORT` 覆盖、非法或超出 1-65535 的值忽略)在本包内没有生产调用方,仅被单元测试直接调用([start_services.py:637-662](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/start_services.py#L637-L662))。 + +### 怎样验证 + +- `tests/unit_tests/test_start_services_ready_hint.py` — 端口横幅(含提前打印与超时省略号)、IPv4/IPv6 探测、子进程端口注入与 `AGENT_SERVER_URL` 删除、`JIUWENSWARM_CLI_PORTS` 抵御旧 .env 覆盖(issue #2749)、子进程提前退出终止等待。 +- `tests/test_start_services_proxy.py` — macOS 代理三态:无显式代理时继承系统代理、显式代理(含空值)保留、非 darwin 平台不动。 +- `tests/unit_tests/test_instance_manager.py::TestStartServicesFallback` — 命名/默认实例回退持久化、区间耗尽返回 1、持久化失败中止、无冲突启动清理残留端口。 +- `tests/system_tests/test_init_workspace.py` — 初始化 CLI 的语言偏好、workspace 创建与集成路径。 + +**相关模块** + +- [instance_manager:多实例配置、端口与进程管理](jiuwenswarm-instance-manager.md) — 端口分配公式、instances.yaml 校验、锁与安全停止的实现细节 +- [jiuwenswarm/common/:公共基础模块](../common-core/jiuwenswarm-common.md) — `app.py` 拉起的 AgentServer/Gateway 子进程由 `common/process_supervision.py` 监管重启;`get_env_file`/`get_user_workspace_dir` 的路径解析也在 common +- [CLI 频道(jiuwenswarm/channels/cli)](../gateway-channels/jiuwenswarm-channels-cli.md) — 端口回退提示中 `jiuwenswarm chat` / TUI 连接方的实现 + **相关文档** - `docs/zh/Quickstart.md` — 改动涉及启动命令、初始化流程或默认启动模式时读 diff --git a/knowledge/repos/jiuwenswarm/components/login-auth/_index.md b/knowledge/repos/jiuwenswarm/components/login-auth/_index.md index 081d98d0..bab18a78 100644 --- a/knowledge/repos/jiuwenswarm/components/login-auth/_index.md +++ b/knowledge/repos/jiuwenswarm/components/login-auth/_index.md @@ -1,7 +1,7 @@ --- title: "登录鉴权与免费模型" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: index tags: [jiuwenswarm] sources: [] @@ -9,7 +9,7 @@ sources: [] # 登录鉴权与免费模型 -- [华为账号登录与免费模型凭据(common/auth)](jiuwenswarm-common-auth.md) +- [华为账号登录与免费模型凭据(common/auth)](jiuwenswarm-common-auth.md) — 覆盖 OAuth/PKCE 登录与待完成登录表、续期并发合并、AES-256-GCM 加密会话存档与跨进程共享、Gateway 凭据消毒、AgentServer 运行时句柄登记表与请求钩子、登录模型目录缓存 ## 代码快速入口(Direct) @@ -18,4 +18,11 @@ sources: [] | PR 描述在做什么 | 精确规则 | 第一批 live 源码 | |---|---|---| -| 登录、华为账号、Account Kit、OAuth、PKCE、id_token、refresh_token、免费模型、积分、APIG、远端配置、auth | 入口 | `jiuwenswarm/common/auth/` | +| 登录、华为账号、Account Kit、OAuth、PKCE、id_token、refresh_token、免费模型、积分、APIG、远端配置、auth | 入口 | `jiuwenswarm/common/auth/service.py`、`jiuwenswarm/common/auth/account_kit.py` | +| 登录会话落盘、加密存档、Gateway/AgentServer 共享登录态 | 入口 | `jiuwenswarm/common/auth/session_store.py` | +| Gateway 转发前的凭据消毒与注入、`jiuwen-login:` 句柄、请求钩子、推送续期 | 入口 | `jiuwenswarm/common/auth/passthrough.py`、`jiuwenswarm/common/auth/login_credentials.py` | +| 登录模型列表、目录缓存、`login_model_settings` | 入口 | `jiuwenswarm/common/auth/model_catalog.py` | + +## 专题入口 + +- [登录模型目录与跨进程凭据句柄](jiuwenswarm-login-models.md) — 说明登录模型在 Gateway 与 AgentServer 间的凭据传递、稳定句柄、请求钩子和目录缓存;OAuth 登录与加密会话持久化见登录主页面。 diff --git a/knowledge/repos/jiuwenswarm/components/login-auth/jiuwenswarm-common-auth.md b/knowledge/repos/jiuwenswarm/components/login-auth/jiuwenswarm-common-auth.md index 09b65cc7..a0d5a105 100644 --- a/knowledge/repos/jiuwenswarm/components/login-auth/jiuwenswarm-common-auth.md +++ b/knowledge/repos/jiuwenswarm/components/login-auth/jiuwenswarm-common-auth.md @@ -1,10 +1,16 @@ --- title: "华为账号登录与免费模型凭据(common/auth)" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/auth/service.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/auth/account_kit.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/auth/session_store.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/auth/passthrough.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/auth/login_credentials.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/auth/model_catalog.py --- # 华为账号登录与免费模型凭据(common/auth) @@ -27,6 +33,42 @@ sources: [] - `jiuwenswarm/common/auth/session_owners.py` — 记录对话会话由哪个账号选了免费模型,让飞书等 IM 通道中没有登录会话的请求也能找回凭据 - `jiuwenswarm/common/auth/net.py` — 登录链路的出网请求封装,以及 SSL 校验开关 +## 登录流程与待完成登录表 + +- 入口是进程级单例 `AuthService`(`get_auth_service`)。`create_authorization_request` 返回 `{authorizeUrl, state, claimToken, expiresIn}`:PKCE verifier 为 32 字节随机数的 base64url、challenge 用 S256,授权请求包含 access_type=offline,用于申请 refresh_token;是否实际返回由服务端响应决定;`expiresIn` 就是 state 有效期 10 分钟(STATE_TTL_S)([service.py L69-L79](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L69-L79)、[account_kit.py L337-L364](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/account_kit.py#L337-L364))。 +- `PendingLogins` 是进程内存表(回调和认领必须落同一个进程):state 为 `token_urlsafe(24)`、claim_token 为 `token_urlsafe(32)`。`begin_callback` 置 `callback_seen=True` 并拒绝同一 state 的第二次回调(授权码一次性,`oauth_callback_replayed`);`finish` 把条目有效期改判为 5 分钟(CLAIM_TTL_S)等发起方来取;claim 在 state 不存在或 claim_token 常量时间比对不符时抛 oauth_state_invalid;discard 对这两种情况静默不操作,只有匹配时才删除([account_kit.py L225-L308](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/account_kit.py#L225-L308))。测试 `test_state_and_claim_token_are_unguessable`、`test_claim_window_restarts_when_callback_lands`、`test_pending_login_expires`。 +- 回调落 ECS 鉴权服务时(配置 `callback_url`):state 末尾拼 `claim_fingerprint(claim_token)`(加盐 SHA-256 取前 32 hex;取哈希是因为 state 会进地址栏、浏览器历史和华为日志),本地构造的指纹须与 ECS 的认领校验约定一致;本页不据此证明远端多实例部署的行为。Gateway 起后台线程按 2s 轮询认领、发起 2 分钟后放慢到 10s、state 过期即放弃;HTTP 202/429/5xx 和网络异常都只是「还没结果」,不当判死,`access_denied` 等错误才是终态;发起方 `claim` 没等到结果时会当场主动去取一次([account_kit.py L38-L49](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/account_kit.py#L38-L49)、[L311-L319](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/account_kit.py#L311-L319)、[L380-L426](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/account_kit.py#L380-L426);[service.py L81-L112](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L81-L112)、[L161-L173](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L161-L173))。 +- `complete_callback` 的失败也会登记进 pending,发起方不必干等到过期:`access_denied`→`oauth_access_denied`、其他回调错误→`oauth_callback_failed`、缺授权码→`oauth_callback_missing_params`、store.create 抛异常→session_store_failed;当前 _flush 自身的落盘异常只告警并保留内存登录态。换码结果从 id_token 取 `openid`/`sub` 作 user_id、`exp` 作过期时刻(缺失回退 1 小时),响应没带 refresh_token 时沿用旧值([service.py L114-L159](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L114-L159)、[account_kit.py L196-L217](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/account_kit.py#L196-L217))。测试 `test_session_store_failure_is_reported_to_the_waiting_client`。 +- 登录成功不等模型目录:会话建好后另起 `auth-catalog-refresh` 后台线程去拉 APIG(不通时最长 10 秒),认领立即返回;认领成功后重拉列表时目录还没好,`models.list` 会自己再发现一次([service.py L150-L158](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L150-L158))。测试 `test_login_result_is_released_without_waiting_for_model_discovery`。 + +## 凭据续期与并发合并 + +- 有效期取 token 的 exp,缺失时默认 1 小时;`REFRESH_AHEAD_S=5 分钟`内到期就续。`try_refresh` 按 user_id 加锁,拿到锁后**重读一次会话**做双重检查——重读按存档 mtime 同步磁盘,能发现另一个进程(AgentServer)刚刷好的结果。结果四分:会话已注销→`None`;续期失败且已过期→`None`(须重新登录);续期失败但旧 token 没过期→照常返回旧会话;`update_credential` 返回 `None`(续期期间被登出)→新凭据丢弃、按失败处理([service.py L39-L41](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L39-L41)、[L260-L288](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L260-L288))。测试 `test_concurrent_refresh_happens_only_once`、`test_refresh_result_discarded_when_session_logged_out`、`test_failed_refresh_of_expired_token_reports_none`、`test_failed_early_refresh_keeps_the_still_valid_token`。 +- 转发热路径用 `ensure_fresh(blocking=False)`:续期丢后台线程(按 user_id 去重,不逐请求起线程),未过期就先带旧 token 发这一次。`live_session`/`resolve_id_token` 取不到凭据抛 `ModelAuthRequired`,reason 只有两种:`not_logged_in` / `session_expired`;**没有会话 id 就是没登录**,绝不回落「本机唯一登录会话」([service.py L234-L250](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L234-L250)、[L290-L305](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L290-L305)、[L335-L357](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L335-L357))。测试 `test_non_blocking_caller_uses_old_token_and_refreshes_in_background`、`test_background_refresh_is_not_spawned_per_request`、`test_no_session_id_never_borrows_the_machines_login`。 +- 已过期的凭据是例外:`expired_login_session` 只查内存/本地缓存判断「这次请求用登录模型且该会话凭据已过期」,它向调用方返回需续期的会话;转发前的线程续期接线需沿 Gateway 调用方核对,续期失败仍可能需要重新登录([passthrough.py L139-L156](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/passthrough.py#L139-L156))。测试 `test_first_message_after_idle_gets_a_refreshed_token`。 +- `logout` 不带 session id 什么都不做——丢了 cookie 的浏览器点退出,不能清掉这台机器其他浏览器正在用的登录;登出同时清模型目录缓存,避免下一个用户看到上一个用户的模型([service.py L185-L197](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/service.py#L185-L197))。测试 `test_logout_without_session_id_touches_nobody`。 + +## 加密会话存档与跨进程共享 + +- 存档 `auth/sessions.json` 形如 `{"v":2,"payload":<信封>}`;信封为 `{version:1, alg:"A256GCM", kid, iv, tag, data}`(tag 与密文分开存是为了和 relay-claw 的 encryptJson 互读,版本位用于区分「格式不对」和「密钥不对」)。主密钥 auth/.install_key 保存 base64url 编码的 32 字节;创建时尝试 chmod 0600,平台不支持时仅记 debug 日志([session_store.py L34-L42](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/session_store.py#L34-L42)、[L141-L191](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/session_store.py#L141-L191))。 +- 密钥生命周期:新建走「不覆盖」原子发布(POSIX 用 `os.link`、Windows 用 `rename`),两个进程首次启动撞上时用先落盘那把;读不了直接抛出——杀毒/备份工具短暂占用时重建会覆盖好密钥,之前保存的会话从此解不开;只有内容确实损坏才改名 `.corrupt-` 留档再重建、旧会话随之作废。`derive_local_secret(label)=sha256(label+主密钥)` 派生专用 HMAC 密钥,不把加密密钥本身交出去([session_store.py L83-L138](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/session_store.py#L83-L138))。测试 `test_key_created_by_another_process_first_wins`、`test_unreadable_key_file_is_never_overwritten`、`test_corrupt_key_file_is_kept_aside_and_regenerated`。 +- 存档读不出(v≠2)、解不开或损坏 → 一律按未登录处理,不做迁移,重新登录即可;会话 TTL 30 天按 `created_at` 起算(比 1 小时的 id_token 长,给续期留时间);同一账号重复登录直接顶掉旧会话,不残留多份凭据([session_store.py L276-L298](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/session_store.py#L276-L298)、[L343-L389](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/session_store.py#L343-L389))。测试 `test_unknown_archive_version_is_treated_as_logged_out`、`test_relogin_replaces_old_session`。 +- 跨进程存档同步(Gateway 写、AgentServer 读):读按存档 mtime 判断要不要重载,文件没变不重复加载;所有写路径(create / update_credential / remove / clear)写前强制重读一次——文件系统时间戳粒度不足时,减少陈旧内存快照覆盖其它进程更新的情况;这不是跨进程写事务锁,也不保证所有并发写无丢失(「注销后又复活」);`_flush` 把整份内存快照加密写 tmp 后原子替换,并记下自己写出的 mtime(自己的写不触发重载);落盘失败仅内存生效并告警([session_store.py L300-L321](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/session_store.py#L300-L321)、[L343-L407](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/session_store.py#L343-L407))。测试 `test_reloads_when_another_process_writes`、`test_logout_preserves_other_process_update_when_archive_mtime_is_unchanged`。 + +## 登录模型目录与跨进程凭据句柄 + +该工作流的职责、顺序、错误边界与源码入口见[登录模型目录与跨进程凭据句柄](jiuwenswarm-login-models.md)。 + +## 怎样验证 + +聚焦单测都在 `tests/unit_tests/common/` 下:`test_auth_account_kit.py`(OAuth 配置/PKCE/换码/续期原语)、`test_auth_service_callback.py`(回调登记与目录后台拉取)、`test_auth_refresh_coalescing.py`(续期合并与失败区分)、`test_auth_session_store.py` 与 `test_auth_session_crypto.py`(落档与密钥)、`test_auth_passthrough.py`(消毒/注入/闲置首条消息)、`test_auth_login_credentials.py` 与 `test_auth_login_credential_refresh.py`(登记表/钩子/推送续期)、`test_auth_model_catalog.py`(目录缓存)。以上均为单元级验证,真实华为 OAuth/ECS/APIG 链路不在其中。 + +**相关文档** + +- [jiuwenswarm/common 公共基础模块](../common-core/jiuwenswarm-common.md) — config.yaml 的读写边界;`models.login_model_settings` 和 user workspace 路径落在它管的配置与工作区里 +- [AgentServer 运行时会话](../agent-runtime/jiuwenswarm-runtime-session.md) — 请求级登录模型条目装配进 runtime 后的会话/执行行为 +- [Gateway 通道](../gateway-channels/jiuwenswarm-channels-cli.md) — 登录状态在通道侧的呈现与转发入口 + **改动路由** - `jiuwenswarm/common/auth/` diff --git a/knowledge/repos/jiuwenswarm/components/login-auth/jiuwenswarm-login-models.md b/knowledge/repos/jiuwenswarm/components/login-auth/jiuwenswarm-login-models.md new file mode 100644 index 00000000..b4691e23 --- /dev/null +++ b/knowledge/repos/jiuwenswarm/components/login-auth/jiuwenswarm-login-models.md @@ -0,0 +1,45 @@ +--- +title: "登录模型目录与跨进程凭据句柄" +created: 2026-10-01 +updated: 2026-10-01 +type: architecture +tags: [jiuwenswarm] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/auth/login_credentials.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/auth/model_catalog.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/common/auth/passthrough.py +--- + +# 登录模型目录与跨进程凭据句柄 + +## 职责和边界 + +说明登录模型在 Gateway 与 AgentServer 间的凭据传递、稳定句柄、请求钩子和目录缓存;OAuth 登录与加密会话持久化见登录主页面。 + +## Gateway 凭据消毒与句柄注入 + +- `normalize_model_auth` 就地改 params:**无条件先删**客户端传来的 `_model_auth` 键(原值非 None 时告警),前端塞什么都作废;之后仅当「有 session_id + APIG 已配置 + 是登录模型(`#index` 后缀取裸名比对)」才由服务端填 `{api_base, api_key(id_token), credential_ref}`。这条路径上所有取数都 `allow_refresh=False`(不为目录或 token 发同步 HTTP);取不到凭据不抛错、只是不注入——AgentServer 侧解析模型时会走到同样的判断并给出面向用户的提示([passthrough.py L27-L136](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/passthrough.py#L27-L136))。测试 `test_client_supplied_auth_is_always_dropped`、`test_request_without_session_gets_nothing_even_if_someone_is_logged_in`、`test_catalog_lookup_never_triggers_a_network_refresh`、`test_expired_session_injects_nothing_instead_of_raising`。 +- `credential_ref` = HMAC-SHA256(派生密钥, `jiuwen-login-credential:`) 的前 32 位 hex:密钥派生自本机主密钥,单拿 openid 算不出句柄;**按账号不按会话**——重新登录会换 session id,句柄不变,新 token 经带凭据请求或更新推送登记后,在跑的成员下一次调用可查到更新值(会话存储同一账号只留一个会话,正好配套)([login_credentials.py L128-L151](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/login_credentials.py#L128-L151))。测试 `test_credential_ref_is_keyed_so_openid_alone_does_not_give_it`、`test_logging_in_again_keeps_the_credential_ref`。 +- `refreshed_credential_for_ref` 响应 AgentServer 发起的续期:按句柄逐账号重算 HMAC 找会话(单向、本机会话少);找不到会话、或续不了且已过期 → 回 `{credential_ref, revoked: True}` 让 AgentServer 立即停用;暂时失败但旧 token 还能用 → 照旧返回旧 token,AgentServer 按重试节奏再来([passthrough.py L89-L121](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/passthrough.py#L89-L121))。测试 `test_gateway_revokes_when_expired_and_refresh_fails`、`test_gateway_keeps_the_old_token_when_refresh_fails_but_it_is_still_valid`、`test_gateway_finds_the_account_after_it_logged_in_again`。 + +## AgentServer 句柄登记表与请求钩子 + +- 模型配置里的 api_key 只是占位值 `jiuwen-login:`:真 token 一小时一换,而配置交出去收不回(集群成员创建时就定了 api_key,追问投递给在跑的团队不会重建成员;SDK 还按 `(api_key, api_base, …)` 缓存 HTTP 客户端)。带凭据的请求到达时 `login_auth_from_params` 校验 api_base/api_key/ref(32 位 hex)后登记进本进程表,登记 TTL 2 小时;该登录模型条目的 api_key 使用占位句柄,真 token 留在登记表中([login_credentials.py L1-L29](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/login_credentials.py#L1-L29)、[L50-L68](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/login_credentials.py#L50-L68)、[L302-L315](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/login_credentials.py#L302-L315))。测试 `test_forwarded_credentials_are_registered_and_replaced_by_a_placeholder`、`test_incomplete_credentials_count_as_absent`。 +- 推送通道只能换 token、不能造登记:`update_token` 只更新已登记的句柄且接入点不变(推送改不了 token 发往哪里);收到 `revoked: True` 立即注销句柄,不必等 token 自然过期([login_credentials.py L181-L223](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/login_credentials.py#L181-L223))。测试 `test_push_cannot_create_a_registration`、`test_revoke_stops_the_handle_immediately`。 +- httpx 请求钩子 `inject_login_credential`(幂等包住 SDK `_build_async_openai_client`、往 event_hooks **追加**而非替换):只处理 Bearer 占位值。登记过期/查不到 → 删掉 Authorization 放行,已安装且执行该钩子时,占位 Authorization 不会发出;SDK 钩子安装失败不能据此保证同样行为;目标地址不在登记时的接入点之下(协议 + 主机含端口一致、路径在其下)也删头放行,避免该钩子把登记 token 注入其他接入点;只有真正替换成功才记「在用」([login_credentials.py L343-L429](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/login_credentials.py#L343-L429))。 +- 无新请求时的续期:run_refresh_requests 被宿主启动后,默认每 60 秒扫登记表,把「15 分钟内到期 + 最近 20 分钟在用(窗口要盖过长工具调用)+ 距上次请求 ≥2 分钟」的句柄经 server_push `auth.credential.refresh` 只带句柄请 Gateway 续(登记与请求钩子更新时间戳,超过活跃窗口后不再请求续期);扫描/发送失败记日志下一轮再试,任务不退出([login_credentials.py L226-L277](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/login_credentials.py#L226-L277))。测试 `test_active_and_expiring_handle_is_due`、`test_idle_handle_is_not_refreshed`、`test_requests_are_throttled_until_retry_interval`。 + +## 登录模型目录缓存 + +- 模型列表是**发现**出来的:拿该会话的 id_token 调 APIG,按远端白/黑名单过滤(id 或 display name 小写比对、去重)后写入 `auth/model_catalog.json`(TTL 30 分钟);内存 memo 有效 = TTL 未过**且**磁盘 mtime 没变(多进程下 Gateway 写缓存、AgentServer 读)。**活动没生效(拉不到配置或 `is_effective=false`)一个都不给**,哪怕本地还有热缓存——这一条统一管住 models.list、目录缓存和请求级凭据透传三个下游([model_catalog.py L34-L47](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/model_catalog.py#L34-L47)、[L148-L173](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/model_catalog.py#L148-L173)、[L200-L252](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/model_catalog.py#L200-L252))。测试 `test_campaign_off_yields_no_models_even_with_a_warm_cache`、`test_get_models_uses_cache_without_network`。 +- 发现失败进 30 秒冷却(APIG 不通时不让每次 models.list 都撞一次);「没登录」不算发现失败、不进冷却,登录后立刻能再发现;登出/换号 `clear_cache` 连缓存文件一起删([model_catalog.py L120-L129](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/model_catalog.py#L120-L129)、[L176-L197](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/model_catalog.py#L176-L197))。测试 `test_failed_discovery_is_not_retried_on_every_models_list`、`test_not_logged_in_does_not_start_the_cooldown`。 +- `build_model_entry` 供两个调用方共用:Gateway 列表展示(api_key 留空)和 AgentServer 请求级构建(api_key 放占位句柄),**都不放真 id_token**、条目形状必须一致。条目带 `source="huawei-maas-login"` + `read_only` + `is_free`;`is_default: True` 的语义是「进聊天窗口的模型下拉」而非默认模型(配置的模型排前面);用户可调的 `context_window` 单独存在 config.yaml `models.login_model_settings.<模型名>`、造条目时合入;写入侧由 `is_login_model` 拦住它们不落 config.yaml([model_catalog.py L255-L322](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/model_catalog.py#L255-L322)、[L325-L369](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/common/auth/model_catalog.py#L325-L369))。测试 `test_build_model_entry_shape`、`test_list_entries_use_apig_invoke_base_and_carry_no_token`、`test_is_login_model`。 + +## 怎样验证 + +源码链接固定在 f0a69728c96b5961d993449f1a901cbd2f4dac5b。聚焦单测选择器和验证范围见[原模块页面](jiuwenswarm-common-auth.md#怎样验证);单元契约不能替代真实服务、模型端点或跨平台集成验证。 + +## 相关文档 + +- [原模块架构与入口](jiuwenswarm-common-auth.md) +- [相邻模块](../common-core/jiuwenswarm-common-model-catalog.md) diff --git a/knowledge/repos/jiuwenswarm/components/observability/_index.md b/knowledge/repos/jiuwenswarm/components/observability/_index.md index 5a67504a..610bf2a2 100644 --- a/knowledge/repos/jiuwenswarm/components/observability/_index.md +++ b/knowledge/repos/jiuwenswarm/components/observability/_index.md @@ -1,7 +1,7 @@ --- title: "可观测性与轨迹存储" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: index tags: [jiuwenswarm] sources: [] @@ -9,7 +9,7 @@ sources: [] # 可观测性与轨迹存储 -- [可观测性:Agent 轨迹采集、存储与保留(jiuwenswarm/observability)](jiuwenswarm-observability.md) +- [可观测性:Agent 轨迹采集、存储与保留(jiuwenswarm/observability)](jiuwenswarm-observability.md) — 准入身份校验与跨会话主体拒绝、有界双队列与快照合并/溢出计数、locked/busy 单次重试与冲突 first-wins、每会话 SQLite 路由与孤儿缓冲、Session 删除墓碑三段式、整页保留与检查点、查看器安全 OTLP 校验 ## 代码快速入口(Direct) @@ -19,3 +19,7 @@ sources: [] | PR 描述在做什么 | 精确规则 | 第一批 live 源码 | |---|---|---| | observability、trajectory、轨迹、OTLP、trace、span、sink、SQLite、retention、保留、turn、sessi… | 入口 | `jiuwenswarm/observability/` | + +## 专题入口 + +- [轨迹保留、检查点与查看器投影边界](jiuwenswarm-trajectory-retention.md) — 说明已存轨迹的整页保留、检查点、OTLP 投影与持久化类型校验;记录准入、队列、writer 和会话删除见可观测性主页面。 diff --git a/knowledge/repos/jiuwenswarm/components/observability/jiuwenswarm-observability.md b/knowledge/repos/jiuwenswarm/components/observability/jiuwenswarm-observability.md index 119a83f9..a91e08cf 100644 --- a/knowledge/repos/jiuwenswarm/components/observability/jiuwenswarm-observability.md +++ b/knowledge/repos/jiuwenswarm/components/observability/jiuwenswarm-observability.md @@ -1,16 +1,29 @@ --- title: "可观测性:Agent 轨迹采集、存储与保留(jiuwenswarm/observability)" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/sink.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/store.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/session_delete.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/retention.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/otlp_payload.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/models.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/observability/test_trace_sink.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/observability/test_session_trajectory_routing.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/observability/test_trajectory_session_delete.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/observability/test_trajectory_retention.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/observability/test_trace_store.py --- # 可观测性:Agent 轨迹采集、存储与保留(jiuwenswarm/observability) 在 AgentServer 中接收 Core 产出的 OTLP span 记录,按会话写入本地 SQLite 轨迹库,并为轨迹查看器提供读取查询、按页保留清理和会话删除。它也负责维护会话的轮次(turn)身份,并把已提交的轨迹修订提示跨进程推送到 Gateway。 +本页源码结论固定在 upstream commit `f0a6972`(f0a69728c96b5961d993449f1a901cbd2f4dac5b),行号见各 GitHub blob 链接;结论来自对该 checkout 的静态阅读,未执行任何测试。 + **从哪里读起** - `jiuwenswarm/observability/runtime.py` — AgentServer 的轨迹运行时生命周期:start/sync/shutdown_trajectory_runtime 向 Core 注册或注销记录消费者;get_trajectory_runtime_sink 返回当前的 sink @@ -30,3 +43,44 @@ sources: [] **改动路由** - `jiuwenswarm/observability/` + +## 数据怎样流动 + +### 准入:只验身份,载荷留给写线程 + +- 路由器 `_consume` 在调用线程上只校验身份:`session_id` 去空白后必须与原值一致(需要 strip 才干净即视为 malformed)、`trace_id`/`span_id` 去空白小写后非空,失败计 `failed` 不入队;`raw_json` 完全不碰——Core 惰性编码它,读它会拖回完整 OTLP encode,载荷类型检查推迟到写线程的 `TraceRecordData.from_core_*`,`_write_batch` 拒绝单条坏记录而不让整批失败([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L815-L866)、[sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L477-L497))。 +- 跨会话主体拒绝:`_record_owner_is_consistent` 要求 `execution_subject_session_id` 为空、等于 `session_id` 或以 `{session_id}_sub_` 开头,否则计 `failed` 并打 warning;final 记录与快照共用该检查([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L62-L70)、[sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L169-L177))。测试:`test_sink_rejects_subject_session_owned_by_another_chat`、`test_sink_accepts_team_member_subject_session`。 + +### 有界队列与溢出行为 + +- `TrajectoryRecordSink` 有两条独立有界队列(final 记录与流帧,`maxsize=queue_size`)加一个 `_snapshot_pending` OrderedDict;final 记录队列满时 `put_nowait` 抛 `Full`,非阻塞计 `dropped`+`dropped_final`,日志明确"SQLite fan-out dropped the record while other configured exporters remain unaffected"([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L90-L102)、[sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L190-L205);`test_sink_queue_full_drops_without_blocking` 断言 elapsed < 1.0s)。帧走独立队列,帧洪峰不会挤掉唯一的权威 final 记录(`test_frame_flood_never_displaces_a_final_record`)。 +- 快照按 `(trace_id, span_id)` 最新 revision 合并:不大于已排队版本的 revision 计 `stale_ignored`;入队前 `record_revision` 必须可解析且 ≥1、身份非空否则计 `failed`;容量按 `_queue.qsize()+len(pending) >= queue_size` 判满,满时有 pending 才逐出最旧 pending 计 `evicted_provisional`;没有可逐出的 pending 则丢弃新快照([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L209-L259))。 +- 流帧绝不合并(每帧是后帧不会重述的增量),帧队列满计 `dropped_frames`,日志说明 live stream 将出现空洞直到该 span 的完整输出到达;单批最多 `max(batch_size, 512)` 帧([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L261-L289)、[sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L423-L440))。 + +### 批量提交与重试语义 + +- `_write_records_with_retry` 只重试消息含 "locked"/"busy" 的 `sqlite3.OperationalError`,且只有一次(0.05s 延迟),其余异常直接抛;整批失败时日志"records remain available only through existing exporters"并计 `failed`,记录不重新入队(避免无限 drain)。注释说明一次重试使两个 5 秒 busy 等待仍落在默认 15 秒 close 期限内([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L42-L44)、[sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L497-L537))。测试:`test_sink_retries_transient_sqlite_contention`。 +- records 与 frames 在 `write_records` 的同一 `BEGIN IMMEDIATE` 事务中提交(两个独立 watermark 原子推进);成功后 `committed+=inserted`、`conflicts+=conflicts`,再在锁外回调 `on_commit(updates)`,回调异常只记日志不上抛([store.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/store.py#L465-L483)、[sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L503-L512))。 +- final 记录冲突 first-wins:`INSERT ... ON CONFLICT(trace_id, span_id) DO NOTHING`,rowcount=0 时比较 `raw_sha256`(未压缩字节摘要,压缩不影响冲突判定);相同则仍更新 current 行,不同则写 `otlp_record_conflicts` 并保留第一条原始记录([store.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/store.py#L500-L601);`test_store_preserves_exact_raw_and_records_hash_conflict`)。`discard_final_span_frames` 开启时,帧先尝试追加再由同批终态记录清理([store.py:763-789](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/store.py#L763-L789));此开关下,终态记录已落库后到达的迟到帧直接不入库([store.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/store.py#L604-L607)、[store.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/store.py#L719-L721))。 + +### 每会话路由、孤儿缓冲与 writer 退役 + +- 路由线程为每个会话惰性创建独立 writer(`replace(settings, database_path=session_database_path(...))`);`sink.start()` 失败的 writer 仍保持注册、靠自身计数器拒绝记录,避免逐条记录重试初始化拖住其它会话(`test_blocked_session_writer_does_not_delay_another_session`)([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L905-L919))。 +- 无 `session_id` 的记录进 `_orphan_pending` 有界缓冲,键为 `(trace_id, span_id[, sequence])`——帧额外加 `sequence`,因为帧是增量,仅按 span 键会让新帧顶掉旧帧留下空洞;满时逐出最旧,同 trace 的会话内 span 到达时一并路由,关闭时全部丢弃计数([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L944-L980))。 +- writer 空闲 ≥300s 且 `sink.is_idle()` 才退役;`close(timeout=1.0)` 未停下的 writer 保持注册,防止对同一数据库开出第二个连接(`test_idle_retirement_keeps_writer_registered_when_close_times_out`)([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L45)、[sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L921-L942))。 + +### 会话删除 begin/abort/commit + +- 墓碑先于排水:`lifecycle.begin` 先置 PREPARED(`accepts_records` 立即为 false),再调 `backend.begin_session_delete`;backend 抛错则回滚墓碑并上抛([session_delete.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/session_delete.py#L87-L103))。墓碑会话的迟到记录在路由器 `_consume`(锁内外各查一次 `trajectory_session_accepts_records`)计 `dropped`,数据库不会被重新打开(`test_late_record_is_rejected_while_session_is_tombstoned`)([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L841-L851))。 +- `abort` 无论 backend 回滚是否抛错都在 finally 中移除墓碑(`test_abort_releases_tombstone_even_when_backend_rollback_fails`);`lifecycle.commit` 先置 PREPARED,再交给 backend.commit;当前路由 backend 会先 begin 排水,再 unlink 数据库与 -wal/-shm sidecar,无 backend 时直接删除这些文件,可重复调用;committed 墓碑上限 1024、prepared 永不逐出(`test_committed_tombstones_are_bounded`)([session_delete.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/session_delete.py#L105-L137)、[session_delete.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/session_delete.py#L165-L172))。 +- 路由器 `begin_session_delete` 只等待目标会话的 `_ingress_pending` 归零(不做进程级队列 join),超时抛 `RuntimeError`;writer 未在期限内停止同样抛 `RuntimeError`(`test_session_delete_waits_only_for_target_ingress`)([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L715-L741))。 + +## 轨迹保留、检查点与查看器投影边界 + +该工作流的职责、顺序、错误边界与源码入口见[轨迹保留、检查点与查看器投影边界](jiuwenswarm-trajectory-retention.md)。 + +## 怎样验证 + +上游自带聚焦单测位于 `tests/unit_tests/observability/`:`test_trace_sink.py`(队列溢出、快照合并、重试:`test_sink_queue_full_drops_without_blocking`、`test_sink_coalesces_pending_live_snapshots_by_identity`、`test_sink_retries_transient_sqlite_contention`)、`test_session_trajectory_routing.py`(路由隔离与清扫)、`test_trajectory_session_delete.py`(墓碑三段式)、`test_trajectory_retention.py`(整页删除与检查点累积)、`test_trace_store.py`(冲突、schema 丢弃、压缩往返)。本页引用的均为测试选择器,本轮未执行。 + +相邻知识:产品层会话生命周期与删除门见 [Runtime Session 协调器](../agent-runtime/jiuwenswarm-runtime-session.md),共享 Runtime 的会话 create/switch/fork/delete 语义见 [共享 Agent Runtime](../agent-runtime/jiuwenswarm-runtime.md)。 diff --git a/knowledge/repos/jiuwenswarm/components/observability/jiuwenswarm-trajectory-retention.md b/knowledge/repos/jiuwenswarm/components/observability/jiuwenswarm-trajectory-retention.md new file mode 100644 index 00000000..576352d8 --- /dev/null +++ b/knowledge/repos/jiuwenswarm/components/observability/jiuwenswarm-trajectory-retention.md @@ -0,0 +1,44 @@ +--- +title: "轨迹保留、检查点与查看器投影边界" +created: 2026-10-01 +updated: 2026-10-01 +type: architecture +tags: [jiuwenswarm] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/models.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/otlp_payload.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/retention.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/sink.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/observability/store.py +--- + +# 轨迹保留、检查点与查看器投影边界 + +## 职责和边界 + +说明已存轨迹的整页保留、检查点、OTLP 投影与持久化类型校验;记录准入、队列、writer 和会话删除见可观测性主页面。 + +### 保留、检查点与过期清扫 + +- 整页删除不变量:一页可删当且仅当页内每条记录 lifecycle ∈ {final, abandoned} 且 `created_at < cutoff`;每个 subject 只删最旧连续前缀;跨 subject 共享的页 key 全删或全不删(阻塞集不动点迭代);trace 级残留记录只在整 trace 全部 settled 且无剩余 turn 页时随 trace 删除([retention.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/retention.py#L541-L542)、[retention.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/retention.py#L596-L630)、[retention.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/retention.py#L633-L705))。测试:`test_retention_removes_only_the_oldest_contiguous_run_of_expired_turns`、`test_a_running_record_holds_back_its_turn_and_every_later_one`、`test_a_turn_shared_by_subagents_goes_only_when_every_subject_may_lose_it`。只有 viewer 展示的 trace 参与按 subject 组页:须有记录声明已知 agent mode 且无外 mode;未组页记录仍可能在整条 trace 已 settled、过期且无剩余 turn 页时被删除([retention.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/retention.py#L545-L557))。 +- `delete_expired` 单事务完成:删页 → 每 subject `advance_checkpoint` 写 `trajectory_retention_checkpoints`(消息列表按内容寻址序列存储)→ 递归可达性回收无主 blobs/sequences → 实际删除记录数大于零时轮换 store_epoch 让读者重建 → commit 后同样仅在有删除时 incremental_vacuum 缩文件([store.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/store.py#L1033-L1062)、[store.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/store.py#L1286-L1319);`test_retention_rotates_the_epoch_and_shrinks_the_file`、`test_retention_keeps_referenced_content_and_reclaims_the_rest`)。 +- 两层过期:库内保留在每个会话 writer 线程启动时与每 3600s 执行(启动时保留清理失败仅记日志不阻断;SQLite 初始化失败仍会令 writer 启动失败);文件级由路由线程每 3600s(首个空闲时刻先扫一次)按 `mtime < now - retention_days*86400` 删除无 live writer 的会话库及 sidecar([sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L363-L398)、[sink.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/sink.py#L759-L813);`test_stale_session_database_is_swept_by_mtime`、`test_sweep_skips_session_with_live_writer`、`test_router_start_runs_initial_sweep`)。 + +### 查看器安全的 OTLP 校验 + +- `strict_otlp_payload` 拒绝 NaN/Infinity 常量与非有限浮点、要求顶层为带 `resourceSpans` 数组的 dict、嵌套 ≤256(先 C 级计数再字符串感知字节遍历);`parse_otlp_payload` 捕获 `RecursionError/TypeError/ValueError/OverflowError` 返回 None——不可解析载荷只以原始字节到达查看器、永不投影([otlp_payload.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/otlp_payload.py#L27-L64)、[otlp_payload.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/otlp_payload.py#L113-L139))。 +- `record_view_facts` 在写入时提取一次为列:`projected` 当且仅当载荷可解析且唯一 span 的 `traceId`/`spanId` 等于记录自身身份;`turn_number` 需为正且 ≤ 2^53-1(按查看器 BigInt 语义读取)([retention.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/retention.py#L322-L371);`test_only_records_the_viewer_projects_belong_to_a_turn`)。 + +### 持久化边界类型与 schema 处置 + +- `TraceRecordData.from_core_record`:`raw_json` 非 bytes-like 抛 `TypeError`;身份缺失或时间戳为负抛 `ValueError`;快照 lifecycle 只接受 running/provisional 并统一归一为 running,`record_revision < 1` 抛 `ValueError`([models.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/models.py#L254-L268)、[models.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/models.py#L322-L337))。帧文本逐字保留(前导/尾随空格是内容,trim 会粘住单词),`sequence` 必须 ≥0、`session_id` 必需([models.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/models.py#L151-L193)、[models.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/models.py#L447-L458))。 +- schema `user_version` ∉ {0, 6} 时整库连同 `-wal`/`-shm` 删除重建而非迁移(`test_incompatible_schema_version_discards_database`);`initialize` 时把 lifecycle=running 的行标记 abandoned(`update_kind='recovered'`,`test_store_restart_marks_unfinished_snapshot_abandoned`)([store.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/store.py#L61-L64)、[store.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/store.py#L410-L452)、[store.py](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/observability/store.py#L1000-L1031))。 + +## 怎样验证 + +源码链接固定在 f0a69728c96b5961d993449f1a901cbd2f4dac5b。聚焦单测选择器和验证范围见[原模块页面](jiuwenswarm-observability.md#怎样验证);单元契约不能替代真实服务、模型端点或跨平台集成验证。 + +## 相关文档 + +- [原模块架构与入口](jiuwenswarm-observability.md) +- [相邻模块](../agent-runtime/jiuwenswarm-runtime-session.md) diff --git a/knowledge/repos/jiuwenswarm/components/symphony-orchestration/_index.md b/knowledge/repos/jiuwenswarm/components/symphony-orchestration/_index.md index 5b2bcfdf..2ee7282c 100644 --- a/knowledge/repos/jiuwenswarm/components/symphony-orchestration/_index.md +++ b/knowledge/repos/jiuwenswarm/components/symphony-orchestration/_index.md @@ -1,7 +1,7 @@ --- title: "Symphony 编排与能力图谱" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: index tags: [jiuwenswarm] sources: [] @@ -9,9 +9,24 @@ sources: [] # Symphony 编排与能力图谱 -- [Symphony 编排集成(jiuwenswarm/symphony)](jiuwenswarm-symphony.md) -- [Symphony 动态图谱演化(evolution)](jiuwenswarm-symphony-evolution.md) -- [Symphony 共享工具(symphony/shared)](jiuwenswarm-symphony-shared.md) +## 什么时候查这里 + +- 调查能力图谱(capability graph)的构建、增量状态、版本化存储与取消/恢复行为。 +- 调查能力指纹(fingerprint)提取、缓存失效与 LLM 适配策略。 +- 调查 Skill 检索索引(taxonomy)、经验候选(experience candidate)的推送与安装、动态 overlay 与演化开关。 + +## 不放什么 + +- Agent 运行时与演化 rail 的接线放 `components/agent-runtime/`。 +- evolution 审批消息在网关/通道侧的处理放 `components/gateway-channels/`。 + +## 目录内容 + +| 遇到什么 | 查看哪里 | 说明 | +|---|---|---| +| 图谱构建流程、checkpoint/恢复、graph_state、指纹适配、SwarmSymphonyService、经验候选安装、skill_retrieval | [Symphony 编排集成(jiuwenswarm/symphony)](jiuwenswarm-symphony.md) | 构建顺序、存储布局、取消语义、runtime 缓存键与安装守卫的源码级细节 | +| 动态 overlay 聚合、事件规范化、运行时权重、evolution 存储与迁移 | [Symphony 动态图谱演化(evolution)](jiuwenswarm-symphony-evolution.md) | 权重公式精确值、重建条件、已实现与意图的边界 | +| S3 读写、LLM 载荷精简、计时、标签归一化、rich 兼容层 | [Symphony 共享工具(symphony/shared)](jiuwenswarm-symphony-shared.md) | 底层共享工具 | ## 代码快速入口(Direct) @@ -21,3 +36,11 @@ sources: [] | PR 描述在做什么 | 精确规则 | 第一批 live 源码 | |---|---|---| | symphony、graph build、图谱构建、fingerprint、能力指纹、evolution、经验演进、experience candidate、… | 入口 | `jiuwenswarm/symphony/adapter.py`、`jiuwenswarm/symphony/build.py`、`jiuwenswarm/symphony/config.py` | + +## 验证入口 + +`tests/unit_tests/symphony/test_graph_state.py`、`test_fingerprint_adapter.py`、`test_direct_service.py`、`test_evolution.py` 与 `test_experience_flow.py` 分别覆盖构建身份、适配、服务取消、legacy overlay 和 Flow 候选安装。 + +## 专题入口 + +- [Symphony 服务、规划与经验候选安装](jiuwenswarm-symphony-service.md) — 说明进程内服务的构建互斥、规划 runtime、经验候选审批安装和 Skill taxonomy 适配;图谱构建与版本产物由 Symphony 主页面说明。 diff --git a/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony-evolution.md b/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony-evolution.md index 29257928..c610a927 100644 --- a/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony-evolution.md +++ b/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony-evolution.md @@ -1,19 +1,28 @@ --- title: "Symphony 动态图谱演化(evolution)" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/evolution/models.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/evolution/aggregate.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/evolution/store.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/evolution/service.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/symphony/test_evolution.py --- # Symphony 动态图谱演化(evolution) 本模块把 Symphony 运行时事件(如计划结果、选中的技能、边的成败和失败归因)追加写入图谱目录下的 evolution 存储。它再把这些事件聚合成动态图谱 overlay,按边、节点、路径统计结果并计算运行时权重,供加载和查询状态使用。 +相关页面:[Symphony 编排集成](jiuwenswarm-symphony.md)(图谱构建与服务层)、[共享工具 shared](jiuwenswarm-symphony-shared.md)、[Agent 运行时](../agent-runtime/jiuwenswarm-runtime.md)、[网关通道规则](../gateway-channels/rules.md)(evolution 审批消息的网关侧)。以下细节核对自上游 checkout(commit `f0a69728c96b5961d993449f1a901cbd2f4dac5b`);标注的测试是源码内已有用例,本页未实际运行。 + +注意区分两套"演化"存储:本页的 `symphony/evolution/`(JSONL 事件 + overlay,JiuwenSwarm 自己聚合)与核心 Flow 引擎的 recipe/技能包存储(`graph_dir` 同级的 `flow/` 目录,经验候选安装走它,见 [Symphony 编排集成](jiuwenswarm-symphony.md))。 + **从这里读起** -- `jiuwenswarm/symphony/evolution/service.py` — 对外的服务函数:record_plan_outcome 记录计划结果,load_dynamic_overlay / rebuild_dynamic_overlay 加载或重建 overlay,evolution_status 返回演化状态, +- `jiuwenswarm/symphony/evolution/service.py` — 对外的服务函数:record_plan_outcome 记录计划结果,load_dynamic_overlay / rebuild_dynamic_overlay 加载或重建 overlay,evolution_status 返回演化状态。 **关键文件** @@ -22,6 +31,44 @@ sources: [] - `jiuwenswarm/symphony/evolution/models.py` — 共享的 key 和归一化函数:edge_key、skill_id,以及边、结果(outcome)、失败归因的归一化 - `jiuwenswarm/symphony/evolution/service.py` — 判断 overlay 是否需要重建(按图谱版本和事件时间),给失败的边加标注,并提供默认失败归因 +## 事件与键的规范化(models.py) + +- Schema 版本:overlay 为 `symphony.dynamic_overlay.v3`,事件为 `symphony.evolution_event.v2`([models.py:7-8](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/models.py#L7-L8))。 +- 边主键 `edge_key` 格式为 `{source}->{target}:{relation_type}`(relation 缺省 `can_feed`);`skill_id` 剥掉 `skill:`/`capability:` 前缀并把下划线归一成连字符,使 `general_writing` 与 `general-writing` 命中同一条目([models.py:33-43](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/models.py#L33-L43))。 +- 结果词表收敛到 `success/failure/no_plan/needs_input`:`failed/error/exception/invalid→failure`,`ready/ok/done/passed→success`,未知值一律按 failure 处理([models.py:93-103](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/models.py#L93-L103))。 +- 失败归因词表 `all_edges/terminal_edge/explicit/success_only`(别名 `terminal/last_edge/last`、`all/plan/plan_level`、`edge/per_edge`),未识别时默认 `terminal_edge`([models.py:106-118](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/models.py#L106-L118))。 + +## 聚合与运行时权重(aggregate.py) + +- 只有 `plan.outcome` 事件产生统计;`plan.created` 事件只提供 query 和 selected_skill_ids 上下文,让单技能、无边的成功运行也能进入路径统计([aggregate.py:51-78](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/aggregate.py#L51-L78)、[158-172](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/aggregate.py#L158-L172))。 +- 运行时权重策略 `skillgraph_linear_v1`:`weight = 1.0 + 0.05 × (成功数 − 失败数)`,夹在 `[0.2, 2.0]`;`needs_input` 计数但不改变权重(视为中性证据,[aggregate.py:30-33](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/aggregate.py#L30-L33)、[391-407](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/aggregate.py#L391-L407))。测试 `test_runtime_weight_uses_symmetric_linear_evidence_and_neutral_missing_input` 固定了精确值:1 成功=1.05、1 失败=0.95、100 成功封顶 2.0、100 失败下限 0.2、20 次 needs_input 仍为 1.0(tests/unit_tests/symphony/test_evolution.py:74)。 +- 每条边统计 success/failure/needs_input/attempt 计数、最多 8 条 representative_queries、last_attribution/last_outcome/last_failure_type/last_updated_at;最终化再算 Laplace 平滑的 `success_rate=(s+1)/(n+2)`、`reliability=(s+1)/(s+f+2)`、`runtime_weight` 和 `confidence_delta`([aggregate.py:335-353](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/aggregate.py#L335-L353))。 +- 节点统计对每条有结果的边把两端各计一次;共享中间节点可在同一计划里累计多次,不是按计划对节点去重([aggregate.py:114-119](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/aggregate.py#L114-L119));路径统计以 `path:` 为 id,签名输入是排序去重后的技能集合 + 规范化排序的边集合,代表 query 和 example_plan_ids 各封顶 8 条([aggregate.py:202-268](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/aggregate.py#L202-L268))。 +- 边级结果解析顺序:边自带 `outcome/status` 优先;无显式结果时,成功事件把边记成功,no_plan 事件跳过边。失败事件按归因策略处理:all_edges 全失败,success_only 全成功,explicit 仅将标记失败的边记失败,terminal_edge 仅末边失败;needs_input 事件使用同样的归因分配,但被归因边计 needs_input,未被归因边计 success([aggregate.py:410-445](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/aggregate.py#L410-L445))。失败事件默认 `terminal_edge` 下上游边记成功、末端边记失败(测试 `test_evolution_failure_attribution_defaults_to_terminal_edge`,test_evolution.py:241)。 + +## 只追加存储(store.py) + +- 布局固定在图谱根目录 `evolution/` 下:`events.jsonl`(append-only JSONL)、`dynamic_graph_overlay.json`、`skill_packs.json`(schema `symphony.skill_pack_marks.v1`)([store.py:11-14](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/store.py#L11-L14)、[122-137](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/store.py#L122-L137))。 +- 进程内用模块级 `threading.RLock` 串行化"追加 + 去重 + 重建 overlay"整个事务(`evolution_store_transaction`);overlay 和 skill packs 都走 `.json.tmp` + replace 原子写;`read_events` 跳过坏行、正数 limit 只取尾部,None/非正数读取全部有效事件([store.py:15-73](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/store.py#L15-L73))。 + +## 服务函数(service.py) + +- `load_dynamic_overlay` 的重建条件:无事件则保持现状;overlay 缺失或 schema 不是 v3、事件 count/last_event_at 与日志不一致、或 current.json 提供非空 version 且与 base_graph_version 不一致时重建([service.py:44-83](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/service.py#L44-L83))。因此换图谱版本不丢学习数据,重建后 `base_graph_version` 更新、计数继续累计(测试 `test_evolution_store_migrates_known_files_and_keeps_learning_across_versions`,test_evolution.py:172)。 +- `prepare_evolution_store` 做一次性迁移:旧布局把 evolution 放在 `versions//evolution/`,现在迁到图谱根 `evolution/`;只拷贝 `events.jsonl` 和 `dynamic_graph_overlay.json`,且不覆盖已存在的目标、不删旧文件(其余文件如 `session_feedback_state.json` 留在原地)([service.py:86-105](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/service.py#L86-L105))。 +- `record_plan_outcome`:归一化 outcome 和边,用 `failed_edges` 给选中边打 `failed=True` 标注;未显式指定归因时默认——失败/需输入且边数 >1 取 `terminal_edge`,单边取 `all_edges`,成功为空串;`detail` 截断到 1000 字符;提供非空 `evidence_id` 时先在该 graph_dir 的整个事件日志中查重(不按 plan_id 分区),命中则返回既有事件并加 `deduplicated: True`,不追加;`rebuild_overlay=True`(默认)在同一事务内同步重建 overlay([service.py:155-231](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/service.py#L155-L231)、[262-273](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/service.py#L262-L273))。 +- `evolution_status` 返回 event_count、last_event_at、overlay_exists/mtime/generated_at、overlay_stats、weight_policy 和 top_edges(按 runtime_weight 降序、再按 attempt_count 降序取前 10 条)([service.py:128-152](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/service.py#L128-L152)、[317-335](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/evolution/service.py#L317-L335))。 + +## 已实现与设计意图的边界 + +- 存储、聚合、重建与状态查询已实现且有单测覆盖(tests/unit_tests/symphony/test_evolution.py)。 +- 消费侧当前是受限的:`SwarmSymphonyService.graph()` 目前传 `dynamic_overlay=None`([service.py:383](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L383)),`_graph_with_runtime_weights` 已能把 overlay 的 runtime_weight 合入 Web 图谱边,但规划路径明确不加载这份 overlay——测试 `test_core_graph_plan_does_not_load_legacy_overlay` 把 `load_dynamic_overlay` patch 成失败来锁定该约束(tests/unit_tests/symphony/test_experience_flow.py:1702)。改动若让 plan 读取该 overlay 会直接打破既有契约。 +- 这些存储 helper 的存在不能单独证明生产计划器实际消费 legacy overlay;生产接线需沿调用方确认,并与核心 Flow 的 recipe/SkillPack 链路分别核对。 + +## 怎样验证 + +- `tests/unit_tests/symphony/test_evolution.py` — 权重精确值、路径聚合、迁移与跨版本学习、terminal_edge 归因、status 读取。 +- `tests/unit_tests/symphony/test_experience_flow.py::test_core_graph_plan_does_not_load_legacy_overlay` — plan 不读 legacy overlay 的回归门。 + **路由** - `jiuwenswarm/symphony/evolution/` diff --git a/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony-service.md b/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony-service.md new file mode 100644 index 00000000..966e2f5c --- /dev/null +++ b/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony-service.md @@ -0,0 +1,52 @@ +--- +title: "Symphony 服务、规划与经验候选安装" +created: 2026-10-01 +updated: 2026-10-01 +type: architecture +tags: [jiuwenswarm] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/config.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/service.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/skill_retrieval/runtime.py +--- + +# Symphony 服务、规划与经验候选安装 + +## 职责和边界 + +说明进程内服务的构建互斥、规划 runtime、经验候选审批安装和 Skill taxonomy 适配;图谱构建与版本产物由 Symphony 主页面说明。 + +## 进程内服务(SwarmSymphonyService) + +- 单例由 `get_swarm_symphony_service()` 提供;`set_swarm_symphony_service` 仅供测试/受控嵌入([service.py:1131-1145](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L1131-L1145))。 +- `_build_guard`(asyncio.Lock)保证进程内只有一个活跃构建;并发触发返回 `{"reason": "graph_preparing", "retryable": False, "build_status": "running"}`([service.py:516-532](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L516-L532))。`start_refresh_graph` 复用仍在运行的后台任务(task 名 `symphony-graph-build`);`cancel_build` 先记录 `update.cancel_requested` 再 `task.cancel("skills.graph.cancel")` 并等待退出,无活跃任务时返回 `build_status: "idle"`([service.py:323-360](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L323-L360))。 +- 构建前先 `probe_model_connection` 探活主模型,失败返回业务 payload「主模型连接测试未通过」并带 `failure_stage=model.probe`,不进入构建([service.py:551-576](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L551-L576))。 +- 存在已发布图谱时,取消后的刷新不会被下一个普通任务隐式重建:`_graph_needs_build` 看到 `build_progress.status == "cancelled"` 返回 False,继续用最后一次原子发布的图谱;若图谱根本不存在仍需构建([service.py:1148-1165](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L1148-L1165));测试 `test_cancelled_graph_build_keeps_the_last_published_graph_for_planning`(test_direct_service.py:2265)。 +- `graph_status()` 故意传 `llm_config=None`:新鲜度跟随构建图谱时的模型身份,当前默认模型变化只影响在线规划、不应让已发布图谱变 stale([service.py:239-243](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L239-L243))。 +- 孤儿日志修复:无活跃任务但 `build_log.jsonl` 末态是 running 时,补写 `update.cancelled` 且 `reason="process_interrupted"`([service.py:1481-1492](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L1481-L1492))。 +- 运行时缓存键为 `(graph_dir, mode, top_k, max_depth, min_edge_confidence, evolution_flow_enabled, llm签名)`;已持有 Flow store 的运行时即使配置变化也不替换(Flow 存储单进程独占),带 `llm_config` 的 plan 使用临时运行时并在结束后关闭([service.py:636-702](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L636-L702));测试 `test_runtime_cache_rebuilds_when_default_llm_changes`(test_direct_service.py:1542)。 +- `plan()` 契约:空 query 直接失败;mode 只接受 `fast`/`beam`(config `_orchestration_mode`,[config.py:301-307](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/config.py#L301-L307));语言 `zh→cn`;返回 payload 必须含 dict 型 `planned_graph`;规划前经 `load_execution_disabled_skills()` 过滤禁用技能([service.py:390-501](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L390-L501))。 +- Web 图谱载荷 `_web_graph_payload` 把节点 id 改写为 `skill:`/`capability:` 前缀,过滤执行禁用技能及其边,并从工作区 skills 目录扫描 SkillPack 附加 `pack:` 节点和 `contains` 边([service.py:1168-1324](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L1168-L1324))。 + +## 经验候选与安装 + +- 候选请求 id 稳定:`symphony_experience_ + sha256("recipe_id:version")[:20]`([service.py:80-84](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L80-L84))。候选推送是 `chat.ask_user_question` 事件,`evolution_meta` 标注 `approval_kind=install`;常规非 force 调用对同一 (recipe_id, version) 在进程内成功推送后去重,defer 后由 `release_candidate` 释放、可在后续成功任务再次出现([service.py:821-874](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L821-L874))。 +- 启动恢复出的候选不在取消/部分成功的运行里弹出,要等一次成功任务提供会话上下文([service.py:769-780](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L769-L780))。 +- `install_candidate` 的守卫顺序:request_id 校验 → evolution 开关门 → 安装锁 → 内存/磁盘 install receipt 重放(`flow_root/install_receipts/.json`,已安装则幂等重放并 acknowledge,不重复拷贝)→ `review_and_prepare_install` verdict 必须 approved → 客户端提供的非空 package_id/integrity 必须与服务端一致(两者可省略,服务端仍校验实际包) → `CapabilityPackager.verify_package_integrity` → artifact 目录必须解析为 `flow_root/packages//skill` 且不是符号链接 → 优先 `recover_symphony_skill_install`(崩溃恢复),否则 `install_symphony_skill_artifact`;receipt 仅在安装成功后用 mkstemp+fsync+os.replace 原子写入([service.py:937-1062](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/service.py#L937-L1062))。关键测试:`test_install_requires_approved_server_artifact`、`test_receipt_write_crash_recovers_installed_skill_after_restart`(tests/unit_tests/symphony/test_experience_flow.py:1274、1340)。 +- 演化激活判定 `evolution_flow_enabled`:优先 `evolution.flow.enabled`,缺省回退旧键 `evolution.enabled`([config.py:84-91](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/config.py#L84-L91)、[136-138](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/config.py#L136-L138));flow 关闭时 list 返回 `enabled: False`,request/install 返回 `flow_disabled`(测试 `test_evolution_disabled_gates_flow_service_operations`,test_experience_flow.py:1751)。 + +## Skill 检索索引(skill_retrieval/runtime.py) + +`SkillTaxonomyRuntime` 是 `openjiuwen.symphony.agent.AgenticSkillRetrievalToolkit` 的薄适配([runtime.py:30-88](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/skill_retrieval/runtime.py#L30-L88)): + +- `start_build` 在空库存时直接拒绝(error code `empty_inventory`),不启动 worker,避免破坏既有可用索引;构建配置按 `SkillIndexBuildConfig` 字段白名单过滤并强制 `preserve_previous_index_on_failure=True`(失败的刷新不得毁掉上一个可用 taxonomy)。 +- LLM 配置取 `symphony.skill_retrieval.llm`,缺项回退默认模型;读取默认模型抛 RuntimeError/ValueError 时退回空 `SwarmLLMConfig()`,再转换为核心 LLM 配置;此适配层本身不实现 taxonomy 兜底算法([runtime.py:127-154](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/skill_retrieval/runtime.py#L127-L154))。 + +## 怎样验证 + +源码链接固定在 f0a69728c96b5961d993449f1a901cbd2f4dac5b。聚焦单测选择器和验证范围见[原模块页面](jiuwenswarm-symphony.md#怎样验证);单元契约不能替代真实服务、模型端点或跨平台集成验证。 + +## 相关文档 + +- [原模块架构与入口](jiuwenswarm-symphony.md) +- [相邻模块](jiuwenswarm-symphony-evolution.md) diff --git a/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony.md b/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony.md index adae7f92..347494f2 100644 --- a/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony.md +++ b/knowledge/repos/jiuwenswarm/components/symphony-orchestration/jiuwenswarm-symphony.md @@ -1,16 +1,28 @@ --- title: "Symphony 编排集成(jiuwenswarm/symphony)" created: 2026-09-30 -updated: 2026-09-30 +updated: 2026-10-01 type: architecture tags: [jiuwenswarm] -sources: [] +sources: + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/build.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/adapter.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/graph_state.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/graph_storage.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/service.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/config.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:jiuwenswarm/symphony/skill_retrieval/runtime.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/symphony/test_graph_state.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/symphony/test_fingerprint_adapter.py + - openJiuwen-ai/jiuwenswarm@f0a69728c96b5961d993449f1a901cbd2f4dac5b:tests/unit_tests/symphony/test_direct_service.py --- # Symphony 编排集成(jiuwenswarm/symphony) 把公开的 openjiuwen.symphony 编排 API 接入 JiuwenSwarm。模块负责进程内的 Symphony 服务、能力图谱的构建与版本化存储、能力指纹、执行经验的演进和候选安装,以及基于 Skill 分类体系(taxonomy)的索引构建。 +相关页面:[动态图谱演化 evolution](jiuwenswarm-symphony-evolution.md)、[共享工具 shared](jiuwenswarm-symphony-shared.md)、[Agent 运行时](../agent-runtime/jiuwenswarm-runtime.md)(消费演化 rail)、[JiuwenSwarm 总体架构](../../architecture.md)。以下细节全部核对自上游 checkout(commit `f0a69728c96b5961d993449f1a901cbd2f4dac5b`);标注的测试是源码内已有用例,本页未实际运行。 + **入口** - `jiuwenswarm/symphony/service.py` — SwarmSymphonyService 和 get_swarm_symphony_service:进程内唯一的服务对象。负责图谱状态、刷新、取消构建,也负责 plan、经验候选的列出、请求和安装 @@ -27,6 +39,55 @@ sources: [] - `jiuwenswarm/symphony/llm.py` — LLMConfig、按请求绑定模型、token 用量统计,以及 JiuwenSwarmChatClient 的 JSON 补全 - `jiuwenswarm/symphony/config.py` — 各子配置的解析和取值约束,以及 symphony/evolution 开关的判定 +## 图谱构建的顺序与检查点 + +[`SymphonyGraphBuilder.build`](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/build.py#L263-L535) 的实际顺序: + +1. `reset_llm_token_usage()` 清零 token 统计,生成 run_id(UTC 时间戳 `YYYYmmddHHMMSS` + `-` + uuid4 hex 前 12 位,[build.py:707-709](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/build.py#L707-L709))。 +2. 在 `graph_dir/.build_runs//checkpoint.json` 写 checkpoint:schema `Symphony-build-checkpoint-v1`,先写 `.json.tmp` 再 `os.replace` 原子落盘,跨记录保留 `started_at`([build.py:675-704](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/build.py#L675-L704))。阶段序列为 `update.start → fingerprint.start → fingerprint.done → graph.start → graph.done → publish.start → publish.done`。 +3. `resume=True` 时用 `latest_incomplete_build` 先按 `updated_at` 找最新 checkpoint,仅当该条状态为 `running` 或 `failed` 时才作为恢复入口;不会跳过最新终态而回退到更旧失败记录([graph_storage.py:124-149](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/graph_storage.py#L124-L149));**已取消(cancelled)的构建不可恢复**。 +4. 扫描 Skill 目录 → `GraphStateBuilder.capability_hashes` → 与旧 `graph_state.json` 的 active 条目 diff 出 added/changed/removed。 +5. `FingerprintService.build(force, progress_callback)` 产出指纹 artifact,先写入 `.build_runs//artifacts/` 作为未发布快照。 +6. `prepare_artifact` 回调在版本目录已完成、`current.json` 尚未切换之前执行:把指纹 artifact 复制进版本目录、写 `llm_token_usage.json`、写新 `graph_state.json`([build.py:419-475](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/build.py#L419-L475))。 +7. 发布成功后 best-effort 删除 `.build_runs//artifacts/`(OSError 只打 warning,[build.py:660-672](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/build.py#L660-L672))。 + +取消语义:模块级 `build_graph` 捕获 `asyncio.CancelledError` 后调用 `cancel_active_checkpoint()`(仅把 status 为 `running` 的 checkpoint 改为 `cancelled`)再重新抛出([build.py:537-608](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/build.py#L537-L608))。取消把 checkpoint 阶段标记为 `update.cancelled`;当它是最新 checkpoint 时,`latest_incomplete_build` 返回 None,但已发布版本与指纹缓存保留(测试 `test_cancelled_build_marks_checkpoint_and_is_not_resumable`,tests/unit_tests/symphony/test_direct_service.py:2202)。 + +`GraphStatus` 字段:exists/stale/skill_count/added_count/changed_count/removed_count/resume_available/checkpoint_dir/detail,detail 取值 `graph is fresh` / `Symphony graph is missing` / `Symphony graph is stale`;`GraphBuildResult` 额外带 reused/extracted/removed/edge/diagnostics/relation_reused/relation_resolved 计数和 `version`([build.py:62-121](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/build.py#L62-L121))。 + +## 版本化存储布局 + +目录约定([graph_storage.py:11-13](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/graph_storage.py#L11-L13)):`current.json` 指针(schema `1.0`)、`versions//` 产物、`.build_runs//` 检查点。`publish_artifact_dir` 用 `os.replace` 把构建产物移入 `versions/`(版本已存在抛 `FileExistsError`),再原子写指针([graph_storage.py:94-121](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/graph_storage.py#L94-L121))。指针解析 `resolve_graph_artifact_dir` 拒绝逃逸出 graph_dir 的路径(`ValueError`),没有 `current.json` 时回退读 graph_dir 本身以兼容旧布局([graph_storage.py:41-60](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/graph_storage.py#L41-L60));但 `graph_exists` 要求指针存在、schema 以 `1.` 开头且 `graph.json` 存在,旧布局不算"已存在"([graph_storage.py:69-79](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/graph_storage.py#L69-L79))。 + +## 增量状态 graph_state.json + +- 条目按 capability `metadata["entrypoint"]` 的父目录作为 relative_path 主键;entrypoint 缺失直接 `ValueError`([graph_state.py:160-167](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/graph_state.py#L160-L167))。 +- `content_hash` 是核心扫描器的完整资产哈希(不是只哈 SKILL.md);读取旧文件时兼容 v1 的 `skill_md_sha256` 键做一次性迁移([graph_state.py:44-51](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/graph_state.py#L44-L51))。 +- `fingerprint_hash = sha256(fingerprint.graph_identity_dict())`;`graph_identity_dict` 排除 documentation 等非图谱质量扩展,仅改变这些指纹扩展不改变 fingerprint_hash;Skill 资产变更仍可改变 content_hash 与构建新鲜度(测试 `test_graph_identity_hash_excludes_non_graph_quality_extensions`,tests/unit_tests/symphony/test_graph_state.py:70)。 +- 移除的 Skill 保留 tombstone:`status="removed"` 并沿用旧 content/fingerprint hash;只有 `status=="active"` 的条目参与增量 diff([graph_state.py:61-66](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/graph_state.py#L61-L66)、[137-148](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/graph_state.py#L137-L148))。 +- 新状态写在版本目录里(`prepare_artifact` 内),作为版本产物准备完成后,由核心发布流程切换 current 指针。 + +## 指纹适配与失效策略 + +- `FingerprintLLMAdapter.invoke` 在调用模型前删掉 `max_tokens` kwarg:思考模型可能先输出推理 token,删除此固定输出上限以避免推理 token 占用预算后截断结构化输出;模型本身的输出限制仍由模型配置负责([adapter.py:70-81](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/adapter.py#L70-L81))。旧上限策略用策略版本 `no-symphony-output-cap-v1` 混入 `configuration_signature`,使旧指纹缓存整体失效([adapter.py:23](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/adapter.py#L23)、[84-88](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/adapter.py#L84-L88))。 +- `fingerprint_settings_from_swarm`:`enable_llm_extraction` 当且仅当传入 llm_config;`enable_llm_evaluation` 恒为 False;`extraction.workers → max_concurrency`、`batch_size → batch_size`、`body_limit` 缺省回退核心默认;`cache_enabled=True`([adapter.py:91-122](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/adapter.py#L91-L122))。 +- 构建期与规划期使用不同关系阈值:`graph_build_orchestration_config_from_swarm` 用 `build.min_edge_confidence`,规划用 `orchestration.min_edge_confidence`([adapter.py:155-167](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/adapter.py#L155-L167))。 +- 图谱身份快照 schema `JiuwenSwarm-symphony-graph-source-v2`,含 capabilities_sha256、排序后的 current_hashes、fingerprint_schema_version、fingerprint_source_snapshot(去掉 captured_at)、fingerprint_sha256、fingerprint_config_sha256、graph_config、llm_sha256([build.py:712-736](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/build.py#L712-L736));stale 判定最终委托核心 `orchestration.status(expected_snapshot).fresh`。 +- 复用计数是源级推断:仅当未 force 且已发布指纹签名与当前一致时才计 reused([build.py:789-810](https://github.com/openJiuwen-ai/jiuwenswarm/blob/f0a69728c96b5961d993449f1a901cbd2f4dac5b/jiuwenswarm/symphony/build.py#L789-L810))。 + +## Symphony 服务、规划与经验候选安装 + +该工作流的职责、顺序、错误边界与源码入口见[Symphony 服务、规划与经验候选安装](jiuwenswarm-symphony-service.md)。 + +## 怎样验证 + +本页未运行任何测试;以下选择器来自源码内既有用例(需在真实 checkout 中执行): + +- `tests/unit_tests/symphony/test_graph_state.py` — 增量状态、legacy 哈希迁移、graph_identity 排除文档扩展、removed tombstone。 +- `tests/unit_tests/symphony/test_fingerprint_adapter.py` — 指纹 artifact 消费、settings 映射、max_tokens 删除、二次构建复用(`test_swarm_graph_build_consumes_canonical_core_artifact`)。 +- `tests/unit_tests/symphony/test_direct_service.py` — 服务层取消/守卫/缓存/孤儿日志修复(`test_cancel_build_aborts_blocked_progress_and_releases_guard`、`test_graph_status_repairs_interrupted_build_log` 等)。 +- `tests/unit_tests/symphony/test_experience_flow.py` — 候选推送幂等、安装 receipt 重放、演化开关门禁。 + **改动路由** - `jiuwenswarm/symphony/adapter.py`