Volition and planning now run as part of cognition (PLAN §8.2 item 3):
- A volition hook derives wants after each cycle from what it perceived
and produced, satisfies answer wants when a reply is sent, raises due,
overdue, expired and conflicting commitments, and expires faded wants.
Events produced by earlier cycles are not re-derived.
- An intend process proposes deciding about the strongest undecided
wants; the candidate competes for the workspace, so attention gates
what the mind decides to pursue. The handler makes value tensions
explicit (caution against disruptive writes, privacy against reading
personal data), resolves them with recorded scores, runs deterministic
formation with commitments, policy preview, goals and values, records
the decision whatever it is, and plans a formed intention in the same
transaction. Operator requests carry their own weight in formation.
- Explicit requests in conversation ("restart <unit>", "logs for <unit>",
"fetch <url>") become wants with proposed means instead of direct
invocations; the reply says the request was recorded and that acting
depends on commitments and policy.
- The executor is woken after commits, runs in a scheduler-owned loop and
reacts to the event bus: a new commitment tightens or abandons affected
intentions (withdrawing their pending approvals), a weakened assumption
or a finished goal triggers reconsideration. A tightened plan may reuse
its strategy; a failed one may not.
- Metacognition gains volition checks: strong wants repeatedly set aside,
intentions with no recorded reason, intentions a commitment forbids,
commitments that cannot be kept, churn, repeated failed execution,
cost outweighing value, and intentions that outlived their goal.
- A plan model role and prompt let a model propose decompositions that
the planner validates. Ablations are a fork-only configuration field.
- Approvals required by an intention are described as coming from a
commitment or value constraint. The expectation after an approved
action is linked to the execution event, not the request.
- The wants.values column of the unreleased migration 0026 is renamed
value_keys: VALUES is reserved and the generic snapshot restore does
not quote identifiers.
- The hostile-output security test now uses a message that asks for no
action, because an operator's "status of <unit>" is legitimately a
read-only status check.
154 lines
5.8 KiB
Go
154 lines
5.8 KiB
Go
// Package brain runs the cognitive cycle for each mind: it gathers new events,
|
|
// asks registered cognitive processes for thought candidates, scores them
|
|
// deterministically, admits the winners to the global workspace, dispatches
|
|
// them to executive handlers and records every step as events (ADR-0001).
|
|
package brain
|
|
|
|
import (
|
|
"context"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/andydixon/noema/internal/attention"
|
|
"github.com/andydixon/noema/internal/beliefs"
|
|
"github.com/andydixon/noema/internal/capabilities"
|
|
"github.com/andydixon/noema/internal/goals"
|
|
"github.com/andydixon/noema/internal/llm/prompts"
|
|
"github.com/andydixon/noema/internal/llm/router"
|
|
"github.com/andydixon/noema/internal/memory"
|
|
"github.com/andydixon/noema/internal/minds"
|
|
"github.com/andydixon/noema/internal/motivation"
|
|
"github.com/andydixon/noema/internal/planning"
|
|
"github.com/andydixon/noema/internal/predictions"
|
|
"github.com/andydixon/noema/internal/volition"
|
|
"github.com/andydixon/noema/internal/workspace"
|
|
"github.com/andydixon/noema/pkg/events"
|
|
"github.com/andydixon/noema/pkg/ids"
|
|
)
|
|
|
|
// Process is a cognitive faculty. Observe must be safe to call concurrently
|
|
// with other processes, must respect ctx, and must not mutate the state it is
|
|
// given; it returns candidates and lets the cycle decide.
|
|
type Process interface {
|
|
Name() string
|
|
Observe(ctx context.Context, s *State) ([]Candidate, error)
|
|
}
|
|
|
|
// Candidate is a proposed thought.
|
|
type Candidate struct {
|
|
Kind string // attend | hypothesis | question | respond | goal | retrieve | reflect | note | action
|
|
Subject string // stable key for refresh and loop detection (entity, event id, proposition hash)
|
|
Summary string // one line, human readable
|
|
Content map[string]any // structured detail for handlers and the UI
|
|
Features attention.Features // scoring inputs
|
|
Confidence float64 // epistemic confidence of the candidate itself
|
|
Evidence []events.EvidenceRef // what it rests on
|
|
SourceEvents []ids.ID // events that triggered it (first becomes causation)
|
|
// filled by the cycle
|
|
ID ids.ID
|
|
Process string
|
|
}
|
|
|
|
// Mode is the coarse cognitive mode.
|
|
type Mode string
|
|
|
|
// Modes. Later phases add reflecting, consolidating and dreaming.
|
|
const (
|
|
ModeAwake Mode = "awake"
|
|
ModeFocused Mode = "focused"
|
|
ModeIdle Mode = "idle"
|
|
ModePaused Mode = "paused"
|
|
ModeReflecting Mode = "reflecting"
|
|
ModeConsolidating Mode = "consolidating"
|
|
ModeDreaming Mode = "dreaming"
|
|
)
|
|
|
|
// State is what processes see: a read-only view of the mind plus service
|
|
// handles. Nil services mean the capability is not available yet.
|
|
type State struct {
|
|
Mind minds.Mind
|
|
SelfModel minds.SelfModel
|
|
Mode Mode
|
|
Cycle int64
|
|
CycleID ids.ID
|
|
Now time.Time
|
|
NewEvents []events.Event // since the last cycle, ascending
|
|
Workspace workspace.State // snapshot before this cycle
|
|
Services Services
|
|
Goals []goals.Goal // active goals, for relevance scoring
|
|
Motiv motivation.State // drives and affect at the start of the cycle
|
|
Weights map[string]float64 // effective attention weights after modulation
|
|
llmCalls atomic.Int32 // model calls made in this cycle; processes and handlers share the cap
|
|
}
|
|
|
|
// AllowLLM reserves one model call against the mind's per-cycle cap and
|
|
// reports whether it was granted, so a chatty cycle cannot exceed the budget.
|
|
func (s *State) AllowLLM() bool {
|
|
cap := int32(s.Mind.Config.LLMCallsPerCycle) // #nosec G115 -- config is normalised to a small non-negative range
|
|
if cap <= 0 || s.Services.LLM == nil {
|
|
return false
|
|
}
|
|
if s.llmCalls.Add(1) > cap {
|
|
s.llmCalls.Add(-1)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Services are read handles processes may use. Extended per phase.
|
|
type Services struct {
|
|
Events EventReader
|
|
Memory *memory.Store
|
|
Beliefs *beliefs.Store
|
|
LLM *router.Router
|
|
Prompts *prompts.Store
|
|
Goals *goals.Store
|
|
Motivation *motivation.Store
|
|
Predictions *predictions.Store
|
|
Capabilities *capabilities.Service
|
|
Volition *volition.Store
|
|
Plans *planning.Store
|
|
// Executor plans formed intentions inside the cycle transaction. It never
|
|
// acts from inside a cycle: acting happens after commit, in its own loop.
|
|
Executor *planning.Executor
|
|
}
|
|
|
|
// EventReader is the subset of the event store processes may call.
|
|
type EventReader interface {
|
|
Get(ctx context.Context, id ids.ID) (events.Event, error)
|
|
}
|
|
|
|
// Handler executes an admitted thought of a given kind. Handlers run
|
|
// sequentially after admission, may write to stores and emit events, and
|
|
// receive the cycle's emitter so provenance links are consistent.
|
|
type Handler func(ctx context.Context, c *Cycle, t workspace.Thought) error
|
|
|
|
// Hook runs after the executive handlers of a cycle, with the events emitted
|
|
// so far, for cross-cutting updates such as drives and affect.
|
|
type Hook interface {
|
|
Name() string
|
|
AfterCycle(ctx context.Context, c *Cycle, emitted []events.Event) error
|
|
}
|
|
|
|
// Registry holds processes, handlers and hooks. It is immutable after construction.
|
|
type Registry struct {
|
|
processes []Process
|
|
handlers map[string]Handler
|
|
hooks []Hook
|
|
}
|
|
|
|
// NewRegistry builds a registry.
|
|
func NewRegistry() *Registry { return &Registry{handlers: map[string]Handler{}} }
|
|
|
|
// AddProcess registers a process.
|
|
func (r *Registry) AddProcess(p Process) *Registry { r.processes = append(r.processes, p); return r }
|
|
|
|
// Handle registers the handler for a thought kind.
|
|
func (r *Registry) Handle(kind string, h Handler) *Registry { r.handlers[kind] = h; return r }
|
|
|
|
// AddHook registers an after-cycle hook.
|
|
func (r *Registry) AddHook(h Hook) *Registry { r.hooks = append(r.hooks, h); return r }
|
|
|
|
// Processes returns the registered processes.
|
|
func (r *Registry) Processes() []Process { return r.processes }
|