91 lines
5.7 KiB
Go
91 lines
5.7 KiB
Go
package config
|
||
|
||
// internal_call.go: the ONE derivation of a config.Stage for a call the ENGINE makes on its own behalf —
|
||
// a bank role (terminologist / type-classifier), a repair span, tomorrow's annotator or Ф2 judge.
|
||
//
|
||
// Why it exists (D39.87 §2): those calls used to be hand-written `config.Stage{Name: …, Role: …, Model: …}`
|
||
// literals — three of them in shipping code plus one in the live rig — and every literal silently dropped
|
||
// every OTHER field the owner had configured. That is how the reasoning knob went missing from the bank
|
||
// roles: nobody "forgot a key", the shape of the code loses fields by construction. Adding the key to three
|
||
// literals would reproduce the defect at the fourth site, so the literals are gone instead: this is the only
|
||
// place in the tree that builds a Stage outside the loader, and TestSyntheticStageSeamIsSingle keeps it that
|
||
// way.
|
||
|
||
// ValidReasoningEffort reports whether v is the engine's NEUTRAL effort vocabulary. ONE definition, shared
|
||
// by the stage key and by every gate that configures a call class, so the two can never drift into accepting
|
||
// different words for the same knob.
|
||
//
|
||
// ⚠ It is deliberately provider-BLIND, and that is a known limit rather than an oversight: the capability
|
||
// layer maps these words per provider, and a per-model enum does not exist in models.yaml. So `medium` on a
|
||
// DeepSeek-shaped model is undefined vendor behaviour, and the vendor's own `xhigh`/`max` are unreachable
|
||
// from our config. Validating against the RESOLVED model's capability would need a new per-model table — a
|
||
// new mechanism, not a use of an existing one — so it stays a named finding, not a silent gap.
|
||
func ValidReasoningEffort(v string) bool {
|
||
switch v {
|
||
case "", "off", "low", "medium", "high":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// InternalCall is everything an engine-internal call says about itself. Every OTHER Stage field is decided
|
||
// ONCE in Stage() below, with the reason written down — so a new call site inherits the decisions instead of
|
||
// re-taking them by accident, and a new Stage FIELD is a compile-visible decision rather than a silent zero
|
||
// (TestInternalCallDecidesEveryStageField).
|
||
type InternalCall struct {
|
||
// Name is the stage name the call is accounted under. It is a request-hash axis and the log axis, so it
|
||
// must be the name the operator sees in `tmctl report` for this call class.
|
||
Name string
|
||
// Role is the engine role — the COST axis (RoleSpentUSD, the per-role sub-budgets) and the log axis.
|
||
Role string
|
||
// Model is the RESOLVED model this call goes to (gates carry their own `model:`, already validated).
|
||
Model string
|
||
// Reasoning is the effort the owner configured for this call CLASS, in the engine's neutral vocabulary
|
||
// ("" | off | low | medium | high). "" means "leave the provider's own default", which is not the same
|
||
// as off — on a capability whose control is extra_body_disable it is the provider's default of THINKING
|
||
// OFF (llm.Capability.applyToBody), which is why the value has to travel rather than be assumed.
|
||
Reasoning string
|
||
}
|
||
|
||
// Stage renders the internal call as the config.Stage the runner's money path takes. It is deliberately
|
||
// TOTAL over Stage's fields: what is set is listed here, and what stays zero stays zero for a written reason.
|
||
func (c InternalCall) Stage() Stage {
|
||
return Stage{
|
||
Name: c.Name,
|
||
Role: c.Role,
|
||
Model: c.Model,
|
||
Reasoning: c.Reasoning,
|
||
// ResolvedModel mirrors Model: an internal call is NOT label-routed (label routing resolves the
|
||
// book's `stages:` at load), so the configured model IS the resolved one. It is set rather than left
|
||
// zero because the load-time exposure walk reads ResolvedModel to name a stage×model pair.
|
||
ResolvedModel: c.Model,
|
||
|
||
// --- deliberately zero ---------------------------------------------------------------------
|
||
// Temperature: 0, and NOT inherited from a parent prose stage. These calls return STRUCTURE that
|
||
// the engine re-parses — a term table, a type verdict, a span replacement — where sampling variance
|
||
// is pure risk; the owner's stage temperature is a STYLE choice about prose and means nothing here.
|
||
// (It is also a request-hash field, so this is the value every existing bank checkpoint was bought
|
||
// at; changing it would re-buy them, which is a reason to be sure, not the reason to choose.)
|
||
//
|
||
// ReasoningMaxTokens: 0 — it reserves the ADDITIVE reasoning buffer (D6.2/D13.6), and the gates that
|
||
// make internal calls carry no such key. That is not an oversight left open here: the loader REFUSES
|
||
// an additive-billing provider for gates.terminology.model / gates.repair.model precisely because
|
||
// the block cannot reserve the buffer, so a call reaching this seam is never on an additive provider.
|
||
// A future gate that wants an additive model must add the key AND lift that refusal together.
|
||
//
|
||
// PromptOverride / PromptPath / PromptVersion: internal calls arrive with their messages ALREADY
|
||
// rendered (each gate resolves and versions its own prompt), so the stage template machinery is not
|
||
// consulted for them.
|
||
//
|
||
// EscalateTo / ResolvedHop: no escalation hop. maybeEscalate is reached from runStage, not from the
|
||
// internal-call paths, and these calls degrade by leaving their work unchanged rather than by paying
|
||
// a second model.
|
||
//
|
||
// LabelModels: see ResolvedModel — no label routing. A model that may not receive the book's content
|
||
// labels is refused loudly by clientFor before any money moves, so this is fail-closed, not silent.
|
||
//
|
||
// FewShot: nil — few-shot is a prose-translation device; a term table has its own prompt.
|
||
//
|
||
// LegacyPrompt / LegacyPrompts / LegacyChannel: retired keys, declared only to be REJECTED at load.
|
||
}
|
||
}
|