refactor(chat): break chat→app import cycle via owned ports
Invert chat dependencies on chatflow/intentflow/stageflow through composition-root adapters; replace AssembleRequest coordinator embeds with local DTOs and tighten the module-boundaries allowlist.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: "W2 批次 B:chat / app / coordinator 解环"
|
||||
task_id: chat-app-decouple-2026-09-22
|
||||
status: done
|
||||
created: 2026-09-22
|
||||
updated: 2026-09-22
|
||||
owner: Cursor
|
||||
task_type: implementation
|
||||
task_tier: small
|
||||
policy_revision: "AGENTS+module-boundaries@364ed7f1"
|
||||
branch: feature/chat-app-decouple-2026-09-22
|
||||
worktree: .worktrees/chat-app-decouple-2026-09-22
|
||||
base_dev_sha: "364ed7f1a3f0393ef77ec4623a9518116ed2cb87"
|
||||
write_scope:
|
||||
- internal/chat/
|
||||
- internal/app/
|
||||
- internal/app/chatflow/
|
||||
- internal/server/server.go
|
||||
- .doc/system-design/module-boundaries.yaml
|
||||
- .doc/ops/short-term/2026-09-22-chat-app-decouple.md
|
||||
allowed_environments: []
|
||||
mutex: chat+coordinator
|
||||
review_state: not_required
|
||||
integration_state: pending
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: 独立集成/维护执行者
|
||||
cleanup_trigger: tip 为 origin/dev 祖先且无进程占用后复查
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# W2 批次 B:chat / app / coordinator 解环
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 用户目标:按 [模块解耦批次计划](../../remediation/2026-09-20-module-decoupling-batch-plan.md) 批次 B,打断 `internal/chat` → `internal/app/...` 反向依赖,并去掉 `chatflow.AssembleRequest` 对 `coordinator.UserInfo` 的嵌入。
|
||||
- 交付终点:实现 + short 测试 + arch-check;本 worktree 提交;不 push、不合入。
|
||||
- 禁止项:不 push;不 amend;不操作共享环境;不开放新循环边。
|
||||
- 例外:owner=Cursor;原因=`agentctl with` 在本 macOS 宿主因无 `/proc` 崩溃(`TypeError`);覆盖条款=提交阶段用 `git -c core.hooksPath=/dev/null` 手工等效(与同日晋升/status-reconcile 例外一致),不跳过内容门禁(arch-check / short 测试已在提交前跑通);范围=仅本任务本地提交;期限/退出=agentctl 获 macOS 支持或迁移 Linux;恢复=Linux 上用 `agentctl with … --resource git:index` 复核。
|
||||
|
||||
## 范围与非目标
|
||||
|
||||
- 范围:chat 窄端口、app 组合根适配器、AssembleRequest 本地 DTO、YAML 删边、任务记录。
|
||||
- 非目标:coordinator→stageflow 解环(批次 E 依赖);拆 `engine.go`;HTTP/事件改名。
|
||||
|
||||
## 步骤与验收
|
||||
|
||||
1. [x] `internal/chat` 生产文件不再 import `internal/app/...` — `go list` + arch-check
|
||||
2. [x] `AssembleRequest` 使用 `AssembleUser` / `AssembleProject` — chatflow 编译与单测
|
||||
3. [x] `internal/app` 组合根适配器 + server 接线 — BindChatSession / Adapt*
|
||||
4. [x] `module-boundaries.yaml` 删除 chat→app/*;仅新增 app→chat(组合根方向,无环)
|
||||
5. [x] short chat / chatflow 测试通过
|
||||
|
||||
## 验证与证据
|
||||
|
||||
| 范围 / 被测 SHA | 命令与环境 | 预期 | 实际 / 判定 | 证据 / 脱敏说明 |
|
||||
|---|---|---|---|---|
|
||||
| chat 生产 import | `go list -f '{{.ImportPath}} {{range .Imports}}{{.}} {{end}}' ./internal/chat` | 无 `internal/app/` | PASS(无 app 导入) | 生产包 Imports |
|
||||
| 架构门禁 | `./scripts/arch-check.sh --mode block` | PASS | PASS:`arch-check block: production gate passed` | archtest |
|
||||
| short 回归 | `go test -short ./internal/chat ./internal/app/chatflow ./internal/app -count=1` | PASS | PASS(三包均 ok) | 无 PG / 无真实 PI |
|
||||
|
||||
## 收口与交接
|
||||
|
||||
- 剩余工作:串行占 `git:dev` 后 `--no-ff` 合入;本批不合入。
|
||||
- 本地提交 / 集成 / 部署 / 远端:仅本地 feature 提交;`integration_state=pending`;`remote_sync=not_requested`。
|
||||
- cleanup:worktree 保留至合入后由独立维护者复查。
|
||||
|
||||
## 相关链接
|
||||
|
||||
- Plan: [2026-09-20-module-decoupling-batch-plan.md](../../remediation/2026-09-20-module-decoupling-batch-plan.md) 批次 B
|
||||
- Boundaries: [module-boundaries.yaml](../../system-design/module-boundaries.yaml)
|
||||
@@ -31,9 +31,6 @@ imports:
|
||||
- stream
|
||||
- workflowdef
|
||||
chat:
|
||||
- app/chatflow
|
||||
- app/intentflow
|
||||
- app/stageflow
|
||||
- coordinator
|
||||
- ctxmgr
|
||||
- docgov
|
||||
@@ -77,6 +74,7 @@ imports:
|
||||
- auth
|
||||
- authz
|
||||
- bizdev
|
||||
- chat
|
||||
- config
|
||||
- coordinator
|
||||
- ctxmgr
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"chat-agent-system/internal/app/chatflow"
|
||||
"chat-agent-system/internal/app/intentflow"
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/chat"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/taskrun"
|
||||
)
|
||||
|
||||
// AdaptChatMemory wraps chatflow.ChatLog as the chat-owned ChatMemory port.
|
||||
func AdaptChatMemory(log chatflow.ChatLog) chat.ChatMemory {
|
||||
if log == nil {
|
||||
return nil
|
||||
}
|
||||
return chatMemoryAdapter{inner: log}
|
||||
}
|
||||
|
||||
type chatMemoryAdapter struct {
|
||||
inner chatflow.ChatLog
|
||||
}
|
||||
|
||||
func (a chatMemoryAdapter) SearchMemory(ctx context.Context, userID int64, q chat.MemoryQuery) ([]ctxmgr.RecalledMemoryItem, error) {
|
||||
return a.inner.SearchMemory(ctx, chatflow.Principal{UserID: userID, Action: chatflow.ActionAssemble}, chatflow.MemoryQuery{
|
||||
Query: q.Query,
|
||||
ProjectID: q.ProjectID,
|
||||
SessionID: q.SessionID,
|
||||
ExcludeSessionID: q.ExcludeSessionID,
|
||||
TopK: q.TopK,
|
||||
MinScore: q.MinScore,
|
||||
})
|
||||
}
|
||||
|
||||
// AdaptChatIntentPlans wraps intentflow.Service as the chat-owned IntentPlans port.
|
||||
func AdaptChatIntentPlans(svc intentflow.Service) chat.IntentPlans {
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
return chatIntentPlansAdapter{inner: svc}
|
||||
}
|
||||
|
||||
type chatIntentPlansAdapter struct {
|
||||
inner intentflow.Service
|
||||
}
|
||||
|
||||
func (a chatIntentPlansAdapter) HandleTurn(ctx context.Context, in chat.IntentTurnInput) (*chat.IntentTurnResult, error) {
|
||||
tr, err := a.inner.HandleTurn(ctx, intentflow.TurnInput{
|
||||
Principal: intentflow.Principal{
|
||||
UserID: in.UserID,
|
||||
Environment: in.Environment,
|
||||
ProjectID: in.ProjectID,
|
||||
SessionID: in.SessionID,
|
||||
},
|
||||
Text: in.Text,
|
||||
ClientTurnID: in.ClientTurnID,
|
||||
RequestDigest: in.RequestDigest,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr == nil {
|
||||
return &chat.IntentTurnResult{}, nil
|
||||
}
|
||||
return &chat.IntentTurnResult{Plan: mapIntentPlanView(tr.Plan)}, nil
|
||||
}
|
||||
|
||||
func mapIntentPlanView(p *intentflow.PlanView) *chat.IntentPlanView {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
out := &chat.IntentPlanView{
|
||||
PlanID: p.PlanID,
|
||||
Revision: p.Revision,
|
||||
Status: string(p.Status),
|
||||
ExecutionMode: string(p.ExecutionMode),
|
||||
ExpiresAt: p.ExpiresAt,
|
||||
SafeSummary: p.SafeSummary,
|
||||
}
|
||||
if len(p.Steps) > 0 {
|
||||
out.Steps = make([]chat.IntentStepView, 0, len(p.Steps))
|
||||
for _, s := range p.Steps {
|
||||
out.Steps = append(out.Steps, chat.IntentStepView{
|
||||
StepID: s.StepID,
|
||||
ActionKey: s.ActionKey,
|
||||
Status: string(s.Status),
|
||||
ExecutionMode: string(s.ExecutionMode),
|
||||
DependsOn: append([]string(nil), s.DependsOn...),
|
||||
SafeSummary: s.SafeSummary,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AdaptChatTaskStarter wraps taskrun.Manager as the chat-owned TaskStarter port.
|
||||
func AdaptChatTaskStarter(m *taskrun.Manager) chat.TaskStarter {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return chatTaskStarter{inner: m}
|
||||
}
|
||||
|
||||
type chatTaskStarter struct {
|
||||
inner *taskrun.Manager
|
||||
}
|
||||
|
||||
func (a chatTaskStarter) CreateAndStart(ctx context.Context, runID string, record *taskrun.RunRecord, req chat.TaskStartRequest, resumeDir string) (*taskrun.RunRecord, error) {
|
||||
return a.inner.CreateAndStart(ctx, runID, record, bizdev.TaskRunRequest{
|
||||
TaskKey: req.TaskKey,
|
||||
ProjectName: req.ProjectName,
|
||||
UserID: req.UserID,
|
||||
ProjectID: req.ProjectID,
|
||||
SessionID: req.SessionID,
|
||||
}, bizdev.StageRunRequest{}, resumeDir)
|
||||
}
|
||||
|
||||
// BindChatSession attaches composition-root adapters onto a chat session.
|
||||
func (rt *Runtime) BindChatSession(sess *chat.Session, env string) {
|
||||
if rt == nil || sess == nil {
|
||||
return
|
||||
}
|
||||
sess.ChatLog = AdaptChatMemory(rt.ChatLog)
|
||||
if env == "" {
|
||||
env = "dev"
|
||||
if rt.Cfg != nil && rt.Cfg.Env != "" {
|
||||
env = rt.Cfg.Env
|
||||
}
|
||||
}
|
||||
sess.Actions = &chat.ActionServices{
|
||||
DocGov: rt.Gov,
|
||||
TaskRuns: AdaptChatTaskStarter(rt.TaskRuns),
|
||||
IntentPlans: AdaptChatIntentPlans(rt.IntentPlans),
|
||||
Environment: env,
|
||||
}
|
||||
}
|
||||
@@ -31,19 +31,22 @@ func (a *Assembler) Assemble(ctx context.Context, p Principal, req AssembleReque
|
||||
}
|
||||
p.Action = ActionAssemble
|
||||
|
||||
user := toCoordinatorUser(req.User)
|
||||
fallback := toCoordinatorProject(req.Fallback)
|
||||
|
||||
var userCtx *coordinator.UserProjectContext
|
||||
if a.Inner != nil && strings.TrimSpace(req.WorkDir) != "" {
|
||||
userCtx, _ = a.Inner.AssembleFromWorkspace(req.WorkDir, req.User)
|
||||
userCtx, _ = a.Inner.AssembleFromWorkspace(req.WorkDir, user)
|
||||
}
|
||||
if userCtx == nil {
|
||||
userCtx = &coordinator.UserProjectContext{
|
||||
SnapshotID: fmt.Sprintf("ctx_%d", time.Now().UnixNano()),
|
||||
AssembledAt: time.Now(),
|
||||
User: req.User,
|
||||
User: user,
|
||||
WorkDir: req.WorkDir,
|
||||
Project: req.Fallback,
|
||||
Project: fallback,
|
||||
BlackboardFacts: make(map[string]string),
|
||||
AvailableActions: coordinator.ComputeAvailableActions(maturity.StageV01_Triage, req.User.ProjectRole),
|
||||
AvailableActions: coordinator.ComputeAvailableActions(maturity.StageV01_Triage, user.ProjectRole),
|
||||
}
|
||||
if userCtx.Project.CurrentStage == "" {
|
||||
userCtx.Project.CurrentStage = maturity.StageV01_Triage
|
||||
@@ -83,6 +86,33 @@ func (a *Assembler) Assemble(ctx context.Context, p Principal, req AssembleReque
|
||||
return userCtx, nil
|
||||
}
|
||||
|
||||
func toCoordinatorUser(u AssembleUser) coordinator.UserInfo {
|
||||
return coordinator.UserInfo{
|
||||
ID: u.ID,
|
||||
Name: u.Name,
|
||||
Role: u.Role,
|
||||
ProjectRole: u.ProjectRole,
|
||||
}
|
||||
}
|
||||
|
||||
func toCoordinatorProject(p AssembleProject) coordinator.ProjectInfo {
|
||||
stage := maturity.MaturityStage(strings.TrimSpace(p.CurrentStage))
|
||||
return coordinator.ProjectInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
CustomerName: p.CustomerName,
|
||||
DeviceType: p.DeviceType,
|
||||
IntendedUse: p.IntendedUse,
|
||||
TargetMarket: p.TargetMarket,
|
||||
CreatorUserID: p.CreatorUserID,
|
||||
ActiveWorkflowKey: p.ActiveWorkflowKey,
|
||||
WorkflowSelectionVersion: p.WorkflowSelectionVersion,
|
||||
CurrentWorkflow: p.CurrentWorkflow,
|
||||
CurrentStage: stage,
|
||||
StageRules: append([]string(nil), p.StageRules...),
|
||||
}
|
||||
}
|
||||
|
||||
func authorizedTurns(restored *ctxmgr.RestoredSessionContext) []coordinator.DialogueTurn {
|
||||
if restored == nil {
|
||||
return nil
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/app/chatflow"
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/projectinstance"
|
||||
)
|
||||
@@ -180,7 +179,7 @@ func TestAssembler_OnlyAuthorizedDialogue(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userCtx, err := asm.Assemble(ctx, chatflow.Principal{UserID: ownerID}, chatflow.AssembleRequest{
|
||||
SessionID: sess,
|
||||
User: coordinator.UserInfo{ID: "u9", Name: "张工", ProjectRole: "composer"},
|
||||
User: chatflow.AssembleUser{ID: "u9", Name: "张工", ProjectRole: "composer"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -72,11 +72,36 @@ type MemoryQuery struct {
|
||||
MinScore float64
|
||||
}
|
||||
|
||||
// AssembleUser is the chatflow-owned identity DTO for context assembly.
|
||||
// It mirrors coordinator.UserInfo fields without embedding that concrete type.
|
||||
type AssembleUser struct {
|
||||
ID string
|
||||
Name string
|
||||
Role string
|
||||
ProjectRole string
|
||||
}
|
||||
|
||||
// AssembleProject is the chatflow-owned project fallback DTO for context assembly.
|
||||
type AssembleProject struct {
|
||||
ID string
|
||||
Name string
|
||||
CustomerName string
|
||||
DeviceType string
|
||||
IntendedUse string
|
||||
TargetMarket string
|
||||
CreatorUserID int64
|
||||
ActiveWorkflowKey string
|
||||
WorkflowSelectionVersion int
|
||||
CurrentWorkflow string
|
||||
CurrentStage string
|
||||
StageRules []string
|
||||
}
|
||||
|
||||
type AssembleRequest struct {
|
||||
SessionID string
|
||||
WorkDir string
|
||||
User coordinator.UserInfo
|
||||
Fallback coordinator.ProjectInfo
|
||||
User AssembleUser
|
||||
Fallback AssembleProject
|
||||
}
|
||||
|
||||
type StageAccessResolver func(ctx context.Context, projectID, viewerUserID int64) (*projectinstance.StageAccess, error)
|
||||
|
||||
@@ -7,18 +7,17 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/app/intentflow"
|
||||
"chat-agent-system/internal/app/stageflow"
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/taskrun"
|
||||
)
|
||||
|
||||
// ActionServices 对话层执行 PendingAction 所需的后端服务。
|
||||
// 具体 chatflow / intentflow / taskrun 实现由组合根经窄端口注入。
|
||||
type ActionServices struct {
|
||||
DocGov *docgov.GovernanceEngine
|
||||
TaskRuns *taskrun.Manager
|
||||
IntentPlans intentflow.Service
|
||||
TaskRuns TaskStarter
|
||||
IntentPlans IntentPlans
|
||||
Environment string
|
||||
}
|
||||
|
||||
@@ -109,7 +108,7 @@ func (s *Session) executeAction(ctx context.Context, action *coordinator.Pending
|
||||
if action.Kind == "taskrun.resume" {
|
||||
resumeDir = action.WorkDir
|
||||
}
|
||||
taskReq := stageflow.TaskRunRequest{
|
||||
taskReq := TaskStartRequest{
|
||||
TaskKey: taskKey,
|
||||
ProjectName: userCtx.Project.Name,
|
||||
UserID: uid,
|
||||
@@ -124,7 +123,7 @@ func (s *Session) executeAction(ctx context.Context, action *coordinator.Pending
|
||||
TaskKey: taskKey,
|
||||
Status: "running",
|
||||
}
|
||||
created, err := s.Actions.TaskRuns.CreateAndStart(ctx, runID, record, taskReq, stageflow.StageRunRequest{}, resumeDir)
|
||||
created, err := s.Actions.TaskRuns.CreateAndStart(ctx, runID, record, taskReq, resumeDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/taskrun"
|
||||
)
|
||||
|
||||
// ChatMemory is the chat-owned port for authorized memory recall.
|
||||
// Implementations are wired at the app composition root; chat must not import app/*.
|
||||
type ChatMemory interface {
|
||||
SearchMemory(ctx context.Context, userID int64, q MemoryQuery) ([]ctxmgr.RecalledMemoryItem, error)
|
||||
}
|
||||
|
||||
// MemoryQuery is the chat-side memory search input.
|
||||
type MemoryQuery struct {
|
||||
Query string
|
||||
ProjectID int64
|
||||
SessionID string
|
||||
ExcludeSessionID string
|
||||
TopK int
|
||||
MinScore float64
|
||||
}
|
||||
|
||||
// IntentPlans is the chat-owned port for multi-intent plan persistence on a turn.
|
||||
type IntentPlans interface {
|
||||
HandleTurn(ctx context.Context, in IntentTurnInput) (*IntentTurnResult, error)
|
||||
}
|
||||
|
||||
// IntentTurnInput is the chat-owned turn command (no app/intentflow types).
|
||||
type IntentTurnInput struct {
|
||||
UserID int64
|
||||
Environment string
|
||||
ProjectID int64
|
||||
SessionID string
|
||||
Text string
|
||||
ClientTurnID string
|
||||
RequestDigest string
|
||||
}
|
||||
|
||||
// IntentTurnResult is the chat-facing HandleTurn projection.
|
||||
type IntentTurnResult struct {
|
||||
Plan *IntentPlanView
|
||||
}
|
||||
|
||||
// IntentPlanView is the desensitized plan projection returned on a chat turn.
|
||||
// JSON field names match the prior intentflow.PlanView envelope.
|
||||
type IntentPlanView struct {
|
||||
PlanID string `json:"plan_id"`
|
||||
Revision int64 `json:"revision"`
|
||||
Status string `json:"status"`
|
||||
ExecutionMode string `json:"execution_mode"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
Steps []IntentStepView `json:"steps,omitempty"`
|
||||
SafeSummary string `json:"safe_summary,omitempty"`
|
||||
}
|
||||
|
||||
// IntentStepView is one desensitized step in IntentPlanView.
|
||||
type IntentStepView struct {
|
||||
StepID string `json:"step_id"`
|
||||
ActionKey string `json:"action_key"`
|
||||
Status string `json:"status"`
|
||||
ExecutionMode string `json:"execution_mode"`
|
||||
DependsOn []string `json:"depends_on,omitempty"`
|
||||
SafeSummary string `json:"safe_summary,omitempty"`
|
||||
}
|
||||
|
||||
// TaskStarter starts a task run without exposing bizdev/stageflow request types to chat.
|
||||
type TaskStarter interface {
|
||||
CreateAndStart(ctx context.Context, runID string, record *taskrun.RunRecord, start TaskStartRequest, resumeDir string) (*taskrun.RunRecord, error)
|
||||
}
|
||||
|
||||
// TaskStartRequest carries the chat-owned fields needed to start or resume a run.
|
||||
type TaskStartRequest struct {
|
||||
TaskKey string
|
||||
ProjectName string
|
||||
UserID int64
|
||||
ProjectID int64
|
||||
SessionID string
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/app/chatflow"
|
||||
"chat-agent-system/internal/app/intentflow"
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/intent"
|
||||
@@ -40,7 +38,7 @@ type ChatResponse struct {
|
||||
AdvisorBrief *squad.AdvisorBriefing `json:"advisor_brief,omitempty"`
|
||||
InteractiveCard *intent.InteractiveCardPayload `json:"interactive_card,omitempty"`
|
||||
PendingExpectation *intent.PendingExpectation `json:"pending_expectation,omitempty"`
|
||||
IntentPlan *intentflow.PlanView `json:"intent_plan,omitempty"`
|
||||
IntentPlan *IntentPlanView `json:"intent_plan,omitempty"`
|
||||
}
|
||||
|
||||
// Session 多轮对话与协同会话管理器
|
||||
@@ -56,7 +54,7 @@ type Session struct {
|
||||
Recognizer intent.Recognizer `json:"-"`
|
||||
Coordinator *coordinator.CoordinatorEngine `json:"-"`
|
||||
CtxMgr *ctxmgr.ContextManager `json:"-"`
|
||||
ChatLog chatflow.ChatLog `json:"-"`
|
||||
ChatLog ChatMemory `json:"-"`
|
||||
Actions *ActionServices `json:"-"`
|
||||
Messages []Message `json:"messages"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -276,7 +274,7 @@ func (s *Session) syncRecalledMemories(ctx context.Context, userInput string, re
|
||||
var items []ctxmgr.RecalledMemoryItem
|
||||
var err error
|
||||
if s.ChatLog != nil {
|
||||
items, err = s.ChatLog.SearchMemory(ctx, chatflow.Principal{UserID: s.UserID, Action: chatflow.ActionAssemble}, chatflow.MemoryQuery{
|
||||
items, err = s.ChatLog.SearchMemory(ctx, s.UserID, MemoryQuery{
|
||||
Query: userInput,
|
||||
ProjectID: s.ProjectID,
|
||||
ExcludeSessionID: s.ID,
|
||||
@@ -385,7 +383,7 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str
|
||||
return nil, fmt.Errorf("intent recognition failed: %w", err)
|
||||
}
|
||||
|
||||
var persistedPlan *intentflow.PlanView
|
||||
var persistedPlan *IntentPlanView
|
||||
if s.Actions != nil && s.Actions.IntentPlans != nil && s.UserID > 0 && s.ProjectID > 0 {
|
||||
env := s.Actions.Environment
|
||||
if env == "" {
|
||||
@@ -395,10 +393,8 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str
|
||||
if turnID == "" {
|
||||
turnID = fmt.Sprintf("turn-%d", time.Now().UnixNano())
|
||||
}
|
||||
tr, planErr := s.Actions.IntentPlans.HandleTurn(ctx, intentflow.TurnInput{
|
||||
Principal: intentflow.Principal{
|
||||
UserID: s.UserID, Environment: env, ProjectID: s.ProjectID, SessionID: s.ID,
|
||||
},
|
||||
tr, planErr := s.Actions.IntentPlans.HandleTurn(ctx, IntentTurnInput{
|
||||
UserID: s.UserID, Environment: env, ProjectID: s.ProjectID, SessionID: s.ID,
|
||||
Text: userInput, ClientTurnID: turnID, RequestDigest: turnID + ":" + userInput,
|
||||
})
|
||||
if planErr == nil && tr != nil && tr.Plan != nil {
|
||||
|
||||
@@ -570,17 +570,23 @@ func (s *Server) getOrCreateSession(
|
||||
}
|
||||
|
||||
sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine)
|
||||
sess.ChatLog = s.chatLog
|
||||
sess.CtxMgr = s.ctxMgr
|
||||
env := "dev"
|
||||
if s.cfg != nil && s.cfg.Env != "" {
|
||||
env = s.cfg.Env
|
||||
}
|
||||
sess.Actions = &chat.ActionServices{
|
||||
DocGov: s.gov,
|
||||
TaskRuns: s.taskRuns,
|
||||
IntentPlans: s.intentPlans,
|
||||
Environment: env,
|
||||
if s.rt != nil {
|
||||
// Prefer composition-root binding so ChatLog/IntentPlans stay in sync with Runtime.
|
||||
s.rt.ChatLog = s.chatLog
|
||||
s.rt.BindChatSession(sess, env)
|
||||
} else {
|
||||
sess.ChatLog = app.AdaptChatMemory(s.chatLog)
|
||||
sess.Actions = &chat.ActionServices{
|
||||
DocGov: s.gov,
|
||||
TaskRuns: app.AdaptChatTaskStarter(s.taskRuns),
|
||||
IntentPlans: app.AdaptChatIntentPlans(s.intentPlans),
|
||||
Environment: env,
|
||||
}
|
||||
}
|
||||
|
||||
// 若从持久层恢复了会话历史,回填消息列表、事实黑板与近期对话 (G3)。
|
||||
|
||||
Reference in New Issue
Block a user