Fabric AI Framework — Deep Dive Q&A

基于 azure-data-intelligence-platform/fabric-ai-framework 仓库的架构、E2E 流程、技术选择详解

📚 25 个深度问题 🇨🇳 中文解答 🔗 含文件引用 🔒 Microsoft 内部参考

🔗 github.com/azure-data-intelligence-platform/fabric-ai-framework

架构总览 · 4 层模型 + 3 大支柱

4 层抽象

层级名称职责面向人群
L1Agents对话循环、规划、Tool 调用编排(Copilot SDK 单一 Fabric Agent + 插件作为 sub-agent)End User / Workload PM
L2Skills(Markdown)声明式 prompt + tool 组合,教 Copilot 怎样为 workload 写出正确代码(CDN 加载)Workload Team / Domain SME
L3MCP Servers无状态工具端点(CRUD / Execute / Observe),让 Copilot 创建资源、执行作业、读取状态Workload Team
L4REST APIsFabric 平台既有 API;由 MCP servers 或生成代码间接调用已存在

📌 心智模型:Skills 教 Copilot 该怎么做;MCP servers 让 Copilot 真的去做并看到结果。两者协同实现 "生成 notebook → 跑 → 检查结果 → 修错 → 进入下一任务" 单会话连贯。

3 大实施支柱

支柱 1 ADC (Agent Dev Compute, 即 Azure Dev Compute — 对外产品名 Azure Container Apps SandboxesMicrosoft.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 架构图

┌─────────────────────────────────────────────────────────────────────┐ │ Surfaces: Fabric Portal (Angular Shell + React Chat Pane) │ │ VS Code Copilot / GitHub Copilot CLI │ └──────────────┬──────────────────────────────┬───────────────────────┘ │ userEntraToken │ mwcToken │ (CE CRUD / Models / List) │ (Sessions / Messaging / ▼ │ Tools / Attachments) ┌─────────────────────────────┐ │ │ Shared Service │ ▼ │ - Copilot Env (CE) CRUD │ ┌─────────────────────────────────┐ │ - generatemwctoken │ │ MWC (Per-Capacity Workload) │ │ - Models / Connections │ │ - Session CRUDL │ │ - Global session listing │ │ - SSE event stream │ │ - 不在消息热路径 │ │ - Secret PUT → KeyVault │ └─────────────────────────────┘ └─────────────┬───────────────────┘ │ copilotWorkload1pToken ▼ ┌─────────────────────────────────┐ │ ADC Control Plane │ │ PUT /sandboxes (microVM) │ │ PUT /secrets (KeyVault) │ │ + Envoy egress rules │ └─────────────┬───────────────────┘ ▼ ┌─────────────────────────────────┐ │ Firecracker microVM │ │ ┌───────────────────────────┐ │ │ │ Container (Agent host) │ │ │ │ - ASP.NET Core │ │ │ │ - GitHub Copilot SDK │ │ │ │ - Skills (CDN cache) │ │ │ │ - InMemorySessionFs │ │ │ └───────────────────────────┘ │ │ Envoy sidecar (TLS MITM, │ │ Bearer 注入, allow-list) │ └─────────────┬───────────────────┘ ▼ OneLake (workspace/<CopilotEnvironment artifact>/sessions/…)

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。

  1. 用户第一条消息(lazy session 创建) — Browser 已选定一个 Copilot Environment artifact(CE)作为 session 容器。Browser 发 POST {mwcBase}/public/workspaces/{wsId}/CopilotEnvironments/{ceId}/sessions/messagestargetSession:"new" + 客户端生成的 draftSessionId)→ MWC ① 先 write-ahead metadata.json 到 OneLake → ② ADC PUT /sandboxes 拿到 sandboxId + containerUrl → ③ ADC PUT /secrets 存 OBO token(SecretRef)→ ④ Container POST /configure 下发 refreshTokensAfterUtc + tokenExpirations。SSE 流先推 session.provisioning,sandbox 就绪后推 session.created { sessionId, draftSessionId }
  2. 容器启动 + skills 加载 — Container 内的 SkillsLoader 调 Fabric Shared API GET /skills/manifest,按返回的 CDN URL 拉 SKILL.md 包并拼到 system prompt;MCP server 端点在配置就绪后由 McpServerRegistry 注册并 health check(详见 Q19)。
  3. 消息转发(SSE 长连) — MWC 内部把这条消息发给容器:POST {containerUrl}/responses(用 copilotWorkload1pToken,audience: fabric/<container-id>)→ 容器流式回 SSE 给 MWC,MWC 透传给 Browser。
  4. Tool 调用 / 外部出站 — SDK 决定调 MCP / bash;所有出站走 Envoy egress sidecar,按 egressRules + secretRef 在 TLS MITM 后注入 Authorization: Bearer,容器进程永远看不到明文 token(详见 Q11/Q18)。
  5. 审批 / 用户输入 — 高风险动作发 permission.request SSE,前端弹审批卡;用户回 POST /permissions/{id}/respond,流恢复。
  6. 每轮持久化(热路径) — 每条消息:① 容器把 events 追加到 InMemorySessionFs(纯内存 ConcurrentDictionary,不落盘);② MWC 只更新 OneLake state.json 两个字段(messagesCount + lastInteractionAt);events.jsonl 此时不落 OneLake
  7. 状态机驱动的 Export(冷路径) — MWC sweeper 检测 Idle ≥ 30min / WaitingFor* ≥ 2h → ① POST {containerUrl}/export 让容器把 InMemorySessionFs 整段 base64 dump 返回 → ② MWC 用 Fabric workload identity PUT 到 OneLake sessions/<id>/ → ③ ADC POST /sandboxes/{id}/stop(Memory-mode snapshot)。
  8. 恢复 — 用户回来发新消息 → MWC POST /sandboxes/{id}/resume(hot path ~1s);若 snapshot 不可用 → Cold restore:PUT /sandboxes 新建 + Container POST /import 从 OneLake 回放 events.jsonl(详见 Q15/Q16)。

Q1

为什么要使用沙箱(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)。

Q2

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 启动,注册 DI
  • CopilotAgentService — 顶层服务,把 HTTP 请求翻译为 SDK 调用
  • CopilotClient — 包装 GH Copilot SDK
  • CopilotSession — 内存中会话状态机
  • InMemorySessionFs — 容器内虚拟文件系统(agent-state / session-state 双层)
  • SkillsLoader — 从 CDN 拉 SKILL.md,本地缓存
  • McpServerRegistry — 注册 / 健康检查 MCP server

HTTP 端点(容器对外)

端点类型用途
GET /healthSystemliveness
POST /configureSystem下发 refreshTokensAfterUtc + tokenExpirations;token 刷新统一走这里
POST /exportSystem把 InMemorySessionFs 整段 base64 dump 给 MWC
POST /importSystem从 OneLake export 预填 VFS(cold restore)
POST /responsesUser主入口,SSE 长连
POST /approveUser响应 permission.request
POST /user-inputUser响应 user_input.request
GET /messagesUser拉取历史(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/
Q3

Shared Service 不在消息路径上 是什么意思

"消息路径" = 用户 → AI → SSE → 用户 这条 高频低延迟 数据通路。

Shared 做什么 / 不做什么

Shared 做Shared 不做
职责Artifact 表 CRUD
Copilot Item 注册
跨 capacity 元数据
消息中转
SSE 推流
Token 注入
调用频率会话创建/重命名/删除时一次
延迟敏感

真正消息路径

Browser ──HTTPS──> MWC(per-capacity)──HTTPS──> 容器 /responses ▲ │ │ └──── SSE ─────────┘ │ └─> LLM / MCP / Bash

📄 依据:engineering/010-architecture/011-sessions-responsibilities.md 明确把 Shared 限定在 "artifact registry"。

Q4

Copilot workload 与其他 workload(如 Notebook)的区别 / 与 Shared 的关系

三方对照

Copilot Workload (MWC)Notebook Workload (MWC)Shared Service
部署per-capacityper-capacity全球单实例
职责会话编排 / SSE / token / ADC 调用Notebook 执行 / Kernel 管理Artifact 注册
是否在消息路径是(自己消息路径)
是否使用 ADC是(容器宿主)否(用自己 compute)
暴露 MCP给 Copilot 用

关系图

┌──────────────────┐ │ Shared Service │ ← 所有 workload 都来登记 artifact └────────┬─────────┘ │ ┌──────────────────────┼──────────────────────┐ ▼ ▼ ▼ ┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Copilot MWC │ │ Notebook MWC │ │ Lakehouse MWC … │ │ (per cap) │ │ (per cap) │ │ │ └───────┬───────┘ └────────┬────────┘ └─────────────────┘ │ 调 MCP │ 暴露 MCP └─────────────────────┘

关键:Copilot workload 把 Notebook workload 当作"MCP Server 提供者"消费;两者通过 Shared 协调 artifact 关系。

Q5

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 行的资源拓扑。

Q6

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 app Azure Dev Compute Proxy、GitHub org github.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 系(开发者工具门面)面向开发者的高层工具 / CLIAzure Developer CLI (azd)、Azure Developer Hub

ADC 属于前者—— 它是 infrastructure-level microVM 平台,跟 Dev Box / DevOps 是同族;不是 Developer CLI 那类前端工具。

📘 公开资料

Internal Flow vs ARM Flow:data-plane host 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 容器
secretsKeyVault 集成;以 secretRef 在 egress 规则里引用,容器永远看不到明文
diskimages容器镜像注册(CI/CD 阶段,按 cluster 缓存;可 Dockerfile 编译或指 OCI registry)

ADC 与 Fabric AI Framework 的关系

┌──────────────── Fabric AI Framework(我们)──────────────┐ │ MWC (per-capacity, Copilot Workload) │ │ ┌───── 调 ADC Control Plane ─────────────────────────┐ │ │ │ PUT /sandboxes / POST /sandboxes/{id}/stop /resume │ │ │ │ PUT /secrets / POST /sandboxes/{id}/egresspolicy │ │ │ └────────────────────┬───────────────────────────────┘ │ └─────────────────────── │ ─────────────────────────────────┘ ▼ ┌──────────────── ADC 平台(不是我们)──────────────────────┐ │ ① Firecracker microVM lifecycle │ │ ② Envoy egress sidecar(TLS MITM + secretRef 注入) │ │ ③ KeyVault Secret API │ │ ④ Disk image registry & cluster-level cache │ │ ⑤ Memory-mode snapshot / restore │ │ ⑥ autoSuspend / autoDelete 平台级兜底 │ └──────────────────────────────────────────────────────────┘

为什么是 ADC,不是别的

选项结论原因
ADC(已选)Firecracker microVM = 硬件级隔离;自带 egress proxy + secret 注入;MS 内部一等公民
AI Foundry偏 training / fine-tuning,不是 agent runtime;没有 egress proxy 模型
ACI普通容器,无 microVM 隔离;自己得做 secret / egress / snapshot
AKS / 自建运维负担巨大;安全模型要从零搭
Q7

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 typeCopilotEnvironment(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 决策)。

Q8

sessionId / sandboxId / containerUrl 三者关系

ID谁分配生命周期形态
fabricSessionIdMWC 创建会话时生成永久(写入 OneLake 文件夹名)GUID
sandboxIdADC PUT /sandboxes 返回会话活跃期间(idle 后可销毁/重建)ADC 内部 ID
containerUrlADC 同时返回跟随 sandboxhttps://{sandboxId}--{port}.proxy.azuredevcompute.io

关系

fabricSessionId (1) ─── (1) sandboxId (1) ─── (1) containerUrl │ │ │ 永久存在 会话期间 派生于 sandboxId 写在 OneLake snapshot/restore 端口路由 │ └─── 多次重建 sandbox(恢复后 sandboxId 换新,但 fabricSessionId 不变)

1:N 时间维度:一个 fabricSessionId 在生命期内可能对应多个 sandboxId(每次 restore 是新 microVM)。同一时刻 1:1:1。

📄 依据:engineering/.education/adc-apis-reference.md 第 244 行附近的响应字段说明。

Q9

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。

Q10

会话存储用的 OneLake 是 workspace 级还是 capacity 级?

层级关系

Tenant └── Capacity ← billing / quota / CMK 在此 └── Workspace ← OneLake 顶层文件夹(namespace 边界) └── CopilotEnvironment artifact ← 用户主动选/建,每 workspace 可多个 └── sessions/ └── <sessionId>/ ← 每个 session 一个文件夹 ├── metadata.json ├── state.json └── session-state/events.jsonl
维度归属层级
OneLake namespace 边界Workspace(每个 workspace 一个顶层文件夹)
会话文件物理位置Workspace 文件夹下 <CopilotEnvironment-artifact>/sessions/<sid>/
CMK / 加密配置Capacity 级(CE artifact 继承 workspace → capacity CMK)
计费 / 配额Capacity 级
访问 ACLWorkspace(继承 Fabric 权限模型)+ CE artifact 权限 + session 路径中 userObjectId 隔离

结论:OneLake 物理是 tenant-level 服务,但用户感知边界是 workspace;session 数据进一步嵌在 CopilotEnvironment artifact 文件夹里,按 CE 隔离 + 按 user 在 CE 内隔离;配额加密按 capacity 计。详见 Q20 关于 CE artifact 的完整说明。

Q11

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

Container memory : "Authorization: " 头不存在 Container disk : 无任何 token 文件 Container env : 无 GH_TOKEN Envoy memory : 临时持有,请求结束即清空 KeyVault : 持久持有,由 MWC 托管 lifecycle

精髓总结

  1. MWC PUT /secrets/{secretId} 把 token 存进 KeyVault
  2. ADC egress 规则只引用 secretRef,不存明文
  3. Envoy proxy 在 TLS MITM 后,按规则网络层注入 Authorization: Bearer {value}
  4. Container 内发的是裸 HTTP,永远看不到 token

📄 依据:engineering/000-decisions/003-A-proxy-vs-noproxy.md(含完整 JSON 示例)。

Q12

UX 怎么实现 / 主要用什么 package / 为什么这么选

仓库现状

仓库里 只有 React 原型Prototypes/UnifiedAIPlatform/react-prototype/),生产 UX 尚未落地。

原型技术栈

分类选择为什么
UI 组件库Fluent UI v9对齐 Fabric 门户视觉
Icon@fabric-msft/fabric-svg-iconsFabric 官方图标包
原始组件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 规范)

详见 engineering/300-frontend/300-!!-frontend-dev-design.md


Q13

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 的复用矩阵

SurfaceUX 是否复用对应 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

  1. 用户装 GitHub Copilot 扩展(如未装)
  2. 命令面板 Ctrl+Shift+PCopilot: Install Custom Skills
  3. 输入 repo gim-home/skills-for-fabric
  4. 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 — 为什么复用

  1. 技术栈不兼容 — Portal 是 Angular Shell;VS Code Chat 是 Electron + webview + 大量 VSCode-API。两者根本嵌不进去
  2. 视觉一致性 — Portal 全员 Fluent UI v9,VS Code 用自有 token 体系,强行嵌会两张皮
  3. 必须渲染 Fabric artifact — Notebook 卡 / Lakehouse table / Dataset preview / Pipeline 图必须用 Portal 已有的 renderer,VS Code Chat 渲染不了
  4. 必须与已有 3 个 Copilot 共存(tri-copilot / immersive-copilot / workspace-copilot),新框架"同时存在并逐步升级",不能直接搬另一个产品的容器(见 300-frontend-dev-design.md §3.2 Non-Goals
  5. 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 框架AngularPortal 整体技术栈一致;只能用 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 怎么进 ReactAngular 拦截 → 合成 client_tool.start/completeReact 永远只看到 UI 事件,tool 调度对它透明
Tools 多源shell / classic artifact 直调;extension artifact 走 postMessageextension 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 原文)。

Q14

Notebook workload 想 customize 它特有的功能,工作大致是?

平台提供 5 种自助贡献资产,Notebook team 不需要平台代码改动即可上线。

5 种贡献类型

类型形态部署典型用途
① SKILL.mdMarkdown 文件 + prompt 片段提交到仓库 → CDN"如何写 Spark 调优 notebook"
② MCP ServerHTTPS 服务(自托管)Workload team 自己部署调 Notebook API(run cell / get output)
③ Frontend ToolMCP-UI 组件包(F13)提交 → CDN bundle对话里渲染 notebook 单元格 preview
④ Client-Side Tool调 Portal Extension APIPortal extension 已存在跳转到 Notebook 编辑器
⑤ Evaluation Set≥50 个测试用例,4 类提交到仓库跑 CI / 保证 ≥95% 通过率

典型工作流(举例:让 Copilot 能 "解读 notebook 错误")

  1. SKILL.md 写一段:"当用户问 notebook 出错时,先调 mcp.notebook.get_last_error,再调 mcp.notebook.explain_traceback…"
  2. MCP Server 在 Notebook workload 内新增两个 endpoint:get_last_error / explain_traceback
  3. (可选)Frontend Tool 渲染 "Stack Trace 卡片",高亮某行代码
  4. Evaluation Set 加 ≥50 个真实出错 notebook 样例,断言 Copilot 输出正确分析
  5. 提 PR → CI 自动跑 eval → 自动部署到 CDN 和 MCP registry

SLA

  • < 2 个工作日 从 PR 合并到生产可用
  • 仅需平台 team 介入的 3 类例外:
    1. 新增 native tool(SDK 内置工具)
    2. 修改容器镜像 / Agent 基础进程
    3. 新增 "全局 building block"(横切多个 workload)
Q15

会话内容是如何写到 OneLake 里的?

这个问题分两层:写什么 / 谁写 / 何时写。设计的核心矛盾是 — 持久性要够(崩了不能丢对话),但热路径(每条消息)的写开销必须最小。

① 大原则:分两类,分别写

类型内容写入频率策略
轻量元数据sandboxId、messagesCount、lastInteractionAt、model、displayName每条消息 / 偶尔同步直写 OneLake JSON(小文件)
重型会话内容events.jsonl(完整对话历史)、agent 创建的产物文件暂停 / 检查点 / 显式 export容器内存 VFS + 定期 export 整段 dump 到 OneLake

② 数据路径:Container 不直接写,MWC 统一持久化

关键设计:GitHub Copilot SDK 通过 ISessionFsHandler 接口把 events.jsonl 写到容器的 InMemorySessionFsConcurrentDictionary,纯内存),不落盘。Container 没有 OneLake 写凭据;所有 OneLake 写都由 MWC 发起。

Browser │ POST /messages ▼ ┌─────────┐ ① POST /export ┌──────────────┐ │ MWC │ ────────────────────────────▶ │ Container │ │ (Copilot│ │ ┌──────────┐ │ │ WL) │ ◀─── ② base64 dump ─────────── │ │ SDK │ │ ──┐ └────┬────┘ (events.jsonl + files/) │ └────┬─────┘ │ │ │ │ ▼ Write │ │ │ ③ ADLS Gen2 PUT │ InMemorySFs │ │ │ (Fabric workload identity) │ (Dictionary) │ │ ▼ └──────────────┘ │ ┌─────────────────┐ ▲ │ │ OneLake │ │ │ │ sessions/<id>/ │ └── 平时只在内存 │ export/... │ │ state.json │ ◀── 每消息只更新 messagesCount/lastInteractionAt └─────────────────┘

三步明确:

  1. MWC 发起 POST {containerUrl}/export 拉数据
  2. Container 把 InMemorySessionFs 字典里所有文件 base64 编码后塞进 JSON 响应给 MWC
  3. 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 SweeperWaitingForInput / WaitingForApprovalwaitThreshold(默认 2 小时同上
MWC SweeperError 持续 ≥ errorThreshold(默认 15 分钟Export + Terminate(彻底删 sandbox)
用户显式调用 POST /sessions/{id}/exportExport 到 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.jsonsandboxState=Provisioning
② 建 sandbox 后写 state.json(含 sandboxId)
③ 写索引指针 indexes/owned/.../sessionId.json
Send Message(热路径)只更新 state.jsonmessagesCount + 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.jsonsandboxState=Suspended, snapshotId)
Resume(冷恢复)建新 sandbox → 读 OneLake export → POST /import 预填 VFS → SDK ResumeSessionAsync 重放 events.jsonl
Share / Unshareindexes/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空文件——文件名本身就是指针。

  • 列表"我的会话" = ListPaths prefix 扫描,O(user 会话数),不全扫
  • 真相源始终是 sessions/ 文件夹;索引坏了可以从源重建
  • 后台调谐 job 定期对账(创建索引漏写 → 补写;session 已删 → 清孤儿索引)

⑥ 并发控制矩阵

文件写者数量机制
metadata.json1(写一次)无需控制
settings.json多(多 tab rename)ETag If-Match + 412 重试
state.json1 主写(同 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 职责划分)。

Q16

Container 的生命周期是怎样的?

Container 生命周期由 MWC 主导 + ADC 平台兜底 双层管理。核心心智:Session 不死,Container 用完即弃 —— 容器是计算实例,可被替换;session 通过 OneLake export 永生。

① 状态机总览(7 个核心状态)

┌─────────────────┐ │ (无) │ └────────┬────────┘ │ 首条消息 (lazy creation, no Create Session API) ▼ ┌─────────────────┐ │ Provisioning │ ← write-ahead metadata.json └────────┬────────┘ Step 失败 │ All 7 steps OK 清理│ │ ▼ ▼ ┌──────────┐ ┌──────────────┐ │Terminated│ │ Active │ ← 处理消息 └──────────┘ │ (Running...) │ ▲ └──┬───────────┘ │ │ Idle ≥ 30 min / │ │ Wait* ≥ 2 hours (MWC sweeper) │ ▼ │ ┌──────────────┐ │ │ Suspending │ Export → OneLake │ └──────┬───────┘ + ADC stop (memory snap) │ │ │ ▼ │ ┌──────────────┐ │ │ Suspended │ ← 24h 上限 → Terminated │ └──────┬───────┘ │ │ 用户发消息 │ ▼ │ ┌──────────────┐ │ │ Resuming │ Restore snap OR cold restore │ └──────┬───────┘ from OneLake export │ │ │ ▼ │ ┌──────────────┐ ┌──────────┐ │ │ Active │ ─ 无心跳 30m ─▶│ Unknown │ │ └──┬───────────┘ └────┬─────┘ │ │ Error ≥ 15min / 用户删除 │ └──────────┴──────────────────────────────────┘

② 七阶段详解

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-suspendautoSuspendPolicy.enabled=false)。原因:ADC 看网络流量判断闲置,但 agent 跑 LLM 长链时容器内忙、外面没流量 → 会被 ADC 误杀。MWC 用应用层的 interactiveState(SDK 事件驱动)判断真正闲置。

2️⃣ Active — 处理消息

容器内 InteractionStateTracker 跟踪 SDK 事件,状态用 fire-and-forget callback 异步报给 MWC(不阻塞 SDK):

SDK 事件interactiveStateUX 显示
容器启动 / ResumeSessionAsync 中Starting"Starting..."
SendAsync() 进行中Running"Thinking..."
SessionIdleEventIdle✓ ready
user_input.request SSEWaitingForInput问号图标
permission.request SSEWaitingForApproval审批图标
SessionErrorEventError❌ 错误

容器并发控制:SDK SendAsync 不线程安全 → 容器用 SemaphoreSlim 串行化,第二条同时来 → 返回 429 Too Many Requests,UX 显示 "agent is busy"。

3️⃣ Suspending → Suspended — 应用层暂停

MWC sweeper 周期扫描,Running 永远不暂停

interactiveState阈值(可配)动作
IdleidleThreshold = 30 minSuspend
WaitingForInput / WaitingForApprovalwaitThreshold = 2 hoursSuspend
ErrorerrorThreshold = 15 minTerminate(直接终结,不是暂停)
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 minMWC sweeper自动清理
Suspended 持续 ≥ 24hMWC sweeper防止 snapshot 堆积
Unknown 状态 ADC 也找不到MWC sweeper心跳丢失 + ADC 404
Provisioning 失败 compensating cleanupMWC步骤失败时回滚
ADC auto-delete @ 48hADC 平台终极兜底(autoDeletePolicy)

MWC DELETE /sandboxes/{id} 后,secret 和 snapshot 一并清除。Session 元数据保留在 OneLake(历史列表用),但所有 compute 资源释放。

7️⃣ Unknown — 失联异常

MWC 在 unknownThreshold(默认 30 min)内没收到容器心跳:

  1. 标记 Unknown
  2. 尝试 GET {containerUrl}/state 直接探活 → 通 → 恢复 Active
  3. 不通 → GET /sandboxes/{id} 问 ADC:
    • Running → 容器内进程崩了 → export + terminate + 起新 sandbox
    • Stopped → 进 Suspended
    • 404Terminated

③ Defense-in-Depth(6 层兜底)

机制阈值作用域
1MWC sweeper:idle suspend30 min正常省资源
2MWC sweeper:Unknown 清理30 min 无心跳失联恢复
3MWC sweeper:Provisioning 调谐卡住 10 min创建失败兜底
4MWC sweeper:orphan 检测每 30 minADC ↔ OneLake 一致性
5MWC sweeper:长 suspend 终结24 hours防止 snapshot 堆积
6ADC auto-delete48 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(镜像升级流程)。


Q17

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. 包含关系图

Session (OneLake,长寿命,跨 device/tab) ├── Prompt 1 ← 一次 POST /messages 调用 │ └── Turn 1.1 ← assistant.turn_start … assistant.turn_end │ ├── event: assistant.message_delta * N │ ├── event: tool.start / tool.complete │ └── event: assistant.turn_end │ session.idle ← 整个 prompt 处理完 │ ├── Prompt 2 ← 必须等 Prompt 1 session.idle 之后才能发 │ ├── Turn 2.1 (LLM 决定调 tool A → tool.complete) │ ├── Turn 2.2 (chained: tool 结果 feed 回 LLM) │ ├── Turn 2.3 (chained: LLM 再调 tool B) │ └── Turn 2.4 (LLM 给最终回答) │ session.idle │ └── Prompt N …

2. 四层概念对照表

层级是什么生命周期存储位置
Session 一次完整对话上下文 OneLake 永久(用户可手动 delete) OneLake:metadata.json / state.json / events.jsonl
Prompt 用户单次输入(message 字段) POST /messagessession.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}/messages
body: { 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

[阶段 A:前置条件就绪] [阶段 B:本地草稿] [阶段 C:实际创建] ───────────────────────── ───────────────────── ───────────────────────── 1. 用户已登录 Entra 用户点 "New Chat" 按钮 用户输入第一条消息按 Send 2. workspace 有 capacity 或在新 Page 打开 Chat Pane 3. 选好 CopilotEnvironment artifact ───────────┐ POST /messages (Q20:可在 picker 里选, │ body: { targetSession:"new", 或自动 reuse 同名 .copilot CE, │ draftSessionId: GUID, 或 Create New CE) │ message:"...", … } 4. Shared 用 userEntraToken 调 │ │ generatemwctoken 拿到 │ ▼ SSE 流 mwcToken(绑 workspaceId+ceId) │ session.provisioning ▼ session.created ← session 此刻才真存在 侧栏出现 "Draft" 灰条目 assistant.turn_start … (只在前端 React state session.idle 里,0 字节 OneLake, 关浏览器即消失)

阶段 C 的服务端展开(来自 014-A-sessions-crud):

  1. MWC 收到 POST /messages,按 draftSessionIdserver-side creation lock
  2. OneLake:在 {workspace}/{CE-artifact}/sessions/{newGuid}/metadata.json(write-once)。
  3. ADC:创建 microVM + egress rules + secret。
  4. Container:MWC 调 /configure 把 sessionContext / 工具清单 / model triple 注入 GitHub Copilot SDK。
  5. SSE 先后发 session.provisioning { draftSessionId, status }session.created { sessionId, draftSessionId }
  6. 之后立即接 assistant.turn_start 开始第一条 prompt 的处理。
  7. 前端收到 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:

  • modeinteractive / plan / autopilot
  • model — LLM 模型切换(前提:在已批准的 model 池里)
  • connectionId — 同名 model 多 connection 时指定
  • context用户每次发问可换 workspace、可点不同的 artifact、cell、visual
  • attachments — 截图 / 文件(要求模型支持 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 时才落到 OneLake
  • state.json(hot path)更新 messageCount / lastActivity / lastMessageAt — 驱动 30 min idle 阈值(Q19)
  • metadata.json 不变(write-once)
  • session display name 首次自动生成时发 session.renamed SSE 事件

10. Session 与 Prompt 的常见误解

误解正解
"应该有个 POST /sessions 来建 session"❌ 没有;session 在第一条 POST /messagestargetSession:"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 概念定义)。


Q18

三种 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,并用两层策略保持新鲜:

  1. Proactive 预检:每次发 MWC 请求前看缓存 token 还剩多久;剩余 < 10 分钟就先 refresh Entra token → 调 generatemwctoken → 拿到新 MWC token → 再发原请求。
  2. 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"

Container 进程 ─── curl https://api.github.com/repos/.../x.csv │ (裸 HTTP,无 Authorization 头) ▼ Envoy sidecar ─── ① TLS terminate(自签 CA) ② match egressRule host=api.github.com ③ 从 KeyVault 取 secretRef → 解出 ghp_xxx ④ 注入 Authorization: Bearer ghp_xxx ⑤ 重新 TLS encrypt 发往 GitHub │ ▼ GitHub 看到完整 Bearer 头,正常响应 容器 memory / disk / env:永远没有 token 痕迹(见 Q11 完整示例)

⑤ Token 刷新:Container-Initiated(推荐路径)

OBO token 有效期 1 小时左右。Container 内有 RefreshTimer

  1. Container timer 到 refreshTokensAfterUtc → 发请求给 MWC(Bearer 用容器自己的 MI token)
  2. MWC 验签:用户原 token + MI token + sandbox ownership 三重校验
  3. MWC OBO 换新的下游 token(多份不同 audience)
  4. MWC 调 ADC PUT /secrets/{secretId} 全量替换(注意:PUT,不是 PATCH)
  5. MWC 调 ADC POST /sandboxes/{id}/egresspolicy 把 egress 规则里的 secretRef 引用更新
  6. 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:

  1. SDK 拿到 401 → 容器在 SSE 流里发 session.reauth_required {}
  2. 前端处理:① 关闭当前 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 → Sharedhttps://analysis.windows.net/powerbi/apiFabric public API + 私有 redirect 服务
Browser → MWCMWC scope(由 generatemwctoken 决定)per-CE token,防跨 artifact 重放
MWC → Containerfabric/<container-id>(per-container)防 token 被其他容器重放
Container → Fabric APIhttps://api.fabric.microsoft.comFabric 主网
Container → OneLakehttps://storage.azure.comADLS Gen2 endpoint
Container → LLM各 LLM provider audienceOpenAI / Anthropic 等
Container → GitHubGitHub 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 表)。


Q19

Skills 是怎么加载的?版本怎么管?

Skills 是 SKILL.md 文件(教 LLM 怎么用 Fabric 各 workload 的 API)。它不进容器镜像,从 CDN 动态加载——这样 workload team 改 skills 不用重建镜像,也不用平台 team 介入。

① 全链路:从仓库到容器

┌─────────────────────────┐ │ skills-for-fabric repo │ ← Workload team 提 PR │ (gim-home/...) │ └────────────┬────────────┘ │ CI/CD 自动跑 ▼ ┌─────────────────────────┐ │ Skills CDN (versioned) │ ← 静态资源,全球分发 └────────────┬────────────┘ ▲ │ ② Download skills package │ ┌────────────┴────────────┐ ① GET /skills/manifest │ Fabric Shared API │ ◀───────────────────────────┐ │ /skills/manifest │ → { cdnUrl, version, [...] }│ └─────────────────────────┘ │ │ ┌───────────┴────────┐ │ Agent Container │ │ (SkillsLoader) │ │ ③ Parse SKILL.md │ │ ④ 拼到 system │ │ prompt │ └────────────────────┘

② 加载时机(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 内容差异)。


Q20

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 typeCopilotEnvironment
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}

APIMethod + Path用途
Create CEPOST /items
body: {type: "CopilotEnvironment", displayName, definition}
新建一个 CE artifact
List CEGET /items?type=CopilotEnvironment列出当前 workspace 所有 CE
Delete CEDELETE /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 到发首条消息

1. 用户打开 Copilot sidecar(在某个 workspace 或 My Workspace) │ ▼ 2. UX List CE artifacts: workspace 内的 + My Workspace 内的私有 CE │ ▼ 3a. 用户选已有 CE 3b. 用户新建 CE(POST /items, type=CopilotEnvironment) │ │ ▼ ▼ 4. Browser 调 generatemwctoken(带 ceArtifactId)→ 拿 mwcToken + mwcRolloutUrl │ ▼ 5. UX 列该 CE 下的 sessions(按 日期分组,逆时序) │ ▼ 6. 用户挑一个 session 恢复 / 或新建一个(lazy 创建,第一条消息触发) │ ▼ 7. POST {mwcRolloutUrl}/.../CopilotEnvironments/{ceId}/sessions/messages { targetSession: "new", draftSessionId, message, ... } │ ▼ 8. SSE: session.provisioning → session.created → assistant.turn_start → ...

⑥ 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 = 共享 sessionsOption 2: 共享 artifact + 私有 sessions(采纳)
权限只看 artifact ACLartifact 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 模型)。


Q21

前端架构: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 ExperienceWorkspace 首页、Admin PortalAngular DI 直接注入 CopilotHostService 调 refreshContext()
Classic ArtifactReport Editor、SQL QueryAngular DI 直接注入
Extension ArtifactNotebook、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 内拦截:

MWC SSE ───▶ CopilotHostService(Angular) │ │ ① 是 tool.call? ▼ ┌──[ 是 ]──┐ │ │ ② 在 manifest 里找 tool │ │ ③ executeAction(tool.invoke) (portal 内部分发) │ │ ④ 拿到 result │ │ ⑤ POST /tool-result {toolCallId, result} │ │ ⑥ 合成 client_tool.start / client_tool.complete │ ▼ │ React Chat Pane(看到的是合成事件) │ └──[ 否 ]──▶ 直接 forward 给 React(assistant.message_delta 等) React 永远看不到 raw tool.call —— 工具调度对它透明

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)。


Q22

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 个 LakehouseShell(列表)+ Page(详细)
Custom ContextSQL 编辑器选了 21-29 行 / Notebook 焦点 cell / 当前打开的 Admin 设置页当前 active page

customContext 三层 scope:workspace-related / artifact-related / global,分别挂在 workspaces[].customContext / artifacts[].customContext / top-level customContext 数组里。

② End-to-End 流程

Shell.getContext() ─┐ ├─▶ CopilotHostService(合并 + 缓存) ActivePage.getContext() ─┘ │ │ 推 client-shape 给 React → 渲染 pills │ │ 发消息时 transform → server-shape(strip-before-send) ▼ Send Message payload → MWC → Container → LLM

③ 两种形状:客户端 (有 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 的 pageSQL 编辑器换选区、Notebook 切焦点 cell(throttled,不会按键回调)
非 active page 调用 → 静默丢弃

⑦ Strip-Before-Send + Pill Suppression

发消息前的 2 步处理:

  1. 从缓存 client-shape 出发,先应用 pill suppression list(用户在本次会话里 dismiss 过的 entry 不发)
  2. 遍历对象,移除每个 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:

[Workspace 1] [Notebook 1] [Lakehouse 1] [SQL 1] [SQL 1: Line 21–29 (non-dismissible)]

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 例子)。


Q23

Tools / Models / Modes:3 个用户可控的会话参数怎么传到后端?

每条 Send Message 都带 3 类参数:能调什么工具(tools)用哪个模型(model triple)怎么用工具(mode)。三者各有不同的注册 / 选择 / 生命周期。

① Tools(客户端工具)

关键约束:Session-scoped,全 portal 一份静态 manifest

GitHub Copilot SDK 当前只在首条消息时设 SessionConfig.Toolssession 内不可动态增减。为了配合这点:

  • 整个 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 发给后端作 clientToolsinvoke / abort 留在前端,由 CopilotHostService 在 SSE 收到 tool.call 时 dispatch。

调用周期(7 步)

  1. 后端 SSE tool.call { toolCallId, tool, input }
  2. Angular 合成 client_tool.start 给 React → Chat Pane 渲染 Tool Card(running)
  3. 查 manifest:
    • 命中executeAction(tool.invoke) 走 portal action handler(classic)或 postMessage(extension)
    • 未命中(page 已卸载):合成错误结果 { status: 'error', error: 'Tool not available...' }
  4. 拿到结果
  5. POST /tool-result { toolCallId, result }
  6. 合成 client_tool.complete 给 React → Tool Card 状态变 complete / error
  7. 后端把 result 喂给 LLM,LLM 可能继续调更多 tool 或出最终答复

② Models(连接式架构)

3 类 Provider,每个 connection 提供多个 model

Providerconnection 来源connectionId
GitHubCopilot用户的 GitHub Copilot 订阅DMTS 管理
AzureAIFoundry用户的 Azure AI Foundry resourceDMTS 管理
FabricCapacityworkspace 所在 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 ModelsGET /metadata/unifiedcopilot/models?workspaceId={wsId}connections[] + default { connectionId, modelId } + lastUsed
List ConnectionsGET .../connections?workspaceId={wsId}per-connection status / billing / scope
List Models / connectionGET .../connections/{cid}/models具体某 connection 下的模型集

Model 元数据驱动 UI

  • supportsVision:false 时禁用图片附件按钮
  • costIndicatorlow / medium / high —— picker 可显示成本提示
  • supportedReasoningEfforts:数组(low | medium | high)+ defaultReasoningEffort —— 是否暴露给用户调还是 Open Question
  • Connection statusAvailable / 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 事件
planLLM 只出计划文本,不执行任何工具不涉及
autopilotLLM 执行工具不问用户

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)。


Q24

会话状态机:interactiveState × sandboxState(11 种 UX 组合)

一个 session 有两个独立的状态维度。它们组合起来决定 Chat Pane 该显示什么 spinner / 卡片 / banner。这是把 Q16 容器生命周期和 SDK 运行态拆开的最干净视角。

① 两个维度

interactiveState — SDK / Copilot 在做什么

含义
StartingSDK 初始化中(容器刚 active,agent 还没接管)
Running正在处理 prompt 或执行 tool
Idle就绪,等下一条消息
WaitingForInputAgent 等用户回答 user_input.request
WaitingForApprovalAgent 等用户审批 permission.request
ErrorSDK 不可恢复错误

sandboxState — ADC sandbox / 容器在做什么

含义
ProvisioningmicroVM 正在建
Active容器跑着
Suspending正在 export + snapshot
Suspended已暂停(memory snapshot 存好),下条消息触发 resume
Resuming从 snapshot 恢复中
Terminated已销毁,要 cold restore(从 OneLake 重放 events.jsonl)
Unknown心跳丢,正在排查

② 组合显示矩阵(Chat Pane 渲染什么)

sandboxStateinteractiveStateChat Pane 显示
Provisioning"Creating session…" spinner
ActiveStarting"Starting…" spinner
ActiveRunning"Thinking…" spinner(流式 reasoning / message)
ActiveIdle✓ Ready(可发新消息)
ActiveWaitingForInput显示 Question Card
ActiveWaitingForApproval显示 Approval Card
ActiveError红色 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.provisioningsandboxState → Provisioning
session.createdsandboxState → Active
session.resumingsandboxState → Resuming
assistant.turn_startinteractiveState → Running
user_input.requestinteractiveState → WaitingForInput
permission.requestinteractiveState → WaitingForApproval
session.idleinteractiveState → Idle
session.errorinteractiveState → Error
session.expired / session.reauth_required同时关流并触发 reauth UX

④ 自动暂停 / 恢复:用户不用管

MWC sweeper 在以下情况主动暂停(详见 Q15 "什么时候发 POST /export"):

  • Idle30 分钟 → Suspending → Suspended(memory snapshot + OneLake export)
  • WaitingForInput / WaitingForApproval2 小时 → 同上
  • Error15 分钟 → 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 阈值)。


Q25

"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. 同一颗"大脑",三种装法

┌──────────────── GitHub Copilot SDK(核心产品)─────────────────┐ │ Agent loop / Tool calls / SSE protocol / Plugin sub-agents │ └────────────────────┬────────────────────────────────────────────┘ │ ┌──────────────────────────────┼──────────────────────────────────────┐ │ │ │ ▼ ▼ ▼ ① GitHub Copilot CLI ② ADC Container ③ CLIPlayground (public, gh copilot) (Fabric portal hoster) (内部 REPL) - 终端文本 UI - MWC POST /messages - Node.js REPL - 用户本地装 - 容器 /responses → SDK - dev/test 工具 - Iteration-1 首发 - SSE 转给浏览器 - 包 SDK + Skills - 自己 device-code 登录 - InMemorySessionFs / OneLake export - 不连 ADC - 用 Fabric Skills repo - 跑 system prompt + tools - 验证 skills

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 /responsesCopilot 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/healthSystemliveness probe
POST/configureSystem下发 refreshTokensAfterUtc + tokenExpirations(每次创建 / token 刷新 / resume)
POST/exportSystem把 InMemorySessionFs 整段 base64 dump 给 MWC
POST/importSystem从 OneLake export 预填 VFS(cold restore)
POST/responsesUser主入口,SSE 长连
POST/approveUser响应 permission.request
POST/user-inputUser响应 user_input.request
GET/messagesUser拉取历史(user/assistant 过滤)

⚠️ 注:早期文档(065-agent-design.md)曾出现 POST /update-tokensGET /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

顶层

Engineering / Decisions

Architecture

Agent

Education / Deployment / Frontend

Specs / Building Blocks & Features

Prototype