Files
Agent-System/internal/app/chatflow/assembler.go
T
weichenandCursor 110c2e810c feat(stageflow): own task/stage types so coordinator drops bizdev
Coordinator now takes a StageTaskRunner and injected context/KB ports.
Chatflow assembler reads maturity tokens instead of the orchestration root.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 11:13:17 +00:00

112 lines
3.2 KiB
Go

package chatflow
import (
"context"
"fmt"
"strings"
"time"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/maturity"
)
// Assembler adapts coordinator.ContextAssembler and ChatLog so prompt context
// only contains the authorized message set.
type Assembler struct {
Inner *coordinator.ContextAssembler
Log ChatLog
}
func NewAssembler(inner *coordinator.ContextAssembler, log ChatLog) *Assembler {
if inner == nil {
inner = coordinator.NewContextAssembler()
}
return &Assembler{Inner: inner, Log: log}
}
func (a *Assembler) Assemble(ctx context.Context, p Principal, req AssembleRequest) (*coordinator.UserProjectContext, error) {
if p.UserID <= 0 {
return nil, ErrUnauthenticated
}
p.Action = ActionAssemble
var userCtx *coordinator.UserProjectContext
if a.Inner != nil && strings.TrimSpace(req.WorkDir) != "" {
userCtx, _ = a.Inner.AssembleFromWorkspace(req.WorkDir, req.User)
}
if userCtx == nil {
userCtx = &coordinator.UserProjectContext{
SnapshotID: fmt.Sprintf("ctx_%d", time.Now().UnixNano()),
AssembledAt: time.Now(),
User: req.User,
WorkDir: req.WorkDir,
Project: req.Fallback,
BlackboardFacts: make(map[string]string),
AvailableActions: coordinator.ComputeAvailableActions(maturity.StageV01_Triage, req.User.ProjectRole),
}
if userCtx.Project.CurrentStage == "" {
userCtx.Project.CurrentStage = maturity.StageV01_Triage
}
if userCtx.Project.CurrentWorkflow == "" {
userCtx.Project.CurrentWorkflow = "intake"
}
}
if a.Log == nil || strings.TrimSpace(req.SessionID) == "" {
return userCtx, nil
}
restored, err := a.Log.Restore(ctx, p, req.SessionID)
if err != nil {
if err == ErrForbidden || err == ErrNotFound || err == ErrUnauthenticated {
userCtx.RecentDialogue = nil
userCtx.DialogueSummary = ""
userCtx.RecalledMemories = nil
userCtx.RecalledMemorySnippet = ""
return userCtx, nil
}
return nil, err
}
if restored == nil {
return userCtx, nil
}
upd := coordinator.ContextUpdate{
ExpectedVersion: userCtx.Version,
BlackboardFill: restored.Facts,
SetRecentDialogue: true,
RecentDialogue: authorizedTurns(restored),
}
if restored.ProjectID > 0 {
upd.ProjectIDIfEmpty = fmt.Sprintf("%d", restored.ProjectID)
}
_ = coordinator.ApplyContextUpdate(userCtx, upd)
return userCtx, nil
}
func authorizedTurns(restored *ctxmgr.RestoredSessionContext) []coordinator.DialogueTurn {
if restored == nil {
return nil
}
turns := make([]coordinator.DialogueTurn, 0, len(restored.Messages))
records := make([]ctxmgr.ChatMessageRecord, 0, len(restored.Messages))
for _, m := range restored.Messages {
if strings.TrimSpace(m.Content) == "" {
continue
}
turns = append(turns, coordinator.DialogueTurn{Role: m.Role, Content: m.Content})
records = append(records, m)
}
cfg := ctxmgr.DefaultWindowConfig()
window, _, _ := ctxmgr.AssembleWindow(records, cfg)
if len(window) > 0 {
turns = turns[:0]
for _, m := range window {
if strings.TrimSpace(m.Content) == "" {
continue
}
turns = append(turns, coordinator.DialogueTurn{Role: m.Role, Content: m.Content})
}
}
return turns
}