Documentation
¶
Overview ¶
Run-scoped holder for the active model Selection and the Source the next model_turn must record, with a distinct "no model selected yet" state.
Resolves the initial active model/effort across the resume and fresh-launch paths.
Model-selection persistence: the per-project model.active and global model.last_used writes through the shared config persistence chokepoint, the model_changed / persist-failure event helpers, and the TUI picker-selection write flow.
Parses model.active / model.last_used values and runs the shape and cross-reference checks.
Model-turn event append helpers, request-effort filtering, and usage-anomaly normalization for the assistant-turn boundary.
Index ¶
- Constants
- func DetermineSelection(ctx Context) (*Selection, Source, error)
- func EmitModelChanged(store *sessions.Store, sel Selection, prior *Selection) error
- func EmitModelEffortUnsupported(store *sessions.Store, provider, modelID, configuredEffort string) error
- func EmitModelGlobalMirrorFailed(store *sessions.Store, targetPath string, sel Selection, ...) error
- func EmitModelPersistFailed(store *sessions.Store, targetPath string, sel Selection, ...) error
- func EmitModelResponse(store *sessions.Store, provider, modelID string, ...) error
- func EmitModelTurn(store *sessions.Store, provider, modelID, effort string, source Source) error
- func EmitTurnStart(store *sessions.Store, sel Selection, source Source, supportedSet []string) (effortToSend string, dropped bool, err error)
- func ParseSelection(raw any) (*Selection, *ShapeError)
- func RecoverActiveSelection(ctx Context) (*Selection, RecoveryOutcome, error)
- func ResolveRequestEffort(sel Selection, supportedSet []string) (effortToSend, configuredEffort string, dropped bool)
- func SumModelResponseTokens(records []sessions.Record) (inputTokens, outputTokens int64)
- func UsageFromStreamUsage(u openai.CompletionUsage, usageValid bool) (inputTokens, outputTokens int64, cached *int64, anomalies []string)
- func WriteActive(projectRoot string, sel Selection) (targetPath string, created bool, reason config.PersistReason, ...)
- func WriteLastUsed(home string, sel Selection) (targetPath string, reason config.PersistReason, errCat fserrors.Category, ...)
- type ActiveState
- type Category
- type Context
- type NoSelectableModelError
- type PickerResult
- type ProviderDiagnostic
- type RecoveryOutcome
- type Selection
- type ShapeError
- type Source
- type UsageAccumulator
Constants ¶
const EffortDefault = "default"
EffortDefault is a provider-advertised token, not the no-parameter choice.
const EffortNoReasoning = provider.EffortNoReasoning
EffortNoReasoning is accepted independently of what any provider advertises.
Variables ¶
This section is empty.
Functions ¶
func DetermineSelection ¶
DetermineSelection resolves the initial active model/effort. It returns a non-nil *Selection with SourceProject, SourceGlobalFallback, or SourceResume when one of paths 1-3 supplies a value; a nil *Selection with SourcePicker when path 4 requires the picker and at least one model is selectable; or a nil *Selection and a *NoSelectableModelError when path 4 finds no selectable model. The returned Source is meaningful only when the error is nil. DetermineSelection never calls os.Exit and, apart from the resume-recovery model_recovery_failed append, never flushes; the caller owns stderr rendering, opening the picker, flushing, and exiting.
func EmitModelChanged ¶
EmitModelChanged appends a model_changed event recording a picker selection. prior is the selection active before the pick: when non-nil the event carries the full prior provider/id/effort tuple, and when nil it omits all three, honoring the validator's all-present-or-all-absent rule for the prior fields. It appends only; the caller owns the flush.
func EmitModelEffortUnsupported ¶
func EmitModelEffortUnsupported(store *sessions.Store, provider, modelID, configuredEffort string) error
EmitModelEffortUnsupported appends a model_effort_unsupported event recording that the configured effort was dropped because the active model does not support it. It appends only; requires the caller to flush the store afterward and route any flush failure through the write-failure protocol — coupling this package intentionally does not take on.
func EmitModelGlobalMirrorFailed ¶
func EmitModelGlobalMirrorFailed(store *sessions.Store, targetPath string, sel Selection, reason config.PersistReason, errCat fserrors.Category, err error) error
EmitModelGlobalMirrorFailed appends a model_global_mirror_failed event for a failed global model.last_used mirror write (step 2). It uses the same reason/category/detail shape as EmitModelPersistFailed but is silent to the user: the caller renders no notice for it. It appends only.
func EmitModelPersistFailed ¶
func EmitModelPersistFailed(store *sessions.Store, targetPath string, sel Selection, reason config.PersistReason, errCat fserrors.Category, err error) error
EmitModelPersistFailed appends a model_persist_failed event for a failed per-project model.active write (step 1), using config.Persist's reason error category, and error verbatim except for lock_timeout, whose detail is the canonical lockTimeoutDetail the validator requires. error_category is passed through unchanged: config.Persist already returns it empty for the no-category reasons (lock_timeout, reread_parse_error), which the omitempty event field drops. It appends only.
func EmitModelResponse ¶
func EmitModelResponse(store *sessions.Store, provider, modelID string, inputTokens, outputTokens int64, cachedInputTokens *int64, cachedPartial bool, anomalies []string, upstreamProvider string) error
EmitModelResponse appends a model_response event recording an assistant turn's token usage. A nil anomalies slice renders as []. It appends only — the caller owns flushing.
func EmitModelTurn ¶
EmitModelTurn appends a model_turn event recording the active provider, model, effort, and selection source at the start of an assistant turn. It appends only — the caller owns flushing.
func EmitTurnStart ¶
func EmitTurnStart(store *sessions.Store, sel Selection, source Source, supportedSet []string) (effortToSend string, dropped bool, err error)
EmitTurnStart resolves the request effort and emits the start-of-turn events. It appends only — never flushing — so the caller owns the post-unsupported flush and the write-failure protocol; the returned dropped flag tells the caller a flush is required. effortToSend is the reasoning-effort value for the provider request ("" sends no parameter). On an append error it returns after the failed append without attempting the remaining one.
func ParseSelection ¶
func ParseSelection(raw any) (*Selection, *ShapeError)
ParseSelection defers provider-effort validation to the active catalog.
func RecoverActiveSelection ¶
func RecoverActiveSelection(ctx Context) (*Selection, RecoveryOutcome, error)
RecoverActiveSelection runs only step 1 against the loaded resume record stream, for the in-TUI /resume seam. Unlike DetermineSelection it does not fall through to the config paths after a no-model_turn or a recovery failure: it reports the RecoveryOutcome so the caller can adopt a clean recovery, clear the active selection and open the model picker on a failure, or leave the active selection unchanged when there was no model_turn. It reuses the same recovery parsing, validation, and model_recovery_failed append as the launch path so a failure appends exactly one model_recovery_failed to ctx.Store. It returns a non-nil error only for an internal failure (a nil config cache, or an inability to append the model_recovery_failed event); the outcome is meaningful only when err is nil. ctx.Warn is normally left nil here so a TUI failure routes through the picker rather than the CLI stderr surface.
func ResolveRequestEffort ¶
func ResolveRequestEffort(sel Selection, supportedSet []string) (effortToSend, configuredEffort string, dropped bool)
ResolveRequestEffort applies the request-time effort filter for sel against the active model's supportedSet. effortToSend is the reasoning-effort value to put on the provider request; "" means send no parameter. configuredEffort echoes sel.Effort for a model_effort_unsupported event. dropped reports whether a concrete configured effort was filtered out as unsupported.
func SumModelResponseTokens ¶
SumModelResponseTokens totals recorded provider token usage across every model_response event in records, applying the same per-event sum the /status overlay surfaces (BuildStatusSnapshot) so the in-flight indicator's session figure stays consistent with /status. An unparseable model_response payload is skipped rather than panicking, matching /status; non-event and non-model_response records are ignored.
func UsageFromStreamUsage ¶
func UsageFromStreamUsage(u openai.CompletionUsage, usageValid bool) (inputTokens, outputTokens int64, cached *int64, anomalies []string)
UsageFromStreamUsage extracts the model_response token counts from a streaming turn's final usage chunk (include_usage), normalizing missing or malformed usage per subfield. usageValid reports whether a usage chunk was actually seen (its choices array is empty, so it is detected by chunk.JSON.Usage.Valid(), not by a content choice); when false — the stream ended without a usage chunk, e.g. an interrupted reply or a provider that ignores stream_options — the counts fall back to zero with usage_missing.
func WriteActive ¶
func WriteLastUsed ¶
func WriteLastUsed(home string, sel Selection) (targetPath string, reason config.PersistReason, errCat fserrors.Category, err error)
WriteLastUsed mirrors sel to model.last_used in <home>/.codemeld/global/settings.yaml through the shared config persistence chokepoint (step 2). It returns the resolved target path together with config.Persist's reason, error category, and error. The global mirror is non-fatal: on failure the caller emits model_global_mirror_failed and surfaces no user-facing notice.
Types ¶
type ActiveState ¶
type ActiveState struct {
// contains filtered or unexported fields
}
ActiveState holds the run-scoped active model selection and the Source value the next model_turn must record. It is the single in-memory owner that /effort, /model, Ctrl-E, Ctrl-M, /resume, /clear, and the plain-prompt turn path all read and mutate, taking over from the run-start on-disk lookup once the run is underway. It represents "no model selected" distinctly from any selection so callers can refuse with "no model selected; pick one via Ctrl-M first" or route to the model picker.
It is deliberately side-effect free: it performs no event append, flush, config write, validation, provider-catalog lookup, or TUI rendering. Callers own validation, notices, model_changed, WriteActive, and WriteLastUsed. The zero value is a valid "no model selected" state.
func NewActiveState ¶
func NewActiveState(sel *Selection, source Source) *ActiveState
NewActiveState builds the run's active-model state from a DetermineSelection result. A non-nil sel stores that selection together with the returned source (SourceProject, SourceGlobalFallback, or SourceResume). A nil sel — the path-4 picker-required outcome — yields a state with no active selection; source is ignored because no model_turn can run until the picker supplies one.
func (*ActiveState) ApplyResumeSwitch ¶
func (s *ActiveState) ApplyResumeSwitch(sel *Selection, source Source)
ApplyResumeSwitch updates the run state after an in-TUI /resume recovery attempt. A non-nil sel replaces the active selection and source with the recovered values; a nil sel clears the active selection so the caller opens the model picker (the path-4 outcome), leaving the run with no model until a pick lands.
func (*ActiveState) ApplyUserSelection ¶
func (s *ActiveState) ApplyUserSelection(sel Selection, degraded bool)
ApplyUserSelection records a user-applied selection from /effort, /model, or a picker after the apply path has run. The selection is always retained for the run, whether or not persistence degraded. The next model_turn source becomes SourcePicker on a clean per-project model.active write and SourcePickerDegraded when that write failed; degraded mirrors PickerResult.Degraded. This ends any prior SourceResume carry-over: once the user picks, the run no longer reports the resumed source.
func (*ActiveState) CarryForward ¶
func (s *ActiveState) CarryForward()
CarryForward carries the current selection and source unchanged across a /clear (alias /new) session switch, so the first model_turn in the fresh session reuses the prior source rather than re-running the on-disk lookup. It is intentionally a no-op on the stored values: it exists as the single canonical target for the production ModelState.CarryForward implementation and is safe to call when no model is selected.
func (*ActiveState) Current ¶
func (s *ActiveState) Current() (sel Selection, source Source, ok bool)
Current returns the active selection and the Source to record on the next model_turn. ok is false when no model is selected yet — the caller must then refuse the turn or route to the model picker rather than starting a turn. The returned Selection and Source are zero values when ok is false.
type Category ¶
type Category string
Category names a shape- or cross-reference-check failure. The set is closed so bootstrap and picker/warning callers can branch on the exact rejection reason and name the offending field and value.
const ( // CategoryNotObject: the value is null or a non-object scalar/list rather than a mapping. CategoryNotObject Category = "value not an object" // CategoryMissingField: a required subfield (provider, id, or effort) is absent. CategoryMissingField Category = "required subfield missing" // CategoryWrongType: a required subfield is present but not a string. CategoryWrongType Category = "subfield wrong type" CategoryEffortInvalid Category = "effort empty" CategoryEffortNotAdvertised Category = "effort not advertised for the selected model" // CategoryProviderUnknown: provider is not an entry in the selectable-provider // catalog (configured providers plus built-in providers outside the provider: map). CategoryProviderUnknown Category = "provider not in selectable catalog" // CategoryModelNotAllowed: id is not in the selected provider's catalog models allowlist. CategoryModelNotAllowed Category = "id not in provider's models allowlist" )
type Context ¶
type Context struct {
Config *config.Cache
Resume bool
ResumeRecords []sessions.Record
Store *sessions.Store
Warn io.Writer
GlobalSettingsPath string
Providers []ProviderDiagnostic
}
Context carries everything DetermineSelection reads. Config is the composed session cache (paths 2-3 inputs and cross-reference source). On a resume run Resume is true and ResumeRecords/Store carry the loaded record stream to scan and the store to append model_recovery_failed to; Warn receives the one-line recovery warning. GlobalSettingsPath and Providers feed the path-4 stderr lines (the global settings absolute path and the ordered provider diagnostics, respectively).
type NoSelectableModelError ¶
type NoSelectableModelError struct {
Lines []string
}
NoSelectableModelError is the typed path-4 outcome returned when the picker would have to open but no model is selectable. Lines holds the exact stderr lines the launch caller writes (one "no providers" line for an empty provider map, otherwise one line per empty-allowlist provider in configured order) before flushing where required and exiting with status 1. Returning it rather than calling os.Exit keeps resume-run flush/exit ownership with the bootstrap caller.
func (*NoSelectableModelError) Error ¶
func (e *NoSelectableModelError) Error() string
type PickerResult ¶
type PickerResult struct {
Degraded bool
}
PickerResult reports the outcome of persisting a picker selection so the caller can choose the source for subsequent model_turn events. Degraded is true when the per-project model.active write failed: the selection stays active in memory for the run (the caller retains ownership), but the next model_turn must record SourcePickerDegraded.
func PersistPickerSelection ¶
func PersistPickerSelection(ctx context.Context, store *sessions.Store, ui tui.NoticeRenderer, sel Selection, prior *Selection, projectRoot, home string) (PickerResult, error)
PersistPickerSelection runs the picker-selection persistence sequence for sel. It appends model_changed, flushes the session at the durability point — routing any flush write failure through the write-failure protocol — and then writes model.active to <projectRoot>/.codemeld/settings.local.yaml, rendering the ignore notice through ui when that write creates the file. Every notice one run produces reaches ui as a single composed line. On a model.active failure it emits model_persist_failed, renders the one-line notice via ui, and returns a degraded result, leaving the in-memory selection with the caller so it stays active for the run. Only after model.active succeeds does it mirror the selection to the global model.last_used file; a mirror failure emits model_global_mirror_failed silently, with no user-facing notice (step 2).
prior is the selection active before this pick (nil on the run's first pick). projectRoot and home are the snapshotted write-target roots. ui receives the user-facing notices. PersistPickerSelection returns a non-nil error for an internal failure to append an event, to record an atomic-write failure, or when the flush itself fails without producing a write reason (a cancelled run context or a marshal failure) — in that last case it returns before any settings write or source transition so the selection is retained for a retry. It never returns an error for the persistence failures themselves, which are surfaced through events, notices, and (for model.active) the degraded result.
type ProviderDiagnostic ¶
type RecoveryOutcome ¶
type RecoveryOutcome int
RecoveryOutcome classifies a step-1 recovery attempt against a loaded resume record stream. DetermineSelection only needs to know whether recovery produced a clean selection, but the in-TUI /resume seam (RecoverActiveSelection) must branch on all three outcomes: a clean recovery adopts the selection, a failure clears the active selection and opens the model picker, and no model_turn leaves the active selection unchanged.
const ( // RecoveryNoModelTurn: the stream carried no model_turn, so there was nothing to // recover. Not a failure — no model_recovery_failed is appended. RecoveryNoModelTurn RecoveryOutcome = iota // RecoveryClean: the most recent model_turn passed the shape and checks // and was restored; the returned *Selection is non-nil. RecoveryClean // RecoveryFailed: the most recent model_turn failed the shape or validation // check; exactly one model_recovery_failed was appended and *Selection is nil. RecoveryFailed )
type Selection ¶
Selection is a parsed model.active or model.last_used value: the chosen provider name, model id, and reasoning effort. All three are required string subfields. The on-disk field is id; model is never accepted as an alias for it.
type ShapeError ¶
ShapeError reports a single shape or cross-reference failure. Field names the offending subfield (provider, id, or effort), empty for a whole-value failure such as CategoryNotObject. Value carries the offending value or, for CategoryWrongType, the Go type that was found, so and callers can quote it in their messages.
func Validate ¶
func Validate(sel *Selection, composedConfig *config.Cache) *ShapeError
Validate treats a built-in provider that declares selectable models outside the provider: map as a catalog entry, so the same model id can appear under two provider ids, disambiguated by provider.
func (*ShapeError) Error ¶
func (e *ShapeError) Error() string
type Source ¶
type Source string
Source identifies which path supplied the active selection. Its domain is the model_turn.source enum. DetermineSelection returns SourceProject, SourceGlobalFallback, or SourceResume alongside a selection, or SourcePicker with a nil selection when the picker must run. SourcePickerDegraded belongs to the enum for the post-persist degraded turn but is never returned here; degradation is decided later, after a picker write fails.
type UsageAccumulator ¶
type UsageAccumulator struct {
// contains filtered or unexported fields
}
UsageAccumulator sums the exact stream usage of every provider round in one agentic turn so the turn's single final model_response records the whole turn's tokens, not just the last round. A tool-call round folds its usage chunk in with Add before handing off to the next round; the final round folds its own and reads Totals. The zero value is an empty accumulator — ToolLoop owns one per turn.
func (*UsageAccumulator) Add ¶
func (a *UsageAccumulator) Add(u openai.CompletionUsage, usageValid bool)
Add folds one round's stream usage into the running total, normalizing the chunk exactly as UsageFromStreamUsage does: each present non-negative subfield adds to the total, and a missing/negative subfield or an absent chunk (usageValid false) records the usage_missing condition without inventing counts.
func (*UsageAccumulator) Totals ¶
func (a *UsageAccumulator) Totals() (inputTokens, outputTokens int64, cached *int64, cachedPartial bool, anomalies []string, hadUsage bool)
Totals returns the summed token counts across every folded round, the combined model_response anomaly slice (usage_missing when any round's usage was missing or malformed, else nil), and hadUsage — whether any round carried a usage chunk at all. hadUsage gates the exact-figure indicator update the same way a single round's usageValid did: a usage chunk arrived somewhere, so the indicator can show a real (possibly partial) total instead of its streamed guess.
func (*UsageAccumulator) UsageRounds ¶
func (a *UsageAccumulator) UsageRounds() int
UsageRounds reports how many folded rounds carried a valid usage chunk. A ToolLoop snapshots this before and after a single round (around Runner.Run) to tell whether *that* round supplied real usage — distinct from hadUsage, which stays true for the rest of the turn once any one round reports usage, and so cannot distinguish a later usage-missing round from one that re-confirms the running total.