Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

日志记录架构

ZeroClaw 只有一个日志记录接口:zeroclaw_log::record! 宏。工作区中的每一次输出——智能体循环活动、通道 I/O、cron 运行、工具调用、内存操作、会话生命周期、错误——都通过它进行。该宏会触发一个 tracing 事件,已安装的订阅者将其传递给两个并列的层:stderr fmt 层(终端输出)和 LogCaptureLayer。fmt 层在 stderr 上打印带颜色、带别名前缀的行(除非使用 --verbose,否则会被静默)。LogCaptureLayer 会生成一个结构化的 LogEvent 并将其分发,经由 writer::record_event,发送到:

  1. 可选的 Observer 桥接(observer_bridge::forward)用于映射到类型化 Prometheus / OTel 事件的那部分操作,但仅在调用方使用 set_observer_bridge 安装了绑定时才启用。当前的生产引导不会安装该桥接。
  2. 面向整个进程的广播通道,用于实时订阅者,例如仪表板的 SSE 流。
  3. 用于 <workspace>/state/runtime-trace.jsonl 的异步 JSONL 写入器(当 [observability] log_persistence"rolling""full""rotating" 时)。

当 Observer bridge 绑定后,其投影和广播发送会先于持久化入队尝试发生。这三个目标具有不同的完整性和持久性保证;共享同一个 LogEvent 并不能使它们可以互换。

请先阅读:attribution 不是 attrs

每个日志事件都携带两条完全独立的结构化数据通道。混淆它们是调用点处最常见的错误,因此请在做其他事情之前先牢记这种区分:

归属信息zeroclaw.*Attrs (attributes.*)
答案 做的以及 在什么情况下具体发生了什么
示例channelagent_aliasmodel_providertoolsession_keycron_job_idbytes_receivedtokens_usedstatus_code、错误负载
Span(跨度)。 在入口点处打开,由该层遍历。调用点。 Event::with_attrs(json!({...}))
出现在调用位置?永不。 不是 record! 参数。是的,这是它唯一可能的来源。

由此得出的规则:如果某个值标识了一个事件归属于谁或哪个作用域,那么它应当来自 span,而绝不能出现在调用点。 归属信息会从调用栈上层打开的 attribution_span! / scope! 包装器中自动流入;当事件触发时,该层会从叶到根遍历 span 作用域,并将每一项贡献合并到该事件的 zeroclaw.* 块中。触发 record! 的调用点不会指明其中任何内容。

由于归因是这种拆分中起承重作用的那一半,也是最容易让人栽跟头的那一半,所以它排在最前面。

归因:一切都源自 span

归因属性从来不是调用点参数。 请再读一遍。Channel composite、agent_alias、model_provider、tool、session_key、cron_job_id:这些都不应被键入到 record! 调用中。它们通过在入口点开启的 tracing span 流入,并在事件触发时由该层遍历获取。如果你发现自己想把 agent_aliastool 传给 record!,请停下:该值已经通过某个 span 处于作用域内,或者本应如此,正确的做法是开启或修复对应的 span,而不是把这个值穿入调用点。

机制全流程:

  1. “事物”(通道、提供方、代理、工具、定时任务、内存后端……)在其结构体旁边实现一次 Attributable 即可。
  2. 其入口点将工作封装在 attribution_span!(self) 中,这会打开一个追踪 span,携带该事物的角色和别名。
  3. 在该 span 内任何位置触发的每个 record!,无论是直接触发还是任意深度的嵌套触发,都会自动继承该归属信息。
  4. 当事件触发时,该层会按 leaf→root(叶到根)遍历 span 作用域,合并每个 Attributable 的贡献,并写入合并后的 zeroclaw.* 块。调用方对此一无所知。

这正是该设计的核心要义:针对每个事物的日志代码为零。你只需实现一次该 trait 并对入口点进行一次包装;其下的每一次输出都会被自动归因,无需额外开销。

Attributable trait

位于 crates/zeroclaw-api/src/attribution.rs 中,因此每个 crate 都可以实现它,而无需依赖 zeroclaw-log

#![allow(unused)]
fn main() {
pub trait Attributable {
    fn role(&self) -> Role;
    fn alias(&self) -> &str;
}
}

工作区中的每个“事物”(一个 TelegramChannel、一个 AnthropicModelProvider、一个 Agent、一个 cron 作业、一个工具、一个内存后端、一个对等组、一个技能包、一个 MCP 包、一个会话)都在其结构体旁实现了一次 Attributable

Role 分类法

嵌套枚举已关闭:

#![allow(unused)]
fn main() {
pub enum Role {
    Swarm,
    Agent,
    Channel(ChannelKind),       // Telegram、Discord、Slack、Matrix、Lark……
    Tool(ToolKind),             // Shell、HttpRequest、FetchUrl 等……
    Cron(CronKind),             // 间隔、定时、Cron、单次
    Provider(ProviderKind),     // 模型、Tts、转录、隧道
    Memory(MemoryKind),         // Sqlite、Json、InMemory、Markdown、Qdrant 等……
    PeerGroup,
    Skill,
    Mcp,
    Sop,
    Session,
    System,
}
}

ChannelKindToolKindCronKindMemoryKind 以及四个 ProviderKind 子枚举(ModelProviderKindTtsProviderKindTranscriptionProviderKindTunnelProviderKind)都是封闭的。通过 strum::IntoStaticStr 得到的变体 snake_case 形式即为 <type>.<alias> 组合中规范的 <type> 部分。添加新实现:扩展相关的 Kind 枚举,仅此而已。

打开一个跨度,在每个入口点执行此操作

使用 attribution_span!(thing) 包裹入口点的工作。该宏返回一个 Span,以结构化字段携带该 thing 的角色和别名。对 future 调用 .instrument(span)(或在同步代码中使用 let _g = span.entered())。未重新建立 span 的派生任务会丢失归属信息:每个会发出数据的 tokio::spawn 主体都必须携带与父级相同的 attribution_span! / scope!,否则其发出的数据将无法归属。

#![allow(unused)]
fn main() {
use zeroclaw_log::Instrument;

let span = zeroclaw_log::attribution_span!(self);  // self 实现了 Attributable
async move {
    // 每条记录!内部自动携带别名绑定的字段
    record!(INFO, Event::new(module_path!(), Action::Start), 频道在线);
    self.poll_loop().await
}.instrument(span).await
}

当事件触发时,该层会沿着 span 作用域从叶节点到根节点遍历,将每个 Attributable 的贡献合并到事件的 zeroclaw.* 归因块中,并发出复合属性(channel = "telegram.clamps"channel_type = "telegram"channel_alias = "clamps"),而调用方无需指定其中任何键。

scope! 宏,非角色上下文

attribution_span! 用于带角色的 Attributable 对象。对于不与某个对象绑定的按作用域标识符(发送者 id、消息 id、回合 id、请求 id),请使用 scope!

#![allow(unused)]
fn main() {
zeroclaw_log::scope!(
    sender: msg.sender.as_str(),
    message_id: msg.id.as_str(),
    => async move { process_message(msg).await }
).await
}

scope! 刻意横跨 attribution/attrs 这条界线:凡是匹配别名绑定的 ATTRIBUTION_FIELDS / COMPOSITE_PREFIXES(位于 crates/zeroclaw-log/src/event.rs)的字段键,都会进入有类型的 zeroclaw.* 归因槽;其余字段则进入事件的 attributes 映射,应用于每一次后代发射。无论哪种情况,该值都会随每一个嵌套的 record! 一同传递,而无需作为调用点参数。

record! 宏及其调用点约定

tracing crate 是 zeroclaw-log 的实现细节:record! / scope! / attribution_span! 宏会展开为 zeroclaw_log::__private::tracing,因此调用点永远不会直接引用 tracing 类型。日志事件宏本身(tracing::{trace,debug,info,warn,error}log::*std::dbg,以及裸用的 anyhow::anyhow!)在 clippy.toml 中被作为 disallowed-macros 在整个 workspace 范围内硬性禁用。由于 CI 中启用了 -D warnings,任何直接使用 tracing::info! 等的代码都会导致构建失败,并附带一条 clippy 消息,指明应改用 ::zeroclaw_log::record!。这并非约定,而是强制执行的规则。

唯一的例外是 crates/zeroclaw-log/ 内少数用于引导流水线的文件,它们带有本地的 #![allow(clippy::disallowed_macros)]。少数几个 crate(zeroclaw-apizeroclaw-spawnzeroclaw-providerszeroclaw-hardwarezeroclaw-log)仍在 Cargo.toml 中列出了 tracing / tracing-subscriber,但仅用于 span 和 subscriber 的底层连接,而非用于发出日志宏。依赖项的存在并不意味着可以调用被禁用的宏。(tokio::spawn 同样通过 disallowed-methods 被禁用;请使用 ::zeroclaw_spawn::spawn!,以便派生的任务继承调用方的归因 span。)

宏是锁定形态的:它接受一个级别、一个 Event 表达式和一个消息字面量。

#![allow(unused)]
fn main() {
use zeroclaw_log::{record, Event, Action, EventCategory, EventOutcome};

record!(INFO, Event::new(module_path!(), Action::Start), 启动步骤);
record!(WARN, Event::new(module_path!(), Action::Fail).with_outcome(EventOutcome::Failure).with_attrs(serde_json::json!({"exit_code": 137})), 工具失败);
}

module_path!() 是事件名称的规范来源:它是调用点的 Rust 模块路径(例如 zeroclaw_channels::telegram),因此事件可被搜索、可跳转到源代码,且不可能拼写错误。工作区中的每个 record! 调用点都使用相同的约定。

宏会自动注入 file!()line!()LogCaptureLayer 将它们作为 _file_line 附加到事件的 attributes 映射中,以便操作人员从日志查看器跳转到源代码。

调用点约定

每次 record! 调用都是一行代码,用于说明发生了什么,而不是谁做的或在什么上下文下做的

  • 级别后的单个位置参数是一个 Event 表达式。
  • 下一个参数是用于人类可读消息的字符串字面量。
  • 以上就是全部内容。Channel、agent_alias、provider、tool、session_key、cron_job_id、model:这些都不是调用点参数。它们都从 span 中流入(参见 Attribution: it all comes from spans)。

形状由 Event 结构体强制约束:未知字段会导致编译错误。

需要属性时

Event::with_attrs(serde_json::json!({...})) 用于单个事件的测量值,以及周围作用域中不存在的临时数据。具体而言:

  • 每事件度量:bytes_receivedtokens_usedretry_countstatus_codequeue_depth
  • 当错误本身就是事件时的错误负载:anyhow 链式文本、HTTP 错误正文、解析错误详情。
  • 外部系统标识符:远程 API 的 request_id、上游跟踪标头。
  • 此刻捕获的派生状态:进行中的请求数、retry-after 秒数。

Attrs 不适用于任何来自周围作用域的内容:channel composite、agent_alias、model_provider、tool、session_key、cron_job_id、sender、message_id 等。这些应放在外层的 attribution_span!scope! 中。

serde 规则:传递原始值,绝不要使用 format!("{}", v)format!("{:?}", v)serde_json::json! 会将字符串序列化为字符串,数字序列化为数字,Vec<T> 序列化为数组,Option<T> 序列化为 null 或值。仅当类型未实现 impl Serialize 时(例如 anyhow::Errorreqwest::Errorstd::io::ErrorPath::DisplayStatusCode)才使用 .to_string() 包装。

占位符规则

Rust 字符串字面量占位符(如 "raw error body: {body}")禁止在 record! 消息中使用。Rust 2021 的隐式格式字符串捕获不会穿透 record!:每个 {var} 都会变成字面子串,不进行替换。转换规则:

#![allow(unused)]
fn main() {
// 错误 — {body} 是字面量,绝不会被插值
record!(WARN, Event::new(module_path!(), Action::Fail), raw error body: {body});

// 正确 —— 正文位于 attrs 中,message 为纯文本说明
record!(WARN, Event::new(module_path!(), Action::Fail).with_attrs(serde_json::json!({"body": body})), 原始错误正文);
}

EventActionEventOutcomeEventCategory

这四个都是定义在 crates/zeroclaw-log/src/event.rs 中的封闭枚举。添加值是唯一的更改点:调用点不会自行创建字符串。

  • Action:封闭动词集合,通过 strum::IntoStaticStr 以 snake_case 形式存储于磁盘:StartCompleteFailCancelSkipTimeoutRetryInboundOutboundSendReceiveConnectDisconnectReconnectSpawnKillTickTriggerScheduleApproveRejectDeferReadWriteDeleteListQueryInvokeDispatchResolveRegisterUnregisterLoadSaveMigrateValidateNote
  • EventOutcomeSuccessFailureUnknownUnknown 是默认值,在序列化时会被跳过(不会写入磁盘上的 event.outcome),因此没有 outcome 键的行隐式表示 Unknown
  • EventCategoryAgentChannelCronMemoryToolProviderSessionSystemInternal。除非通过 Event::with_category(...) 覆盖,否则派生自最内层的角色范围。

工具输入/输出传递

中央工具执行器(crates/zeroclaw-runtime/src/agent/tool_execution.rs::execute_one_tool)会用 invoke/complete/fail 事件包裹每一次 Tool::execute(args) 调用。每个事件的名称是 module_path!()(即执行器自身的模块),而非硬编码字符串;通过 Action 和严重程度来区分它们:

  1. 在运行之前:record!(DEBUG, Event::new(module_path!(), Action::Invoke).with_category(EventCategory::Tool).with_attrs(...)),并在 attrs 中包含 tooltool_call_id 以及完整的 input
  2. 运行 execute(args).await
  3. 成功时(r.success):调用 record!(DEBUG, ... Action::Complete),并在 attrs 中记录 Outcome::Success、持续时间以及 tool / tool_call_id / input / output
  4. 工具报告失败时(!r.success):使用 record!(WARN, ... Action::Fail),并在 attrs 中记录 Outcome::Failure、持续时间以及 tool / tool_call_id / input / error / output
  5. execute 返回 Err 时:使用 Outcome::Failure、持续时间以及 attrs 中以 debug 格式化的错误调用 record!(ERROR, ... Action::Fail)

这些事件是在围绕调用打开的 scope! 风格的 span 内发出的(target = "zeroclaw_log_internal_scope",字段 tool = <name>),因此 tool 字段也会随每个后代发出项一同传递。各工具的 Tool::execute 实现无需添加任何日志代码。

LogCaptureLayer 与磁盘存储架构

crates/zeroclaw-log/src/layer.rs 中的 layer 是一个 tracing-subscriber Layer,它:

  1. 在创建/记录目标为 "zeroclaw_log_internal_attribution" 的 span 时(即 attribution_span! 宏所开启的目标):将 role 和 alias 字段解析为 ZeroclawAttribution 快照,并存储在该 span 的 extensions 中。
  2. 在以目标 "zeroclaw_log_internal_scope" 创建/记录 span 时(由 scope! 打开):解析临时 kvp 并以类似方式暂存它们。
  3. 在事件发出时,目标为 "zeroclaw_log_event"record! 宏触发所经过的目标):从 zc_* 字段集构建一个 LogEvent,从叶节点到根节点遍历 span 作用域并合并找到的每个归因快照,将 zc_attrs JSON 数据块解析为事件的 attributes,附加来自自动捕获的源代码位置的 _file/_line,然后将最终事件交给 writer::record_event,它按以下顺序进行分发:
    • 绑定 Observer 时用于映射 Prometheus / OTel 类型化事件的 Observer 桥接器(observer_bridge.rs)。
    • 安装发送器时,面向当前 SSE/仪表板订阅者的广播钩子(broadcast.rs)。
    • JSONL 持久化(writer.rs),仅在启用 log_persistence 时最后才发送到异步写入器队列。

磁盘上的 JSON 结构(event.rs 中的 LogEvent):

{
  "id": <uuid>,
  @timestamp: 2026-05-16T10:08:59.002Z,
  "severity_number": 9,
  "severity_text": INFO,
  事件: { category: channel, action: inbound, 结果: 成功 },
  service: { "name": zeroclaw, version: "0.8.5" },
  trace_id: <turn id>,
  span_id: <sub-span id>,
  zeroclaw: {
    channel: telegram.clamps,
    "channel_type": Telegram,
    channel_alias: "clamps",
    agent_alias: "clamps",
    "model_provider": anthropic.clamps,
    "model_provider_type": anthropic,
    model_provider_alias: "clamps",
    "模型": claude-sonnet-4-6
  },
  "消息": 入站消息,
  attributes: { sender: "...", _file: "...", _line: 42 },
  schema_version: 2
}

@timestampchrono::DateTime<Utc>,序列化为带 Z 的 RFC 3339 格式。架构版本为 2;较旧的 version: 1 行会在守护进程启动时由 migrate::migrate_legacy_jsonl_in_place 就地迁移。

不同的交付渠道具有不同的保障

writer::record_event 会构建一次持久化值,然后从同一个 LogEvent 派生其他投递内容。每个目标都有独立的契约:

目的地所有者合同与损失边界
可选的类型化 Observer 桥接observer_bridge.rsforward 在显式绑定 Observer 之前是空操作,而当前的生产环境启动流程并未绑定 Observer。绑定后,它会同步转发,但只投影 project 所识别的操作;当前映射可能会遗漏某些操作或默认字段。应将其视为选择性指标/跟踪投影,而不是完整的事件日志,并检查 project 以了解当前的字段映射。
直播broadcast.rs 及其消费者将结构化事件发送给当前进程内的订阅者。订阅者只能看到其订阅后发出的事件,有界接收器可能会滞后,网关 SSE 适配器会跳过滞后的帧。仅用于广播的临时属性可能出现在经过身份验证的实时帧中,但不会包含在持久化的 JSONL 中。这是一条实时通知路径,而不是可重放的证据。
持久化 JSONLwriter.rs在不阻塞运行时的情况下,将序列化事件入队。有界队列已满时可能丢弃事件,工作线程写入失败会作为警告处理,而周期性的 sync_all 仅作用于当前活动文件。新的 UTC 日期首次追加前执行的按日轮换,以及导致超过阈值的追加后执行的按大小轮换,都可能在未先同步活动文件的情况下将其重命名,因此,该同步周期无法保证刚刚轮换出的归档文件的持久性。随后,持久化模式决定是截断活动文件、无限期保留,还是进行轮换。这是尽力而为的操作历史记录,而不是事务性审计日志。

不要使用 Observer 输出或 SSE 传递来证明每个规范事件都已保留。反过来,也不要假定 JSONL 中缺失的行从未被发出:它可能已经到达实时广播,并在绑定时到达 Observer bridge,之后才被持久化队列丢弃或处理失败。

读取器游标属于一个活动文件

GET /api/logs 会解析写入器当前的活动路径并调用 reader::load_page。读取器扫描这一个 JSONL 文件,保留最新的匹配窗口,并按从新到旧的顺序返回事件。它不会合并轮转后的归档文件。

主要分页游标是 next_cursor_line_offset,即当前页面上最早匹配事件之后的字节偏移量。调用方将其作为 until_line_offset 传回;下一次扫描会在该行之前停止,并返回更早的匹配项。纯追加会保留现有游标所指向的前缀,因此后续事件不会干扰进行中的遍历。

偏移量不是持久的事件标识,也不是跨文件检查点。每当活动文件的字节内容被替换或其路径发生变化时,它就会失效:

  • rolling trim 会将保留的尾部内容流式写入临时文件,然后将其重命名以覆盖活动路径。
  • rotating 会将活动文件重命名为归档文件;下一次追加操作会创建新的活动文件。
  • 模式迁移会通过临时文件和原子重命名来重写活动文件。
  • 重新加载守护进程配置可以应用新的持久化路径。

越过其中任何一个边界后,都应从最新页面重新开始分页。复用旧编号可能导致重复、跳过或返回无关的行,因为 API 不会在游标中附加文件标识或生成元数据。旧版时间戳/ID 游标仍为保持兼容性而保留,但根据 #8012 已弃用,因为 ID 的字典序排序可能会跳过排序键相同的事件。

持久化策略负责重写和保留

config.rs 中的 StoragePolicy 仅控制 JSONL 的目标位置。Observer 和 broadcast 的传递与之保持独立。

策略活动文件行为留存负责人
none不会再写入新的 JSONL。无。
rolling追加操作超过 max_entries 后,仅将最新的非空行流式写入临时文件,然后将其重命名以覆盖活动文件。写入器会保留配置的活动窗口大小。它不会创建任何归档,也不会管理早期 rotating 配置留下的归档。
full追加写入,不进行由写入器管理的截断或轮转。文件的增长以及任何外部轮换均由操作员负责。
rotating在新 UTC 日期的首次追加之前,或追加达到字节阈值之后,将活动文件重命名为带时间戳的存档文件。每次轮转成功后,写入器会先按存留时间、再按数量清理匹配的归档文件。删除操作尽力而为,绝不会导致外层追加操作失败。

按时间和数量的保留策略仅在轮转后执行。它们不会持续扫描清理,不适用于 fullrolling,也不会删除任意相邻文件:归档发现仅接受由活动路径的带时间戳归档命名格式生成的名称。实时 /api/logs 读取器仍只能看到活动文件;归档是离线诊断产物。

模式迁移就是重写活动文件

启用持久化且活动路径存在时,writer::init_from_config 会在启动磁盘工作线程之前运行 migrate::migrate_legacy_jsonl_in_place。迁移器通过临时文件流式处理非空行,将包含 timestamp 但不含 @timestamp 的旧格式行进行转换,保留已是当前格式的行,跳过格式错误的 JSON 并发出警告,同步临时文件,然后将其原子重命名并覆盖活动路径。

迁移采用尽力而为策略。其廉价的架构检查会在第一条非空行处停止。迁移会在该行格式错误,或包含 timestamp 但不包含 @timestamp 时运行;任何其他可解析的 JSON 都会被视为当前格式,即使其属于未知或无效架构,因此后续的旧版行可能仍未完成迁移。如果迁移返回错误,初始化会发出警告并继续,因此后续的 v2 追加内容可能会与 v2 读取器无法反序列化的旧行共存。轮换后的归档不会迁移。

LogEvent 是架构的唯一事实来源。对于每次架构变更,评估迁移兼容性、活动文件反序列化、HTTP 和 RPC 序列化/使用方,以及架构和运维人员文档;仅更新行为或兼容性发生变化的边界。RPC 日志接口位于 crates/zeroclaw-runtime/src/rpc/types.rsdispatch.rs 中。替换活动文件的迁移会使字节偏移游标失效,而不会重写现有字节的附加式兼容变更则不会。

LogConfigObservabilityConfig

zeroclaw-log 定义了自己的最小 LogConfig(在 crates/zeroclaw-log/src/config.rs 中):log_persistence, log_persistence_path, log_persistence_max_entries, log_persistence_max_bytes, log_persistence_rotate_daily, log_persistence_retention_max_files, log_persistence_retention_max_age_days, log_tool_io, log_tool_io_truncate_bytes, log_tool_io_denylist。这打破了原本会形成的依赖循环:zeroclaw-config::ObservabilityConfig 承载完整 schema(包含 TOML 反序列化和校验),而运行时会在启动时以及守护进程配置重载后,通过 crates/zeroclaw-runtime/src/observability/runtime_trace.rs::to_log_config 转换为 LogConfig。结果是:zeroclaw-config 可以在不反转依赖树的情况下 record!,同时日志持久化和轮转策略的变更仍会在下一次守护进程重载时生效。

订阅者安装

守护进程通过以下方式安装全局订阅者:

#![allow(unused)]
fn main() {
zeroclaw_log::install_global_subscriber(
    recording_filter.as_deref(),   // Option<&str> — --log-level 标志(如果已设置)
    &default_filter,               // &str — 在没有标志和 RUST_LOG 时的后备过滤器
    cli.verbose,                   // bool — 控制 stderr fmt(终端)层
);
}

两个独立的轴:录制底线(到达 LogCaptureLayer 的内容,按 flag → RUST_LOG → 默认值的顺序解析)与终端显示(stderr fmt 层,除非 verbose 为 true,否则完全静默)。这一次调用即可在 tracing-subscriber::Registry 之上设置带 agent 别名前缀的终端格式化器以及 LogCaptureLayersrc/main.rs 是唯一调用它的地方。测试使用 zeroclaw_log::try_install_capture_subscriber() + zeroclaw_log::subscribe_or_install(),通过广播钩子抽取已发出的事件,而无需在测试 crate 中命名任何 tracing 类型。

何时扩展封闭枚举

  • 新增通道实现:向 ChannelKind 添加一个变体。其 snake_case 形式即为磁盘上的 channel_type 字符串。仅当变体名称 snake_case 后无法得到期望值时(例如 OpenAi"openai"),才需添加 #[strum(serialize = "...")]
  • 新工具实现(工作区内置):添加到 ToolKind
  • 新增 cron 调度结构:添加到 CronKind
  • 新模型 / TTS / 转录 / 隧道提供方:将其添加到 ProviderKind 下相关的 *ProviderKind 子枚举中。
  • 新增内存后端:添加到 MemoryKind
  • 全新的 Role 系列(PeerGroup / Skill / Mcp 获得子类型):随时使用其自身的 Kind 进行嵌套:模式是统一的。

然后在新结构体旁添加 impl Attributable for Xfn role() -> Role::Family(Kind::Variant)fn alias() -> &str { &self.alias }),并用 attribution_span!(self) 包裹其入口点。其余部分该层会自动处理。

操作符注意事项

有关配置项(log_persistencelog_tool_io、OTel 导出)和查询语法,请参阅日志与可观测性