docs(comments): 注释中文化 · internal/runtime/orchestration(29 文件,纯替换)
- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态) - 裁决编号与 design 节号(02.4.3 / 11.4.2 / design/05 §9 等)逐字保留 - 复核:go build 绿;orchestration 及其子包(audit/comm)测试绿;gofmt 干净
This commit is contained in:
1 parent
fe90f64a56
commit
eb1828ee7a
29 files changed
+751
-949
No files matched your search
@@ -12,28 +12,23 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
)
|
||||
|
||||
// Constants for the sub-agent dispatch tool (design/05 §1 创建, §7 并行与安全).
|
||||
// 子代理派发工具(design/05 §1 创建, §7 并行与安全)的常量。
|
||||
const (
|
||||
// SpawnToolName is the registered name of the tool the agent calls to
|
||||
// dispatch background sub-agents.
|
||||
// SpawnToolName 是 agent 调用以派发后台子代理的工具的注册名。
|
||||
SpawnToolName = "subagent_spawn"
|
||||
|
||||
// DefaultSpawnWait is how long a foreground dispatch waits for its batch
|
||||
// before handing the turn back to the model. It mirrors the runtime's
|
||||
// established "前台运行,超过 2 秒自动移入后台" threshold
|
||||
// (DevDocs/design/04-运行时.md §8.5, internal/runtime/task). The sub-agents keep
|
||||
// running either way; only whether the tool result already carries their
|
||||
// output changes.
|
||||
// DefaultSpawnWait 是前台派发在把回合交还模型之前等待其批次的时间。它沿用运行时
|
||||
// 既定的「前台运行,超过 2 秒自动移入后台」阈值
|
||||
// (DevDocs/design/04-运行时.md §8.5, internal/runtime/task)。子代理无论如何都
|
||||
// 继续运行;变的只是工具结果是否已带上它们的输出。
|
||||
DefaultSpawnWait = 2 * time.Second
|
||||
|
||||
// maxSpawnBatch caps how many sub-agents one dispatch may create. It bounds the
|
||||
// queue a single model call can build (design/05 §7 防失控); the parallel
|
||||
// execution limit itself stays with the scheduler's MaxParallel.
|
||||
// maxSpawnBatch 限制一次派发最多创建多少个子代理。它约束单次模型调用能堆积的
|
||||
// 队列长度(design/05 §7 防失控);并行执行上限本身仍由调度器的 MaxParallel 负责。
|
||||
maxSpawnBatch = 8
|
||||
|
||||
// maxSpawnTextRunes caps how many runes of one sub-agent's result text are
|
||||
// handed back to the spawning model, so a runaway sub-agent cannot flood the
|
||||
// front session's context. The scheduler keeps the full text on its snapshot.
|
||||
// maxSpawnTextRunes 限制交回派发模型的单个子代理结论文本的字符数,使失控的子代理
|
||||
// 无法淹没前台会话的上下文。调度器在其快照上保留完整文本。
|
||||
maxSpawnTextRunes = 4000
|
||||
|
||||
// minSpawnTimeoutSeconds 与 maxSpawnTimeoutSeconds 界定按调用覆盖
|
||||
@@ -50,14 +45,12 @@ const (
|
||||
SubagentResultSubject = "subagent.result"
|
||||
)
|
||||
|
||||
// SpawnTool is the runtime entry point that lets the running agent dispatch
|
||||
// sub-agents (design/05 §1). It is the only production caller of
|
||||
// (*Scheduler).Spawn: the model invokes it like any other tool, the scheduler
|
||||
// applies the parallel limit, per-agent timeout and abort machinery, and the tool
|
||||
// hands back either the merged result or — when the batch is still running after
|
||||
// the foreground wait — a dispatch acknowledgement.
|
||||
// SpawnTool 是让运行中的 agent 派发子代理的运行时入口(design/05 §1)。它是
|
||||
// (*Scheduler).Spawn 唯一的生产调用方:模型像调用任何其他工具一样调用它,调度器施加
|
||||
// 并行上限、按代理时限与中止机制,工具则交回合并结果,或者 —— 前台窗口内批次仍在跑时
|
||||
// —— 一条派发确认。
|
||||
//
|
||||
// It is safe for concurrent use and holds no per-call state.
|
||||
// 它可安全并发使用,且不持有任何按调用的状态。
|
||||
type SpawnTool struct {
|
||||
sched *Scheduler
|
||||
wait time.Duration
|
||||
@@ -65,11 +58,10 @@ type SpawnTool struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// SpawnOption customizes a SpawnTool built by NewSpawnTool.
|
||||
// SpawnOption 定制由 NewSpawnTool 构建的 SpawnTool。
|
||||
type SpawnOption func(*SpawnTool)
|
||||
|
||||
// WithSpawnWait overrides how long a foreground dispatch waits for its batch. A
|
||||
// non-positive value keeps DefaultSpawnWait.
|
||||
// WithSpawnWait 覆盖前台派发等待其批次的时间。非正值保持 DefaultSpawnWait。
|
||||
func WithSpawnWait(d time.Duration) SpawnOption {
|
||||
return func(t *SpawnTool) {
|
||||
if d > 0 {
|
||||
@@ -78,7 +70,7 @@ func WithSpawnWait(d time.Duration) SpawnOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithSpawnLogger replaces the tool's logger (tagged source "orchestration/spawn").
|
||||
// WithSpawnLogger 替换该工具的 logger(标注来源 "orchestration/spawn")。
|
||||
func WithSpawnLogger(l *slog.Logger) SpawnOption {
|
||||
return func(t *SpawnTool) { t.log = spawnLog(l) }
|
||||
}
|
||||
@@ -95,8 +87,8 @@ func WithSpawnTimeout(d time.Duration) SpawnOption {
|
||||
}
|
||||
}
|
||||
|
||||
// NewSpawnTool builds the dispatch tool over sched. A nil scheduler is tolerated:
|
||||
// Execute then fails fast with ErrNoScheduler instead of panicking.
|
||||
// NewSpawnTool 在 sched 上构建派发工具。调度器为 nil 也被容忍:Execute 随后以
|
||||
// ErrNoScheduler 快速失败,而不是 panic。
|
||||
func NewSpawnTool(sched *Scheduler, opts ...SpawnOption) *SpawnTool {
|
||||
t := &SpawnTool{sched: sched, wait: DefaultSpawnWait, log: spawnLog(nil)}
|
||||
for _, opt := range opts {
|
||||
@@ -107,12 +99,11 @@ func NewSpawnTool(sched *Scheduler, opts ...SpawnOption) *SpawnTool {
|
||||
return t
|
||||
}
|
||||
|
||||
// Name returns the tool identifier.
|
||||
// Name 返回工具标识符。
|
||||
func (t *SpawnTool) Name() string { return SpawnToolName }
|
||||
|
||||
// Description explains the tool to the model: what a sub-agent is, that it runs in
|
||||
// an isolated context in parallel, and that a slow batch is handed to the
|
||||
// background rather than blocking the conversation (design/05 §1/§2).
|
||||
// Description 向模型解释该工具:什么是子代理、它在隔离上下文中并行运行、慢批次会被
|
||||
// 交给后台而不阻塞对话(design/05 §1/§2)。
|
||||
func (t *SpawnTool) Description() string {
|
||||
return "Dispatch one or more sub-agents to work in parallel on independent parts of a task. " +
|
||||
"Each sub-agent runs in its own isolated context and returns a summary plus its result. " +
|
||||
@@ -120,11 +111,9 @@ func (t *SpawnTool) Description() string {
|
||||
"results later; prefer several sub-agents over one serial chain for independent work."
|
||||
}
|
||||
|
||||
// Parameters returns the input JSON Schema: either a single goal shorthand or a
|
||||
// tasks array for a parallel batch, plus an optional persona (the role the
|
||||
// sub-agent runs as, design/22 §8-2), an optional background flag, an optional
|
||||
// per-dispatch tool whitelist (design/05 §7) and an optional per-call timeout
|
||||
// override.
|
||||
// Parameters 返回输入的 JSON Schema:单个 goal 简写,或用于并行批次的 tasks 数组,
|
||||
// 外加可选的 persona(子代理运行时所充当的角色,design/22 §8-2)、可选的 background
|
||||
// 标志、可选的按派发工具白名单(design/05 §7)与可选的按调用超时覆盖。
|
||||
func (t *SpawnTool) Parameters() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
@@ -149,15 +138,13 @@ func (t *SpawnTool) Parameters() (map[string]any, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute dispatches the requested sub-agents, then either returns their merged
|
||||
// results (the batch settled inside the foreground window) or a dispatch
|
||||
// acknowledgement (it moved to the background, reported later on the bus).
|
||||
// Execute 派发所请求的子代理,随后要么返回它们的合并结果(批次在前台窗口内结算),
|
||||
// 要么返回一条派发确认(批次已转入后台,稍后经总线报告)。
|
||||
//
|
||||
// The sub-agents are spawned on a detached context: their lifetime is their own
|
||||
// (bounded by the scheduler's per-agent timeout and Abort/CancelAll), never the
|
||||
// dispatching turn's, so a slow batch is not killed the moment the tool returns
|
||||
// (design/04 §2 后台不阻塞前台). Context values are kept so the sub-agent span —
|
||||
// and the flow lane it roots — still chains under the step that dispatched it.
|
||||
// 子代理在一个脱离的上下文上派发:它们的生命周期属于自己(受调度器的按代理时限与
|
||||
// Abort/CancelAll 约束),从不是派发回合的,因此慢批次不会在工具返回的那一刻被杀
|
||||
// (design/04 §2 后台不阻塞前台)。上下文值被保留,使子代理 span —— 以及它作为根的
|
||||
// 泳道 —— 仍串在派发它的那一步之下。
|
||||
func (t *SpawnTool) Execute(ctx context.Context, args any) (any, error) {
|
||||
m, ok := args.(map[string]any)
|
||||
if !ok {
|
||||
@@ -170,9 +157,8 @@ func (t *SpawnTool) Execute(ctx context.Context, args any) (any, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", SpawnToolName, err)
|
||||
}
|
||||
// Reject an unknown target role before anything starts: a batch must be
|
||||
// all-or-nothing, otherwise the sub-agents already dispatched would keep running
|
||||
// with their result never reported to the model (design/22 §8-2: 未知角色明确报错,
|
||||
// 在任何东西启动之前就拒绝未知的目标角色:批次必须是全有或全无,否则已经派发出的
|
||||
// 子代理会继续运行,而它们的结果永远不上报给模型(design/22 §8-2: 未知角色明确报错,
|
||||
// 不静默回落到默认角色).
|
||||
if perr := t.sched.CheckPersonas(specs); perr != nil {
|
||||
return nil, fmt.Errorf("%s: %w", SpawnToolName, perr)
|
||||
@@ -197,9 +183,8 @@ func (t *SpawnTool) Execute(ctx context.Context, args any) (any, error) {
|
||||
return t.foreground(ids, specs), nil
|
||||
}
|
||||
|
||||
// foreground waits up to t.wait for the dispatched batch to settle. A settled
|
||||
// batch is merged into the tool result; a batch still running when the window
|
||||
// closes is announced in the background and acknowledged immediately.
|
||||
// foreground 至多等待 t.wait 让已派发的批次结算。结算的批次被合并进工具结果;窗口
|
||||
// 关闭时仍在运行的批次改到后台宣告,并立即被确认。
|
||||
func (t *SpawnTool) foreground(ids []string, specs []AgentSpec) any {
|
||||
waitCtx, cancel := context.WithTimeout(context.Background(), t.wait)
|
||||
defer cancel()
|
||||
@@ -212,9 +197,8 @@ func (t *SpawnTool) foreground(ids []string, specs []AgentSpec) any {
|
||||
return doneSpawnResult(res)
|
||||
}
|
||||
|
||||
// await blocks until the batch settles and broadcasts the merged outcome as the
|
||||
// design/05 §2 "结束后写执行总结 → 返回主会话" hand-off. It runs on a detached
|
||||
// goroutine; a nil bus or an unknown id ends it quietly.
|
||||
// await 阻塞到批次结算,并把合并结果作为 design/05 §2「结束后写执行总结 → 返回主会话」
|
||||
// 的交接广播出去。它运行在脱离的 goroutine 上;总线为 nil 或 id 未知时安静结束。
|
||||
// parentSession 是该批次所属的父会话,随载荷一起广播,使中继能把回填结果
|
||||
// 归属到父会话的实时流,而不是发一张无归属的帧。
|
||||
func (t *SpawnTool) await(ids []string, parentSession string) {
|
||||
@@ -235,36 +219,35 @@ func (t *SpawnTool) await(ids []string, parentSession string) {
|
||||
}
|
||||
}
|
||||
|
||||
// SpawnResult is the payload the dispatch tool returns to the spawning model: the
|
||||
// batch's merged outcome when it settled inside the foreground window, or a
|
||||
// dispatch acknowledgement when it moved to the background.
|
||||
// SpawnResult 是派发工具返回给派发模型的载荷:批次在前台窗口内结算时是它的合并结果,
|
||||
// 转入后台时是一条派发确认。
|
||||
type SpawnResult struct {
|
||||
// Status is "done" (settled) or "running" (dispatched to the background).
|
||||
// Status 是 "done"(已结算)或 "running"(已转入后台)。
|
||||
Status string `json:"status"`
|
||||
// Summary is a one-line aggregate over the batch.
|
||||
// Summary 是对该批次的单行聚合。
|
||||
Summary string `json:"summary"`
|
||||
// Agents holds one entry per dispatched sub-agent, in dispatch order.
|
||||
// Agents 持有每个已派发子代理的一个条目,按派发顺序排列。
|
||||
Agents []SpawnAgentResult `json:"agents"`
|
||||
}
|
||||
|
||||
// SpawnAgentResult is one sub-agent's entry in a SpawnResult.
|
||||
// SpawnAgentResult 是 SpawnResult 中单个子代理的条目。
|
||||
type SpawnAgentResult struct {
|
||||
// ID is the sub-agent id (also the id the WS subagent_event frames carry).
|
||||
// ID 是子代理 id(也是 WS subagent_event 帧所携带的 id)。
|
||||
ID string `json:"id"`
|
||||
// Goal is the objective the sub-agent was given.
|
||||
// Goal 是交给该子代理的目标。
|
||||
Goal string `json:"goal"`
|
||||
// Role is the bound role/persona, if any: the dispatched target persona when one
|
||||
// was named, otherwise the legacy role label.
|
||||
// Role 是绑定的角色/persona(若有):指名了派发目标角色时用该角色,否则是旧的
|
||||
// 角色标签。
|
||||
Role string `json:"role,omitempty"`
|
||||
// Mode is the bound behavior mode, if any.
|
||||
// Mode 是绑定的行为模式(若有)。
|
||||
Mode string `json:"mode,omitempty"`
|
||||
// Status is the lifecycle status at return time.
|
||||
// Status 是返回时刻的生命周期状态。
|
||||
Status string `json:"status"`
|
||||
// Summary is the execution summary written when the sub-agent settled.
|
||||
// Summary 是子代理结算时写入的执行摘要。
|
||||
Summary string `json:"summary,omitempty"`
|
||||
// Text is the sub-agent's result text (capped at maxSpawnTextRunes).
|
||||
// Text 是子代理的结果文本(上限 maxSpawnTextRunes)。
|
||||
Text string `json:"text,omitempty"`
|
||||
// Steps is how many loop steps the sub-agent ran.
|
||||
// Steps 是子代理执行的循环步数。
|
||||
Steps int `json:"steps,omitempty"`
|
||||
}
|
||||
|
||||
@@ -275,7 +258,7 @@ type SubagentResultEvent struct {
|
||||
*SpawnResult
|
||||
}
|
||||
|
||||
// doneSpawnResult maps a settled batch onto the tool result.
|
||||
// doneSpawnResult 把已结算的批次映射为工具结果。
|
||||
func doneSpawnResult(res *MergedResult) *SpawnResult {
|
||||
out := &SpawnResult{Status: "done", Agents: make([]SpawnAgentResult, 0)}
|
||||
if res == nil {
|
||||
@@ -297,7 +280,7 @@ func doneSpawnResult(res *MergedResult) *SpawnResult {
|
||||
return out
|
||||
}
|
||||
|
||||
// runningSpawnResult acknowledges a batch that is still running in the background.
|
||||
// runningSpawnResult 确认一个仍在后台运行的批次。
|
||||
func runningSpawnResult(ids []string, specs []AgentSpec) *SpawnResult {
|
||||
out := &SpawnResult{
|
||||
Status: "running",
|
||||
@@ -317,10 +300,9 @@ func runningSpawnResult(ids []string, specs []AgentSpec) *SpawnResult {
|
||||
return out
|
||||
}
|
||||
|
||||
// spawnSpecs decodes the tool arguments into one AgentSpec per sub-agent. A
|
||||
// non-empty tasks array is a parallel batch and takes precedence over the single
|
||||
// goal shorthand. dftTimeout is the dispatch-layer default (WithSpawnTimeout);
|
||||
// a non-positive value leaves the scheduler's per-agent timeout in charge.
|
||||
// spawnSpecs 把工具参数解码成每个子代理一个 AgentSpec。非空的 tasks 数组是并行批次,
|
||||
// 优先于单个 goal 简写。dftTimeout 是派发层默认值(WithSpawnTimeout);非正值则让
|
||||
// 调度器的按代理时限说了算。
|
||||
func spawnSpecs(m map[string]any, parentSession string, dftTimeout time.Duration) ([]AgentSpec, error) {
|
||||
tasks, err := taskList(m["tasks"])
|
||||
if err != nil {
|
||||
@@ -360,8 +342,8 @@ func spawnSpecs(m map[string]any, parentSession string, dftTimeout time.Duration
|
||||
return specs, nil
|
||||
}
|
||||
|
||||
// specFromMap reads one AgentSpec out of a decoded arguments object (the top-level
|
||||
// object or one tasks entry). A blank goal wraps ErrMissingGoal.
|
||||
// specFromMap 从已解码的参数对象(顶层对象或 tasks 中的一个条目)读取一个 AgentSpec。
|
||||
// goal 为空时包装 ErrMissingGoal。
|
||||
func specFromMap(m map[string]any, parentSession string) (AgentSpec, error) {
|
||||
goal := strings.TrimSpace(stringArg(m, "goal"))
|
||||
if goal == "" {
|
||||
@@ -369,11 +351,10 @@ func specFromMap(m map[string]any, parentSession string) (AgentSpec, error) {
|
||||
}
|
||||
personaID := strings.TrimSpace(stringArg(m, "persona"))
|
||||
roleID := strings.TrimSpace(stringArg(m, "role"))
|
||||
// A named persona is the role the sub-agent actually runs as, so it also fills
|
||||
// the binding metadata: leaving role and persona disagreeing would make the tool
|
||||
// result, the progress events and the ws subagent_event frame name a role the
|
||||
// sub-agent is not answering as. Omitting persona keeps the pre-existing
|
||||
// role-only path byte for byte (design/22 §8-2 backward compatibility).
|
||||
// 被指名的 persona 就是子代理实际充当的角色,因此它也填充绑定元数据:让 role 与
|
||||
// persona 互相矛盾会导致工具结果、进度事件与 ws subagent_event 帧点名一个子代理
|
||||
// 并未以之作答的角色。省略 persona 则逐字节保持既有的纯 role 路径
|
||||
// (design/22 §8-2 向后兼容)。
|
||||
if personaID != "" {
|
||||
roleID = personaID
|
||||
}
|
||||
@@ -389,9 +370,8 @@ func specFromMap(m map[string]any, parentSession string) (AgentSpec, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// taskList reads a tasks array of objects. A missing or empty array yields nil; a
|
||||
// non-array or an entry that is not an object is rejected so a malformed batch
|
||||
// never silently degrades into a single spawn.
|
||||
// taskList 读取由对象组成的 tasks 数组。缺失或空数组产出 nil;非数组或条目不是对象则
|
||||
// 被拒绝,使畸形批次绝不静默退化成单次派发。
|
||||
func taskList(v any) ([]map[string]any, error) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
@@ -411,12 +391,12 @@ func taskList(v any) ([]map[string]any, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// spawnStringProp builds a string property schema.
|
||||
// spawnStringProp 构建一个字符串属性 schema。
|
||||
func spawnStringProp(desc string) map[string]any {
|
||||
return map[string]any{"type": "string", "description": desc}
|
||||
}
|
||||
|
||||
// spawnTasksProp builds the tasks array property schema for a parallel batch.
|
||||
// spawnTasksProp 构建并行批次的 tasks 数组属性 schema。
|
||||
func spawnTasksProp() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "array",
|
||||
@@ -438,8 +418,8 @@ func spawnTasksProp() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// spawnWhitelistProp builds the tool_whitelist property schema (design/05 §7
|
||||
// 裁决 02.4.1):缺省全开,显式列出则只放行所列。
|
||||
// spawnWhitelistProp 构建 tool_whitelist 属性 schema(design/05 §7
|
||||
// 裁决 02.4.1):缺省全开,显式列出则只放行所列。
|
||||
func spawnWhitelistProp(desc string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "array",
|
||||
@@ -452,8 +432,8 @@ func spawnWhitelistProp(desc string) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// spawnTimeoutProp builds the timeout_seconds property schema (design/05 §7
|
||||
// 裁决 02.4.4 的按调用覆盖)。dft 非正时描述为「沿用派发默认」。
|
||||
// spawnTimeoutProp 构建 timeout_seconds 属性 schema(design/05 §7
|
||||
// 裁决 02.4.4 的按调用覆盖)。dft 非正时描述为「沿用派发默认」。
|
||||
func spawnTimeoutProp(dft time.Duration) map[string]any {
|
||||
desc := "Per-call timeout override in seconds for the sub-agent run (optional; omitted keeps the " +
|
||||
"configured sub-agent default)."
|
||||
@@ -468,29 +448,28 @@ func spawnTimeoutProp(dft time.Duration) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// spawnPersonaProp builds the persona property schema. It is the design/22 §8-2 派发
|
||||
// switch: naming a role makes the sub-agent stand in for it (that role's
|
||||
// instructions and memory scope) instead of working as the spawning session's role.
|
||||
// spawnPersonaProp 构建 persona 属性 schema。它是 design/22 §8-2 的派发开关:指名一个
|
||||
// 角色会让子代理替班该角色(用该角色的指令与记忆作用域),而不是以派发会话的角色工作。
|
||||
func spawnPersonaProp() map[string]any {
|
||||
return spawnStringProp("Persona/role id the sub-agent runs AS: it answers with that role's " +
|
||||
"instructions and reads/writes that role's memory scope (optional, default: the current " +
|
||||
"role). A role id this runtime does not know is rejected. Supersedes role.")
|
||||
}
|
||||
|
||||
// stringArg reads a string argument, tolerating a missing or non-string value.
|
||||
// stringArg 读取一个字符串参数,容忍缺失或非字符串值。
|
||||
func stringArg(m map[string]any, key string) string {
|
||||
s, _ := m[key].(string)
|
||||
return s
|
||||
}
|
||||
|
||||
// boolArg reads a boolean argument, tolerating a missing or non-boolean value.
|
||||
// boolArg 读取一个布尔参数,容忍缺失或非布尔值。
|
||||
func boolArg(m map[string]any, key string) bool {
|
||||
b, _ := m[key].(bool)
|
||||
return b
|
||||
}
|
||||
|
||||
// spawnLog returns l tagged with source "orchestration/spawn", replacing a nil
|
||||
// logger with a discard one so the tool can always log.
|
||||
// spawnLog 返回标注了来源 "orchestration/spawn" 的 l,并把 nil logger 换成丢弃式
|
||||
// logger,使该工具总能记录日志。
|
||||
func spawnLog(l *slog.Logger) *slog.Logger {
|
||||
if l == nil {
|
||||
l = slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
|
||||
Reference in new issue
Block a user