文档导航
文档 / Session View Contract

Session View Contract

本文档是 feature/session-contract-ui-backend 分支上 Web UI session 优化工作的共享契约(contract)。

该分支的目标是改进 Web UI 的 session 发现、分组、标注和可读性。后端的改动是为了配合这项 UI 工作,提供一套稳定的 session 视图契约。这并不是一次纯后端的架构重写。

Problem

当前的 Web UI 经常把原始的 session key 同时当作地址和语义数据源。这迫使前端代码必须从诸如 agent:*:webchat:*:cli::subagent::cron::thread: 这样的字符串中推断概念。

这种做法很脆弱,因为一个 session key 可能编码了多个相互独立的概念:

  • 路由地址
  • agent/workspace 归属
  • 入口 surface
  • 外部 channel 身份
  • direct/group/channel 的会话拓扑
  • thread/topic 修饰符
  • subagent/task 来源
  • cron 来源或投递
  • 遗留兼容形态

UI 不应该拥有这些解释逻辑。后端必须提供一个 UI-ready 的 session 视图,前端则应基于该视图进行渲染。

Product Model

Web UI 有两个不同的 surface,它们绝不能彼此混为一谈:

  • Conversations:日常导航 surface,从以用户为中心的视角呈现用户希望打开、阅读并继续的条目。
  • Sessions:更底层的账本/调试 surface,涵盖所有已持久化的运行时记录,包括 WebChat、CLI、channel thread、cron 运行、subagent、系统任务,以及删除、过滤和原始 key 检查。

Conversations 并不是一个更小的 Sessions 表。它应按用户入口点对条目进行分组:

  • Chats:sessionKind: "chat"
  • Channels:sessionKind: "channel"
  • Automations:sessionKind: "cron"

Task 和 system session 通常应留在 Sessions 账本中,除非某个专门的后台工作 UI 明确选择纳入它们。

New chat 只是一个 WebChat 创建流程:

New chat -> choose agent -> create/open WebChat

它绝不能创建 cron job、channel session、subagent task 或 system/task session。cron 和 channel 的创建/配置属于它们各自的 Automations/Channels surface。

API

首选的 RPC:

rpc.call("sessions.list", { limit: 200, view: "session-list-v1" })

如果使用 REST 端点,那么在聊天 session 选择器依赖它获取更大列表之前,/api/sessions 应支持等价的 limitview 参数。当前默认的 session 列表大小对于选择器 UI 来说可能太小。

响应形态保持向后兼容:

interface SessionsListResponseV1 {
  sessions: SessionListItemV1[];
  count: number;
  ts: number;
}

只发送 { limit } 的现有调用方必须继续正常工作。

Contract Fields

interface SessionListItemV1 {
  key: string;
  sessionId?: string;

  // Legacy/stored agent id. Kept for compatibility with existing callers.
  agentId?: string;

  // Effective routing/workspace owner. New UI should prefer this.
  effectiveAgentId: string;

  sessionKind: "chat" | "channel" | "task" | "cron" | "system" | "unknown";

  surface:
    | "webchat"
    | "cli"
    | "tui"
    | "mcp"
    | "slack"
    | "feishu"
    | "wecom"
    | "telegram"
    | "discord"
    | "dingtalk"
    | "matrix"
    | "qq"
    | "cron"
    | "subagent"
    | "unknown";

  conversationKind: "main" | "direct" | "group" | "channel" | "unknown";

  thread?: {
    id: string;
    kind: "thread" | "topic";
  } | null;

  title: string;
  subtitle?: string;
  groupLabel: string;

  updatedAt: number;
  messageCount: number;
  status: string;

  runStatus:
    | "idle"
    | "queued"
    | "running"
    | "interrupted"
    | "failed"
    | "timeout"
    | "cancelled";

  // Whether the current Web UI should enable its standard chat composer.
  interactive: boolean;

  channelContext?: {
    name?: string;
    id?: string;
    accountId?: string;
    peerId?: string;
    threadId?: string;
  };

  parent?: {
    key: string;
    taskId?: string;
    spawnDepth?: number;
  } | null;

  cron?: {
    jobId?: string;
    sessionTarget?: "main" | "isolated" | "current" | "session";
    originSessionKey?: string;
    targetSessionKey?: string;
  } | null;
}

Field Semantics

key

公开的 session 路由地址。UI 可以用它来打开、恢复、复制、删除和调试一个 session。UI 绝不能解析它来进行语义分类。

sessionId

可用时的后端 transcript/存储标识。它不是主要的 UI 路由地址。

agentId

遗留或已存储的 agent id。它仍然保留,以兼容现有的 CLI/TUI/MCP 以及较旧的 UI 路径。

effectiveAgentId

应当用于 UI 归属、workspace/路由显示以及 agent 标记的 agent id。它处理了那些已存储 agentId 可能为 main、但路由实际应跟随另一个 agent 的遗留记录。

sessionKind

session 的生命周期分类:

  • chat:面向人的交互式聊天 session,包括 WebChat、CLI、TUI、MCP 以及 main-agent 的聊天 session。
  • channel:外部平台会话 session。
  • task:运行时/后台 task session,尤其是 subagent 工作。
  • cron:由 cron 拥有的隔离运行 session。
  • system:当后端暴露内部/系统 session 时使用。
  • unknown:后端无法对该记录进行分类。

该字段是 Conversations surface 的主要分组依据。如果某个设计草图把这个概念称为 conversationKind: "chat" | "channel" | "cron" | "task",那么该概念在契约中的名称是 sessionKind

surface

产生该 session 视图的入口 surface 或平台。例如:webchatclituimcpfeishuslacktelegramcronsubagent

当前的终端 TUI session 是 CLI 兼容的。它们通常应报告 surface: "cli",因为现有的 TUI gateway 路径创建的是 CLI session 并使用 CLI gateway 客户端契约。只有当未来的 TUI 路径明确把 session 标记为 TUI 所有时,才使用 surface: "tui"

已知的公开 channel surface 与 channel 适配器契约保持一致:slackdiscordfeishudingtalkwecomqqmatrixtelegram。未知或尚未公开的适配器应降级为 surface: "unknown",同时在 channelContext 中保留显示用的元数据。

conversationKind

会话拓扑:

  • main:某个 agent 的 main/私有 session。
  • direct:一对一的 direct/私有会话。
  • group:group、room 或多人会话。
  • channel:平台 channel/广播式会话。
  • unknown:后端无法对拓扑进行分类。

thread 和 topic 信息必须通过 thread 表达,而不是新增 conversationKind: "thread"

thread

用于平台 thread/topic 上下文的可选修饰符。它不替代 conversationKind

title

主要的面向用户的标签。UI 应优先使用它而非原始 key

subtitle

次要上下文,例如来源、agent、channel、parent 或近期上下文。

groupLabel

UI 分组标签。session 选择器和 Sessions 页面的分组应使用此字段,而不是从 key 推导分组。

updatedAt

用于按时间排序和相对时间显示的 Epoch 毫秒数。

messageCount

用于显示的 transcript/消息数量。

status

已持久化的 session 生命周期状态。当前后端取值包括 runningdonefailedkilledtimeout。前端应将其视为一个后端生命周期字符串,并使用 runStatus 来显示 idle/running 的 turn 标记。

runStatus

用于显示当前/最近一次 turn 的运行时 task 状态。这与已持久化的 session 生命周期状态是分开的。

interactive

当前 Web UI 是否应为该记录启用其标准的聊天 composer。这与可见性不是一回事。一条非交互式记录仍可以作为可读条目出现在 Conversations 或 Sessions 中。

默认规则:

  • WebChat 记录应是交互式的。
  • CLI/TUI/MCP 记录是兼容性账本记录;除非后端明确将其标记为交互式,否则不要启用 Web UI composer。
  • channel 记录可从 Web UI 阅读,但除非存在安全的 channel 回复流程,否则标准的 WebChat composer 应保持禁用。
  • cron、subagent、task 和 system 记录通常应为非交互式。

如果 UI 之后需要更细的区分,应新增一个结构化字段(例如 openMode),而不是从 key 推断行为。

channelContext

可选的外部 channel 身份和投递元数据。除非某个功能明确需要路由细节,否则在 UI 中它应仅用于显示。

不要将其与 sessions.list 中已存在的遗留 channel 字段混淆。遗留字段仍保留给 CLI/TUI/较旧的调用方,且可能是一个字符串。新 UI 应使用 surfacechannelContext

parent

可选的 subagent/task parent 关系。

cron

可选的 cron 元数据。cron 元数据不会自动把一个已有的 webchat/channel session 变成 cron session。

error_id

session.event.error 负载上可选的简短引用 id(并会以 (ref: <error_id>) 的形式渲染进面向用户的错误文本中)。它把该错误与其持久化的 turn_errors 记录关联起来,用于诊断。该字段是增量添加的;在较旧的 gateway 上或未写入记录时可能不存在。

Backend Rules

后端负责 session 的分类和显示规范化。

后端在生成时应优先使用显式的结构化数据,再退回到遗留的 key fallback:

  1. session 记录字段,包括 agent id、显示名、channel 字段、投递上下文、parent session key 和来源元数据。
  2. 路由/来源元数据,例如 source kind、channel kind、channel id、thread id 和交互模式。
  3. task 运行时记录,用于 runStatus、subagent/task 状态和 parent 关系。
  4. cron job/session 元数据,用于 cron 拥有的隔离运行以及 cron 投递上下文。
  5. 仅在后端规范化内部作为兼容性 fallback 时才解析遗留 key。

前端绝不能重复后端的 key 解析逻辑。

后端兼容性要求:

  • 保持 sessions.list { limit } 正常工作。
  • 保持 sessions.create -> { key, sessionId } 正常工作。
  • 保持 sessions.resolve({ key }) 正常工作。
  • 保持 chat.history({ sessionKey }) 正常工作。
  • 保持现有记录字段,例如 agent_idagentIdupdated_atupdatedAtmessage_countentry_countsourceKindchannelKind
  • 新增契约字段,但不删除或更改旧的形态。
  • 把现有字段视为兼容性输出,而不是新 Web UI 的权威语义来源。
  • 保持现有 CLI 和当前终端 TUI 的 gateway 契约精简:sessions.create({ kind: "cli" })sessions.list({ limit })sessions.resolve({ key })chat.history({ sessionKey }) 不得新增必填的新参数。

Frontend Rules

前端应基于契约字段进行渲染:

  • 使用 sessionKind 作为 Conversations 的主要分区:Chats、Channels 和 Automations。
  • 使用 groupLabel 进行分区内的二级分组。
  • 使用 title 作为主要文本。
  • 使用 subtitle 作为次要文本。
  • 使用 effectiveAgentId 进行 agent 标记和 agent 归属显示。
  • 使用 messageCount 显示消息数量。
  • 使用 updatedAt 显示相对时间。
  • 使用 runStatus 显示运行时标记。
  • 使用 sessionKindsurfaceconversationKind 决定图标、颜色和高层视觉处理。
  • 使用 interactive 决定是否启用标准的 Web UI composer。
  • threadtopic 视为修饰符,而非独立的会话类型。
  • 仅在 open/resume/copy/delete/RPC/debug 操作中使用 key

如果 UI 需要某个缺失的语义字段,后端应将其加入本契约,而不是由前端从 key 推导。

Sessions 页面规则:

  • 显示 sessions.list 返回的所有记录,包括 WebChat、CLI、channel、channel thread/topic、cron、subagent、task、system 和 unknown 记录。
  • 保持原始 key 可见或便于检查。
  • 相较于日常导航分组,更侧重过滤、删除、状态检查、调试以及 resume/open 操作。

New chat 规则:

  • 只创建 WebChat session。
  • 询问或推断目标 agent,然后调用 WebChat 创建/打开流程。
  • 不创建 cron、channel、subagent、task 或 system 记录。

Forbidden Frontend Behavior

前端绝不能:

  • 解析 session.key 来判断 session kind
  • 解析 session.key 来判断 agent 归属
  • :webchat::cli::subagent::cron::thread::topic: 等 key token 进行分组
  • 从 key 片段推断外部 channel 类型
  • 在显式的 debug/copy 上下文之外,把原始 key 当作主要的面向用户的标签
  • 仅因为 cron 投递进了某个普通的 webchat/channel session,就把它渲染成 cron
  • New chat 用作创建 cron、channel、subagent、task 或 system session 的通用入口

临时性的 fallback 逻辑应被隔离、清晰标注,且不应成为主要的 UI 路径。

Display Examples

WebChat

{
  "key": "agent:main:webchat:default",
  "sessionId": "0d2d6f3e-8a41-40de-a3d4-5f05a3c4557a",
  "agentId": "main",
  "effectiveAgentId": "main",
  "sessionKind": "chat",
  "surface": "webchat",
  "conversationKind": "direct",
  "thread": null,
  "title": "Web chat",
  "subtitle": "main",
  "groupLabel": "Web chat",
  "updatedAt": 1760000000000,
  "messageCount": 42,
  "status": "done",
  "runStatus": "idle",
  "interactive": true,
  "parent": null,
  "cron": null
}

CLI

{
  "key": "agent:main:cli:a1b2c3d4",
  "agentId": "main",
  "effectiveAgentId": "main",
  "sessionKind": "chat",
  "surface": "cli",
  "conversationKind": "main",
  "title": "CLI session",
  "subtitle": "main",
  "groupLabel": "CLI",
  "updatedAt": 1760000000000,
  "messageCount": 12,
  "status": "done",
  "runStatus": "idle",
  "interactive": false
}

Subagent Task

{
  "key": "agent:main:subagent:760b927a",
  "agentId": "main",
  "effectiveAgentId": "main",
  "sessionKind": "task",
  "surface": "subagent",
  "conversationKind": "unknown",
  "title": "Subagent task",
  "subtitle": "Spawned from Web chat",
  "groupLabel": "Subagents",
  "updatedAt": 1760000000000,
  "messageCount": 8,
  "status": "running",
  "runStatus": "running",
  "interactive": false,
  "parent": {
    "key": "agent:main:webchat:default",
    "taskId": "task-123",
    "spawnDepth": 1
  },
  "cron": null
}

Cron-Owned Isolated Run

{
  "key": "cron:daily-summary:run:abc123",
  "effectiveAgentId": "main",
  "sessionKind": "cron",
  "surface": "cron",
  "conversationKind": "unknown",
  "title": "Daily summary",
  "subtitle": "Cron isolated run",
  "groupLabel": "Cron",
  "updatedAt": 1760000000000,
  "messageCount": 4,
  "status": "done",
  "runStatus": "idle",
  "interactive": false,
  "cron": {
    "jobId": "daily-summary",
    "sessionTarget": "isolated"
  }
}

Cron Delivery Into Existing Channel Session

cron 投递元数据可能存在,但已有的 channel session 仍保留其原本的视觉身份。

{
  "key": "agent:main:feishu:group:oc_123",
  "agentId": "main",
  "effectiveAgentId": "main",
  "sessionKind": "channel",
  "surface": "feishu",
  "conversationKind": "group",
  "title": "Launch room",
  "subtitle": "Feishu group",
  "groupLabel": "Feishu",
  "updatedAt": 1760000000000,
  "messageCount": 31,
  "status": "done",
  "runStatus": "idle",
  "interactive": false,
  "channelContext": {
    "name": "feishu",
    "id": "oc_123"
  },
  "cron": {
    "jobId": "launch-check",
    "sessionTarget": "session",
    "targetSessionKey": "agent:main:feishu:group:oc_123"
  }
}

External Channel Thread

{
  "key": "agent:main:slack:group:C123:thread:1717000000.000100",
  "agentId": "main",
  "effectiveAgentId": "main",
  "sessionKind": "channel",
  "surface": "slack",
  "conversationKind": "group",
  "thread": {
    "id": "1717000000.000100",
    "kind": "thread"
  },
  "title": "C123 thread",
  "subtitle": "Slack thread",
  "groupLabel": "Slack",
  "updatedAt": 1760000000000,
  "messageCount": 16,
  "status": "done",
  "runStatus": "idle",
  "interactive": false,
  "channelContext": {
    "name": "slack",
    "id": "C123",
    "threadId": "1717000000.000100"
  }
}

Legacy Agent Mismatch

如果已存储的 agentIdmain,但实际的路由/workspace 所有者是另一个 agent,UI 必须显示 effectiveAgentId

{
  "key": "agent:kid-project:webchat:test",
  "agentId": "main",
  "effectiveAgentId": "kid-project",
  "sessionKind": "chat",
  "surface": "webchat",
  "conversationKind": "direct",
  "title": "Kid project",
  "subtitle": "Web chat",
  "groupLabel": "Web chat",
  "updatedAt": 1760000000000,
  "messageCount": 5,
  "status": "done",
  "runStatus": "idle",
  "interactive": true
}

Backend Test Expectations

后端契约测试至少应覆盖:

  • WebChat session
  • CLI session
  • subagent task session
  • cron 拥有的隔离 session
  • cron 投递进已有 session
  • 外部 channel session,例如 Feishu、Slack 或 Telegram
  • thread/topic 修饰符
  • 已存储 agentIdeffectiveAgentId 不同的遗留记录
  • unknown/fallback 记录,但仍能产生可用的 titlegroupLabelrunStatus
  • 当前终端 TUI 记录保持 CLI 兼容,除非未来的 TUI 路径明确将其标记为 TUI 所有
  • WebChat、CLI/TUI、channel、cron、subagent、task 和 system 记录的 interactive 默认值

Frontend Acceptance Criteria

  • Conversations 侧边栏先按 sessionKind 分组,再按 groupLabel 分组。
  • 聊天 session 选择器根据契约显示 titlesubtitlegroupLabeleffectiveAgentId 以及 interactive 行为。
  • Sessions 页面显示 titlesubtitleeffectiveAgentIdmessageCountupdatedAtrunStatus 以及原始 key/debug 操作入口。
  • 没有任何新的前端逻辑为了语义分类而解析 session.key
  • 现有的 open/resume/copy/delete 行为仍使用 key
  • WebChat、CLI、subagent、cron 和外部 channel session 均基于契约字段渲染。
  • New chat 只创建 WebChat session。
  • unknown 或缺失的值能够优雅降级。
  • fallback 逻辑不会重新把 key 解析变成主要路径。
在 GitHub 上编辑此页(英文原稿) OpenSquilla 文档 · 中文社区翻译