package loop import ( "context" "log/slog" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent" "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 // 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 // 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 }