Files
Agent-System/internal/coordinator/context_update.go
T
weichenandCursor 84c0841acb feat(app): add composition root, stageflow, ContextUpdate, and SSE transport
Move construction and document OnOutbox out of server, give HTTP/CLI a shared task use case, and block already-cleared import/event arch rules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 05:40:15 +00:00

145 lines
3.9 KiB
Go

package coordinator
import (
"errors"
"chat-agent-system/internal/intent"
)
var (
ErrNilContext = errors.New("coordinator: nil user project context")
ErrStaleContext = errors.New("coordinator: stale context version")
ErrInvalidUpdate = errors.New("coordinator: empty context update")
)
// ContextUpdate is the only typed write against UserProjectContext.
// Callers must supply ExpectedVersion from a prior read; a mismatch fails closed.
type ContextUpdate struct {
ExpectedVersion int64
BlackboardFill map[string]string
BlackboardSet map[string]string
SetPendingExpectation bool
PendingExpectation *intent.PendingExpectation
ExpirePending bool
SetInteractiveCard bool
InteractiveCard *intent.InteractiveCardPayload
TickInteractive bool
DismissCardStatus string // cancelled | resolved | expired; then nil
SetPersonalizedGuidance bool
PersonalizedGuidance string
SetRecentDialogue bool
RecentDialogue []DialogueTurn
ProjectIDIfEmpty string
}
// ApplyContextUpdate applies one versioned patch. Concurrent writers with the
// same ExpectedVersion: exactly one succeeds, the rest get ErrStaleContext.
func ApplyContextUpdate(ctx *UserProjectContext, u ContextUpdate) error {
if ctx == nil {
return ErrNilContext
}
ctx.mu.Lock()
defer ctx.mu.Unlock()
if u.ExpectedVersion != ctx.Version {
return ErrStaleContext
}
applied := false
if len(u.BlackboardFill) > 0 {
if ctx.BlackboardFacts == nil {
ctx.BlackboardFacts = make(map[string]string, len(u.BlackboardFill))
}
for k, v := range u.BlackboardFill {
if _, exists := ctx.BlackboardFacts[k]; !exists {
ctx.BlackboardFacts[k] = v
}
}
applied = true
}
if len(u.BlackboardSet) > 0 {
if ctx.BlackboardFacts == nil {
ctx.BlackboardFacts = make(map[string]string, len(u.BlackboardSet))
}
for k, v := range u.BlackboardSet {
ctx.BlackboardFacts[k] = v
}
applied = true
}
if u.ExpirePending && ctx.PendingExpectation != nil && !ctx.PendingExpectation.ExpiresAt.IsZero() {
// caller decides freshness; apply just clears
ctx.PendingExpectation = nil
applied = true
}
if u.SetPendingExpectation {
ctx.PendingExpectation = u.PendingExpectation
applied = true
}
if u.TickInteractive && ctx.InteractiveCard != nil {
ctx.InteractiveCard.TTLTurns--
if ctx.InteractiveCard.TTLTurns <= 0 || ctx.InteractiveCard.CurrentStatus != "active" {
ctx.InteractiveCard.CurrentStatus = "expired"
ctx.InteractiveCard = nil
}
applied = true
}
if u.DismissCardStatus != "" && ctx.InteractiveCard != nil {
ctx.InteractiveCard.CurrentStatus = u.DismissCardStatus
ctx.InteractiveCard = nil
applied = true
}
if u.SetInteractiveCard {
ctx.InteractiveCard = u.InteractiveCard
applied = true
}
if u.SetPersonalizedGuidance {
ctx.PersonalizedGuidance = u.PersonalizedGuidance
applied = true
}
if u.SetRecentDialogue {
ctx.RecentDialogue = u.RecentDialogue
applied = true
}
if u.ProjectIDIfEmpty != "" && ctx.Project.ID == "" {
ctx.Project.ID = u.ProjectIDIfEmpty
applied = true
}
if !applied {
return ErrInvalidUpdate
}
ctx.Version++
return nil
}
func (c *UserProjectContext) applyPending(exp *intent.PendingExpectation) {
if c == nil {
return
}
_ = ApplyContextUpdate(c, ContextUpdate{ExpectedVersion: c.Version, SetPendingExpectation: true, PendingExpectation: exp})
}
func (c *UserProjectContext) applyCard(card *intent.InteractiveCardPayload) {
if c == nil {
return
}
_ = ApplyContextUpdate(c, ContextUpdate{ExpectedVersion: c.Version, SetInteractiveCard: true, InteractiveCard: card})
}
func (c *UserProjectContext) dismissCard(status string) {
if c == nil {
return
}
_ = ApplyContextUpdate(c, ContextUpdate{ExpectedVersion: c.Version, DismissCardStatus: status})
}
func (c *UserProjectContext) applyBlackboard(key, value string) {
if c == nil {
return
}
_ = ApplyContextUpdate(c, ContextUpdate{ExpectedVersion: c.Version, BlackboardSet: map[string]string{key: value}})
}