{"_id":"@apprun/ai-workspace","_rev":"3-7058d286950215a25df93da2280783c8","name":"@apprun/ai-workspace","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.0":{"name":"@apprun/ai-workspace","version":"0.1.0","license":"MIT","_id":"@apprun/ai-workspace@0.1.0","maintainers":[{"name":"yysun","email":"yiyisun@gmail.com"}],"dist":{"shasum":"d84ba77050cf95969fe284b82f119f651afaa88c","tarball":"https://registry.npmjs.org/@apprun/ai-workspace/-/ai-workspace-0.1.0.tgz","fileCount":42,"integrity":"sha512-5pgiPIRMCXj5HQtqm973zChUl5HUDWF9ALx++Sfgtipaa5QtbaSPYFLEuUuBWAOvN+vepQ5hcobJFCczwBIWxg==","signatures":[{"sig":"MEYCIQDkUtcpn51HUIhAO39yBlAt2sEy5GcpIA9uwtAPtB3L4gIhANXsY7o8orsk9Wc+sghqzKcNCcMoy3qpMCKz30kMsojS","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":189503},"type":"module","engines":{"node":"^22.19.0 || >=24.0.0"},"exports":{"./host":{"types":"./dist/host/index.d.ts","import":"./dist/host/index.js"},"./client":{"types":"./dist/client/index.d.ts","import":"./dist/client/index.js"},"./protocol":{"types":"./dist/protocol/index.d.ts","import":"./dist/protocol/index.js"}},"gitHead":"998f402c1b9f0ad85a0f1985002d0658194999cf","scripts":{"build":"node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json","prepack":"npm run build && npm run test:boundaries","test:boundaries":"node --test test/package-boundaries.test.mjs && tsc -p test/browser-consumer/tsconfig.json --noEmit"},"_npmUser":{"name":"yysun","email":"yiyisun@gmail.com"},"_npmVersion":"11.13.0","description":"DeepSeek-backed AI Workspace host with isolated browser protocol and client subpaths.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.0","dependencies":{"@deepseek-ai/dsh-sdk-client":"0.1.0-rc.8"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^6.0.3","@types/node":"^22.20.1"},"_npmOperationalInternal":{"tmp":"tmp/ai-workspace_0.1.0_1787346488636_0.11394067978302869","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@apprun/ai-workspace","version":"0.2.0","license":"MIT","_id":"@apprun/ai-workspace@0.2.0","maintainers":[{"name":"yysun","email":"yiyisun@gmail.com"}],"bin":{"ai-workspace":"dist/cli.js"},"dist":{"shasum":"c2bf84bf3196d7b2fc9be2e18cd5b03d4c980daa","tarball":"https://registry.npmjs.org/@apprun/ai-workspace/-/ai-workspace-0.2.0.tgz","fileCount":50,"integrity":"sha512-bGM1uKSS46SLtM8JGrFHwW3159XBTiohqwJOALw24m7qzbpXfuBur0duKfTNW2coWkM+wjSh7a6qYqknkv+Pkg==","signatures":[{"sig":"MEUCIGhmOKrHKFUlB1rrJXnJ53ZO+coCdbmBDeAiap53WoUpAiEAu0YwEzd+g0PCe2I8k4GUVPNjwXwq1IIR2yfAg7RfY24=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":237507},"type":"module","engines":{"node":"^22.19.0 || >=24.0.0"},"exports":{"./host":{"types":"./dist/host/index.d.ts","import":"./dist/host/index.js"},"./client":{"types":"./dist/client/index.d.ts","import":"./dist/client/index.js"},"./protocol":{"types":"./dist/protocol/index.d.ts","import":"./dist/protocol/index.js"}},"gitHead":"72ab70f1abeaad9e0fec586b331971ea908eaf33","scripts":{"build":"node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json && node -e \"require('node:fs').chmodSync('dist/cli.js', 0o755)\"","prepack":"npm run build && npm run test:boundaries","test:boundaries":"node --test test/package-boundaries.test.mjs && node --test test/application-events.test.mjs && node --test test/cli.test.mjs && tsc -p test/browser-consumer/tsconfig.json --noEmit"},"_npmUser":{"name":"yysun","email":"yiyisun@gmail.com"},"_npmVersion":"11.13.0","description":"DeepSeek-backed AI Workspace host with isolated browser protocol and client subpaths.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.0","dependencies":{"@deepseek-ai/dsh-sdk-client":"0.1.0-rc.8"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^6.0.3","@types/node":"^22.20.1"},"_npmOperationalInternal":{"tmp":"tmp/ai-workspace_0.2.0_1787403573767_0.36053558357379467","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@apprun/ai-workspace","version":"0.3.0","description":"DeepSeek-backed AI Workspace host with isolated browser protocol and client subpaths.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/apprunjs/dsh-app.git","directory":"packages/ai-workspace"},"homepage":"https://github.com/apprunjs/dsh-app/tree/main/packages/ai-workspace#readme","bugs":{"url":"https://github.com/apprunjs/dsh-app/issues"},"type":"module","sideEffects":false,"bin":{"ai-workspace":"dist/cli.js"},"publishConfig":{"access":"public"},"exports":{"./protocol":{"types":"./dist/protocol/index.d.ts","import":"./dist/protocol/index.js"},"./client":{"types":"./dist/client/index.d.ts","import":"./dist/client/index.js"},"./host":{"types":"./dist/host/index.d.ts","import":"./dist/host/index.js"},"./testkit":{"types":"./dist/testkit/index.d.ts","import":"./dist/testkit/index.js"}},"scripts":{"build":"node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json && node -e \"require('node:fs').chmodSync('dist/cli.js', 0o755)\"","test:boundaries":"node --test test/package-boundaries.test.mjs && node --test test/application-events.test.mjs && node --test test/session-contract-conformance.test.mjs && node --test test/run-coordinator.test.mjs && node --test test/harness-diagnostics.test.mjs && node --test test/session-diagnostics.test.mjs && node --test test/client-transport.test.mjs && node --test test/protocol-extensions.test.mjs && node --test test/session-controller.test.mjs && node --test test/pms3-composition.test.mjs && node --test test/cli.test.mjs && node --test test/compatibility.test.mjs && tsc -p test/browser-consumer/tsconfig.json --noEmit && node --test test/packed-consumer.test.mjs","prepack":"npm run build && npm run test:boundaries"},"engines":{"node":"^22.19.0 || >=24.0.0"},"dependencies":{"@deepseek-ai/dsh-sdk-client":"0.1.0-rc.8"},"devDependencies":{"@types/node":"^22.20.1","typescript":"^6.0.3"},"gitHead":"bf1221458d402971f238e06d2e84bc7c36c0de1f","_id":"@apprun/ai-workspace@0.3.0","_nodeVersion":"22.22.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-0MtTlwJ08ivMWGFCctkpgwnpHvabBfkc4YcctXYg9+y0dqPTQHZiMnmH0K/XPNoHvDCyA+WYC+DuwvEjwVYTDA==","shasum":"6caff28385967a231aa1686056c3852ad080a6f9","tarball":"https://registry.npmjs.org/@apprun/ai-workspace/-/ai-workspace-0.3.0.tgz","fileCount":88,"unpackedSize":499948,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGsXtEyZ3RCD0BAYZvRR4rd8zAqpoLqJC+NlVPmQEwIyAiArrSU1wSvYcnkKlicET5Vl/e9sJc27nBCtKqUTHh3F1w=="}]},"_npmUser":{"name":"yysun","email":"yiyisun@gmail.com"},"directories":{},"maintainers":[{"name":"yysun","email":"yiyisun@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ai-workspace_0.3.0_1787714774621_0.42982779989228703"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-21T21:08:08.513Z","modified":"2026-08-26T03:26:14.916Z","0.1.0":"2026-08-21T21:08:08.789Z","0.2.0":"2026-08-22T12:59:34.018Z","0.3.0":"2026-08-26T03:26:14.759Z"},"license":"MIT","description":"DeepSeek-backed AI Workspace host with isolated browser protocol and client subpaths.","maintainers":[{"name":"yysun","email":"yiyisun@gmail.com"}],"readme":"# @apprun/ai-workspace\n\n**[English](./README.md) · [简体中文](./README.zh-CN.md)**\n\n一个可组合的应用基础层，用于在 DeepSeek Harness SDK 之上构建面向浏览器的聊天与 Agent 应用。\n\n`@deepseek-ai/dsh-sdk-client` 负责运行 Harness；`@apprun/ai-workspace` 补上将它安全暴露给浏览器所需的应用边界：公开事件、HTTP/SSE 会话、重连恢复、有界传输，以及可复用的 Run/Client 生命周期状态机。\n\n## 为什么需要这个包\n\nDeepSeek Harness SDK 是 Node 侧的子进程与 JSON-RPC 客户端。它负责启动运行时、执行 Prompt 和流式接收内部通知，但刻意不处理浏览器身份、HTTP 会话启动、Server-Sent Events、断线重放，也不判断哪些内部数据适合公开。\n\n因此，浏览器应用还需要一层机制来：\n\n- 把内部细节丰富的 Harness 通知投影为明确的公开协议；\n- 管理每个浏览器会话和 Run 的隔离边界；\n- 检测 SSE 缺口，并通过有界重放或权威重同步修复，而不是在状态不完整时静默继续；\n- 对容量、payload、重放、timeout 和 backpressure 设置有限边界；\n- 把传输结果与应用策略、领域状态分离。\n\n这层机制概念上不大，却包含大量容易出错的状态。没有共享契约时，应用通常会复制参考前后端，随后逐渐错过生命周期和恢复逻辑的修复。这个包统一承担可复用机制；身份认证、持久化、领域事件、授权、审计和 UI 仍由你的应用负责。\n\n## 提供的能力\n\n- **浏览器安全的协议边界。** 默认协议不会转发 raw reasoning、Tool payload、Harness notification 或内部诊断。未知或无效事件封闭失败；应用扩展必须显式注册并经过运行时解码。\n- **有边界的默认 Session stream。** 内存 `SessionService` 提供 Session/Run fencing、单调递增的 SSE event ID、有界重放、重连修复、backpressure 处理、过期回收，以及可选的权威扩展 baseline。\n- **可替换的 Session 契约。** 自定义 `SessionStreamService` 可以继续配合相同的 coordinator、protocol 和 client；`/testkit` 提供 conformance coverage。替代实现必须自行承担持久化和 stream 机制，但接口能避免应用集成依赖包内部实现。\n- **Run 生命周期协调。** `createHarnessService` 管理子进程健康状态和 active-run guard；`createRunCoordinator` 管理 admission、dispatch、projection、settlement 和 release，应用路由无需接管整条 Promise 链。\n- **有界的 HTTP/SSE Client。** `createAgentClient` 与 `createSessionController` 提供强类型解码、响应大小上限、统一请求 deadline、EventSource 重连、bootstrap 恢复、Run lock，以及对未知提交结果的保守处理。\n- **可运行的脚手架。** `ai-workspace init` 创建 canonical React 参考应用，不会静默安装依赖，也不会替你选择或写入凭据。\n- **机制与策略分离。** 包负责传输、生命周期、顺序和封闭默认值；应用负责 authority、存储、扩展语义、重试决策和展示。\n\n## 如何选择\n\n| 选择 | 适用场景 |\n| --- | --- |\n| 直接使用 DeepSeek Harness SDK | Node-only worker、CLI 或后端集成，不需要浏览器会话、SSE 重放或公开协议边界。 |\n| `@apprun/ai-workspace/host`、`/protocol` 和 `/client` | 面向浏览器的应用，希望复用基础能力，但保留自定义路由、存储和 UI。 |\n| `createRunCoordinator` + `createSessionController` | 大多数浏览器聊天或 Agent 应用。复用高层状态机，通过 typed hook 加入应用策略。 |\n| 自定义 `SessionStreamService` | 需要持久化/分布式 Session 或不同的 stream 语义，并愿意实现契约和运行 conformance suite。 |\n\n## 安装\n\n```bash\nnpm install @apprun/ai-workspace\n```\n\n需要 Node `^22.19.0 || >=24.0.0`。`/host` 与 `/testkit` 仅用于 Node；`/protocol` 与 `/client` 可安全进入浏览器依赖图。\n\n如果希望从维护中的 React 参考应用开始，而不是手工连接所有组件：\n\n```bash\nnpx @apprun/ai-workspace init my-agent-app\ncd my-agent-app\ncp .env.example .env\n# 在 .env 中填写 DEEPSEEK_API_KEY\nnpm install\nnpm run dev\n```\n\nInitializer 会复制与当前包版本匹配的 canonical example，把 workspace consumer 固定到 CLI 包版本，并初始化 Git。它不会安装依赖或写入凭据。脚手架源码见 [`examples/react`](../../examples/react)。\n\n## 组件关系\n\n```text\n浏览器 UI\n    ↕ 状态与应用操作\n@apprun/ai-workspace/client\n    ↕ 有界 HTTP + 封闭 SSE 协议\n应用自有路由与 authority 检查\n    ↕\nSessionService ← createRunCoordinator → createHarnessService\n                                            ↕\n                               DeepSeek Harness SDK/runtime\n```\n\n应用策略包围整条链路。这个包不验证请求身份、不决定哪些应用/领域数据可以公开、不授权领域 Action，也不持久化业务状态。\n\n## Library 快速开始\n\n### Host（Node 后端）\n\n下面使用的 `scrubbedParentEnv` 来自独立包 `@deepseek-ai/dsh-subprocess`，需要与 `@apprun/ai-workspace` 一同安装。它会在加载 `.env` 前捕获经过清洗的父进程环境；本包不捆绑或重新导出它。\n\n```ts\nimport {\n  createHarnessService,\n  createRunCoordinator,\n  loadProjectEnv,\n  readSupportedSettings,\n  SessionService,\n} from '@apprun/ai-workspace/host'\nimport { HTTP_ERRORS } from '@apprun/ai-workspace/protocol'\nimport { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'\n\nconst inheritedBase = Object.freeze(scrubbedParentEnv())\nloadProjectEnv('.env')\nconst settings = readSupportedSettings(process.env)\n\nconst harness = createHarnessService({\n  inheritedBase,\n  settings,\n  runtime: {\n    processCwd: '/absolute/path/to/harness-runtime',\n    entrypoint: '/absolute/path/to/harness-runtime/bin.mjs',\n    sdkCwd: '/absolute/path/to/project',\n    settingsBaseDir: '/absolute/path/to/project',\n  },\n})\nconst sessions = new SessionService()\nconst coordinator = createRunCoordinator({ harness, sink: sessions })\n\nawait coordinator.start()\n\n// 在你的 HTTP 路由中使用（这里以 Express 为例，也可使用其他 Node HTTP framework）：\napp.post('/api/sessions', (_req, res) => {\n  const session = sessions.createSession()\n  if (session === null) return res.status(503).json(HTTP_ERRORS['capacity-exhausted'])\n  res.json(session)\n})\n\napp.post('/api/prompt', async (req, res) => {\n  const result = await coordinator.dispatchRun({\n    sessionId: req.body.sessionId,\n    runtimeSessionId: req.body.sessionId,\n    text: req.body.text,\n  })\n  res.status(result.accepted ? 202 : result.status).json(result.accepted ? { accepted: true, runId: result.runId } : result.body)\n})\n\napp.get('/api/stream', (req, res) => {\n  if (typeof req.query.session !== 'string' || typeof req.query.token !== 'string') {\n    return res.status(400).json(HTTP_ERRORS['invalid-request'])\n  }\n  const lastEventId = req.headers['last-event-id']\n  const result = sessions.openStream(\n    req.query.session,\n    req.query.token,\n    res,\n    typeof lastEventId === 'string' ? lastEventId : null,\n  )\n  if (result.status !== 200) res.status(result.status).json(result.body)\n})\n```\n\n这段代码只展示生命周期连接，不是完整的授权示例。暴露这些路由前，应验证 request body，并加入应用自己的 Host、Origin、身份认证、scope、rate limit 和错误处理策略。Canonical React backend 展示了一套完整的本地开发组合。\n\n### Client（浏览器）\n\n```ts\nimport { createSessionController } from '@apprun/ai-workspace/client'\n\nconst controller = createSessionController()\nconst unsubscribe = controller.subscribe((state) => render(state))\ncontroller.start()\n\nasync function onSubmit(text) {\n  await controller.submit(text)\n}\n\nfunction dispose() {\n  controller.stop()\n  unsubscribe()\n}\n```\n\n`createSessionController` 管理 bootstrap、EventSource 重连、有界 reboot backoff 和 prompt-admission fencing。如果网络失败发生在服务端可能已接收请求之后，提交结果就是未知的；controller **绝不会自动重发**。它会暴露 `state.ambiguousAdmission`，由应用按自己的策略调用 `controller.retryAmbiguous()` 或 `controller.dismissAmbiguous()`。\n\n`state.ambiguousAdmission` 包含原始 `sessionId`。如果重连替换了 Session，未知请求仍锁定在原 Session，`retryAmbiguous()` 会拒绝把它发给替代 Session。新 Session 不能证明旧写入成功或失败。\n\n需要完全控制时，可以独立使用 `createAgentClient`、`reduceSessionOutput` 和 `parseSseEvent`；controller 是增量能力，不是强制替代。\n\n## 组合应用通道\n\n扩展可以增加新的、显式注册的事件名，但不会放宽或覆盖默认 assistant protocol。Host 和 Client 两侧都会保留字面量事件名与解码 payload 的关联：\n\n```ts\nimport { SessionService } from '@apprun/ai-workspace/host'\nimport { defineProtocolExtension, isRecord } from '@apprun/ai-workspace/protocol'\n\nconst reasoning = defineProtocolExtension({\n  name: 'public-reasoning-summary',\n  decode(value) {\n    if (isRecord(value) && typeof value.summary === 'string') return { summary: value.summary }\n    throw new TypeError('invalid public reasoning summary')\n  },\n})\n\nconst businessSurface = defineProtocolExtension({\n  name: 'business-surface',\n  decode(value) {\n    if (isRecord(value) && typeof value.status === 'string') return { status: value.status }\n    throw new TypeError('invalid business surface')\n  },\n})\n\nconst registered = [reasoning, businessSurface] as const\nconst sessions = new SessionService<\n  { status: string },\n  typeof registered\n>({\n  extensions: registered,\n  produceExtensionSnapshots: ({ sessionId, context }) => [{\n    extension: businessSurface,\n    payload: { status: `${sessionId}:${context?.status ?? 'unknown'}` },\n  }],\n})\n```\n\nSnapshot producer 必须同步，只会收到 `{sessionId, context}`。输出是应用自有的权威替换状态；包不会从 live event 推断 snapshot 语义。完整的 default/extension baseline 会在发布 stream 或返回 HTTP 200 前完成准备。无效、重复、重入、过多或过大的输出会在不消耗 event ID 的情况下封闭失败，并可重试。\n\nController 使用同一个封闭 registry，并在默认 assistant reducer 之外处理扩展：\n\n```ts\nconst controller = createSessionController({\n  extensions: registered,\n  bootstrapSession: ({ client, signal }) => client.request(\n    '/api/domain-session',\n    { method: 'POST', signal },\n    decodeDomainSession, // 返回 {sessionId, token, capabilities, ...}\n  ),\n  admitPrompt: ({ client, session, text, signal }) => client.request(\n    '/api/domain-prompt',\n    { method: 'POST', signal, body: JSON.stringify({ sessionId: session.sessionId, text }) },\n    decodeAdmission,\n  ),\n  onExtensionEvent: ({ session, event }) => {\n    // `session` 保留协商出的 capability 字段；`event` 是已注册\n    // extension tuple 对应的判别联合。\n    applyApplicationEvent(session, event)\n  },\n})\n```\n\n每次 coordinator request 可通过 `projectBrowserEvent` 丢弃或同步重发 Host-approved 默认事件，通过 `mapTerminal` 折叠校验后的终态 history，并通过 `onComplete` 在唯一一次 settlement attempt 后清理应用投影状态。公开思考和 Tool event 应来自独立的应用 producer/projector；不要把默认 answer text 或 raw model reasoning 重新分类为公开思考。\n\n## 架构：机制与策略\n\n包负责 Harness 子进程生命周期、SSE 传输机制与顺序、有界默认值、强类型失败状态、协议协商和扩展 plumbing。应用负责身份认证、请求 authority、Session storage/ownership、应用事件 schema、公开策略、诊断策略、重试/重启决策、UI 和领域语义。\n\n|  | 包保证 | 本地默认实现（`SessionService`、`createHarnessService`） | 始终由消费方负责 |\n| --- | --- | --- | --- |\n| Session 存储 | `SessionStreamService` 接口形状 | 内存 `Map`，到期淘汰 | 需要时提供持久化实现 |\n| Run fencing | 原样传递不透明 `ref`，从不解析 | `ref` 为 Run ID 字符串 | 更丰富的 `ref`，例如携带领域 generation/selection ID |\n| 边界（容量、过期、重放、输出） | 有限且可注入 | 与最初单应用示例一致的固定常量 | 适合实际部署的取值 |\n| 诊断 | 为生命周期、transport loss、容量、重放、baseline rejection、overflow 和 coordinator hook failure 提供稳定 typed hook | 不连接 logger，包内无 `console.*` 输出 | logger/metrics backend、保留期限和脱敏策略 |\n| Auth / Origin / Host 检查 | 无 | 无 | 完全由应用负责；包不预设认证方式 |\n| Protocol 扩展 | 默认封闭的新事件 registry，以及两阶段权威 baseline 机制 | 不注册任何扩展 | 事件名、decoder、snapshot 语义和边界 |\n| Client 传输 | `AgentClient.request` 不会把未经检查的 cast 声称为 `T` | `postSessions`/`postPrompt` 使用包内 decoder | 所有自定义 route 的 decoder |\n\n仓库内的 React example 是可运行的本地开发默认值，不是所有消费方都必须暴露或授权的规范。\n\n## 兼容性与迁移\n\n- **默认路径保持增量兼容。** `SessionService`、`createHarnessService`、`createAgentClient`、`reduceSessionOutput`、`parseSseEvent` 和边界常量在本次发布中保持既有形状与默认行为；`AgentClient.request` 是唯一明确例外，见下文。\n- **`AgentClient.request` 的强类型结果必须提供 decoder。** `request(path, init)` 返回 `ApiResult<unknown>`；`request<T>(path, init, decode)` 返回 `ApiResult<T>`。此前依赖 unchecked cast 的调用（例如没有第三个参数的 `client.request<Foo>(path, init)`）现在必须提供 decoder。`postSessions`/`postPrompt` 已在包内完成解码，因此默认 bootstrap/admission 路径无需修改。\n- **Controller ambiguity 现在绑定 Session。** 读取 `ambiguousAdmission` 的代码必须处理新增的 `sessionId`。Rebootstrap 不再清除它；应用应展示明确的恢复/放弃决策，不能把替代 Session 当成成功或失败证据。\n- **旧的 `submit(text, { bypassLock: true })` 形态仍可编译，但不再绕过恢复 fencing。** Options 参数已弃用并被忽略。使用 `retryAmbiguous()` 重试当前 Session 的确切 ambiguity；如果应用的有界重试策略需要等待本次尝试，使用 `retryAmbiguousAndWait()`；显式放弃则使用 `dismissAmbiguous()`。\n- **Extension generic 继续采用 payload-first。** 既有 `ProtocolExtension<MyPayload>` 和 `defineProtocolExtension<MyPayload>(...)` 标注仍可编译。新代码省略显式 generic，即可推导字面量事件名，并获得异构 tuple 的判别联合。\n- **自定义 controller request 自行承担 cancellation。** `bootstrapSession` 和 `admitPrompt` 都会收到 `AbortSignal`。Bootstrap cancellation 是正常 supersession；admission 一旦调用，后续 cancellation 仍表示结果未知。\n- **Protocol version negotiation 为 opt-in。** `AI_WORKSPACE_PROTOCOL_VERSION_HEADER`（`x-ai-workspace-protocol-version`，小写；直接读取 `request.headers` 时也必须使用小写 key）是 client 可在 Session bootstrap 发送的可选 header。不发送时得到与当前完全一致的 `SessionBootstrap` response；发送不兼容版本时得到明确的 `HTTP_ERRORS['protocol-version-mismatch']`，而不是按惯例静默接受。应用可在自己的 bootstrap route 中接入 `negotiateProtocolVersion(headerValue)`。\n- **`SessionStreamService` 的 Run fencing 刻意保持不透明。** 自定义实现可以让 `admitPrompt` 返回任意 `ref`（字符串、携带额外领域身份的对象等）；包和 `createRunCoordinator` 只会把它原样传回 `onRunEvent`/`settleRun`。\n\n## 测试自定义实现\n\n```ts\nimport { runSessionStreamServiceConformance } from '@apprun/ai-workspace/testkit'\nimport { createMySessionService } from './my-session-service.js'\n\nrunSessionStreamServiceConformance('my custom SessionStreamService', () => createMySessionService())\n```\n\n通过 `node --test` 运行。Conformance suite 基于 Node 内置 test runner，不依赖应用使用的其他测试框架。`/testkit` 还导出 `FakeSdkHarness`、`FakeSseResponse`、`createFakeAgentClient`、`FakeEventSource`，以及用于测试上层组合的封闭参考扩展 `statusPingExtension`。\n\n## 诊断环境与兼容性问题\n\n```bash\nnpx @apprun/ai-workspace doctor\n```\n\n该命令用纯文本报告 Node 版本、已安装包版本、固定的 SDK/runtime 是否存在，以及已知配置不匹配。这样可以在提交 Prompt 前发现问题，而不必通过 raw JSON-RPC traffic 排查。\n\n## 包结构\n\n- **`/host`** — 仅 Node。Harness lifecycle、默认内存 `SessionStreamService`、Run coordinator、诊断和应用事件投影。\n- **`/protocol`** — 浏览器安全。封闭的默认 SSE/HTTP wire contract、校验 helper、protocol version negotiation 和 extension registry；不导入 Node、Express、React 或 DeepSeek SDK。\n- **`/client`** — 浏览器安全。可配置 HTTP transport、默认 output reducer 和 framework-neutral Session controller。\n- **`/testkit`** — 仅 Node，只用于测试。不要在生产代码中导入。\n\n## 示例与支持\n\n- [`examples/react`](../../examples/react) — `ai-workspace init` 使用的 canonical 本地开发应用。\n- [`examples/project-manager`](../../examples/project-manager) — 使用注册扩展事件和应用自有策略的较大示例。\n- [Changelog](../../CHANGELOG.md) · [中文更新日志](../../CHANGELOG.zh-CN.md)\n- [Issue tracker](https://github.com/apprunjs/dsh-app/issues)\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.zh-CN.md","homepage":"https://github.com/apprunjs/dsh-app/tree/main/packages/ai-workspace#readme","repository":{"type":"git","url":"git+https://github.com/apprunjs/dsh-app.git","directory":"packages/ai-workspace"},"bugs":{"url":"https://github.com/apprunjs/dsh-app/issues"}}