Load the knowledge profile on planning and ground the coach
The controller loads the profile asynchronously on entering planning, mirroring the tasks fetch: generation-guarded goroutine, status enum (idle/pending/ready/absent/error), projected into State only while planning and only when a source is set. The loaded text is cached and passed to the coach as grounding; SetKnowledgePath repoints the file at runtime. nil source degrades to no view and an ungrounded coach. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+111
-1
@@ -17,6 +17,7 @@ import (
|
||||
"antidrift/internal/ai"
|
||||
"antidrift/internal/domain"
|
||||
"antidrift/internal/evidence"
|
||||
"antidrift/internal/knowledge"
|
||||
"antidrift/internal/statemachine"
|
||||
"antidrift/internal/store"
|
||||
"antidrift/internal/tasks"
|
||||
@@ -47,6 +48,16 @@ const (
|
||||
tasksError = "error"
|
||||
)
|
||||
|
||||
const knowledgeTimeout = 10 * time.Second
|
||||
|
||||
const (
|
||||
knowledgeIdle = "idle"
|
||||
knowledgePending = "pending"
|
||||
knowledgeReady = "ready"
|
||||
knowledgeAbsent = "absent"
|
||||
knowledgeError = "error"
|
||||
)
|
||||
|
||||
const (
|
||||
driftDebounce = 10 * time.Second
|
||||
driftTimeout = 30 * time.Second
|
||||
@@ -107,6 +118,13 @@ type Controller struct {
|
||||
tasksList []tasks.Task
|
||||
tasksGen int
|
||||
|
||||
knowledgeSrc knowledge.Source
|
||||
knowledgeStatus string
|
||||
knowledgeText string // cached grounding the coach reads
|
||||
knowledgePath string // selected path; "" = adapter default
|
||||
knowledgeChars int
|
||||
knowledgeGen int
|
||||
|
||||
allowedClasses []string // durable: the active session's allowed window classes
|
||||
judge ai.DriftJudge
|
||||
driftStatus string
|
||||
@@ -167,6 +185,15 @@ type TasksView struct {
|
||||
Tasks []TaskView `json:"tasks,omitempty"`
|
||||
}
|
||||
|
||||
// KnowledgeView projects the ephemeral planning knowledge state (the standing
|
||||
// profile that grounds the coach). The profile text is intentionally omitted —
|
||||
// only its presence, source path, and size cross the wire.
|
||||
type KnowledgeView struct {
|
||||
Status string `json:"status"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Chars int `json:"chars,omitempty"`
|
||||
}
|
||||
|
||||
// WindowView / BucketView / EvidenceView are the evidence projection.
|
||||
type WindowView struct {
|
||||
Class string `json:"class"`
|
||||
@@ -194,6 +221,7 @@ type State struct {
|
||||
Evidence *EvidenceView `json:"evidence"`
|
||||
Coach *CoachView `json:"coach,omitempty"`
|
||||
Tasks *TasksView `json:"tasks,omitempty"`
|
||||
Knowledge *KnowledgeView `json:"knowledge,omitempty"`
|
||||
Drift *DriftView `json:"drift,omitempty"`
|
||||
}
|
||||
|
||||
@@ -319,6 +347,13 @@ func (c *Controller) stateLocked() State {
|
||||
}
|
||||
st.Tasks = tv
|
||||
}
|
||||
if c.knowledgeSrc != nil {
|
||||
kstatus := c.knowledgeStatus
|
||||
if kstatus == "" {
|
||||
kstatus = knowledgeIdle
|
||||
}
|
||||
st.Knowledge = &KnowledgeView{Status: kstatus, Path: c.knowledgePath, Chars: c.knowledgeChars}
|
||||
}
|
||||
}
|
||||
if c.runtimeState == domain.RuntimeActive {
|
||||
status := c.driftStatus
|
||||
@@ -366,6 +401,7 @@ func (c *Controller) EnterPlanning() error {
|
||||
c.runtimeState = next
|
||||
c.resetCoachLocked()
|
||||
c.startTasksFetchLocked()
|
||||
c.startKnowledgeFetchLocked()
|
||||
return c.persistLocked()
|
||||
}
|
||||
|
||||
@@ -429,6 +465,79 @@ func (c *Controller) startTasksFetchLocked() {
|
||||
}()
|
||||
}
|
||||
|
||||
// SetKnowledge injects the Knowledge source. Mirrors SetTasks. A nil source
|
||||
// keeps the planning knowledge view absent and the coach ungrounded.
|
||||
func (c *Controller) SetKnowledge(s knowledge.Source) {
|
||||
c.mu.Lock()
|
||||
c.knowledgeSrc = s
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetKnowledgePath selects an explicit profile path (session-only; not
|
||||
// persisted). While planning, it re-loads immediately so the indicator and the
|
||||
// cached grounding update. An empty path resets to the adapter default.
|
||||
func (c *Controller) SetKnowledgePath(path string) {
|
||||
c.mu.Lock()
|
||||
c.knowledgePath = strings.TrimSpace(path)
|
||||
planning := c.runtimeState == domain.RuntimePlanning
|
||||
if planning {
|
||||
c.startKnowledgeFetchLocked()
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if planning {
|
||||
c.notify()
|
||||
}
|
||||
}
|
||||
|
||||
// startKnowledgeFetchLocked kicks off an asynchronous Load when a source is set.
|
||||
// Mirrors startTasksFetchLocked: generation-guarded, discards stale or
|
||||
// post-planning results, and notifies on completion. The loaded text is cached
|
||||
// in knowledgeText for the coach to read. Caller holds mu.
|
||||
func (c *Controller) startKnowledgeFetchLocked() {
|
||||
c.knowledgeText = ""
|
||||
c.knowledgeChars = 0
|
||||
if c.knowledgeSrc == nil {
|
||||
c.knowledgeStatus = knowledgeIdle
|
||||
return
|
||||
}
|
||||
c.knowledgeGen++
|
||||
gen := c.knowledgeGen
|
||||
c.knowledgeStatus = knowledgePending
|
||||
src := c.knowledgeSrc
|
||||
path := c.knowledgePath
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), knowledgeTimeout)
|
||||
defer cancel()
|
||||
prof, err := src.Load(ctx, path)
|
||||
|
||||
c.mu.Lock()
|
||||
if gen != c.knowledgeGen || c.runtimeState != domain.RuntimePlanning {
|
||||
c.mu.Unlock()
|
||||
return // stale or left planning: discard
|
||||
}
|
||||
if err != nil {
|
||||
c.knowledgeStatus = knowledgeError
|
||||
c.knowledgeText = ""
|
||||
c.knowledgeChars = 0
|
||||
if prof.Path != "" {
|
||||
c.knowledgePath = prof.Path
|
||||
}
|
||||
} else if prof.Text == "" {
|
||||
c.knowledgeStatus = knowledgeAbsent
|
||||
c.knowledgeText = ""
|
||||
c.knowledgeChars = 0
|
||||
c.knowledgePath = prof.Path
|
||||
} else {
|
||||
c.knowledgeStatus = knowledgeReady
|
||||
c.knowledgeText = prof.Text
|
||||
c.knowledgeChars = len(prof.Text)
|
||||
c.knowledgePath = prof.Path
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.notify()
|
||||
}()
|
||||
}
|
||||
|
||||
// AllowedClassesForTest exposes the session allowed classes for tests.
|
||||
func (c *Controller) AllowedClassesForTest() []string {
|
||||
c.mu.Lock()
|
||||
@@ -545,13 +654,14 @@ func (c *Controller) RequestCoach(intent string) error {
|
||||
c.coachErr = ""
|
||||
c.coachProposal = nil
|
||||
coach := c.coach
|
||||
grounding := c.knowledgeText
|
||||
c.mu.Unlock()
|
||||
c.notify()
|
||||
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), coachTimeout)
|
||||
defer cancel()
|
||||
prop, err := coach.Coach(ctx, intent)
|
||||
prop, err := coach.Coach(ctx, intent, grounding)
|
||||
|
||||
c.mu.Lock()
|
||||
if gen != c.coachGen || c.runtimeState != domain.RuntimePlanning {
|
||||
|
||||
Reference in New Issue
Block a user