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>
145 lines
3.9 KiB
Go
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}})
|
|
}
|