【角色视觉体系(红线:每个角色自己的头像/背景,绝不串用昔涟)】
- persona.go:Persona 加 Visual{avatar, chatBackground{landscape,portrait}} + DefaultAvatar;每个角色各自配。
- cyrene.yaml:昔涟配 avatar(3F-Q-Happy-1) + chatBackground(横/竖屏)。
- PersonaStore 加 Persona(id);PersonaSummary 加 avatar/chatBackground;collectPersonas 按角色取
Persona(id) 视觉(无视觉→空,前端回退默认占位,绝不串角色)。
- 前端:types PersonaRecord 加 avatar/chatBackground;persona.ts personaAvatar(p)=p?.avatar||默认占位
(殊无昔涟 fallback);store 加 currentPersona;App 挂载 fetchPersonas 取默认角色;
TopBar/chat 头像改 personaAvatar(currentPersona)(写死昔涟头像已清除)。
【时间注入(她自然知道现在几点,深夜催睡交给人格/capacity)】
- options.go Options 加 Timezone;loop.go Loop 加 timeLocation/timeZoneName + New 解析 + loadTimezone;
time.go currentTimeBlock()(当前时间+周几+时区,每轮刷新);memory.go assembleContext 在「工作记忆」
前注入时间 system(keyed 在 timeLocation!=nil,无时区不注入不破坏测试);wire.go Timezone=a.cfg.Timezone。
go build/vet/test 全绿(gateway/loop/persona);前端 tsc/vite build 75模块绿。红线无写死昔涟头像。
113 lines
6.1 KiB
Go
113 lines
6.1 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/persist"
|
|
"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)
|
|
// 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.
|
|
Checkpoint *persist.Store
|
|
// Tracer emits a structured span for every step, LLM call and tool call.
|
|
Tracer *trace.Tracer
|
|
// 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
|
|
}
|