Files
Cyrene_v2/internal/runtime/loop/options.go
T
AskaEth 1a85de555d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(emotion): A1 情绪系统接线 + refactor: 拆分 channelbridge.go
A1 情绪接线:
- loop 回合边界钩子(TurnEnd/OnTurnEnd/settleTurn, Result.Mood); 每轮一次非每步
- persona/emotion: React(valence五档) + Machine.Move(BFS最短合法路径一步) + Observer(observe/decay/好感度,仅变化才emit); 好感度步长读角色配置(cyrene delta=10, 0..700)
- 落库 Mood(web session_persist.res.Mood + IM channelbridge/persist; user消息不带mood); WS mood事件(BroadcastMood,仅变化); per-persona隔离; 群聊/陌生人只读不写不发
- cmd/cyrene/emotion.go 生产接线 + wire.go 注入 loop.Options.OnTurnEnd
- 22 测试(状态机被loop调用/Mood非空落库/mood帧shape/只变化才发/群聊只读) + 真二进制冒烟
- Docs: api-client v0.32 §3.7 情绪事件 + 开源对照清单(companion 应用对照)
- 诚实: 本机无LLM凭据→未活体验证'模型返回后完整轮次'一环

拆分 channelbridge.go 816→257(纯搬移零逻辑): connect/inbound/outbound/blacklist/helpers.go; A1 的 IM mood 调用点落在新 inbound.go, 故与本提交同批

回滚 6 个 gofmt 顺手格式化文件(capacity_loop_test/flow/source_test/findchannel_test/admin_channels_platform_test/history) 缩 diff, 含 history.go 注释被转弯引号
2026-09-30 08:51:38 +08:00

132 lines
7.4 KiB
Go

package loop
import (
"context"
"log/slog"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/guardrail"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// Options configures a Loop. All fields are optional and nil is tolerated until
// the step that actually needs the field: the agent is required for Run (Run
// returns ErrNilAgent otherwise); a missing tool dispatcher causes a requested
// call to be fed back as an error; a missing retriever/persona skips memory
// injection; a missing extractor/store skips the memory hook; a missing
// checkpoint skips persistence; a missing tracer skips tracing.
type Options struct {
// Agent is the loop's execution unit: an instruction string plus a backend
// adapter, an optional tool dispatcher and optional guardrails.
Agent *agent.Agent
// Tools is the tool dispatcher used to execute model-requested calls. When
// nil the loop falls back to the agent's own dispatcher (if any).
Tools *tool.Dispatcher
// Capacity, when non-nil and enabled, filters the tool schemas advertised to
// the model each round to the allowed set for the current capacity state
// (design/14 唤醒式能力渐进 §4): the loop only exposes the allowed set to the
// model — a view, not a hard boundary (Invoke is left unblocked so an
// emergency call still works). Nil (the default) keeps the pre-existing
// behavior: every registered tool is advertised.
Capacity *capacity.Decider
// OnCapacity, when non-nil, receives capacity_awaken transition events the
// loop observes while deciding a round (design/14 §4), so a caller can
// surface the "能力点亮" progression to the frontend. Nil = no-op.
OnCapacity func(capacity.Event)
// OnAudit, when non-nil, receives an AuditEvent every time the loop denies a
// tool call in a restricted (group/stranger) environment (design/11 §3 §8),
// so the rejection lands in the audit trail and the frontend can render the
// small "xx 工具在群聊被拒" note. Nil = the rejection is still fed back to the
// model as a soft not_allowed result, just not recorded to an external sink.
OnAudit func(AuditEvent)
// OnTurnEnd, when non-nil, is called once per settled turn (a Run that
// returned a Result) with the turn's user/assistant text and the persona the
// loop is bound to, so a caller can advance state that follows the persona —
// the emotion machine and affection curve of design/01 §2. Its return value
// is recorded on Result.Mood and carried to the persisted assistant message.
// Nil = the loop reports nothing and Result.Mood stays empty.
OnTurnEnd TurnEndHook
// Memory is the long-term memory store used to persist extracted items.
Memory *memory.Store
// Retriever injects relevant memories into the context, re-evaluated every
// round (see assembleContext) so retrieval tracks the latest conversation.
Retriever *memory.Retriever
// WorkingMemory is the per-session sliding window, provided for the
// orchestrator to inspect; the loop does not mutate it.
WorkingMemory *memory.WorkingMemory
// Extractor derives memory items to persist after every step.
Extractor memory.Extractor
// Checkpoint persists a durable snapshot of the loop state at every step and,
// on a later Run with the same sessionID, resumes an interrupted run from it
// (design §1 durable execution). *persist.Store satisfies the narrow
// checkpointStore interface; nil disables both persistence and resume.
Checkpoint checkpointStore
// Tracer emits a structured span for every step, LLM call and tool call.
Tracer *trace.Tracer
// Flow, when non-nil, accumulates the run's trace spans into a per-session
// flow graph and publishes ws.graph_update on each span transition (design/19
// §1). Nil disables the realtime flow visualization.
Flow *FlowPublisher
// Guards validates request/response payloads on the streaming path, where
// RunStep is bypassed. On the non-streaming path the agent runs its own
// guardrails inside RunStep.
Guards *guardrail.Pipeline
// PersonaID scopes memory retrieval and extraction. Blank skips both.
PersonaID string
// Timezone is the IANA zone used to render the per-round "current time"
// system message (e.g. "Asia/Shanghai"). Empty disables time injection
// entirely (backward compatible); a name that fails to load also disables it.
Timezone string
// MaxRounds is the tool-loop upper bound. Non-positive selects
// DefaultMaxRounds (8).
MaxRounds int
// OnStream, when non-nil, switches the loop to the streaming path: the model
// adapter's ChatStream is driven directly and each structured stream event is
// forwarded to this callback (model.StreamEvent), so the frontend can render
// body text, tool calls and (eventually) reasoning segments independently.
OnStream func(model.StreamEvent)
// ToolExecution selects how a batch of tool calls is run: "parallel" (the
// default) or "sequential". Empty means parallel.
ToolExecution string
// ToolParallelism caps how many tool calls run concurrently when ToolExecution
// is "parallel". Non-positive selects DefaultToolParallelism (4).
ToolParallelism int
// Hooks installs optional per-batch/per-call tool-loop hooks. Every field is
// nil-tolerant; these are reserved for "敏感操作代审" (design/11) and the
// future asynchronous-tool path. A zero Hooks value is a no-op.
Hooks ToolLoopHooks
// MaxContextTokens is the context budget that triggers overflow compaction.
// Non-positive (the zero value) disables compaction, preserving the loop's
// previous behavior. Setting it to a positive value (e.g.
// DefaultMaxContextTokens) enables compaction: when the loop's estimated
// context exceeds the budget it summarizes older turns into a leading summary
// message while keeping the recent tail (including trailing tool results)
// intact (design §1.3). Nil-tolerant: leaving it unset changes nothing.
MaxContextTokens int
// Compactor compacts an over-budget conversation. Nil selects the built-in
// structural compactor (no model call); a non-nil value lets a caller inject
// an LLM-backed or custom summarizer. It is only invoked when
// MaxContextTokens is positive.
Compactor Compactor
// Vision, when non-nil, rewrites an image-bearing user message at the start
// of Run so the main model can reason over the image (design/06 §0). The
// concrete resolver (vision.MessageResolver) decides via the vision Router
// whether to keep the image for a vision-capable main model or fold it into a
// text description from the vision/OCR tool ("先看图再对话"). Nil keeps the
// image content as-is, passing it through to a vision-capable main model.
Vision ImageResolver
// TransformContext, when non-nil, is called immediately before every model
// step with the assembled message list (after memory injection and any
// compaction) and the current round number (1-based). It may project, filter
// or re-order the messages the model sees. It is the loop's equivalent of
// OpenClaw's transformContext (design §1.3). Nil = no projection.
TransformContext func(ctx context.Context, messages []model.Message, round int) []model.Message
// Log is the base logger. Nil is replaced with a discard logger tagged with
// source "runtime/loop".
Log *slog.Logger
}