8000 refactor(agent): consolidate chat and thread domains by sheepbox8646 · Pull Request #856 · felinics/Memoh · GitHub
[go: up one dir, main page]

Skip to content

refactor(agent): consolidate chat and thread domains - #856

Merged
sheepbox8646 merged 4 commits into
mainfrom
codex/refactor-agent-chat-thread
Jul 24, 2026
Merged

sheepbox8646 merged 4 commits into
mainfrom
codex/refactor-agent-chat-thread

Conversation

@sheepbox8646
Copy link
Copy Markdown
Member

Summary

  • Consolidate agent orchestration, runtime, context, decision, event, and tool concerns under internal/agent.
  • Split chat persistence and presentation into explicit internal/chat/{event,message,thread,timeline,view} packages.
  • Remove the synthetic Conversation domain and resolve external channel conversations directly to a bot plus its active internal thread.
  • Move inbound adaptation and discuss driving to Channel-owned packages, leaving turn.Service as the Channel-to-Agent boundary.
  • Add architecture guards for the new package ownership rules.

Compatibility

  • Keep a single Go module; no go.work is introduced.
  • Keep PostgreSQL schemas, tables, columns, and migration history unchanged.
  • Preserve REST, SSE, WebSocket, and generated SDK wire fields such as session_id.
  • Preserve gRPC protobuf field names/numbers and the legacy JSON SessionID adapter.
  • Preserve existing OpenAPI schema names through explicit Swagger aliases; regenerated OpenAPI and SDK output is byte-for-byte unchanged.

Validation

  • go test ./...
  • go test -race ./internal/agent/application -run 'TestStartTurnInitializesIdempotencyRegistryConcurrently|TestStartTurnStreamsEvents|TestStartTurnRejectsDuplicateIdempotencyKey'
  • mise run lint:go
  • pnpm exec eslint . --ignore-pattern '.claude/worktrees/**'
  • mise x sqlc@1.31.1 -- sqlc generate
  • mise run swagger-generate
  • mise run sdk-generate
  • Restarted the development Server and Channel; both report healthy.
  • Live API smoke: login, bot list, session create/list/get/update, message list, SSE heartbeat, session delete, and post-delete 404 all passed.

@chen-ran
Copy link
Copy Markdown
Member
  • 请补充解释 application/ 的意图,是否会成为新的万能包,边界要明确划分
  • 改动目标是「高内聚,低耦合」,需要补充turn的流程梳理,改动前、改动后。可以在 docs/superpowers/specs/2026-07-17-channel-boundary-design.md 中绘制 泳道图 及 模块耦合分析
  • 进一步评估该pr与其他pr的冲突风险,是否有前后置关系

Copy link
Copy Markdown
Member Author

@chen-ran 回应这三点。按本轮约定,图直接放在评论里,不再修改 spec。

1. application/ 的意图和边界

这个担忧成立:internal/agent/application 目前仍然很大,本 PR 不把它描述成已经完成内部拆分的终态。它的定位是 Agent application layer / use-case orchestrator,不是新的共享领域包,也不是放置“不知道该放哪里”的代码。

它只负责:

  • 实现 turn.Service,编排 start / resume / retry / edit / continuation 等 Agent 用例;
  • 协调 context、memory、compaction、decision、message persistence 和 runtime selection;
  • 把纯数据的 StartTurnCommand 转换为一次 Agent 执行,并把 runtime 输出映射为 turn.Event。

它不拥有:

  • 外部平台适配、ACL、route、trigger 和 delivery:属于 internal/channel;
  • Thread / Message 的生命周期和持久化模型:属于 internal/chat/thread、internal/chat/message;
  • 入站事实的 projection / rendering:属于 internal/chat/timeline;
  • 模型循环和 ACP/native 执行:属于 internal/agent/runtime;
  • Tool 实现:属于 internal/agent/tool;
  • gRPC、Echo、fx 装配:分别属于 agent/turn/grpctransport、handlers、cmd/**。

当前生产代码中,直接依赖 application 的只有 composition root 和本地 Web/API adapter;外部 Channel 不能依赖它,只能依赖 agent/turn、agent/event、agent/decision。反向依赖由 internal/arch 机械守卫:chat storage 不能依赖 Agent/Channel,timeline 只能使用 Turn 数据契约,runtime/tool/context 都不能反向依赖 application。

所以这里允许 application 有较高 fan-out,因为它是编排层;但要保持低 fan-in、禁止下层反向引用。后续新增能力如果形成独立状态或生命周期,应继续下沉为 context/decision/runtime/chat 下的兄弟包,application 只保留用例编排,不新增通用 utils、领域实体或存储实现。

目前还有一个明确的残留耦合:application 为 prompt identity 和 conversation type 仍消费少量 channel 词汇类型;它没有依赖 Channel runtime/manager/adapter,但若继续收紧边界,应把这组值对象抽成中立 port,而不是扩大该依赖。

2. Turn 流程:改动前

sequenceDiagram
    autonumber
    participant CH as Channel inbound / discuss
    participant ROUTE as channel/route + conversation.Service
    participant PIPE as internal/pipeline
    participant ADAPTER as agent/turn/inprocess.Adapter
    participant FLOW as conversation/flow.Resolver
    participant STORE as session / message / compaction
    participant RUNTIME as agent.Agent / ACP

    CH->>ROUTE: ResolveConversation(external conversation/thread)
    ROUTE->>STORE: 创建或读取 synthetic Chat + Session
    CH->>PIPE: 持久化入站事件,IC -> RC

    alt chat
        CH->>ADAPTER: StartTurn(mode=chat)
        ADAPTER->>FLOW: 转换为 conversation.ChatRequest
        FLOW->>STORE: history / memory / compaction / persistence
        FLOW->>RUNTIME: 调用 native 或 ACP
        RUNTIME-->>FLOW: stream chunks
        FLOW-->>ADAPTER: conversation.StreamChunk
    else discuss
        CH->>PIPE: NotifyRC
        PIPE->>ADAPTER: StartTurn(mode=discuss)
        ADAPTER->>FLOW: resolve config / persistence hooks
        ADAPTER->>RUNTIME: 调用 native 或 ACP
        RUNTIME-->>ADAPTER: stream chunks
    end

    ADAPTER-->>CH: turn.Event
    CH-->>CH: render / outbound delivery

    Note over PIPE,FLOW: pipeline、inprocess adapter、flow 都承担了一部分编排职责
Loading

主要问题不是没有 Turn port,而是 port 后面还有 inprocess -> conversation/flow -> agent 的转接层;同时 conversation 混合了 synthetic Chat、请求 DTO、UI projection 和 flow orchestration,pipeline 又同时包含 Channel 入站投影与 discuss driver。

3. Turn 流程:改动后

sequenceDiagram
    autonumber
    participant CH as Channel inbound
    participant ROUTE as channel/route
    participant THREAD as chat/thread
    participant TIMELINE as chat/timeline
    participant DISCUSS as channel/discuss
    participant TURN as agent/turn.Service
    participant APP as agent/application.Service
    participant STORE as chat/message + agent/context/decision
    participant RUNTIME as agent/runtime/native 或 acp

    CH->>ROUTE: Resolve external route -> BotID
    CH->>THREAD: Get/Create active ThreadID
    CH->>TIMELINE: 持久化入站事实并生成 timeline projection

    alt chat
        CH->>TURN: StartTurn(chat, ThreadID)
    else discuss
        CH->>DISCUSS: Notify rendered context
        DISCUSS->>TURN: StartTurn(discuss, ThreadID)
    end

    TURN->>APP: application.Service 直接实现 port
    APP->>STORE: context / memory / compaction / decision / turn persistence
    APP->>RUNTIME: 选择并调用 native 或 ACP runtime
    RUNTIME-->>APP: stream chunks
    APP-->>TURN: 有序 turn.Event
    TURN-->>CH: Events / Errs
    CH-->>CH: render / outbound delivery

    Note over TURN,RUNTIME: chat 与 discuss 在同一个 Turn application boundary 收敛
Loading

模块依赖收敛后是单向的:

flowchart LR
    CH["Channel"] -->|StartTurn / resume| TURN["agent/turn\n纯 Port"]
    API["Local Web/API adapter"] --> APP["agent/application\n用例编排"]
    APP -->|implements| TURN

    APP --> CTX["agent/context"]
    APP --> DEC["agent/decision"]
    APP --> RT["agent/runtime"]
    APP --> MSG["chat/message"]
    APP --> TH["chat/thread"]
    APP --> TL["chat/timeline"]

    CH --> DEC
    CH --> TH
    CH --> TL
    TL -->|只使用数据契约| TURN
    RT --> TOOL["agent/tool"]

    MSG -.禁止反向依赖.-> APP
    TH -.禁止反向依赖.-> APP
    RT -.禁止反向依赖.-> APP
    TOOL -.禁止反向依赖.-> APP
Loading

这里的核心变化是:删除 synthetic Conversation domain;外部 conversation/thread 只作为 Channel route key,内部运行统一落到 Bot + Thread;application.Service 直接实现 Turn port;原 pipeline 按所有权拆到 channel/inbound、channel/discuss 和 chat/timeline。

4. 与其他开放 PR 的冲突和前后置关系

我按当前各 PR head 用 git merge-tree 做了真实三方合并模拟。下面的数字是 conflict record 数,不是简单的同名文件数:

PR main 当前冲突 与 #856 冲突 判断
#854 Provider OAuth 0 0 独立,可任意顺序
#849 ACP replacement hooks 0 3 中风险;冲突集中在迁移后的 agent/application,建议先合 #849,再把 #856 rebase 一次并复验 ACP hooks
#839 UI showcase 1 1 冲突来自它自身的 packages/ui gitlink,#856 不改 UI submodule,语义独立
#834 RuntimeRouter tests 0 0 独立,可任意顺序
#801 session-runtime backend 0 40 高风险;它修改的 sessionruntime、conversation/flow、message 等正是 #856 的搬迁目标
#802 Web runtime cutover 0 45 高风险;与 #801 同一 stack
#830 ChatView sync 1 57 高风险;明确叠在 #801/#802 上,且新增文件需要迁移到 application/runtime/chat 新路径
#716 pipeline parity 10 84 很高;同时改 flow、pipeline、message、session,且当前已与 main 有存量冲突
#717 context/compaction 51 127 很高;叠在 #716 上,且大量新增文件落在本 PR 已重命名的目录

没有 schema、REST/SSE/WS、protobuf 或 Go module 层面的硬前置依赖;这里主要是源码集成顺序问题。

最省总返工量的顺序是:

  1. 先落体量小且 ready 的 fix(acp): hide and reject unsupported retry/edit #849;
  2. 若 runtime stack 准备近期合并,则按 feat(runtime): integrate distributed session runtime #801 -> feat(web): observe canonical session runtime state #802 -> feat(web): chatview sync-layer refactor — single writer + stable row identity #830 落地;
  3. 若 context stack 准备近期合并,则按 feat(context): compaction summary parity for discuss and pipeline chat #716 -> feat(compaction): dependable zero-config default compaction #717 落地;
  4. 最后统一 rebase refactor(agent): consolidate chat and thread domains #856,一次完成目录映射和行为复验。

但 #830、#717 仍是 Draft,不应该无限期阻塞 #856。如果这两个 stack 短期不会落地,另一条明确可执行的路线是先合 #856,然后由对应 stack 在 rebase 时按新边界迁移;这会把冲突成本留给后合并的分支。无论选哪条路线,都不建议把这些高冲突分支与 #856 直接机械 merge 后解决,因为很多冲突是“新增文件应该归到哪个新 package”的语义冲突,不只是路径冲突。

@chen-ran
Copy link
Copy Markdown
Member

大体上我赞同这个pr的方向,我觉得

  • 需要明确列出 application → channel、tool → channel、runtime → chat、Thread→Route DB 耦合。
  • 补全 DiscussDriver 的实际职责
  • 继续推动模块内聚

@sheepbox8646
sheepbox8646 marked this pull request as ready for review July 23, 2026 08:09
@sheepbox8646
sheepbox8646 requested review from a team July 23, 2026 08:09
@sheepbox8646
sheepbox8646 force-pushed the codex/refactor-agent-chat-thread branch from 480799f to e97629c Compare July 23, 2026 15:19
Copy link
Copy Markdown
Member Author

已按 review 意见把剩余边界和兼容性补齐。下面是这次实际落地的耦合清单、DiscussDriver 职责,以及回归证据。

四条耦合已显式拆开

原耦合 原问题 当前归属 / 适配点 防回退守卫
application → channel Agent application 直接读取 channel.Store / channel.ChannelConfig 来拼平台身份上下文 Agent 定义 PlatformIdentitySource;internal/agent/adapter/channelidentity 在 composition root 投影 Channel 配置 TestApplicationDoesNotDependOnChannel
tool → channel Agent tools 和 internal/messaging.Executor 直接依赖 Channel 的消息、附件、联系人和发送类型 中立消息契约下沉到 internal/messaging;channelmessaging、channelcontact、channelthread 适配器负责进入 Channel;通用脱敏注册表移到 internal/redact TestAgentToolStaysOnNeutralPorts
runtime → chat Session runtime 直接依赖 internal/chat/view,ACP pool 直接读取 thread.Service / Thread UI runtime view 归入 internal/agent/view;ACP runtime 只消费 SessionDescriptorReader,由 internal/agent/adapter/acpsession 投影 Thread TestAgentRuntimeDoesNotDependOnChat
Thread → Route DB Thread service 直接查询/更新 bot_channel_routes,Session SQL 直接 join Route 表 Thread 只保存 opaque route_id;Channel 的 route.ThreadCoordinator 负责 active-thread 选择,EnrichThreads 负责列表投影 TestThreadStorageDoesNotReachRouteDB

DiscussDriver 的实际职责

DiscussDriver 现在只持有依赖、worker registry、配置快照和生命周期;原来堆在一个文件里的行为拆成了明确协作者:

flowchart LR
    D["driver.go<br/>worker registry + lifecycle + config snapshots"] --> W["worker.go<br/>RC buffering + idle timeout + re-entry"]
    RC["RenderedContext"] --> W
    W --> H["history.go<br/>load persisted Turn Responses"]
    W --> C["cursor.go<br/>load / advance durable cursor"]
    H --> T["trigger.go<br/>compose RC + TR into StartTurnCommand"]
    C --> T
    T --> R["runner.go<br/>turn.Service.StartTurn + outcome reduction"]
    R --> P["projector.go<br/>Agent events → Channel stream events"]
    P --> UI["RouteHub / Web UI"]
    R --> C
Loading

拆分时保留了原语义:冷启动游标锚定、RC 合并、private conversation addressed 判定、ACP skipped/clean-terminal 才推进游标、native runtime 失败后仍推进,以及 tool approval / user input / reasoning / text / terminal 事件投影。

兼容性和真实链路验证

  • Go 全量 race:go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
  • Lint:mise run lint
  • sqlc:固定 sqlc@1.31.1 重新生成,生成目录前后 checksum 一致
  • OpenAPI / SDK:mise run sdk-generate 后无生成差异
  • PostgreSQL:迁移 up → down → up 通过;相关 integration tests 通过
  • Runtime backend:Redis 8、Valkey 8、PostgreSQL fencing、pgvector bootstrap 全部通过
  • 从 PR 工作树重启开发栈,确认 Server 实际挂载该工作树;Server / Channel / PostgreSQL / pgvector / Web 健康
  • REST Thread CRUD:create / list / get / patch / delete / deleted 404 通过,列表仍返回 route_id、route_conversation_type 和 route metadata
  • REST Channel + SSE:完整收到 started → agent events → final → completed;Route 的 active_session_id 与 Thread 一致,user / assistant 消息均落库
  • WebSocket:无 Thread 启动时创建 Thread;同一 Thread continuation 成功,history 持久化
  • Discuss WebSocket:start / message / end 完整,user / assistant 持久化
  • Standalone Channel:health 通过,13 个 adapter 正常注册
  • 未引入 go.work,数据库 schema 和迁移内容未变
  • 当前 PR Actions 全绿:Go Test、Lint、Redis、Valkey、PostgreSQL fencing、pgvector、PostgreSQL migrations、Server/Web/Workspace amd64/arm64 构建全部通过

对应补齐提交:e97629cc9 refactor(agent): enforce agent and channel boundaries。

@sheepbox8646
sheepbox8646 merged commit 76efe2f into main Jul 24, 2026
15 checks passed
ChrAlpha added a commit to ChrAlpha/Memoh that referenced this pull request Jul 24, 2026
…tion

Mechanical layer: accepted directory-rename relocations
(conversation/flow -> agent/application, contextfrag -> agent/context/
fragment, historyfrag -> agent/context/history, compaction -> agent/
context/compaction, contextlimit -> agent/context/limit, pipeline ->
chat/timeline, acpagent -> agent/runtime/acp, conversation -> agent/turn,
message -> chat/message, session -> chat/thread), moved the 17 restack
files left at the old internal/agent root into runtime/native, and swept
import paths/identifiers (Resolver->Service, req.SessionID->ThreadID,
conversation aliases) across 109+ files.

Semantic resolutions:
- application/service.go: restack's resolve() semantics under felinics#856 naming;
  kept felinics#847 ForkContextSourceMessageIDs and felinics#808/felinics#845 workspace-target
  notice as unions; ResolveRunConfigResult regains ContextBudgetMaxTokens.
- prepareRunConfig: query materialization stays OWNED by the context view
  (ContextQueryMaterialized contract) — main's eager append-to-Messages
  block was not adopted; hook texts + system sections restored.
- service_acp: union of restack's ContextView ACP sections/lifecycle/
  budget with upstream felinics#811 attachments, felinics#821 reasoning effort and
  leading-user cleanup; busy-turn gate ordered before attachment
  resolution; persistACPRound keeps the lifecycle parameter.
- continuation/user-input/tool-approval: resumeAgentSession abstraction
  kept, workspace-target threading added (ctx + ChatRequest); stage-1
  budget-trim signals ported into service_continuation.go.
- Broke two illegal layer edges introduced by relocation: turn no longer
  imports chat/timeline (forwardedFromValue inlined) and agent/context/
  fragment no longer imports turn (kind literals inlined); moved
  DiscussContextBuilder/DiscussContextInput from chat/timeline into
  contextview to satisfy the timeline-only-depends-on-turn arch guard.
- cmd/internal/core/providers.go now wires ContextViewApplier and
  LoopReselectMode (previously silently unwired in composition); discuss
  path gained ContextBudgetMaxTokens/lifecycle/tool-exchange threading
  with stale ContextSourceFrags cleared before pumpDiscussNative.
- Known gap carried forward: the discuss ContextView builder cannot attach
  at the turn.Service transport boundary (RC/TR richness is flattened
  before the command); discuss ACP keeps the designed fallback prompt.
- sqlc/swagger/sdk regenerated; deltas vs main are restack's net additions
  (context lifecycle queries/API/types, messages session_mode columns).
@chen-ran
chen-ran deleted the codex/refactor-agent-chat-thread branch September 11, 2026 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0