架构总览 · 4 层模型 + 3 大支柱
4 层抽象
| 层级 | 名称 | 职责 | 面向人群 |
|---|---|---|---|
| L1 | Agents | 对话循环、规划、Tool 调用编排(Copilot SDK 单一 Fabric Agent + 插件作为 sub-agent) | End User / Workload PM |
| L2 | Skills(Markdown) | 声明式 prompt + tool 组合,教 Copilot 怎样为 workload 写出正确代码(CDN 加载) | Workload Team / Domain SME |
| L3 | MCP Servers | 无状态工具端点(CRUD / Execute / Observe),让 Copilot 创建资源、执行作业、读取状态 | Workload Team |
| L4 | REST APIs | Fabric 平台既有 API;由 MCP servers 或生成代码间接调用 | 已存在 |
📌 心智模型:Skills 教 Copilot 该怎么做;MCP servers 让 Copilot 真的去做并看到结果。两者协同实现 "生成 notebook → 跑 → 检查结果 → 修错 → 进入下一任务" 单会话连贯。
3 大实施支柱
支柱 1 ADC (Agent Dev Compute, 即 Azure Dev Compute — 对外产品名 Azure Container Apps Sandboxes,Microsoft.App/sandboxGroups) — Firecracker microVM 沙箱,Envoy egress proxy,secret 注入,snapshot/restore。
支柱 2 OneLake Copilot Environment — 会话状态、artifact、审批快照统一存放于 workspace 级 OneLake 文件夹。
支柱 3 GitHub Copilot SDK — Agent 核心引擎,复用 CLI 验证过的 reasoning loop / tool calling / 审批模型。
ASCII 架构图
End-to-End 主流程
关键心智:没有独立的 Create Session API;session 在用户发出第一条消息时被 lazy 创建。Browser 从不直接调容器,所有消息走 Browser → MWC → Container。Browser 用 userEntraToken 调 Shared(CE artifact CRUD / Models),用 mwcToken 调 MWC(messaging / tools / attachments);详见 Q18 / Q20 / Q21。
- 用户第一条消息(lazy session 创建) — Browser 已选定一个 Copilot Environment artifact(CE)作为 session 容器。Browser 发
POST {mwcBase}/public/workspaces/{wsId}/CopilotEnvironments/{ceId}/sessions/messages(targetSession:"new"+ 客户端生成的draftSessionId)→ MWC ① 先 write-aheadmetadata.json到 OneLake → ② ADCPUT /sandboxes拿到sandboxId + containerUrl→ ③ ADCPUT /secrets存 OBO token(SecretRef)→ ④ ContainerPOST /configure下发refreshTokensAfterUtc+tokenExpirations。SSE 流先推session.provisioning,sandbox 就绪后推session.created { sessionId, draftSessionId }。 - 容器启动 + skills 加载 — Container 内的
SkillsLoader调 Fabric Shared APIGET /skills/manifest,按返回的 CDN URL 拉 SKILL.md 包并拼到 system prompt;MCP server 端点在配置就绪后由McpServerRegistry注册并 health check(详见 Q19)。 - 消息转发(SSE 长连) — MWC 内部把这条消息发给容器:
POST {containerUrl}/responses(用copilotWorkload1pToken,audience:fabric/<container-id>)→ 容器流式回 SSE 给 MWC,MWC 透传给 Browser。 - Tool 调用 / 外部出站 — SDK 决定调 MCP / bash;所有出站走 Envoy egress sidecar,按
egressRules+secretRef在 TLS MITM 后注入Authorization: Bearer,容器进程永远看不到明文 token(详见 Q11/Q18)。 - 审批 / 用户输入 — 高风险动作发
permission.requestSSE,前端弹审批卡;用户回POST /permissions/{id}/respond,流恢复。 - 每轮持久化(热路径) — 每条消息:① 容器把 events 追加到 InMemorySessionFs(纯内存 ConcurrentDictionary,不落盘);② MWC 只更新 OneLake
state.json两个字段(messagesCount+lastInteractionAt);events.jsonl此时不落 OneLake。 - 状态机驱动的 Export(冷路径) — MWC sweeper 检测 Idle ≥ 30min / WaitingFor* ≥ 2h → ①
POST {containerUrl}/export让容器把 InMemorySessionFs 整段 base64 dump 返回 → ② MWC 用 Fabric workload identity PUT 到 OneLakesessions/<id>/→ ③ ADCPOST /sandboxes/{id}/stop(Memory-mode snapshot)。 - 恢复 — 用户回来发新消息 → MWC
POST /sandboxes/{id}/resume(hot path ~1s);若 snapshot 不可用 → Cold restore:PUT /sandboxes新建 + ContainerPOST /import从 OneLake 回放 events.jsonl(详见 Q15/Q16)。
为什么要使用沙箱(Agent 宿主)
根本原因:Agent 的 reasoning loop 是不可信代码的执行者。LLM 会生成并执行任意 bash / Python / az CLI,把它放进 Fabric 主进程等于把 root shell 暴露给模型。
沙箱解决的 4 件事
| 需求 | 沙箱手段 |
|---|---|
| 隔离 | Firecracker microVM — 硬件级 KVM,比容器更强;每会话独立内核。 |
| 凭证保护 | 容器内永远拿不到明文 token;Envoy 在 TLS MITM 后按 egress rule 注入 Authorization: Bearer。 |
| 出站管控 | egress allow-list(白名单域名)+ 全部经 Envoy,0.0.0.0/0 默认 deny。 |
| 生命周期 | 支持 suspend / snapshot / restore;OTel 全程审计。 |
📄 决策依据:engineering/000-decisions/002-adc-vs-foundry-vs-aci.md(为什么选 ADC 而非 AI Foundry / ACI)。
Agent 框架的细节
技术栈
- 运行时:ASP.NET Core (.NET 8)
- 核心引擎:GitHub Copilot SDK(与 CLI 共享 reasoning loop)
- Skills:Markdown 文件,运行时从 CDN 拉取并热加载
- MCP:Workload 自托管 HTTPS endpoint,由容器在
/configure时注册
3 种工作模式
| 模式 | 行为 | 审批 |
|---|---|---|
| Interactive | 每个 tool call 都问 | 逐项审批 |
| Plan | 先生成计划,用户改/确认后再执行 | 计划级 + 关键步 |
| Autopilot | 全自动执行(受 egress / approval policy 约束) | 仅 high-risk 弹窗 |
关键类
Program— Kestrel 启动,注册 DICopilotAgentService— 顶层服务,把 HTTP 请求翻译为 SDK 调用CopilotClient— 包装 GH Copilot SDKCopilotSession— 内存中会话状态机InMemorySessionFs— 容器内虚拟文件系统(agent-state / session-state 双层)SkillsLoader— 从 CDN 拉 SKILL.md,本地缓存McpServerRegistry— 注册 / 健康检查 MCP server
HTTP 端点(容器对外)
| 端点 | 类型 | 用途 |
|---|---|---|
GET /health | System | liveness |
POST /configure | System | 下发 refreshTokensAfterUtc + tokenExpirations;token 刷新统一走这里 |
POST /export | System | 把 InMemorySessionFs 整段 base64 dump 给 MWC |
POST /import | System | 从 OneLake export 预填 VFS(cold restore) |
POST /responses | User | 主入口,SSE 长连 |
POST /approve | User | 响应 permission.request |
POST /user-input | User | 响应 user_input.request |
GET /messages | User | 拉取历史(user/assistant 过滤) |
⚠️ 静态身份参数(userId / tenantId / containerId)在 PUT /sandboxes 时通过 environment 字段注入,不走 /configure(因为 env 不可变后置 PATCH)。/configure 只承载随时间变化的动态值。
容器内文件系统布局
/app/ # 镜像内置(agent binaries)
/agent-state/ # 长期 agent 配置(mounted)
├── skills-cache/
└── mcp-registry.json
/session-state/ # 当前 session 专属(mount or in-memory)
├── messages.jsonl
├── state.json
└── artifacts/
Shared Service 不在消息路径上 是什么意思
"消息路径" = 用户 → AI → SSE → 用户 这条 高频低延迟 数据通路。
Shared 做什么 / 不做什么
| Shared 做 | Shared 不做 | |
|---|---|---|
| 职责 | Artifact 表 CRUD Copilot Item 注册 跨 capacity 元数据 | 消息中转 SSE 推流 Token 注入 |
| 调用频率 | 会话创建/重命名/删除时一次 | — |
| 延迟敏感 | 否 | — |
真正消息路径
📄 依据:engineering/010-architecture/011-sessions-responsibilities.md 明确把 Shared 限定在 "artifact registry"。
Copilot workload 与其他 workload(如 Notebook)的区别 / 与 Shared 的关系
三方对照
| Copilot Workload (MWC) | Notebook Workload (MWC) | Shared Service | |
|---|---|---|---|
| 部署 | per-capacity | per-capacity | 全球单实例 |
| 职责 | 会话编排 / SSE / token / ADC 调用 | Notebook 执行 / Kernel 管理 | Artifact 注册 |
| 是否在消息路径 | 是 | 是(自己消息路径) | 否 |
| 是否使用 ADC | 是(容器宿主) | 否(用自己 compute) | 否 |
| 暴露 MCP | — | 给 Copilot 用 | — |
关系图
关键:Copilot workload 把 Notebook workload 当作"MCP Server 提供者"消费;两者通过 Shared 协调 artifact 关系。
SandboxGroup 是什么意思
- ARM 资源类型:
Microsoft.App/sandboxGroups - 语义:ADC 的 "区域级组织单元",对应一个 VNet 子网、一组 quota 和一套默认 egress policy
- 用途:每个 region × tenant 的 Copilot capacity 注册一个 SandboxGroup;所有 microVM 都注入到该 group 的 VNet
- 类比:相当于 ACI 的 "container group",但语义在网络隔离 + 配额层面
📄 依据:engineering/100-solution-deployment/100-solution-deployment.md 第 5–30 行的资源拓扑。
ADC 全称 / 平台定位
⚡ 权威结论:ADC = Azure Dev Compute("Dev" 不是 "Developer")。Microsoft 内部存在两套指代同一底座的命名:
• 仍在使用的基础设施名:Azure Dev Compute(host azuredevcompute.io、Entra app "Azure Dev Compute Proxy"、Microsoft GitHub org github.com/Azure-Dev-Compute);
• 公开产品名:Azure Container Apps Sandboxes(Early Access → Public Preview,ARM 类型 Microsoft.App/SandboxGroups)。
最新公开 RBAC 已改名为 Container Apps SandboxGroup Data Owner(取代旧名 Dev Compute SandboxGroup Data Owner)。
名词
- 缩写
- ADC
- 团队内部叫法
- Agent Dev Compute(Fabric AI Framework 决策文档里的写法,002-adc-vs-foundry-vs-aci.md)
- Azure 平台官方叫法
- Azure Dev Compute(注意是 Dev,不是 Developer) — 体现在 API host
https://management.azuredevcompute.io、Entra appAzure Dev Compute Proxy、GitHub orggithub.com/Azure-Dev-Compute - 对外产品名(已发布)
- Azure Container Apps Sandboxes(ARM 资源
Microsoft.App/SandboxGroups),Early Access(联系 MS 销售开通;与 Container Apps Dynamic Sessions 是兄弟资源) - RBAC 角色名
- 旧(Fabric AI repo 里):
Dev Compute SandboxGroup Data Owner→ 新(公开 docs):Container Apps SandboxGroup Data Owner - 本质
- 基于 Firecracker microVM 的多租户、安全沙箱平台 + Envoy egress proxy + KeyVault 集成;目标场景:安全执行 LLM 生成 / 不可信代码、可挂起的 dev 环境、可拓展的 agent runtime、CI/CD 一次性构建环境
- 面向
- 所有需要"安全执行不可信 / LLM 生成代码"的内部产品(Copilot 系列、Agent 平台等共用),外部企业客户通过 Container Apps Sandboxes 接入
"Dev" 不是 "Developer" — 命名族对照
| 命名族 | 语义偏向 | 例子 |
|---|---|---|
Dev 系(基础设施) | 面向工程基础设施、平台层 | Azure Dev Compute、Microsoft Dev Box、Azure DevOps、Azure DevTest Labs、Azure Dev Tunnels |
Developer 系(开发者工具门面) | 面向开发者的高层工具 / CLI | Azure Developer CLI (azd)、Azure Developer Hub |
ADC 属于前者—— 它是 infrastructure-level microVM 平台,跟 Dev Box / DevOps 是同族;不是 Developer CLI 那类前端工具。
📘 公开资料:
- microsoft/azure-container-apps · docs/early/sandboxes-overview.md — Container Apps Sandboxes 概览。原文:"Sandboxes are a first-class resource type (
Microsoft.App/SandboxGroups) in Container Apps, alongside apps, jobs, and dynamic sessions";能力:sub-second 启动、强隔离、scale-to-zero、bring-your-own OCI image、suspend/resume(memory + disk) - github.com/Azure-Dev-Compute — Microsoft 内部 GitHub 组织(无公开 repo,但组织名确认 "Dev" 写法)
- Azure RBAC 操作清单 — Microsoft.App/sandboxGroups
- CLI 示例(公开预览阶段,可能变动):
az role assignment create --role "Container Apps SandboxGroup Data Owner" --scope …
azuredevcompute.io 上没有独立 learn.microsoft.com 页面。Fabric AI Framework 用 "Internal Flow"(直连 management.azuredevcompute.io + Managed Identity),企业客户走 "ARM Flow"(建 Microsoft.App/SandboxGroups ARM 资源后通过 ARM scope 间接 data-plane)。
📝 Microsoft 内部 wiki(eng.ms)只对 MS 员工开放,需要凭企业凭证访问 https://eng.ms/ 搜索 "Azure Dev Compute"。如果你能进 eng.ms,会看到与本文档一致的命名。
ADC 提供什么(4 类 ARM 资源)
| 资源类型 | 语义 |
|---|---|
Microsoft.App/sandboxGroups | 区域 × 租户级组织单元(VNet 子网 + 配额 + 默认 egress policy);详见 Q5 |
| sandboxes(在 sandboxGroup 下) | 单个 Firecracker microVM 实例,承载一个 agent 容器 |
| secrets | KeyVault 集成;以 secretRef 在 egress 规则里引用,容器永远看不到明文 |
| diskimages | 容器镜像注册(CI/CD 阶段,按 cluster 缓存;可 Dockerfile 编译或指 OCI registry) |
ADC 与 Fabric AI Framework 的关系
为什么是 ADC,不是别的
| 选项 | 结论 | 原因 |
|---|---|---|
| ADC(已选) | ✅ | Firecracker microVM = 硬件级隔离;自带 egress proxy + secret 注入;MS 内部一等公民 |
| AI Foundry | ❌ | 偏 training / fine-tuning,不是 agent runtime;没有 egress proxy 模型 |
| ACI | ❌ | 普通容器,无 microVM 隔离;自己得做 secret / egress / snapshot |
| AKS / 自建 | ❌ | 运维负担巨大;安全模型要从零搭 |
📄 依据:engineering/000-decisions/002-adc-vs-foundry-vs-aci.md(决策记录)+ engineering/.education/adc-apis-reference.md(ADC API 全表)+ engineering/.education/adc-sandbox-lifecycle.md(平台层 lifecycle 机制)。
workspace 里的 session 存储是用户独享还是 workspace 共享?是不是 singleton?
一句话结论
不是 singleton。当前设计(engineering/010-architecture/012-csm-management.md Ready-to-Review):每个 workspace 可以有 多个 CopilotEnvironment artifact(旧名称:CSM / Copilot Sessions Manager)。用户用 Copilot 之前要先选一个 CE,或者新建一个。一个 CE artifact 里可以放多个 session;artifact 在共享 workspace 里时,session 内容默认按用户隔离(私有 sessions in shared artifacts)。
历史背景:旧 .copilot singleton 设计已被取代
specs/building-blocks/copilot-item.md 旧设计是 "每 workspace 一个 .copilot item,自动创建,409 防重复"。这个设计在 engineering/ 目录被取代:
- 用户在 my-workspace 不一定有 capacity,不一定能放 artifact —— singleton 模型挡住了私有 session 路径(006-artifact-model.md)
- 团队 / 项目隔离需要 —— 想分多套 session 做不同事,singleton 撑不开
- CMK / 网络策略按 workspace 走,一个 workspace 多个 CE 让用户能挑合适的"宿主"
CE artifact 的本质
| 属性 | 值 |
|---|---|
| artifact type | CopilotEnvironment(URL 段 CopilotEnvironments) |
| 创建 | 用户主动:POST /v1/workspaces/{wsId}/items with {type: "CopilotEnvironment", displayName, definition} |
| 每 workspace 数量 | 多个,按需要建 |
| 存储 | OneLake 文件夹(artifact 自带),路径 {workspace}/{CE-artifact}/sessions/<sessionId>/ |
| 权限 | workspace Contributor+ 可建;artifact 共享后他人可访问,但 session 默认私有(详见 006-B Option 2) |
| session 数量 | 一个 CE 可容纳多个 sessions(同用户跨任务、或多用户私有) |
OneLake 目录示意
workspace-XYZ/
├── CopilotEnv-Team-Alpha/ # CE artifact #1(共享 in 该 workspace)
│ ├── indexes/
│ │ ├── owned/<tenantOid>/<aliceOid>/<sessionId>.json # 空文件 = 指针
│ │ └── owned/<tenantOid>/<bobOid>/<sessionId>.json
│ └── sessions/
│ ├── <Alice 的 session>/ # Alice 私有
│ └── <Bob 的 session>/ # Bob 私有
└── CopilotEnv-Personal-Sandbox/ # CE artifact #2(同 workspace 多个 CE)
└── sessions/...
📌 心智:CE artifact 是 "装 session 的盒子",workspace 里盒子可多个;每个盒子有自己的 OneLake 文件夹、CMK 配置、共享策略。新建 session 之前必须先选定 / 创建一个盒子,详见 Q20。
📄 依据:engineering/010-architecture/012-csm-management.md(CE artifact CRUD + generatemwctoken)+ 013-sessions-store.md(目录结构)+ 004-session-storage.md(Option A: artifact + OneLake)+ 006-artifact-model.md(Shared Artifact, Isolated Sessions 决策)。
sessionId / sandboxId / containerUrl 三者关系
| ID | 谁分配 | 生命周期 | 形态 |
|---|---|---|---|
fabricSessionId | MWC 创建会话时生成 | 永久(写入 OneLake 文件夹名) | GUID |
sandboxId | ADC PUT /sandboxes 返回 | 会话活跃期间(idle 后可销毁/重建) | ADC 内部 ID |
containerUrl | ADC 同时返回 | 跟随 sandbox | https://{sandboxId}--{port}.proxy.azuredevcompute.io |
关系
1:N 时间维度:一个 fabricSessionId 在生命期内可能对应多个 sandboxId(每次 restore 是新 microVM)。同一时刻 1:1:1。
📄 依据:engineering/.education/adc-apis-reference.md 第 244 行附近的响应字段说明。
sandbox 和用户的关系
当前实现
per-user-per-session 一个用户的一个 session 拥有一个独立 sandbox。
设计权衡(Open Question)
| 选项 | 优点 | 缺点 |
|---|---|---|
| 每 session 1 sandbox(当前) | 会话间彻底隔离 snapshot 简单 | 冷启动多 资源密集 |
| 每用户 1 sandbox(未来候选) | 用户切换 session 时秒级 资源利用率高 | 跨 session 状态污染风险 退出策略复杂 |
📄 文档里方向不一:100-solution-deployment.md 第 188–191 行问答倾向 per-user;copilot-service.md 实现描述 per-session。
会话存储用的 OneLake 是 workspace 级还是 capacity 级?
层级关系
| 维度 | 归属层级 |
|---|---|
| OneLake namespace 边界 | Workspace(每个 workspace 一个顶层文件夹) |
| 会话文件物理位置 | Workspace 文件夹下 <CopilotEnvironment-artifact>/sessions/<sid>/ |
| CMK / 加密配置 | Capacity 级(CE artifact 继承 workspace → capacity CMK) |
| 计费 / 配额 | Capacity 级 |
| 访问 ACL | Workspace(继承 Fabric 权限模型)+ CE artifact 权限 + session 路径中 userObjectId 隔离 |
结论:OneLake 物理是 tenant-level 服务,但用户感知边界是 workspace;session 数据进一步嵌在 CopilotEnvironment artifact 文件夹里,按 CE 隔离 + 按 user 在 CE 内隔离;配额加密按 capacity 计。详见 Q20 关于 CE artifact 的完整说明。
SecretRef 的精髓 — 用一个完整例子说明
场景
Alice 在 Fabric Copilot 中要求 "把这个 dataset 同步到我的 GitHub repo"。Copilot 需要用 Alice 的 GitHub PAT 调 GitHub API,但这个 PAT 绝不能进容器。
四步
Step 1 — MWC 把 token 存进 KeyVault
PUT https://adc.azure.com/secrets/alice-gh-pat
Authorization: Bearer <mwc-managed-identity>
Content-Type: application/json
{
"value": "ghp_xxxxxxxxxxxxxxxxxxxxxxx",
"scope": "session:abc-123"
}
返回:{ "secretId": "alice-gh-pat", "kvRef": "https://kv-adc-eus.vault.azure.net/secrets/alice-gh-pat/v1" }
Step 2 — ADC egress 规则仅引用 secretRef,不存明文
{
"egressRules": [
{
"host": "api.github.com",
"method": "*",
"inject": {
"header": "Authorization",
"valueTemplate": "Bearer {value}",
"secretRef": "alice-gh-pat" ← 只存 ID
}
}
]
}
Step 3 — Envoy 在 TLS MITM 后按规则注入 header
# 容器内进程发出(裸 HTTP,无 Authorization 头)
curl https://api.github.com/repos/alice/foo/contents/x.csv
# Envoy 在出站时:
# 1. TLS terminate(用自签 CA)
# 2. match egressRule host=api.github.com
# 3. 从 KeyVault 取 secretRef=alice-gh-pat
# 4. 注入 Authorization: Bearer ghp_xxxx
# 5. 重新 TLS encrypt 发往 GitHub
Step 4 — 容器永远看不到 token
精髓总结
- MWC
PUT /secrets/{secretId}把 token 存进 KeyVault - ADC egress 规则只引用
secretRef,不存明文 - Envoy proxy 在 TLS MITM 后,按规则网络层注入
Authorization: Bearer {value} - Container 内发的是裸 HTTP,永远看不到 token
📄 依据:engineering/000-decisions/003-A-proxy-vs-noproxy.md(含完整 JSON 示例)。
UX 怎么实现 / 主要用什么 package / 为什么这么选
仓库现状
仓库里 只有 React 原型(Prototypes/UnifiedAIPlatform/react-prototype/),生产 UX 尚未落地。
原型技术栈
| 分类 | 选择 | 为什么 |
|---|---|---|
| UI 组件库 | Fluent UI v9 | 对齐 Fabric 门户视觉 |
| Icon | @fabric-msft/fabric-svg-icons | Fabric 官方图标包 |
| 原始组件 | Radix UI(~25 包) | 无障碍 + 可定制 primitives |
| 样式 | Tailwind CSS v4 | 原子化,快速迭代 |
| 框架 | React 18/19 | 主流 |
| 构建 | Webpack 5 + ts-loader | 原型期够用 |
真正落地架构(规划)
- Fabric Portal:Angular Shell + 嵌入 React Chat Pane(通过
createRoot桥接) - 核心服务:
CopilotHostService(Angular)— context 合并、tool 调度、SSE 处理、token 缓存 / 401 重试CopilotHttpService(Angular)— Shared API(CE / Models)+ MWC API(Sessions / Messaging)的 HTTP/SSE 客户端CopilotPaneApi(facade)— React 端只持有这个 facade,对后端调用完全透明(详见 Q21)
- React Chat Pane 组件库:基于
@fabric-msft/copilot-react(Client Engineering Systems 提供的成熟、可复用 Copilot 组件包,对齐 Microsoft Copilot Design 规范)
UX 是否直接复用 GitHub Copilot / VS Code Copilot 的 UX?
一句话结论
分 surface 答:CLI 100% 复用、VS Code 100% 复用、Fabric Portal 不复用(自建 Angular Shell + React Chat Pane)。三者共享 同一 SDK + 同一套 Skills/MCP,区别只在 UI 这层"皮"。
1. 三个 surface 的复用矩阵
| Surface | UX 是否复用 | 对应 Decision | 实现路径 |
|---|---|---|---|
| CLI | ✅ 100% 复用 | D-3 "CLI First" | 直接用 gh copilot |
| VS Code | ✅ 100% 复用 | D-6 "SDK is core, surfaces are hosters" | GitHub Copilot 扩展 + Custom Skills |
| Fabric Portal | ❌ 不复用 | D-7 "no continuity between VS Code and Fabric" | 自建 Angular Shell + React Chat Pane |
2. CLI Surface — 如何"复用"
本质:把 GH Copilot CLI 当成现成 agent runner,Fabric 只贡献 skills 包 + 远端 MCP endpoint,零行 UI 代码。
# 用户唯一要做的三步
gh copilot skills install gim-home/skills-for-fabric@latest # 装 Fabric skill 包
gh copilot skills list # 验证装好了
gh copilot # 进 CLI 对话
- 没有 Fabric 自己的 CLI 二进制 — 用户用的就是 GitHub Copilot CLI
- Fabric team 维护一个 GitHub repo
gim-home/skills-for-fabric,按 GH Copilot 的 skill 规范打包 - Skills 通过 GitHub releases 分发("distributed as code, not as cloud services")
- MCP server 部分:CLI 调远端的 Fabric MCP(HTTPS endpoint)
3. VS Code Surface — 如何"复用"
同样是 把 GitHub Copilot 扩展当宿主,Fabric 注入 skills,不写 VS Code extension。
- 用户装
GitHub Copilot扩展(如未装) - 命令面板
Ctrl+Shift+P→Copilot: Install Custom Skills - 输入 repo
gim-home/skills-for-fabric - Skills 出现在 Copilot skill picker 中
顺带白拿一个好处:VS Code 的 GH Copilot 扩展本身就和 CLI 共享 session 历史(Cristian 在 02/24 会议上演示过:在 CLI 里开的对话能继续在 VS Code 里聊)。所以 CLI ↔ VS Code 跨 surface 的连续性是 GitHub 自带的,Fabric 不用做。
但 Fabric MVP 仅承诺 P0 的 Local-Surface 能力(见 local-agent-dev.md Out of scope):
- ❌ 不写原生 VS Code extension
- ❌ 本地不存 session(ephemeral)
- ❌ MCP tools 本地不可用(只能直接 REST)
- ❌ 没有 RBAC 强制(假设用户自己有权限)
4. Fabric Portal — 为什么不复用
- 技术栈不兼容 — Portal 是 Angular Shell;VS Code Chat 是 Electron + webview + 大量 VSCode-API。两者根本嵌不进去
- 视觉一致性 — Portal 全员 Fluent UI v9,VS Code 用自有 token 体系,强行嵌会两张皮
- 必须渲染 Fabric artifact — Notebook 卡 / Lakehouse table / Dataset preview / Pipeline 图必须用 Portal 已有的 renderer,VS Code Chat 渲染不了
- 必须与已有 3 个 Copilot 共存(tri-copilot / immersive-copilot / workspace-copilot),新框架"同时存在并逐步升级",不能直接搬另一个产品的容器(见
300-frontend-dev-design.md §3.2 Non-Goals) - D-7:用户心智不同 — Cristian: "I don't think you will start in VS Code and continue in Fabric…" Portal 围绕"我的 Fabric 资产",VS Code 围绕"我的代码",不连贯
5. Portal 自己选了什么方案?
方案:Angular Shell 嵌一个 React Chat Pane,靠 Bridge 通信。
┌──────────── Fabric Shell (Angular) ────────────────┐
│ │
│ Shared Experience / Classic / Extension Artifact │ ← 各 Page 提供 context + tool
│ │ │
│ ▼ │
│ ┌─── CopilotHostService (Angular) ────────────┐ │ ← 全部脏活在这层
│ │ · context merge · client-tool dispatch │ │
│ │ · SSE processing · token 缓存 / 401 重试 │ │
│ │ · HTTP: Shared API (CSM/Models) │ │
│ │ · HTTP/SSE: MWC API (Messaging/Tools) │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ CopilotPaneApi facade │
│ ┌── CopilotHostComponent (Angular) ─────────┐ │
│ │ ▼ createRoot() │ │
│ │ ┌────── Chat Pane (React + Fluent v9) ──┐│ │ ← 纯 UI
│ │ │ Header · New Session / Sessions ││ │
│ │ │ Messages· User/Assistant/Tool/Approval││ │
│ │ │ Input · Pills/Attach/Model/Mode/Send││ │
│ │ └───────────────────────────────────────┘│ │
│ └────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘
6. 关键设计权衡
| 问题 | 选择 | 原因 |
|---|---|---|
| Shell 框架 | Angular | Portal 整体技术栈一致;只能用 Angular 接 Shell 的 routing / extension API |
| Chat Pane 框架 | React + Fluent UI v9 | 更适合 streaming/SSE UI;社区 SDK 范式(GH Copilot Chat 也是 React);将来易于摘出 |
| State 管理 | useState / useReducer / Context | 不引入 Redux,state 限定在 Chat Pane 内 |
| HTTP/SSE 谁管 | Angular 层 | token 缓存、refresh、401 重试都集中在 CopilotHostService;React 从不直接调后端 |
| tool.call 怎么进 React | Angular 拦截 → 合成 client_tool.start/complete | React 永远只看到 UI 事件,tool 调度对它透明 |
| Tools 多源 | shell / classic artifact 直调;extension artifact 走 postMessage | extension iframe 隔离,必须用消息桥 |
7. 真正"跨 surface 复用"的东西
UI 三种皮各做各的,但下游全部共享:
- GitHub Copilot SDK — 同一个 agent runtime
- Skills 仓库 — CLI / VS Code / Portal 都装同一份
gim-home/skills-for-fabric - MCP servers — Fabric workload 团队部署一次,三种 surface 都能调(D-9: MCP 只是 stateless execution layer)
- Model 池 — D-8 curated SaaS 模型集
这就是 D-6 的核心:SDK is the core product; UX is just a hoster.
📄 依据:decisions.md(D-3 CLI First / D-6 SDK as core / D-7 No VS Code↔Fabric continuity)+ engineering/300-frontend/300-!!-frontend-dev-design.md(Angular Shell + React Chat Pane 架构、CopilotHostService、CopilotPaneApi)+ specs/stages/local-agent-dev.md(CLI/VS Code 安装命令、scope)+ meetings/2026-02-24 Meeting with Cristian Petculescu.md(SDK as hoster、disjoint session 原文)。
Notebook workload 想 customize 它特有的功能,工作大致是?
平台提供 5 种自助贡献资产,Notebook team 不需要平台代码改动即可上线。
5 种贡献类型
| 类型 | 形态 | 部署 | 典型用途 |
|---|---|---|---|
| ① SKILL.md | Markdown 文件 + prompt 片段 | 提交到仓库 → CDN | "如何写 Spark 调优 notebook" |
| ② MCP Server | HTTPS 服务(自托管) | Workload team 自己部署 | 调 Notebook API(run cell / get output) |
| ③ Frontend Tool | MCP-UI 组件包(F13) | 提交 → CDN bundle | 对话里渲染 notebook 单元格 preview |
| ④ Client-Side Tool | 调 Portal Extension API | Portal extension 已存在 | 跳转到 Notebook 编辑器 |
| ⑤ Evaluation Set | ≥50 个测试用例,4 类 | 提交到仓库 | 跑 CI / 保证 ≥95% 通过率 |
典型工作流(举例:让 Copilot 能 "解读 notebook 错误")
- SKILL.md 写一段:"当用户问 notebook 出错时,先调 mcp.notebook.get_last_error,再调 mcp.notebook.explain_traceback…"
- MCP Server 在 Notebook workload 内新增两个 endpoint:
get_last_error/explain_traceback - (可选)Frontend Tool 渲染 "Stack Trace 卡片",高亮某行代码
- Evaluation Set 加 ≥50 个真实出错 notebook 样例,断言 Copilot 输出正确分析
- 提 PR → CI 自动跑 eval → 自动部署到 CDN 和 MCP registry
SLA
- < 2 个工作日 从 PR 合并到生产可用
- 仅需平台 team 介入的 3 类例外:
- 新增 native tool(SDK 内置工具)
- 修改容器镜像 / Agent 基础进程
- 新增 "全局 building block"(横切多个 workload)
会话内容是如何写到 OneLake 里的?
这个问题分两层:写什么 / 谁写 / 何时写。设计的核心矛盾是 — 持久性要够(崩了不能丢对话),但热路径(每条消息)的写开销必须最小。
① 大原则:分两类,分别写
| 类型 | 内容 | 写入频率 | 策略 |
|---|---|---|---|
| 轻量元数据 | sandboxId、messagesCount、lastInteractionAt、model、displayName | 每条消息 / 偶尔 | 同步直写 OneLake JSON(小文件) |
| 重型会话内容 | events.jsonl(完整对话历史)、agent 创建的产物文件 | 暂停 / 检查点 / 显式 export | 容器内存 VFS + 定期 export 整段 dump 到 OneLake |
② 数据路径:Container 不直接写,MWC 统一持久化
关键设计:GitHub Copilot SDK 通过 ISessionFsHandler 接口把 events.jsonl 写到容器的 InMemorySessionFs(ConcurrentDictionary,纯内存),不落盘。Container 没有 OneLake 写凭据;所有 OneLake 写都由 MWC 发起。
三步明确:
- MWC 发起
POST {containerUrl}/export拉数据 - Container 把 InMemorySessionFs 字典里所有文件 base64 编码后塞进 JSON 响应给 MWC
- MWC 用自己的 Fabric workload identity 把数据 PUT 到
onelake.dfs.fabric.microsoft.com
MWC 什么时候发 POST /export?
不是周期 export,而是状态机驱动 + 显式请求(依据 014-D §8 Suspension Flow):
| 触发源 | 条件 | 行为 |
|---|---|---|
| MWC Sweeper(主因) | interactiveState=Idle 持续 ≥ idleThreshold(默认 30 分钟) | 转 Suspending → POST /export → 写 OneLake → ADC POST /sandboxes/{id}/stop |
| MWC Sweeper | WaitingForInput / WaitingForApproval ≥ waitThreshold(默认 2 小时) | 同上 |
| MWC Sweeper | Error 持续 ≥ errorThreshold(默认 15 分钟) | Export + Terminate(彻底删 sandbox) |
| 用户显式 | 调用 POST /sessions/{id}/export | Export 到 OneLake,不暂停 sandbox(备份/迁移用) |
| 运维迁移 | tenant 迁移 / 容量迁移 | Export → 在目标侧 Import |
⚠️ Running 状态永远不 suspend / 不 export — 不打断 agent 思考链 | ||
为什么 export 后还要 ADC Memory snapshot?—— ADC memory snapshot 是热路径(恢复 ~1s,进程状态完整保留),OneLake export 是冷路径兜底(source of truth)。当宿主机维护或容量问题让 memory snapshot 失败时,MWC 建新 sandbox + 从 OneLake import 重放 events.jsonl,慢但保证不丢。
③ OneLake 目录布局
<workspace>/<copilot-environment-artifact>/
├── indexes/
│ ├── owned/<tenantOid>/<userOid>/<sessionId>.json # 空文件,文件名即指针
│ └── shared/<tenantOid>/<userOid>/<sessionId>.json # {sharedBy, sharedAt, permissions}
└── sessions/
└── <fabricSessionId>/
├── metadata.json # 写一次永不动 {agent, createdAt}
├── settings.json # 偶尔写 (ETag) {model, displayName, modelConnectionId}
├── state.json # 每消息写 {sandboxId, lastInteractionAt, messagesCount}
├── session-state/
│ └── events.jsonl # export 时写入 (完整对话)
└── files/ # agent 创建的产物文件 (export 时打包)
④ 写入时机(每个事件对应哪些文件)
| 事件 | OneLake 写入 |
|---|---|
| Create Session(首消息触发) | ① Write-ahead:先写 metadata.json(sandboxState=Provisioning)② 建 sandbox 后写 state.json(含 sandboxId)③ 写索引指针 indexes/owned/.../sessionId.json |
| Send Message(热路径) | 只更新 state.json(messagesCount + lastInteractionAt,2 字段)⚠️ events.jsonl 不落 OneLake,只在容器内存 |
| Rename / Change Model | 更新 settings.json(ETag If-Match) |
| Suspend(暂停或长闲置) | MWC POST /export → 容器返回 VFS dump(base64)→ MWC 写 sessions/<id>/export/+ 更新 state.json(sandboxState=Suspended, snapshotId) |
| Resume(冷恢复) | 建新 sandbox → 读 OneLake export → POST /import 预填 VFS → SDK ResumeSessionAsync 重放 events.jsonl |
| Share / Unshare | 在 indexes/shared/... 加/删指针文件 |
| Delete | 删整个 session 文件夹 + 所有 indexes 指针 |
⑤ 三个核心机制
1) 三文件拆分(最小化热路径字节)
metadata.json不可变 → write-once,永不重写settings.json偶变 → ETag 乐观锁,rename/换模型时改state.json高频 → 每消息只动 3 个字段;天然单写者(一个 sandbox 只对应一个 MWC 实例)
2) Write-Ahead 写日志模式
metadata.json 写入 OneLake 早于 sandbox 创建,原因:
- API 超时但实际成功 → 调谐 sweeper 能通过 metadata 找到孤儿 sandbox(label 匹配)
- UX 立即出现"Provisioning..."状态指示
- 幂等重放:用同一
draftSessionId重试 → 检测到 Provisioning 状态 → 续作而非新建
3) 索引是可重建投影(Eventually Consistent)
indexes/owned/{tenant}/{user}/{sessionId}.json 是空文件——文件名本身就是指针。
- 列表"我的会话" =
ListPathsprefix 扫描,O(user 会话数),不全扫 - 真相源始终是
sessions/文件夹;索引坏了可以从源重建 - 后台调谐 job 定期对账(创建索引漏写 → 补写;session 已删 → 清孤儿索引)
⑥ 并发控制矩阵
| 文件 | 写者数量 | 机制 |
|---|---|---|
metadata.json | 1(写一次) | 无需控制 |
settings.json | 多(多 tab rename) | ETag If-Match + 412 重试 |
state.json | 1 主写(同 sandbox 同 MWC 实例)+ 边界多 tab | 天然单写者 + ETag 乐观锁兜底,max(lastInteractionAt) 合并 |
| 跨 session | 完全隔离 | 无争用,各 session 写自己文件夹 |
⑦ 鉴权 / 传输
- 协议
- ADLS Gen2 兼容 REST
- Endpoint
onelake.dfs.fabric.microsoft.com- MWC → OneLake
- Fabric workload identity(一等公民身份)
- Container 内访问
- 如启用 Fabric Fuse,通过 ADC egress proxy 注入
Authorization: Bearer {onelakeToken};token 用 SecretRef 引用 KeyVault,容器内永远看不到明文(详见 Q13)
⑧ 为什么 events.jsonl 不每条消息都写 OneLake?
成本 vs 持久性的权衡:
- 对话越长,events.jsonl 越大,每条消息全量 PUT → 吞吐崩
- 留内存 + 定期 export → 热路径只动
state.json三字段,毫秒级 - 持久性兜底有三层:① ADC 自动 sandbox memory snapshot(暂停时) ② 显式 export 到 OneLake(暂停 / 检查点) ③ cold restore 时从 OneLake 重放
- 最坏情况丢失:"最后一次 export 之后的对话"——重要节点(暂停、长闲置)都会触发 export
📄 依据:engineering/000-decisions/004-session-storage.md(选项决策 A/B/C/D)+ engineering/010-architecture/013-sessions-store.md(目录结构 + ETag)+ engineering/010-architecture/014-D-session-state.md(write-ahead + 调谐 sweeper)+ engineering/030-sessions/034-session-export-import.md(VFS export/import)+ engineering/010-architecture/011-sessions-responsibilities.md(MWC vs Shared vs Container 职责划分)。
Container 的生命周期是怎样的?
Container 生命周期由 MWC 主导 + ADC 平台兜底 双层管理。核心心智:Session 不死,Container 用完即弃 —— 容器是计算实例,可被替换;session 通过 OneLake export 永生。
① 状态机总览(7 个核心状态)
② 七阶段详解
1️⃣ Provisioning — Lazy 创建,首条消息触发
没有独立的 Create Session API,第一条 POST /messages 触发:
MWC ADC Container
├─ ① Write-ahead metadata.json (sandboxState=Provisioning) → OneLake
│
├─ ② PUT /sandboxes ──────────▶│
│ {diskImage, env, egressRules,
│ lifecycle: {autoSuspend.enabled=false,
│ autoDelete.enabled=true, interval=172800},
│ labels: {session-id, artifact-id, managed-by, created-at}}
│ ├─ 建 microVM + 启动容器进程
│◀── sandboxId, containerUrl ──┤
│
├─ ③ 写 state.json (含 sandboxId)
│
├─ ④ PUT /secrets ─────────────▶│ KeyVault 存 OBO token (SecretRef)
│
├─ ⑤ POST /configure ──────────────────────────────────────────▶│
│ ├─ 容器加载 token/model/skills
│◀───────────────── 200 OK ──────────────────────────────────────┤
│
├─ ⑥ 更新 state.json (sandboxState=Active)
│
└─ ⑦ POST /responses ──────────────────────────────────────────▶│
└─ SDK SendAsync 处理首条消息
⚠️ 关键决策:MWC 显式关闭 ADC 自带的 idle 5 分钟 auto-suspend(autoSuspendPolicy.enabled=false)。原因:ADC 看网络流量判断闲置,但 agent 跑 LLM 长链时容器内忙、外面没流量 → 会被 ADC 误杀。MWC 用应用层的 interactiveState(SDK 事件驱动)判断真正闲置。
2️⃣ Active — 处理消息
容器内 InteractionStateTracker 跟踪 SDK 事件,状态用 fire-and-forget callback 异步报给 MWC(不阻塞 SDK):
| SDK 事件 | interactiveState | UX 显示 |
|---|---|---|
| 容器启动 / ResumeSessionAsync 中 | Starting | "Starting..." |
SendAsync() 进行中 | Running | "Thinking..." |
SessionIdleEvent | Idle | ✓ ready |
user_input.request SSE | WaitingForInput | 问号图标 |
permission.request SSE | WaitingForApproval | 审批图标 |
SessionErrorEvent | Error | ❌ 错误 |
容器并发控制:SDK SendAsync 不线程安全 → 容器用 SemaphoreSlim 串行化,第二条同时来 → 返回 429 Too Many Requests,UX 显示 "agent is busy"。
3️⃣ Suspending → Suspended — 应用层暂停
MWC sweeper 周期扫描,Running 永远不暂停:
| interactiveState | 阈值(可配) | 动作 |
|---|---|---|
Idle | idleThreshold = 30 min | Suspend |
WaitingForInput / WaitingForApproval | waitThreshold = 2 hours | Suspend |
Error | errorThreshold = 15 min | Terminate(直接终结,不是暂停) |
Running | — | 从不暂停 |
MWC sweeper Container ADC OneLake
├─ TryTransition Active → Suspending
├─ POST /export ──────────▶│
│◀── base64 dump (events.jsonl + files/)
├─ Write export ─────────────────────────────────────────▶│
│ │ Durable checkpoint
├─ POST /sandboxes/{id}/stop ────────────▶│
│◀── snapshotId, snapshotBlobUri ─────────│ Memory-mode snapshot (process state preserved)
├─ Update state.json (Suspended) ──────────────────────────▶│
└─ TryTransition Suspending → Suspended
为什么先 export 再 stop?Memory snapshot 恢复快(~1s)但可能失败(host 维护、容量问题)。OneLake export 是冷恢复的 source of truth,双保险。
4️⃣ Resuming → Active — 用户回来发消息
User MWC ADC Container
├─ POST /messages ▶│
├─ Suspended → Resuming
│◀── SSE: session.resuming (transparent to user)
├─ POST /sandboxes/{id}/resume ─▶│
│◀── Running, containerUrl ──────│ Memory 恢复 ~1s,进程状态完整
├─ GET /health ─────────────────────────▶│
│◀──────────────────── 200 ─────────────│ readiness check
├─ POST /configure (fresh tokens) ──────▶│ token 可能已过期
│◀──────────────────── 200 ─────────────│
├─ Resuming → Active
└─ POST /responses ─────────────────────▶│ 继续,无需 replay events.jsonl
Memory mode 的妙处:SDK 会话状态保留在进程内存中 —— 不需要 export/import 每个 suspend/resume 周期,热路径快。
5️⃣ Cold Restore — Resume 失败兜底
memory snapshot 不可用时(snapshot 过期、host 维护、容量问题):
MWC ──PUT /sandboxes──── ADC: 新建 sandbox(disk image 启动,全新进程)
──POST /configure─── Container: 加载 token / model / skills
──POST /import────── Container: 从 OneLake export 预填 InMemorySessionFs
SDK.ResumeSessionAsync(sessionId) ← 重放 events.jsonl
→ 对话状态完整重建(慢,几秒到几十秒)
6️⃣ Terminated — 终结(6 个触发源)
| 触发 | 触发方 | 说明 |
|---|---|---|
用户 DELETE /sessions/{id} | UX | 主动删除 |
Error 持续 ≥ 15 min | MWC sweeper | 自动清理 |
Suspended 持续 ≥ 24h | MWC sweeper | 防止 snapshot 堆积 |
Unknown 状态 ADC 也找不到 | MWC sweeper | 心跳丢失 + ADC 404 |
| Provisioning 失败 compensating cleanup | MWC | 步骤失败时回滚 |
| ADC auto-delete @ 48h | ADC 平台 | 终极兜底(autoDeletePolicy) |
MWC DELETE /sandboxes/{id} 后,secret 和 snapshot 一并清除。Session 元数据保留在 OneLake(历史列表用),但所有 compute 资源释放。
7️⃣ Unknown — 失联异常
MWC 在 unknownThreshold(默认 30 min)内没收到容器心跳:
- 标记
Unknown - 尝试
GET {containerUrl}/state直接探活 → 通 → 恢复 Active - 不通 →
GET /sandboxes/{id}问 ADC:Running→ 容器内进程崩了 → export + terminate + 起新 sandboxStopped→ 进Suspended404→Terminated
③ Defense-in-Depth(6 层兜底)
| 层 | 机制 | 阈值 | 作用域 |
|---|---|---|---|
| 1 | MWC sweeper:idle suspend | 30 min | 正常省资源 |
| 2 | MWC sweeper:Unknown 清理 | 30 min 无心跳 | 失联恢复 |
| 3 | MWC sweeper:Provisioning 调谐 | 卡住 10 min | 创建失败兜底 |
| 4 | MWC sweeper:orphan 检测 | 每 30 min | ADC ↔ OneLake 一致性 |
| 5 | MWC sweeper:长 suspend 终结 | 24 hours | 防止 snapshot 堆积 |
| 6 | ADC auto-delete | 48 hours | 平台层终极兜底 |
④ Session vs Container 寿命对比(最重要的心智)
| 概念 | 寿命 | 标识 | 角色 |
|---|---|---|---|
| Session | 持久(OneLake 里直到用户删) | fabricSessionId | 业务实体,跨 sandbox |
| Container / Sandbox | 短暂(最长 48h + Suspended 24h) | sandboxId | 计算实例,可被替换 |
| VM Memory snapshot | 短暂(resume 一次性消耗) | snapshotId | 热路径恢复用 |
| OneLake export | 跟随 session 持久 | sessions/<id>/export/ | 冷路径 source of truth |
核心心智:Session 是不死的;Container 是用完即弃的计算容器;任何时刻 container 死了,下次发消息会 cold restore 一个新的,从 OneLake 重放历史,用户感知只是"慢一点"。
⑤ Image 升级也是同一机制
新容器镜像版本发布时:
- 正在 Active 的容器:继续跑老镜像(不打断用户)
- 下次 Suspend / Terminate / Cold restore:建新 sandbox 自动用新镜像
- SDK 内部状态通过 events.jsonl 重放:跨版本兼容,无需用户感知
本质:cold restore 既是 BCDR 兜底,也是 rolling upgrade 通道。
📄 依据:engineering/010-architecture/014-D-session-state.md(状态机 + 7 阶段 + sweeper)+ engineering/.education/adc-sandbox-lifecycle.md(ADC 平台机制 / Memory vs Disk)+ engineering/010-architecture/014-A-sessions-crud.md(lazy creation)+ engineering/060-agent/067-agent-image-versioning.md(镜像升级流程)。
Session 和 Prompt 是什么关系?Session 的 CRUD 怎样?用户什么时候能新建 Session?
一句话结论
① 关系:Session ⊇ 多个 Prompt ⊇ 1+ Turn ⊇ 多个 SSE Event。Session 是长寿命容器(OneLake 永久),Prompt 是用户一次性输入(一次 POST /messages),Turn 是 LLM 单轮闭环。
② CRUD:不存在 POST /sessions,Session 由第一条消息懒创建;List / Get / Delete / PATCH 走 MWC,跨 artifact List All 走 Shared。
③ 何时建:用户点 "New Chat" 只产生本地草稿(client-side),真正发出第一条 prompt 时才向后端建 session — 关掉浏览器啥也没创建。
1. 包含关系图
2. 四层概念对照表
| 层级 | 是什么 | 生命周期 | 存储位置 |
|---|---|---|---|
| Session | 一次完整对话上下文 | OneLake 永久(用户可手动 delete) | OneLake:metadata.json / state.json / events.jsonl |
| Prompt | 用户单次输入(message 字段) |
从 POST /messages 到 session.idle |
追加到 events.jsonl |
| Turn | LLM 一个完整 turn_start…turn_end | 一个 prompt 可有 1 或多个 turn | 每 turn 有 turnId,作为 event metadata |
| Event | SSE 流中的最小粒度 | 毫秒级 | 不单独持久化,聚合进 events.jsonl |
3. Session CRUD 全景 — 注意:没有 Create API
关键设计:Session 由 Messages API 懒创建,不存在独立的 POST /sessions。原因:Session creation 跨 OneLake metadata、ADC sandbox、Container 三层非事务,且如果先 create 再发消息,用户关掉浏览器就会留下空壳 sandbox 占用 ADC 容量。改为"第一条消息即建 session"后,每个 session 至少有一条消息,没消息就没 session。
| 操作 | HTTP | 路径 | 路由层 | Token |
|---|---|---|---|---|
| Create(懒创建) | POST |
/workspaces/{w}/CopilotEnvironments/{a}/messagesbody: { targetSession:"new", draftSessionId, message, … } |
MWC | mwcToken |
| List(per artifact) | GET |
/workspaces/{w}/CopilotEnvironments/{a}/sessions |
MWC | mwcToken |
| List All(cross-artifact,左侧栏用) | GET |
{cluster}/metadata/unifiedcopilot/sessions[?workspaceId=…] |
Shared(后端 fan-out) | userEntraToken |
| Get(详情 + 状态) | GET |
/workspaces/{w}/CopilotEnvironments/{a}/sessions/{s} |
MWC | mwcToken |
| Get Messages(历史对话) | GET |
/workspaces/{w}/CopilotEnvironments/{a}/sessions/{s}/messages |
MWC | mwcToken |
| Update(仅改 displayName) | PATCH |
/workspaces/{w}/CopilotEnvironments/{a}/sessions/{s} |
MWC | mwcToken(Contributor 角色 + 仅 createdBy) |
| Delete | DELETE |
/workspaces/{w}/CopilotEnvironments/{a}/sessions/{s} |
MWC | mwcToken |
| Abort(停当前 turn,不删 session) | POST |
…/sessions/{s}/abort |
MWC | mwcToken |
📌 Auto-naming:新 session 的 displayName 默认为空,第一条回复后 MWC 用一次轻量 LLM call「Summarize this conversation in 5 words or fewer」生成名字,并通过 SSE session.renamed 推送给前端更新左侧栏。用户随时可手动 PATCH 覆盖。
4. 用户什么时候能"新建"一个 Session?
"新建"动作分三个阶段,前两阶段都不向后端建 session:
阶段 C 的服务端展开(来自 014-A-sessions-crud):
- MWC 收到
POST /messages,按draftSessionId加 server-side creation lock。 - OneLake:在
{workspace}/{CE-artifact}/sessions/{newGuid}/写metadata.json(write-once)。 - ADC:创建 microVM + egress rules + secret。
- Container:MWC 调
/configure把 sessionContext / 工具清单 / model triple 注入 GitHub Copilot SDK。 - SSE 先后发
session.provisioning { draftSessionId, status }→session.created { sessionId, draftSessionId }。 - 之后立即接
assistant.turn_start开始第一条 prompt 的处理。 - 前端收到
session.created后,把 React state 里的 draft 替换为真 sessionId,并把后续请求 URL 改成…/sessions/{sessionId}/messages。
同一 draftSessionId 的重试是幂等的:
- SSE 中断重连 / 用户连点 Send / 多 tab 撞车 → MWC 检测到同
draftSessionId已有 session:- 状态
Provisioning→ 复用现有 session,把 SSE 重新挂上去; - 状态
Active→ 把 message 投递为本次 prompt(idempotent replay);
- 状态
- 所以"双击发送"不会建出两个 session,at-most-once 创建。
"New Chat" 触发点(前端 UX 入口):
- 左侧 Session 列表上方的 + New Chat 按钮;
- 切换到一个新 Page 后打开的 Copilot Pane 也是空 draft(如果这个 page 没有关联 session);
- 切换 mode (Plan/Interactive/Autopilot) 不会 建 session — 这些字段每条消息都能改;
- 切换 model / context / attachments 不会 建 session — 全是 per-prompt 字段。
5. 一个 Prompt 的完整 SSE 时序
POST /workspaces/{w}/CopilotEnvironments/{a}/sessions/{s}/messages
{
"message": "List my warehouses",
"mode": "interactive",
"model": "claude-sonnet-4.6",
"context": { "workspaces":[...], "artifacts":[...] },
"attachments": [ ... ]
}
│ (SSE stream opens)
▼
[ session.provisioning ] ← 仅新 session(lazy creation)
[ session.created ] ← 仅新 session
──────────────── Turn 1 ────────────────
assistant.turn_start
assistant.reasoning_delta × N ← 推理 token 流
assistant.message_delta × N ← 回答 token 流
tool.start / tool.progress / tool.complete ← server-side bash
tool.call → POST /tool-result ← client-side(如 navigate)
permission.request → POST /permissions/{id}/respond ← 流暂停
user_input.request → POST /user-input/{id}/respond ← 流暂停
artifact.created / artifact.updated ← Sync Service 派发
assistant.turn_end { turnId }
──────────────── Turn 2 (chained) ───────
… (tool 结果继续 feed 给 LLM 触发的新 turn) …
──────────────────────────────────────────
session.idle ← 整个 prompt 才算处理完,UI 解锁 Send 按钮
6. 并发模型 ── 关键不变量
| 规则 | 实现位置 | 违反时 |
|---|---|---|
| 1 concurrent prompt / session | 容器内 SemaphoreSlim _sessionLock |
429 AgentBusy / MWC 层 409 SessionBusy |
Send button 禁用 while isLoading |
React Chat Pane | UI 前置防护 |
| Lazy session — 首个 prompt 才建 session | MWC + targetSession:"new" + draftSessionId |
同 draftSessionId 的重试会被去重,不会建出第二个 session |
| Abort 只取消当前 turn,session 保留 | POST /abort → SDK CancellationToken |
SSE 流以 session.idle 结尾 |
7. 每个 Prompt 可独立配置的字段
下面这些字段每个 prompt 都可以改,无须重建 session:
mode—interactive/plan/autopilotmodel— LLM 模型切换(前提:在已批准的 model 池里)connectionId— 同名 model 多 connection 时指定context— 用户每次发问可换 workspace、可点不同的 artifact、cell、visualattachments— 截图 / 文件(要求模型支持 vision)
但下面这些字段是 session-scoped,只在第一个 prompt 生效:
agent— agent type(默认GitHubCopilot)- session 的 storage workspace + CSM artifact 绑定
8. Prompt 和 Context 是两条独立的输入流
| 维度 | message 字段 | context 字段 |
|---|---|---|
| 含义 | 用户说什么 | 用户当前在哪个画面、选了什么 |
| 来源 | Chat 输入框 | Shell + Active Page(Angular 收集) |
| 结构 | 纯文本 | 结构化:workspaces[] + artifacts[] + customContext |
| 透传给 LLM | 作为 user message | 作为 system/tool 上下文,container 不缓存,按 prompt 透传 |
| 跨 prompt 是否变化 | 每次都变 | 可变(用户切 workspace 中途换都行) |
9. 一个 Prompt 处理完后会落地哪些状态
events.jsonl(容器内InMemorySessionFs)追加 N 条 events — Suspend / Export 时才落到 OneLakestate.json(hot path)更新messageCount/lastActivity/lastMessageAt— 驱动 30 min idle 阈值(Q19)metadata.json不变(write-once)- session display name 首次自动生成时发
session.renamedSSE 事件
10. Session 与 Prompt 的常见误解
| 误解 | 正解 |
|---|---|
"应该有个 POST /sessions 来建 session" | ❌ 没有;session 在第一条 POST /messages(targetSession:"new")时由 MWC 懒创建,避免空壳 sandbox |
| "点 New Chat 就调后端建 session" | ❌ 点按钮只产生 client-side 草稿(侧栏的灰条目),关掉浏览器啥也没创建 |
| "一个 prompt = 一个 turn" | ❌ 一个 prompt 可触发多个 chained turn(LLM 调 tool → 拿结果 → 再调 tool → 再生成) |
| "session.idle = session 结束" | ❌ session.idle 只是当前 prompt 处理完;session 还在活,等下一个 prompt |
| "session 终止 = 数据丢失" | ❌ Session 内容在 OneLake 永久;container 终止只是丢内存执行环境,下次发 prompt 触发 cold restore(见 Q19) |
| "context 是 session 级别" | ❌ context 是 prompt 级别,每条消息可变 |
| "两个浏览器 tab 可同时发 prompt" | ❌ 后发的会 409 SessionBusy,必须等前一个 session.idle |
| "双击 Send 会建两个 session" | ❌ draftSessionId + MWC creation lock 保证 at-most-once,重试只会复用同一 session |
📄 依据:engineering/010-architecture/014-A-sessions-crud.md(Session CRUD 全表、为什么没有 POST /sessions、draftSessionId 幂等、auto-naming)+ engineering/010-architecture/014-B-messages-api.md(POST /messages 字段表、并发 1)+ engineering/010-architecture/015-messaging-streaming.md(SSE event 全表)+ engineering/010-architecture/014-D-session-state.md(_sessionLock SemaphoreSlim、per-session send lock、interactiveState 状态机)+ engineering/300-frontend/300-!!-frontend-dev-design.md(前端 sendMessage 流程、draft session UX、CopilotUIEvent 联合类型)+ specs/building-blocks/session.md(Session 概念定义)。
三种 Token 各自做什么?整个鉴权链路是怎样的?
容器内 永远看不到任何明文 token,但仍能调 Fabric API、OneLake、LLM、GitHub —— 靠的是 4 个角色 × 3 类 token × 1 个 Envoy egress 注入。理解这套体系是看懂所有 secret / refresh / re-auth 流程的前提。
① 关键 Token 一览
| Token | 谁颁发 | 谁持有 | 用途 / Audience |
|---|---|---|---|
userEntraToken(aka userFabricToken) |
Entra ID(用户登录) | Browser → Shared(Authorization header) | Copilot Environment artifact CRUD、global session listing、Models / Connections 发现、generatemwctoken 换 MWC token;audience: https://analysis.windows.net/powerbi/api |
mwcToken |
Shared (generatemwctoken) |
Browser → MWC(Authorization header) | Session CRUD / messaging / tools / attachments;scoped to (workspaceId, ceArtifactId);浏览器侧自己缓存 + 主动 / 被动刷新(详见 §②) |
copilotWorkload1pToken |
Entra ID(1st party app) | MWC(app identity) | audience 因目标而异:fabric/<container-id>(→ Container /responses)、https://adc.azure.com(→ ADC)、https://storage.azure.com(→ OneLake) |
| OBO 下游 token(多份) | Entra(On-Behalf-Of flow,MWC 发起) | KeyVault(以 secretRef ID 引用) |
容器出站调外部服务时由 Envoy 网络层注入;典型 audience:Fabric API、OneLake、LLM endpoint、GitHub PAT |
⚠️ 区分点:Browser 永远不直接持有 copilotWorkload1pToken 或 OBO 下游 token。它只看到 userEntraToken(调 Shared)和 mwcToken(调 MWC)。所有"容器侧 token"都被 MWC + KeyVault + Envoy 包起来。
② 浏览器侧:MWC Token 怎么获取 / 怎么刷
MWC token 用 generatemwctoken 私有 API(Shared 提供)从 userEntraToken 换得,scope 到具体的 (workspaceId, ceArtifactId) 二元组。
POST https://{clusterUri}/metadata/v201606/generatemwctoken
Authorization: Bearer {userFabricToken}
Content-Type: application/json
{
"type": "[Start] GetMWCToken",
"workloadType": "CopilotEnvironment",
"workspaceObjectId": "{workspaceId}",
"artifactObjectIds": ["{ceArtifactId}"],
"capacityObjectId": "{capacityId}",
"asyncId": "{guid}",
"iframeId": "{guid}"
}
→ { mwcToken, mwcRolloutUrl, expiresAt }
CopilotHostService(Angular 层)按 (workspaceId, ceArtifactId) 缓存 MWC token,并用两层策略保持新鲜:
- Proactive 预检:每次发 MWC 请求前看缓存 token 还剩多久;剩余 < 10 分钟就先 refresh Entra token → 调
generatemwctoken→ 拿到新 MWC token → 再发原请求。 - Reactive 兜底:万一发 MWC 时收到
401(比如刚预检完到发请求之间过期了)→ 刷一次 MWC token → 重试一次;如果第二次还 401,向用户报错。
Model 发现 / global session 列举 / CE CRUD 都走 Shared,直接用 userEntraToken 不走 MWC token。
③ Bootstrap 鉴权链(首条消息时,容器侧)
Browser ─── mwcToken ─────▶ MWC
│
│ ① OBO 换下游 token(多份不同 audience)
│ ② PUT /secrets/{secretId} (KeyVault) ─▶ ADC
│ 存所有 OBO token(Fabric/OneLake/LLM/GitHub)
│
│ ③ PUT /sandboxes(egressRules.inject.secretRef = secretId)
▼
ADC ──── 建 Firecracker microVM + Envoy sidecar
▼
Container started
│
│ ④ POST /configure ──── copilotWorkload1pToken
│ audience: fabric/<container-id>
│ body: { refreshTokensAfterUtc, tokenExpirations }
│
└─ Container 启动 timer,记下 "啥时候该让 MWC 续 token"
④ 出站请求时怎么"看不到 token"
⑤ Token 刷新:Container-Initiated(推荐路径)
OBO token 有效期 1 小时左右。Container 内有 RefreshTimer:
- Container timer 到
refreshTokensAfterUtc→ 发请求给 MWC(Bearer 用容器自己的 MI token) - MWC 验签:用户原 token + MI token + sandbox ownership 三重校验
- MWC OBO 换新的下游 token(多份不同 audience)
- MWC 调 ADC
PUT /secrets/{secretId}全量替换(注意:PUT,不是 PATCH) - MWC 调 ADC
POST /sandboxes/{id}/egresspolicy把 egress 规则里的 secretRef 引用更新 - MWC 调 Container
POST /configure给新的refreshTokensAfterUtc→ timer 重设
关键点:整个刷新过程不重启容器,进程内 SDK / SDK session 状态不丢;下次出站请求 Envoy 会自动用新 token。Suspend 期间 timer 停;Resume 后 MWC 主动调 /configure 给一个新 expiry。
⑥ Reactive 再认证(OBO 失败兜底)
当用户 refresh token 过期 / consent revoke 时,OBO 会失败,容器拿不到下游服务的新 token:
- SDK 拿到 401 → 容器在 SSE 流里发
session.reauth_required {} - 前端处理:① 关闭当前 SSE 流;② 弹"重新登录"(触发 Entra redirect);③ 用户登录后拿新
userEntraToken;④ 调generatemwctoken拿新 MWC token;⑤ 重发上一条失败的请求—— 这次 MWC 端能拿到新 user token 跑 OBO,更新 KeyVault,容器 timer 重置。
⚠️ 没有专门的 /reauth API。前端在 reauth 后直接重发原 Send Message 请求即可:MWC 端验完新 user token,OBO 走通后,正常下发新 expiry。
⑦ 多 audience 的必要性
| 目标 | audience | 谁需要 |
|---|---|---|
| Browser → Shared | https://analysis.windows.net/powerbi/api | Fabric public API + 私有 redirect 服务 |
| Browser → MWC | MWC scope(由 generatemwctoken 决定) | per-CE token,防跨 artifact 重放 |
| MWC → Container | fabric/<container-id>(per-container) | 防 token 被其他容器重放 |
| Container → Fabric API | https://api.fabric.microsoft.com | Fabric 主网 |
| Container → OneLake | https://storage.azure.com | ADLS Gen2 endpoint |
| Container → LLM | 各 LLM provider audience | OpenAI / Anthropic 等 |
| Container → GitHub | GitHub PAT(DMTS 托管) | code repo / skills |
⑧ 关键不变量(团队介绍必讲)
- 🔒 容器进程地址空间从不持有任何 token 明文 —— 全靠 Envoy 网络层注入
- 🌐 浏览器只看到 2 个 token:
userEntraToken(→ Shared)和mwcToken(→ MWC) - 🔁 Token 刷新发生在 KeyVault + egress rule 层,不打断容器进程 / SDK 状态
- 🎯 每个 audience 一份 token,不复用 —— 防止权限放大
- ⚠️
PUT /secrets是 全量替换,并发 PUT 必须串行化(MWC 内部用锁) - 🚨 Reauth 是用户感知的——SSE 推
session.reauth_required,前端关流 → 重新登录 → 刷 MWC token → 重发原请求
📄 依据:engineering/020-token-refresh/024-token-refresh-container.md(Bootstrap + 周期 refresh 时序图)+ 023-token-refresh-reactive.md(Reactive flow + MI / egress 规则)+ 020-token-via-egress-proxy.md(Secrets vs Managed API Connections)+ 040-container-spec.md(App identity / audience 表)。
Skills 是怎么加载的?版本怎么管?
Skills 是 SKILL.md 文件(教 LLM 怎么用 Fabric 各 workload 的 API)。它不进容器镜像,从 CDN 动态加载——这样 workload team 改 skills 不用重建镜像,也不用平台 team 介入。
① 全链路:从仓库到容器
② 加载时机(4 个关键节点)
| 时机 | 行为 | 是否阻塞首消息 |
|---|---|---|
| 容器启动 | SkillsLoader 调 Shared GET /skills/manifest → 拉 CDN 包 → 解析 | 是(在 POST /responses 前完成) |
| Shared 不可达 | Fallback 用容器镜像里 baked 的 /app/skills/ | 否 |
| 新 session 创建 | SDK 把已加载的 skills 拼到 system prompt(每 session 独立 snapshot) | 否 |
| 用户切 workspace / artifact 中途 | Open Question 是否动态切换 skills 还没定(067-skills-dynamic-switching.md) | — |
③ 关键设计:Skills 与镜像解耦
| 变化 | 需要重建镜像? |
|---|---|
| 改 SKILL.md 内容 | ❌ 不需要 —— CDN 重发,新 session 自动用新版 |
| 新增 SKILL.md | ❌ 不需要 —— manifest 自动包含 |
| 修改 SDK / Agent runtime / native tools | ✅ 需要 —— 走 067-agent-image-versioning 流程 |
| 新增 native tool(SDK 内置工具) | ✅ 需要 |
| 修改容器基础进程 | ✅ 需要 |
④ 版本管理 / 滚动升级
- 新 session 启动时自动拉最新 skill 版本(manifest 决定)
- 已有 session 保持创建时加载的 skills 版本(防止 prompt 中途变化让 LLM 行为漂移)
- 下次该 session cold restore(resume 失败兜底)时,新容器会重新拉 skills —— 这是天然的 rolling upgrade 通道
⑤ 与 MCP Server / Plugin 怎么协同
注意区分 3 类自助贡献机制:
| 类型 | 形态 | 谁加载 | 典型工作 |
|---|---|---|---|
| Skill | SKILL.md(Markdown 文档 + prompt) | SkillsLoader 启动时 / CDN 拉取 | 教 Copilot "Spark 调优该写什么样的代码" |
| MCP Server | HTTPS endpoint(workload 自托管) | McpServerRegistry 启动后注册 + health check |
给 Copilot 一个 get_last_error 真实工具调 |
| Frontend Tool(F13) | MCP-UI 组件包(CDN bundle) | 前端按 tool_id 注册到 Chat Pane | 在对话里渲染 notebook 单元格预览卡片 |
⑥ 关键不变量
- 📦 Skills 是"distributed as code, not as cloud services"(D-3 / D-5)—— CLI / VS Code / Portal 都装同一份
gim-home/skills-for-fabric - 🔁 一个 session 内 skills 不变(不污染 prompt),跨 session 自动用最新
- 🛟 容器镜像里有 baked skills 作为 fallback,保证 Shared 不可达也能启动
- ⚙️ 改 SKILL.md ≠ 改镜像 —— PR 合并到生产 < 2 工作日(详见 Q14)
📄 依据:engineering/060-agent/066-skills-loading.md(manifest + CDN + fallback)+ 065-agent-design.md(SkillsLoader / McpServerRegistry 类)+ 067-agent-image-versioning.md(哪些变化需要重建镜像)+ 067-skills-dynamic-switching.md(动态切换 open question)+ 068-skills-content-design.md(service vs desktop 内容差异)。
Copilot Environment Artifact 是什么?为什么 session 要寄宿在 artifact 里?
Copilot Environment(CE)是一种 Fabric artifact 类型,专门承载 copilot session。它在 OneLake 里有自己的文件夹,所有 session 数据(metadata / state / events / 文件)都存在这个 artifact 的 OneLake 路径下。把 session 包在 artifact 里,是 fabric-ai-framework 的核心架构决定(004-session-storage.md 选项 A)。
① 为什么不是直接放 OneLake,而是包成 artifact?
| 需求 | artifact 自带的能力 |
|---|---|
| CMK(客户托管密钥) | artifact 继承 workspace → capacity 的 CMK 设置,无需额外做 |
| BCDR / 灾备 | OneLake 自身的副本机制覆盖(和其他 Fabric 数据一致) |
| 租户 / capacity 迁移 | workspace 在 capacity 间挪 → CE artifact 自动跟着挪,session 历史不丢 |
| 分享 / 协作 | artifact 是 Fabric 一等公民,已有完整 share / permission UI;不需要造分享系统 |
| 删除 | DELETE artifact = 删整个 OneLake 文件夹 + 所有 session(自动级联) |
| 权限审计 | workspace ACL + artifact 权限 + session-level 隔离三层 |
📌 心智:CE 是 "装 session 的 Fabric 标准盒子"。盒子继承 Fabric 平台的 CMK / 备份 / 共享 / 权限 / 迁移能力,团队不用自己造一遍。
② CE artifact 的属性
| 字段 | 值 |
|---|---|
| artifact type | CopilotEnvironment |
| URL 段 | CopilotEnvironments(MWC API 用);旧名为 CopilotSessionsManager / CSM |
| 每 workspace 数量 | 多个(不是 singleton)—— 用户主动选 / 建 |
| definition payload | {"model": "gpt-5.1", "agent": "GitHubCopilot"} |
| OneLake 文件夹 | artifact 创建时由平台自动生成("XML item 定义" + 自动 CRUD handler) |
| 权限 | workspace Contributor+ 可建;其他用户访问看 artifact share + session owner 设定 |
③ CRUD API(走 Shared,Browser 直接调)
Base:https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}
Auth:Bearer {userEntraToken}
| API | Method + Path | 用途 |
|---|---|---|
| Create CE | POST /itemsbody: {type: "CopilotEnvironment", displayName, definition} | 新建一个 CE artifact |
| List CE | GET /items?type=CopilotEnvironment | 列出当前 workspace 所有 CE |
| Delete CE | DELETE /items/{itemId} | 删 CE + 全部 session |
④ 从 CE 到 MWC:generatemwctoken 桥
公共 GET /v1/workspaces/{wsId}/items/{itemId} 只返回 artifact metadata,不带 MWC 端点。要调 MWC 必须先经一个私有 API 拿 MWC token + rollout URL:
POST https://{clusterUri}/metadata/v201606/generatemwctoken
Authorization: Bearer {userFabricToken}
{
"type": "[Start] GetMWCToken",
"workloadType": "CopilotEnvironment", ← 关键:用 CE 类型
"workspaceObjectId": "{workspaceId}",
"artifactObjectIds": ["{ceArtifactId}"], ← 必须带 CE 的 GUID
"capacityObjectId": "{capacityId}",
"asyncId": "{guid}",
"iframeId": "{guid}"
}
→ { mwcToken, mwcRolloutUrl, expiresAt }
之后所有 session / messaging / tools / attachments API 都走 {mwcRolloutUrl}/public/workspaces/{wsId}/CopilotEnvironments/{ceId}/...,Bearer {mwcToken}。详见 Q18 ② 浏览器侧 MWC token 刷新策略。
⑤ 用户流程:从打开 sidecar 到发首条消息
⑥ Session 在 CE 里的目录结构
{workspace}/{CE-artifact}/
├── indexes/ # 可重建的投影
│ ├── owned/<tenantOid>/<userOid>/<sessionId>.json # 空文件 = 指针
│ └── shared/<tenantOid>/<userOid>/<sessionId>.json # {sharedBy, sharedAt, permissions}
└── sessions/
└── <fabricSessionId>/
├── metadata.json # 写一次永不动 {agent, createdAt}
├── settings.json # 偶尔写 (ETag) {model, displayName, modelConnectionId}
├── state.json # 每消息写 {sandboxId, lastInteractionAt, messagesCount}
├── session-state/
│ └── events.jsonl # export 时写入 (完整对话)
└── files/ # agent 创建的产物文件
⑦ Permission Model:私有 sessions in 共享 artifact
设计 trade-off(006-artifact-model.md):
| Option 1: 共享 artifact = 共享 sessions | Option 2: 共享 artifact + 私有 sessions(采纳) | |
|---|---|---|
| 权限 | 只看 artifact ACL | artifact ACL + session owner / sharing |
| 体验 | 对方写 = 看你 session | 对方写 = 用这个 CE 自己开 session |
| GDPR | 简单 | 需要 admin 工具找出哪些 artifact 含哪些用户 sessions |
| my-workspace blocker | 必须用 my-workspace 才能私有,my-workspace 不一定有 capacity → 卡住 | 共享 workspace 也能放私有 session,去掉 capacity 障碍 |
⑧ 关键不变量
- 📦 每个 session 必须寄宿在某个 CE artifact 下,没有"裸 session"
- 🔁 同一 workspace 可以有多个 CE,分别给不同团队 / 项目 / 私有场景
- 🆔
generatemwctoken的 scope 是 (workspaceId, ceArtifactId),换 CE 必须换 token - 🧹 删 CE = 删该 CE 下所有 session(artifact 自动级联)
- 📜 旧名 CSM (Copilot Sessions Manager) 仍出现在文件命名(
012-csm-management.md)和前端 doc 路径段中,与CopilotEnvironment是同一概念
📄 依据:engineering/010-architecture/012-csm-management.md(CE artifact CRUD + generatemwctoken 完整签名)+ 011-sessions-responsibilities.md(Shared vs MWC 职责)+ 013-sessions-store.md(OneLake 目录)+ 004-session-storage.md(A/B/C/D 选项对比)+ 006-artifact-model.md(共享/私有 session 模型)。
前端架构:Angular Shell + React Chat Pane 怎么协作?
Fabric Portal 是 Angular,但 Chat Pane 用 React 实现。两边通过 CopilotHostService(Angular 服务)+ CopilotHostComponent(Angular-React 桥)+ CopilotPaneApi(facade)三层粘合。React 永远不直接调后端。
① 架构总览
┌────────────────── Fabric Shell (Angular) ──────────────────┐
│ │
│ ┌── Shared Experience ──┐ ┌── Classic Artifact ──┐ │
│ │ context: workspace │ │ context: visual │ │
│ │ tools: navigate │ │ tools: edit visual │ │
│ └────────┬───────────────┘ └────────┬─────────────┘ │
│ │ Direct call │ Direct call │
│ ▼ ▼ │
│ ┌── Extension Artifact (iframe) ──┐ │
│ │ context: focused cell │ ←─ postMessage │
│ │ tools: run cell │ (Extension API) │
│ └────────┬────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──── CopilotHostService (Angular, 单例) ─────────────┐ │
│ │ · gather + merge context(Shell + Page) │ │
│ │ · dispatch client tools via executeAction() │ │
│ │ · SSE processing(拦截 tool.call → invoke → 合成 │ │
│ │ client_tool.start/complete 推给 React) │ │
│ │ · MWC token 缓存 / 401 重试 │ │
│ │ · CopilotHttpService 子服务负责 HTTP/SSE │ │
│ │ - Shared API(userEntraToken) │ │
│ │ - MWC API(mwcToken) │ │
│ └─────────────────────────┬────────────────────────────┘ │
│ │ CopilotPaneApi facade │
│ ┌──── CopilotHostComponent (Angular) ──────────────┐ │
│ │ ▼ createRoot(domElement) │ │
│ │ ┌── Chat Pane (React + Fluent v9) ─────────────┐│ │
│ │ │ Header: New Session / Sessions ││ │
│ │ │ Messages: User / Assistant / Tool / Approval ││ │
│ │ │ Input: Pills / Attach / Model / Mode / Send ││ │
│ │ └──────────────────────────────────────────────┘│ │
│ └───────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
② 三个 page 类型怎么贡献 context / tool?
| Page 类型 | 例子 | 怎么连 CopilotHostService |
|---|---|---|
| Shared Experience | Workspace 首页、Admin Portal | Angular DI 直接注入 CopilotHostService 调 refreshContext() |
| Classic Artifact | Report Editor、SQL Query | Angular DI 直接注入 |
| Extension Artifact | Notebook、Lakehouse(iframe) | Extension Client API(client.copilot.refreshContext())→ postMessage → CopilotHostService |
③ CopilotPaneApi facade — React 唯一的后端入口
Angular 把这个对象作为 prop 传给 React,React 调 facade 方法就行:
interface CopilotPaneApi {
// Session management
listSessions(): Promise<SessionInfo[]>;
getSession(sessionId: string): Promise<SessionInfo>;
updateSession(sessionId: string, displayName: string): Promise<void>;
deleteSession(sessionId: string): Promise<void>;
// Messaging — Angular 内部处理 SSE(含 tool.call 拦截、returnToolResult)
// 返回流只含 UI 相关事件,React 拿到的是 CopilotUIEvent[]
// 新 session 时 sessionId 传 null,request 里带 targetSession:'new' + draftSessionId
sendMessage(sessionId: string | null, request: SendMessageRequest): Observable<CopilotUIEvent>;
getMessages(sessionId: string): Promise<Message[]>;
// Turn 控制 — abort 同时取消 backend turn + in-progress 的 client tool
abort(sessionId: string): Promise<void>;
respondToPermission(sessionId, requestId, approved, feedback?): Promise<void>;
respondToUserInput(sessionId, requestId, answer): Promise<void>;
// Models(走 Shared,scope 到 workspace)
listModels(workspaceId: string): Promise<ModelOption[]>;
// Attachments
addAttachment(sessionId, type, resourceId): Promise<AttachmentInfo>;
listAttachments(sessionId): Promise<AttachmentListInfo>;
removeAttachment(sessionId, attachmentId): Promise<void>;
}
④ SSE 处理:Angular 拦截 tool.call,React 只看 UI 事件
后端 SSE 有 18+ 种事件,但 tool.call 不能直接给 React —— 它需要触发本地 action、等结果、回传给 MWC。Angular 在 CopilotHostService 内拦截:
React 看到的 UI 事件类型(精简版):
type CopilotUIEvent =
| { type: 'session.provisioning'; draftSessionId; status }
| { type: 'session.created'; sessionId; draftSessionId; status }
| { type: 'assistant.turn_start' }
| { type: 'assistant.message_delta'; delta }
| { type: 'assistant.message'; content; messageId }
| { type: 'assistant.reasoning_delta'; delta }
| { type: 'assistant.reasoning'; content }
| { type: 'assistant.turn_end'; turnId }
| { type: 'tool.start' | 'tool.progress' | 'tool.complete'; ... } // 服务器端 tool(直传)
| { type: 'client_tool.start' | 'client_tool.complete'; ... } // 客户端 tool(Angular 合成)
| { type: 'permission.request'; ... }
| { type: 'user_input.request'; ... }
| { type: 'artifact.created' | 'artifact.updated'; ... }
| { type: 'session.idle' | 'session.error' | 'session.expired' | 'session.reauth_required' | 'session.renamed' | 'session.resuming'; ... };
⑤ React State Model(关键字段)
interface SessionState {
sessionId: string;
draftSessionId: string | null; // session.created 之前非 null
displayName: string;
selectedModel: string;
selectedModelProviderType: 'GitHubCopilot' | 'AzureAIFoundry' | 'FabricCapacity';
selectedModelConnectionId: string | null; // Fabric Capacity 时 null
executionMode: 'plan' | 'interactive' | 'autopilot';
interactiveState: 'Starting' | 'Running' | 'Idle' | 'WaitingForInput' | 'WaitingForApproval' | 'Error';
sandboxState: 'Provisioning' | 'Active' | 'Suspending' | 'Suspended' | 'Resuming' | 'Terminated' | 'Unknown';
messages: ChatMessage[];
streamingBlocks: TurnBlock[]; // 当前 turn 内的 reasoning / message / tool 块
attachments: AttachmentInfo[];
attachmentTokenBudget: { used: number; total: 50000 };
isLoading: boolean;
pendingQuestion: PendingQuestion | null;
pendingPermission: PendingPermission | null;
}
- 本地 UI 状态:
selectedModel/executionMode/selectedModelConnectionId都是 client-side,每次发消息时随 request 带过去 - 从后端来的状态:
interactiveState/sandboxState(详见 Q24)经 Get Session API 拿到,活动流期间根据 SSE 事件本地推进 - 不暴露给 React:MWC token、host URL、CE artifact 坐标(workspaceId / artifactId)—— 都在 CopilotHostService 内部
- 用 React 内置
useState/useReducer/ Context 管理;不引入 Redux
⑥ 组件库:@fabric-msft/copilot-react
Chat Pane 基于 Client Engineering Systems 提供的 @fabric-msft/copilot-react—— 成熟、对齐 Microsoft Copilot Design 规范,并按 Fabric 视觉风格定制。无需自造组件。
⑦ 关键不变量
- 🔐 React 永远不直接调后端 —— 所有 HTTP / SSE / token 都在 Angular 层
- 📡 SSE 处理拆两阶:Angular 拦截
tool.call→ 本地 dispatch → 合成事件 → React - 🔁 Context 和 Tool manifest 都由
CopilotHostService收集(详见 Q22 / Q23) - 🎨 用 Fluent UI v9 +
@fabric-msft/copilot-react,不引入 VS Code Copilot Chat 那套 - 📐 React 用 hooks + Context,不引入 Redux
📄 依据:engineering/300-frontend/300-!!-frontend-dev-design.md §4-§6.9(架构图、CopilotPaneApi 接口、CopilotUIEvent 类型、React state model)。
Context 系统:前端怎么把"用户正在做什么"告诉 LLM?
LLM 需要知道当前 workspace / artifact / 用户在 page 内选了什么。前端按规则自动从 Shell + 当前 page 收集 context,合并、缓存,并随每条消息发给后端。用户看到的"context pill"和发给后端的 payload 是两个不同形状。
① Context 的 3 类
| 类型 | 例子 | 谁提供 |
|---|---|---|
| Workspaces 已打开 | 当前 active workspace(通常 1 个) | Shell |
| Artifacts 已打开 | 1 个 Report + 1 个 Notebook + 1 个 Lakehouse | Shell(列表)+ Page(详细) |
| Custom Context | SQL 编辑器选了 21-29 行 / Notebook 焦点 cell / 当前打开的 Admin 设置页 | 当前 active page |
customContext 三层 scope:workspace-related / artifact-related / global,分别挂在 workspaces[].customContext / artifacts[].customContext / top-level customContext 数组里。
② End-to-End 流程
③ 两种形状:客户端 (有 pill 元数据) vs 服务端 (纯数据)
客户端 shape(含 UI 用的 reference 字段):
interface CopilotContext {
workspaces: CopilotWorkspaceContext[];
artifacts: CopilotArtifactContext[];
customContext: CopilotCustomContextEntry[]; // global
}
interface CopilotCustomContextEntry {
descriptor: string; // snake_case,作为 server shape 里的属性名
value: object; // LLM 看到的数据
reference?: { // 可选 UI 元数据 —— 有它才渲染成 pill("explicit")
label: string; // 仅 bare label;前缀由平台拼
icon: PortalSvgIcon;
dismissible: boolean;
};
}
服务端 shape(无 pill 概念,customContext 折叠成 Record):
interface CopilotContextPayload {
workspaces: CopilotWorkspaceContextPayload[];
artifacts: CopilotArtifactContextPayload[];
customContext: Record<string, object>; // descriptor → value 的 map
}
两个推论:
- descriptor 必须 scope 内唯一—— 重复会在转 Record 时静默覆盖
- pill 状态完全不影响 payload—— 用户 dismiss / workload 没设 reference,LLM 看到的数据一致
④ Pill 渲染规则
| 来源 | 是否要 reference 才出 pill | 显示 |
|---|---|---|
| workspaces[*] | 否(自动) | workspace 显示名 + workspace 图标,dismissible |
| artifacts[*] | 否(自动) | artifact 显示名 + 类型图标,dismissible |
| artifacts[i].customContext[*] | 要 | <Artifact>: <label>(如 SQL 1: Line 21-29) |
| workspaces[i].customContext[*] | 要 | <Workspace>: <label> |
| top-level customContext[*] | 要 | <label>(如 Admin tenant settings) |
💡 workload 只给 bare label(如 Line 21-29),平台自动加父级前缀(SQL 1:)—— 这样改 artifact 命名规则只动平台。
⑤ CopilotManifest:page 怎么注册 context provider
interface CopilotManifest {
name: string;
featureSwitch?: boolean | string;
match: (url: string) => boolean | undefined; // 当前 URL 匹配才参与
getContext: PortalAction | ExtensionAction; // dispatched 后返回 Partial<CopilotContext>
}
- URL-scoped:只有
match()返回 true 的 第一个 manifest 参与(context 与当前 URL 关联) - featureSwitch:falsy 的 manifest 不参与
- getContext:classic artifact 用
PortalAction(Angular DI handler),extension artifact 用ExtensionAction(跨 iframe 经 postMessage)
⑥ refreshContext:何时被调?
| 触发 | 谁能触发 | 典型场景 |
|---|---|---|
| 自动(平台) | — | 新 session 创建时 1 次 + 每条 Send Message 之后 1 次(用户 dismiss 的 pill 也借此回来) |
| On-demand(Shell) | Shell | 用户切 workspace / 切 artifact |
| On-demand(Active Page) | 当前匹配 manifest 的 page | SQL 编辑器换选区、Notebook 切焦点 cell(throttled,不会按键回调) |
| 非 active page 调用 → 静默丢弃 | ||
⑦ Strip-Before-Send + Pill Suppression
发消息前的 2 步处理:
- 从缓存 client-shape 出发,先应用 pill suppression list(用户在本次会话里 dismiss 过的 entry 不发)
- 遍历对象,移除每个
reference字段;customContext 数组折叠成 Record
suppression list 每次 send 完清空—— workload 只要下次 getContext 还返回这个 entry,pill 就回来。
⑧ Size Limit
序列化后的 context payload 必须 ≤ 8 KB。超了:丢弃 page-specific,只发 Shell context。Shell 自己就超 → 写 warning(视为 Shell bug)。
⑨ 例子:SQL 编辑器选了 21–29 行
用户在 SQL 编辑器选了 21-29 行,背景还开着 1 个 Notebook + 1 个 Lakehouse;提示词 "把选中的 SQL 改成从 Lakehouse 1 读数据"。
客户端合并后渲染的 pill:
server payload(strip 后)—— LLM 看到:3 个 artifact(含 SQL artifact 的 selected_query 字段+ full_query + editor_state),但没有任何 pill 字段。
⑩ 关键不变量
- 🎯 Context 是 URL-scoped:只有当前匹配 manifest 的 page 贡献 page-specific
- 👁 Pill 形状 ≠ payload 形状:UI 状态(reference / dismiss)不影响 LLM 看到的数据
- 🪒 8 KB 序列化上限,超了优先保 Shell context
- 🔁 自动 refresh:新 session + 每发完一条消息(不要 workload 自己 watch 重置)
- 🐌 On-demand refresh 必须 throttled(不许 keystroke 触发)
📄 依据:engineering/300-frontend/300-!!-frontend-dev-design.md §6.6 Context(类型定义、pill 规则、refresh 触发、SQL 例子)。
Tools / Models / Modes:3 个用户可控的会话参数怎么传到后端?
每条 Send Message 都带 3 类参数:能调什么工具(tools)、用哪个模型(model triple)、怎么用工具(mode)。三者各有不同的注册 / 选择 / 生命周期。
① Tools(客户端工具)
关键约束:Session-scoped,全 portal 一份静态 manifest
GitHub Copilot SDK 当前只在首条消息时设 SessionConfig.Tools,session 内不可动态增减。为了配合这点:
- 整个 portal 只有一份静态
CopilotToolManifest(不是每 page 一份) - 所有 team(Shell / Report / Notebook / OneLake / …)的工具都列在这一个文件里
- 未来 SDK 支持动态再换 API;当前先简单 → 用 featureSwitch 控制启用
ToolDefinition 接口
interface ToolDefinition {
readonly name: string; // 全 portal 唯一
readonly description: string;
readonly parameters: JSONSchema;
readonly invoke: PortalAction | ExtensionAction;
readonly abort: PortalAction | ExtensionAction;
readonly featureSwitch?: boolean | string;
}
interface CopilotToolManifest {
readonly tools: readonly ToolDefinition[];
}
name / description / parameters 发给后端作 clientTools;invoke / abort 留在前端,由 CopilotHostService 在 SSE 收到 tool.call 时 dispatch。
调用周期(7 步)
- 后端 SSE
tool.call { toolCallId, tool, input } - Angular 合成
client_tool.start给 React → Chat Pane 渲染 Tool Card(running) - 查 manifest:
- 命中:
executeAction(tool.invoke)走 portal action handler(classic)或 postMessage(extension) - 未命中(page 已卸载):合成错误结果
{ status: 'error', error: 'Tool not available...' }
- 命中:
- 拿到结果
POST /tool-result { toolCallId, result }- 合成
client_tool.complete给 React → Tool Card 状态变 complete / error - 后端把 result 喂给 LLM,LLM 可能继续调更多 tool 或出最终答复
② Models(连接式架构)
3 类 Provider,每个 connection 提供多个 model
| Provider | connection 来源 | connectionId |
|---|---|---|
GitHubCopilot | 用户的 GitHub Copilot 订阅 | DMTS 管理 |
AzureAIFoundry | 用户的 Azure AI Foundry resource | DMTS 管理 |
FabricCapacity | workspace 所在 capacity 内置 | null(不需要 connection) |
每条消息发 model triple
用户在 picker 选一个 model,前端内部解析成 (modelConnectionId, modelProviderType, modelId),作为 3 个独立字段发到 Send Message:
{
"message": "Refactor this notebook ...",
"model": "gpt-5.1",
"modelProviderType": "AzureAIFoundry",
"modelConnectionId": "conn-xxx",
"mode": "interactive",
...
}
Model 发现 API:走 Shared
用 userEntraToken 调(不需要 MWC token):
| API | 路径 | 返回关键字段 |
|---|---|---|
| List All Models | GET /metadata/unifiedcopilot/models?workspaceId={wsId} | connections[] + default { connectionId, modelId } + lastUsed |
| List Connections | GET .../connections?workspaceId={wsId} | per-connection status / billing / scope |
| List Models / connection | GET .../connections/{cid}/models | 具体某 connection 下的模型集 |
Model 元数据驱动 UI
supportsVision:false 时禁用图片附件按钮costIndicator:low/medium/high—— picker 可显示成本提示supportedReasoningEfforts:数组(low | medium | high)+defaultReasoningEffort—— 是否暴露给用户调还是 Open Question- Connection
status:Available / Warning / Unavailable;status code 如AuthExpired/PermissionDenied/EndpointUnreachable/CapacityNotAssigned→ 决定要不要弹"重新授权"
切模型
Model 是本地 UI 状态,每发一条消息时随 request 带过去。首条消息确定 session 初始 model;中途切了,下一条消息生效(MWC 更新 session metadata)。
③ Modes(执行模式:3 选 1)
| Mode | 行为 | Tool 审批 |
|---|---|---|
interactive 默认 | LLM 边规划边执行,每个工具调用前都问用户 | 有:permission.request 事件 |
plan | LLM 只出计划文本,不执行任何工具 | 不涉及 |
autopilot | LLM 执行工具不问用户 | 无 |
Mode 是客户端本地状态,作为 mode 字段发给后端,不存为 session 属性。
Plan → Execute 转换
当前是 plan 模式 + 收到 session.idle(LLM 出完计划)→ 在最后一条 assistant message 上渲染两个按钮:
- Start implementation → 切到
interactive模式 → 自动发{ message: "Execute the plan.", mode: "interactive" } - Start with autopilot → 切到
autopilot模式 → 自动发{ message: "Execute the plan.", mode: "autopilot" }
这套用户行为对齐 GitHub Copilot CLI / VS Code Chat,体验跨 surface 一致。
④ 三者怎么打包发到后端
POST {mwc}/.../CopilotEnvironments/{ceId}/sessions/{sid}/messages
Authorization: Bearer {mwcToken}
{
"message": "...",
"mode": "interactive",
"model": "gpt-5.1",
"modelProviderType": "AzureAIFoundry",
"modelConnectionId": "conn-xxx",
"context": { /* workspaces / artifacts / customContext */ },
"clientTools": [ { name, description, parameters }, ... ],
"attachments": [ /* inline images (base64) */ ]
}
⑤ 关键不变量
- 🔒 Tool manifest 是 portal-wide 静态 + featureSwitch 控制,session 内不变(SDK 限制)
- 🪪 Model 三元组(connectionId + providerType + modelId)每条消息都发,session 内可换
- 👁
supportsVision = false→ 禁用图片附件按钮 - 📋 Plan / Interactive / Autopilot 是客户端状态,每条消息发一次
- ✨ Plan → Execute 是 UX 糖:自动发 "Execute the plan." 并切 mode;用户也能手动改
📄 依据:engineering/300-frontend/300-!!-frontend-dev-design.md §5.3-5.5(Send Message + Models API)+ §6.4-6.5(Model / Mode)+ §6.7(Client Tools)。
会话状态机:interactiveState × sandboxState(11 种 UX 组合)
一个 session 有两个独立的状态维度。它们组合起来决定 Chat Pane 该显示什么 spinner / 卡片 / banner。这是把 Q16 容器生命周期和 SDK 运行态拆开的最干净视角。
① 两个维度
interactiveState — SDK / Copilot 在做什么
| 值 | 含义 |
|---|---|
Starting | SDK 初始化中(容器刚 active,agent 还没接管) |
Running | 正在处理 prompt 或执行 tool |
Idle | 就绪,等下一条消息 |
WaitingForInput | Agent 等用户回答 user_input.request |
WaitingForApproval | Agent 等用户审批 permission.request |
Error | SDK 不可恢复错误 |
sandboxState — ADC sandbox / 容器在做什么
| 值 | 含义 |
|---|---|
Provisioning | microVM 正在建 |
Active | 容器跑着 |
Suspending | 正在 export + snapshot |
Suspended | 已暂停(memory snapshot 存好),下条消息触发 resume |
Resuming | 从 snapshot 恢复中 |
Terminated | 已销毁,要 cold restore(从 OneLake 重放 events.jsonl) |
Unknown | 心跳丢,正在排查 |
② 组合显示矩阵(Chat Pane 渲染什么)
sandboxState | interactiveState | Chat Pane 显示 |
|---|---|---|
Provisioning | — | "Creating session…" spinner |
Active | Starting | "Starting…" spinner |
Active | Running | "Thinking…" spinner(流式 reasoning / message) |
Active | Idle | ✓ Ready(可发新消息) |
Active | WaitingForInput | 显示 Question Card |
Active | WaitingForApproval | 显示 Approval Card |
Active | Error | 红色 error banner + 重试 |
Suspended | (保留) | "Paused" badge — 发消息会自动 resume |
Resuming | — | "Resuming session…" spinner |
Terminated | — | "Session ended" — 提示 restore 或新建 |
Unknown | — | "Reconnecting…" spinner |
③ 状态从哪里来?
初始 / 不在活动流:调 Get Session API
GET {mwc}/.../CopilotEnvironments/{ceId}/sessions/{sid}
→ { sessionId, displayName, model, interactionState, sandboxState?, ... }
interactionState 字段映射到 interactiveState;当 sandbox 不是 Active 时返回 sandboxState。
活动 SSE 流中:根据事件本地推进
| SSE 事件 | 状态变化 |
|---|---|
session.provisioning | sandboxState → Provisioning |
session.created | sandboxState → Active |
session.resuming | sandboxState → Resuming |
assistant.turn_start | interactiveState → Running |
user_input.request | interactiveState → WaitingForInput |
permission.request | interactiveState → WaitingForApproval |
session.idle | interactiveState → Idle |
session.error | interactiveState → Error |
session.expired / session.reauth_required | 同时关流并触发 reauth UX |
④ 自动暂停 / 恢复:用户不用管
MWC sweeper 在以下情况主动暂停(详见 Q15 "什么时候发 POST /export"):
Idle≥ 30 分钟 → Suspending → Suspended(memory snapshot + OneLake export)WaitingForInput/WaitingForApproval≥ 2 小时 → 同上Error≥ 15 分钟 → Suspending → Terminated(彻底销毁)Running永远不暂停(不打断 agent 思考链)
用户回来发消息 → MWC 看到 Suspended → 自动 Resuming(memory mode)→ Active;resume 失败 → cold restore(PUT 新 sandbox + POST /import)。对用户完全透明,只在 spinner 上看一下"Resuming session…"。
⑤ "Session 不死,Container 用完即弃"
架构上 fabricSessionId 是永久(OneLake 文件夹名),sandboxId 在生命期内可能换多次(每次 cold restore 是新 microVM)。也就是说同一 sessionId 时间维度上 1:N sandboxId。详见 Q8。
⑥ 关键不变量
- 🔢 两个维度独立—— 一个 Suspended 的 sandbox 可以"保留"上次的 interactiveState,并不重置
- 📺 Chat Pane 把组合渲染到唯一一个状态条 / 卡片,不让用户面对两个状态字段
- ⏱ 自动 suspend 阈值:Idle 30min / Waiting 2h / Error 15min
- 🛡 Cold restore 走 OneLake export(events.jsonl 重放),即使容器被销毁也能恢复对话
- 🔁 Outside SSE → 轮询 Get Session;inside SSE → 按事件本地推进
📄 依据:engineering/300-frontend/300-!!-frontend-dev-design.md §6.1(Session State + Combined UX Display + Suspension & Resume)+ 014-D-session-state.md(状态机后端实现 + sweeper 阈值)。
"Copilot CLI" 在 Fabric Copilot 里到底指什么?
一句话结论
"Copilot CLI" 在这个 codebase 里同时指三个东西,互相不要混:
① GitHub Copilot CLI(外部产品)—— 用户在终端跑的 gh copilot,是 Fabric 之外的平行 surface,iteration 1 的首发 UX;
② GitHub Copilot SDK / Agent(同源引擎)—— 装在 ADC Container 里的 LLM 编排引擎,是 Fabric portal 内每个 session 的"大脑"。Cristian 原话:「the orchestrator is the Copilot CLI agent」;
③ CLIPlayground(内部工具)—— 仓库 Playground/CLIPlayground/,团队开发用的 Node.js REPL,包装 Copilot SDK + Fabric Skills 做本地试。
"SDK 才是核心产品,CLI 和 Web 都只是 hoster"—— Decision D-6(decisions.md)。理解这句,就理解了三者的关系。
1. 同一颗"大脑",三种装法
2. 三个意义详解
① GitHub Copilot CLI(公开产品,iteration 1 首发的 Fabric UX)
| 维度 | 内容 |
|---|---|
| 定位 | Fabric portal 之外的平行 surface — 让 power user 在终端直接用 Fabric 能力 |
| 关键决策 | D-3 (2026-02-26):iteration 1 的初始 UX 就是 GitHub Copilot CLI;先用文本 UI 验证 Skills + MCP 模型,再投入更复杂的图形化界面 |
| 典型命令 | gh copilot start --workspace "Sales Analytics" --item "DW_Sales" --type warehouse(启动一个 workspace/item scoped session) |
| Skills 来源 | 本地 clone 的 fabric-skills repo(GitHub 上的"single source of truth");CLI 在 session 启动时把对应 Workload + Item-type 的 plugins 注入 system prompt |
| Session 存储 | Workspace-bound → 持久化到该 workspace 的 Copilot item;不 scope → ephemeral(不持久化) |
| 认证 | Standard device-code flow(同 gh auth login) |
| 跟 Fabric portal 关系 | D-7:两条 disjoint experience,不做 session continuity。CLI 起的 session 不会出现在 portal 侧栏,反之亦然(P0 范围内) |
| 执行环境 | 不走 ADC!本地 Copilot CLI 在用户机器上跑 — 没有 microVM 隔离,工具调用受 CLI 自身权限影响(见 specs/stages/local-agent-dev.md "Execution" 一行:CLI = Local AI orchestrator) |
② GitHub Copilot SDK / Agent(装在 ADC Container 里,Fabric portal 的"内核")
| 维度 | 内容 |
|---|---|
| 定位 | Fabric portal Copilot 的真正执行引擎 — 每个 ADC microVM 里都跑着一份 GitHub Copilot SDK(agent loop + tool calling + SSE 协议) |
| 关键决策 | D-6 (2026-02-24):SDK is the core product — CLI 和 web 都是 hoster;orchestrator 本质上就是 Copilot CLI agent,sub-agents 是 plugins |
| 调用路径 | Browser → MWC POST /messages → Container /responses → Copilot SDK loop → 决定调 tool / 输出 message → SSE 回流 |
| 跟 ① 的关系 | 同一颗内核,不同 hoster。CLI 在终端跑,ADC Container 在云上跑;SDK 自身一致,差别在 hoster 提供的 I/O 和 storage |
| 持久化 | InMemorySessionFs → idle 30 min 触发 Export → OneLake events.jsonl(Q15) |
| 跟用户感知 | 用户在 portal 里聊天时感觉不到 SDK 存在,只看到 Chat Pane + Copilot 回复;SDK 完全在容器内 |
③ CLIPlayground(仓库内部工具,给团队开发用)
| 维度 | 内容 |
|---|---|
| 位置 | Playground/CLIPlayground/ — Node.js REPL prototype |
| 目的 | 本地试 Fabric Skills 而不用启 Fabric portal / MWC / ADC;skill 作者写完 SKILL.md 立刻可试 |
| 实现 | 包装 GitHub Copilot SDK(同 ②)+ FabricSkills/skills 目录里的 SKILL.md 作为 system prompt |
| Provider | 可切:github-models(PAT)/ openai / copilot-cli(fall back 到 gh copilot auth token) |
| 跟生产关系 | 不连 ADC、不连 OneLake、不持久化 session;纯开发体验工具 |
3. 怎么判断文档/会议里说的是哪个
| 线索词 | 大概率指 |
|---|---|
| "gh copilot"、"terminal"、"device code login"、"local"、"iteration 1" | ① 公开产品 |
| "SDK"、"agent loop"、"Container"、"/responses"、"InMemorySessionFs"、"orchestrator" | ② SDK / Agent in Container |
| "Playground"、"REPL"、"npm start"、"GITHUB_TOKEN"、"skill 本地试" | ③ CLIPlayground |
| "SDK is the core product"、"CLI and web are just hosters" | 跨①②的设计原则(D-6) |
4. 常见混淆与正解
| 误解 | 正解 |
|---|---|
| "Fabric portal 里点 Send 也是走 Copilot CLI" | ❌ portal 走的是Container 内的 Copilot SDK,不是 gh copilot。两个 hoster 共享同一颗 SDK 内核 |
| "我从 CLI 起的 session 能在 Fabric portal 侧栏看到" | ❌ D-7 决定不做 CLI ↔ Portal session continuity(至少 P0 范围内)。两条独立 experience |
| "CLI 也用 ADC microVM 隔离" | ❌ CLI 在用户本地机器跑(Local AI orchestrator),没有 ADC。这也是为什么有人提议"local-only CLI first, hosted later"来规避多租户安全 |
| "CLIPlayground 就是 GitHub Copilot CLI" | ❌ 完全不同。Playground 是 Fabric 团队的 dev REPL,跟外部 gh copilot 命令不是一个 binary。它只是"借" SDK 来跑,给 skill 作者本地调试 |
| "VS Code 里的 Copilot 跟 Fabric Copilot 是一回事" | ❌ VS Code Copilot 扩展也是 SDK 的另一个 hoster(D-6)。能力上类似 ① CLI surface — local + workspace-bound session,不连 ADC |
📄 依据:decisions.md(D-3 首发 CLI、D-6 SDK 是核心、D-7 无 CLI↔Portal continuity)+ 2026-02-24 Cristian meeting("orchestrator is the Copilot CLI agent")+ specs/building-blocks/local-development.md(CLI / VS Code / Claude 三 local surface)+ specs/stages/local-agent-dev.md(Local Agent Dev stage:CLI、Copilot、Claude;execution = Local AI orchestrator)+ Playground/CLIPlayground/README.md(内部 REPL 包 SDK + Skills)。
附录 A — 关键 API 速查
ADC Control Plane(MWC 调)
| 方法 | 路径 | 用途 |
|---|---|---|
| PUT | /sandboxes | 创建 microVM(disk image + egress + ports + labels) |
| GET | /sandboxes/{id} | 查询状态(含 ports[].url = containerUrl) |
| POST | /sandboxes/{id}/stop | 暂停(Memory-mode snapshot,进程状态保留) |
| POST | /sandboxes/{id}/resume | 从 snapshot 恢复(~1s 热路径) |
| POST | /sandboxes/{id}/snapshot | 手动 checkpoint(Memory / Disk 两种模式) |
| POST | /sandboxes/{id}/egresspolicy | 更新 egress 规则(token 刷新时换 SecretRef 引用) |
| DELETE | /sandboxes/{id} | 终结 sandbox(自动 GC secret/snapshot) |
| PUT | /secrets/{id} | 存 token 到 KeyVault(PUT 是全替换,see SecretRef in Q11) |
| PUT | /diskimages | 注册容器镜像(CI/CD 阶段,按 cluster 缓存) |
Agent Container(MWC 调容器)
| 方法 | 路径 | 类型 | 用途 |
|---|---|---|---|
| GET | /health | System | liveness probe |
| POST | /configure | System | 下发 refreshTokensAfterUtc + tokenExpirations(每次创建 / token 刷新 / resume) |
| POST | /export | System | 把 InMemorySessionFs 整段 base64 dump 给 MWC |
| POST | /import | System | 从 OneLake export 预填 VFS(cold restore) |
| POST | /responses | User | 主入口,SSE 长连 |
| POST | /approve | User | 响应 permission.request |
| POST | /user-input | User | 响应 user_input.request |
| GET | /messages | User | 拉取历史(user/assistant 过滤) |
⚠️ 注:早期文档(065-agent-design.md)曾出现 POST /update-tokens 和 GET /readiness,但当前 040-container-spec.md 是权威——token 刷新统一走 /configure,readiness 不再单独暴露。
SSE 事件清单
| 事件 | 含义 |
|---|---|
session.provisioning | 沙箱创建中 |
session.created | 会话就绪 |
assistant.turn_start | 开始一轮 |
assistant.message_delta | 流式 token |
tool.call / tool.start / tool.progress / tool.complete | 工具生命周期 |
permission.request | 请求审批 |
user_input.request | 请求用户输入 |
artifact.created / artifact.updated | 产物变化 |
session.idle / session.error / session.expired / session.reauth_required | 会话状态 |
session.renamed / session.resuming | 恢复/重命名 |
附录 B — 参考文档清单
所有链接指向 GitHub repo: azure-data-intelligence-platform/fabric-ai-framework
顶层
- README.md — 4 层架构表
- decisions.md — 12 项 D-1..D-12 决策
Engineering / Decisions
- engineering/000-decisions/001-decisions.md
- engineering/000-decisions/002-adc-vs-foundry-vs-aci.md
- engineering/000-decisions/003-A-proxy-vs-noproxy.md — SecretRef JSON 示例
- engineering/000-decisions/004-session-storage.md
Architecture
- engineering/010-architecture/010-!!-architecture.md
- engineering/010-architecture/011-sessions-responsibilities.md
- engineering/010-architecture/013-sessions-store.md
Agent
Education / Deployment / Frontend
- engineering/.education/adc-apis-reference.md
- engineering/100-solution-deployment/100-solution-deployment.md
- engineering/300-frontend/300-!!-frontend-dev-design.md
Specs / Building Blocks & Features
- specs/building-blocks/plugin.md
- specs/building-blocks/plugin-contribution.md
- specs/building-blocks/copilot-item.md
- specs/features/08-change-approval.md
- specs/features/09-copilot-item.md
- specs/features/13-inline-tool-ui-platform.md
- specs/features/14-evaluation-platform.md