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 }