diff --git a/.agents/notes/archived/architecture/2026-09-02-durable-image-offload.i18n.yaml b/.agents/notes/archived/architecture/2026-09-02-durable-image-offload.i18n.yaml new file mode 100644 index 0000000000..4968362944 --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-02-durable-image-offload.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/archived/architecture/2026-09-02-durable-image-offload.md +2026-09-02-durable-image-offload.md: db03e9f52499c1790b3a0c03853f3aea23315bec +2026-09-02-durable-image-offload.zh.md: 2b6d0a4aa65f9a39b88888506f7e85b4cfff48cf diff --git a/.agents/notes/archived/architecture/2026-09-02-durable-image-offload.md b/.agents/notes/archived/architecture/2026-09-02-durable-image-offload.md new file mode 100644 index 0000000000..db03e9f524 --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-02-durable-image-offload.md @@ -0,0 +1,56 @@ +# Agent Note: Durable image offload by surface replacement + +Status: implemented +Archived: 2026-09-10 + +English | [中文](2026-09-02-durable-image-offload.zh.md) + +## Problem + +Request-size image offload was recomputed from scratch on every request. Each route collected every image occurrence on the derived surface, oldest first, and once the accumulated bytes exceeded its budget it rounded the excess up to a whole removal quantum and replaced that many oldest occurrences with placeholder text, per the [unified image request pipeline](../feature/2026-08-20-unified-image-request-pipeline.md). Nothing remembered where the previous request stopped; the prefix was stable only because the arithmetic over an append-only history repeated itself. + +That stability failed wherever the arithmetic inputs moved. The [Files inline fallback](../../archived/bug-fix/2026-08-21-deepseek-files-inline-fallback.md) rebuilt a request under a 20 MiB inline budget with a 10 MiB quantum, offloading far more images for that one request, and the next request in file mode brought them back. The pi-ai route used a quantum of one byte, so its prefix moved on almost every request. Compaction lowered the total and returned previously offloaded images. A route switch moved every step boundary. Each move changed the model-visible prefix and invalidated the provider cache prefix. + +The same recomputation broke the repository invariant that model-visible input is reconstructable from the session log. Which representation was dispatched, the exact derived request-version byte lengths, and the route budgets and quanta were runtime or configuration facts that never entered the log; `request/header` records call config, system prompt, and tools only. Provider usage anchors token totals and cannot recover the image set, and the [route-priced estimate](../../archived/feature/2026-08-24-route-priced-image-request-pressure.md) documented that it did not reproduce the fallback budget. No consumer could pair a logged assistant response with the image set its request carried. + +## Decision + +The offload of an image occurrence is a durable surface fact recorded the way compaction records its reductions: a `surfaceOp: replace` node. + +**Marked copies on the surface.** `ImageBlock` gains `offloaded?: true`. An offloaded occurrence lives in a replacement event of the same type as the node that carried it (`user/message` or `tool/result`) whose content is a copy of the original with those blocks marked; `sourceEventSeqs` points at the replaced node. The session core, its event map, its derivation, and its validation are unchanged. `deriveMessages()` sends the marked block; serialization renders every marked block as `offloadedImageText` with the currently resolved access path and prepares only retained occurrences. + +**Only advances.** A replacement never reverts. When a budget grows, a route changes, or compaction lowers the total, the marked copies stay, so the model-visible prefix and the provider cache prefix move only forward. + +**Adapters project, never decide.** An image-capable route enforces an `LlmImageRequestBudget` (`representation`, `maxBytes`, `maxImages`, both quanta) over the retained occurrences' exact request-version bytes. When they still exceed the budget, in file mode, under the inline fallback's tighter budget, or under the pi-ai bound, the adapter fails the attempt with `IMAGE_OFFLOAD_REQUIRED` and `LlmFailure.offloadImages` naming how many more oldest occurrences must be offloaded, computed with the shared `requiredImageOffload()`. Nothing plans an offload before dispatch. + +**Recovery owned by `dsh-compaction-image-offload`.** Image offload is compaction in another capacity dimension: the provider rejects a request, durable history is reduced, the step retries. The executor is a sibling of `compaction-tool-result-pruner` in the compaction group and listens on the `agent/request-error` waterfall. On `IMAGE_OFFLOAD_REQUIRED` it walks the surface in model request order, marks the first `offloadImages` retained occurrences, and for each node that carried one appends the seam's `compaction/prune` shadow price followed by the marked copy, then returns the `retry` action without spending the provider retry budget or logging `llm/retry`. Assistant nodes carry model output, not input images, and are skipped. When nothing remains to offload it delegates and the failure reaches ordinary recovery. The loop re-runs the step over the replaced surface and logs the fresh `request/header` every surface replacement gets; the agent loop is unchanged. + +**Token accounting.** `priceImages` receives the surface's `ImageBlock`s and prices a marked one as its placeholder text; the DeepSeek and replay pricing no longer reproduce any offload arithmetic. The meter needs no new state: the `compaction/prune` event and the replacement reprice the node like a tool-result prune. + +**Other consumers.** Compaction reconstructs each selected event through `deriveEventMessage()` and sees the marks. Resume, fork, and replay reproduce the surface from the log. Text-only routes keep their separate whole-history substitution. + +## Alternatives considered + +**Keep recomputing the offload point per request.** Stable only while the arithmetic inputs held still; the inline fallback, the pi-ai quantum, compaction, and route switches all moved the prefix, and no consumer could reconstruct a historical request's image set. + +**Record each request's projection outcome as a log-only event.** Restores reconstructability but not stability: the recorded outcome is not a decision input, so every oscillation still happens and the log merely documents it, with two sources of truth for the offloaded set that can disagree. + +**Log the full projected request body.** Everything except the offload decision is already derivable; repeating the history per request grows the log quadratically to record one position. + +**A log-only `image/offload` watermark event applied by session derivation.** An earlier revision recorded the offload point as a position (event seq plus block path) and had `Session.deriveMessages()` mark every occurrence at or before it. That gave the session a second mechanism for changing model-visible history next to `surfaceOp: replace`, with its own validation, fold, cache invalidation, and required-on-read event, and placed the logic in the core instead of on the plugin that decides. Issue #3041 asks that a permanent eviction use a surface-changing event rather than a request-projection event; because the offload never reverts, it is one. + +**Let each adapter append the replacement.** The adapter owns the budgets but not the session surface; a surface change appended below the loop lets two adapters define the surface differently. Adapters report the count they need instead. + +**Plan the offload before dispatch, in the loop or in a plugin.** The loop knows the exact prepared route before deriving each request, so planning there never spends a failed attempt; but it puts a route-specific policy into the one component every profile shares, changes the documented step order, and bypasses the `agent/request-error` waterfall that context-overflow compaction and retry already use for the same repair-and-retry pattern. A pre-step plugin keeps the loop unchanged but cannot see the step's own messages or the first request's route, so the failure path stays necessary, and the plan needs every route to declare its budget on its model info. Handling only the failure costs one attempt per quantum crossing (64 MiB in DeepSeek file mode, 20 MiB on pi-ai) and matches the planned direction: routes will stop checking sizes locally, send every image, and the provider will report what it cannot cache, which is exactly a failure naming an offload point. + +**Keep a transient extra offload for the inline fallback and exact-byte overflow.** Would have sent an unlogged projection in exactly the cases the invariant exists for; the failure-and-advance path costs one serialization attempt and keeps every dispatched request derivable from the log. + +## Consequences + +Offloaded images never return automatically when budgets grow, a larger route is selected, or compaction lowers the total; recovery is the read-only path in the placeholder, which the model uses deliberately. A Files outage or a temporary switch to a small-budget route offloads permanently; both are accepted for the same reason. Each offload copies the carrying node's message into the log; images are references, so the copy is the node's text plus block metadata. + +Every dispatched request's image set is determined by the log alone, across file mode, inline fallback, resume, fork, retry, and compaction, and the provider cache prefix no longer oscillates. The execution-world access path embedded in placeholder and handle text is still resolved at serialization time; that gap exists for retained images too and belongs to a separate decision about recording the execution-world mapping. The route-local byte checks are provisional: once the provider reports uncacheable images itself, `requiredImageOffload()` and the route budgets go away while the marked copies and the recovery branch stay as they are. + +## Testing + +`packages/llm/llm/tests/content.spec.ts` pins arbitrary-depth image traversal, the offload count including the 129-to-64 MiB quantum example, and placeholder projection. `packages/compaction/compaction-image-offload/tests/image-offload.spec.ts` pins the `IMAGE_OFFLOAD_REQUIRED` replace-and-retry path with its `compaction/prune` shadow prices and without a retry event, the untouched original node, request-order counts after an earlier surface replacement, and the exhausted case that delegates downstream. Adapter specs pin placeholder projection, prepared-only-retained reads, and the exact-byte failure with its count; `route-pricing.spec.ts` pins placeholder pricing for a marked replacement. The `inline-image-prompt` TypeScript SDK snapshot replays an authored `IMAGE_OFFLOAD_REQUIRED` attempt through the shipped profile and pins the replacement and the retried request. diff --git a/.agents/notes/archived/architecture/2026-09-02-durable-image-offload.zh.md b/.agents/notes/archived/architecture/2026-09-02-durable-image-offload.zh.md new file mode 100644 index 0000000000..2b6d0a4aa6 --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-02-durable-image-offload.zh.md @@ -0,0 +1,56 @@ +# Agent Note: 以表层替换实现持久的图片 offload + +Status: implemented +Archived: 2026-09-10 + +[English](2026-09-02-durable-image-offload.md) | 中文 + +## 问题 + +请求级图片 offload 过去在每次请求时都从头重算。每条路由按最老优先的顺序收集派生表层上的全部图片出现位置,累计字节一旦超过预算,就把超出部分向上取整到整个删除量子,把这么多最老的出现位置替换为占位文本,见[统一图片请求管线](../feature/2026-08-20-unified-image-request-pipeline.zh.md)。没有任何东西记住上一次请求停在哪里,前缀稳定只是因为对 append-only 历史做同样的算术会得到同样的结果。 + +算术的输入一动,这种稳定就失效。[Files 内联回退](../../archived/bug-fix/2026-08-21-deepseek-files-inline-fallback.md)会用 20 MiB 内联预算和 10 MiB 量子重建请求,那一次省略多得多的图片,下一次 file 模式的请求又把它们带回来。pi-ai 路由的量子是一个字节,前缀几乎每次请求都会移动。compaction 降低总量,让先前省略的图片回归。切换路由会移动每个台阶边界。每一次移动都改变模型可见前缀,并让 provider 的缓存前缀失效。 + +同样的重算也破坏了仓库不变量:模型可见输入必须能从 session log 重建。实际发出的表示方式、派生请求版本的精确字节长度、路由预算和量子都是运行时或配置事实,从不进入日志,`request/header` 只记录调用配置、系统提示词和工具。provider usage 只锚定 token 总量,恢复不了图片集合,[按路由定价的估计](../../archived/feature/2026-08-24-route-priced-image-request-pressure.md)也写明它不复现回退预算。没有任何消费方能把一条已记录的助手响应和它的请求携带的图片集合配对。 + +## 决定 + +一个图片出现位置的省略是一条持久的表层事实,记录方式和 compaction 记录它的缩减一样:一个 `surfaceOp: replace` 节点。 + +**表层上带标记的副本。** `ImageBlock` 新增 `offloaded?: true`。被省略的出现位置存在于一条与原承载节点同类型的替换事件里(`user/message` 或 `tool/result`),内容是原消息的副本,只把那些块打上标记;`sourceEventSeqs` 指向被替换的节点。session 核心、它的事件表、派生和校验都不变。`deriveMessages()` 原样发送带标记的块;序列化把每个带标记的块渲染为带当前已解析访问路径的 `offloadedImageText`,只准备保留的出现位置。 + +**只前进。** 替换永不回退。预算变大、路由切换或 compaction 降低总量时,带标记的副本留在原地,所以模型可见前缀和 provider 缓存前缀只向前移动。 + +**adapter 只投影,不决定。** 支持图片的路由按保留的出现位置的精确请求版本字节执行一个 `LlmImageRequestBudget`(`representation`、`maxBytes`、`maxImages` 与两个量子)。当它们仍超过预算,无论是 file 模式、内联回退更紧的预算还是 pi-ai 上限,adapter 都以 `IMAGE_OFFLOAD_REQUIRED` 让本次尝试失败,并在 `LlmFailure.offloadImages` 中用共享的 `requiredImageOffload()` 算出还需省略多少最老的出现位置。发送前没有任何规划。 + +**恢复归 `dsh-compaction-image-offload`。** 图片省略是 compaction 在另一个容量维度上的实例:provider 拒绝请求,持久历史被缩减,step 重试。执行器是 compaction 组里 `compaction-tool-result-pruner` 的兄弟包,监听 `agent/request-error` waterfall。收到 `IMAGE_OFFLOAD_REQUIRED` 时,它按模型请求顺序遍历表层,给前 `offloadImages` 个保留的出现位置打标记,为每个承载了其中任一位置的节点先追加 seam 的 `compaction/prune` 影子价格,再追加带标记的副本,然后返回 `retry` 动作,不占提供方重试预算,也不记录 `llm/retry`。assistant 节点承载的是模型输出而不是输入图片,直接跳过。没有可省略的出现位置时向下游委托,失败进入普通恢复路径。循环在替换后的表层上重跑该 step,并像每次表层替换后一样记录新的 `request/header`;agent loop 不变。 + +**token 记账。** `priceImages` 接收表层的 `ImageBlock`,把带标记的按占位文本定价;DeepSeek 和 replay 的定价不再复现任何 offload 算术。meter 不需要新状态:`compaction/prune` 事件加替换节点,和工具结果剪枝一样重新为该节点定价。 + +**其他消费方。** compaction 通过 `deriveEventMessage()` 重建每个选中事件,看得到标记。resume、fork 和重放从日志复现表层。纯文本路由保留各自的全历史替换。 + +## 考虑过的替代方案 + +**继续每次请求重算 offload 位置。** 只在算术输入不动时稳定,内联回退、pi-ai 量子、compaction 和路由切换都会移动前缀,且没有消费方能重建历史请求的图片集合。 + +**用 log-only 事件记录每次请求的投影结果。** 恢复了可重建性但没有稳定性:记录的结果不是决策输入,每一种抖动照旧发生,日志只是把它记下来,且省略集合有两个可能不一致的事实来源。 + +**记录完整的投影后请求体。** 除 offload 决定外一切都已可派生,为记录一个位置而每次请求重复整段历史会让日志平方级增长。 + +**由 session 派生应用的 log-only `image/offload` 水位事件。** 更早的一版把 offload 点记成一个位置(事件序号加块路径),由 `Session.deriveMessages()` 给位于它及之前的每个出现位置打标记。这让 session 在 `surfaceOp: replace` 之外多出第二种改变模型可见历史的机制,带着自己的校验、折叠、缓存失效和读取时必须识别的事件,并且把逻辑放进了核心而不是做决定的插件。issue #3041 要求永久淘汰使用改变表层的事件而不是请求投影事件;省略永不回退,它就是永久淘汰。 + +**让各个 adapter 自己追加替换。** adapter 拥有预算,但不拥有会话表层;在循环之下追加表层变更会让两个 adapter 对表层做出不同定义。adapter 改为上报它需要的数量。 + +**在发送前规划省略,无论放在循环里还是插件里。** 循环在派生每个请求之前就知道精确的已准备路由,在那里规划永远不会多花一次失败的尝试;但这会把一条路由专属的策略放进所有 profile 共用的那个组件,改变已记录的 step 顺序,还绕过了上下文溢出 compaction 和重试已经在用的同一套 `agent/request-error` waterfall。pre-step 插件不改循环,但看不到 step 自己的消息和第一个请求的路由,失败路径仍然必需,而且规划要求每条路由在模型信息上声明预算。只处理失败的代价是每越过一个量子多一次尝试(DeepSeek file 模式 64 MiB,pi-ai 20 MiB),并且和既定方向一致:路由将不再本地检查大小,全部发送,由 provider 报告无法缓存的部分,那正是一个指明省略点的失败。 + +**为内联回退和精确字节溢出保留临时的额外省略。** 恰好会在不变量所针对的场景发送未记录的投影;失败再推进的路径只多花一次序列化尝试,且让每个已发出请求都可由日志派生。 + +## 后果 + +预算变大、选中更大的路由或 compaction 降低总量时,被省略的图片不会自动回归;恢复手段是占位文本中的只读路径,模型需要时主动使用。一次 Files 故障或临时切到小预算路由会永久省略,两者出于同一理由被接受。每次省略会把承载节点的消息复制一份进日志;图片只是引用,复制的是该节点的文本和块元数据。 + +每个已发出请求的图片集合都仅由日志决定,覆盖 file 模式、内联回退、resume、fork、重试和 compaction,provider 缓存前缀不再来回变化。占位文本和句柄文本里嵌入的执行世界访问路径仍在序列化时解析;这个缺口对保留的图片同样存在,属于另一个关于记录执行世界映射的决定。路由本地的字节检查是过渡实现:provider 自己报告无法缓存的图片之后,`requiredImageOffload()` 和路由预算会被删掉,带标记的副本和恢复分支保持不变。 + +## 测试 + +`packages/llm/llm/tests/content.spec.ts` 钉住任意深度的图片遍历、包括 129 到 64 MiB 量子示例在内的省略计数,以及占位投影。`packages/compaction/compaction-image-offload/tests/image-offload.spec.ts` 钉住 `IMAGE_OFFLOAD_REQUIRED` 替换并重试、带 `compaction/prune` 影子价格且不追加重试事件的路径、原节点保持不变、更早一次表层替换之后的请求顺序计数,以及向下游委托的耗尽情况。adapter 测试钉住占位投影、只读取保留图片以及带数量的精确字节失败;`route-pricing.spec.ts` 钉住带标记替换的占位定价。`inline-image-prompt` TypeScript SDK 快照通过发布的 profile 重放一次手工编写的 `IMAGE_OFFLOAD_REQUIRED` 尝试,钉住替换和重试后的请求。 diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index cded9f6c7e..0d758eec58 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -358,6 +358,9 @@ "architecture/2026-09-01-streamed-tool-call-identity.i18n.yaml": "sha256:5193bf7429a0300f743ea7e6087c3175a7d28c2c468ca1cfd1f77702b68b6399", "architecture/2026-09-01-streamed-tool-call-identity.md": "sha256:303433b95c22571bf6a801da604b42a4c7231f5f6d07daeb518354f0d7019141", "architecture/2026-09-01-streamed-tool-call-identity.zh.md": "sha256:53728d3ee06b29d6023e28024fb3ab6fa4a7473ae5e55368cd19690719d120f3", + "architecture/2026-09-02-durable-image-offload.i18n.yaml": "sha256:d06155597e7df4ca4c15a2902ab664d69cc2fde1e7963e61543d20a49930d2b9", + "architecture/2026-09-02-durable-image-offload.md": "sha256:02b7becba3dd73a4e627b85705e8bd5dd55978cb041f4d3a0fd1420a76e4de92", + "architecture/2026-09-02-durable-image-offload.zh.md": "sha256:a0464b8272086d663ee698efd0b2edd6224d40770d7a8fd88a6d8bfe030b2a1d", "architecture/2026-09-02-protocol-specific-model-listing-discovery.i18n.yaml": "sha256:ba8a62a9fa3263de4162709cb1aaf81f6ad4feee5011d7a5d8b24eb92ad81d30", "architecture/2026-09-02-protocol-specific-model-listing-discovery.md": "sha256:9db9c47559c17f0e2931b9e646f1d2d17b590df4d5699d4e779ed6d7f65fc371", "architecture/2026-09-02-protocol-specific-model-listing-discovery.zh.md": "sha256:9a266590d49093a097bcdb5d09865757cdd07625cfaaddce1303046d580551f1", diff --git a/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml index f0da51ef73..07c15ff846 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md -2026-07-23-client-plugin-loading-model.md: c5576b148c5991dd498d3aed605e3c2e3395774b -2026-07-23-client-plugin-loading-model.zh.md: c7d6982c2680995bd4698ddbff052a7708f69997 +2026-07-23-client-plugin-loading-model.md: d0f9b20f0adabda6cc7132e5411bcadd60c9e108 +2026-07-23-client-plugin-loading-model.zh.md: 27d5026ed03509d6408a16389246cb1321a37959 diff --git a/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md b/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md index c5576b148c..d0f9b20f0a 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md +++ b/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md @@ -58,12 +58,12 @@ What happens between `dsh web` starting and the UI appearing? Three stages: the Why is the roster yml rows and not a scan? Because which plugins compose into a deployment is a composition decision, not a package property — a package declaring `dsh.client` in the repo does not mean this deployment mounts it, so discovery-by-scan cannot make that call; the node half scans only what the tree actually mounted. -**Phase one — the module face.** The injected HTML installs `window.__ModuleLoader__` in queue mode, starts preloading every application combo URL, executes every bootstrap combo URL as a blocking classic script, assigns `window.__DSH_BOOT__`, and then starts the Vite main module. The kernel calls the facade's `create()` with the raw graph and shell seeds. The facade removes and materializes the modules registration with a bootstrap `require` that rejects every external, then calls its `createClientModuleSystem` export. The modules bundle parses the graph, constructs the system, memoizes its own exports, retains the instance in its module closure, and switches the same facade to live registration. The kernel then prefetches every `immediately` row in parallel. Rows in the same application combo share its execution; separate combos load independently when an immediate row, a requested dependency, or ordinary entry import reaches them. A prefetch failure is swallowed here because phase two's import retries and owns the loud failure. `immediately` remains a registration barrier, not a package identity. +**Phase one — the module face.** The injected HTML installs `window.__ModuleLoader__` in queue mode, starts preloading every application combo URL, executes every bootstrap combo URL as a blocking classic script, assigns `window.__DSH_BOOT__`, and then starts the Vite main module. The kernel calls the facade's `create()` with the raw graph and shell seeds. The facade removes and materializes the modules registration with a bootstrap `require` that rejects every external, then calls its `createClientModuleSystem` export. The modules bundle parses the graph, constructs and returns the system, memoizes its own exports, and switches the same facade to live registration. The kernel then prefetches every `immediately` row in parallel. Rows in the same application combo share its execution; separate combos load independently when an immediate row, a requested dependency, or ordinary entry import reaches them. A prefetch failure is swallowed here because phase two's import retries and owns the loud failure. `immediately` remains a registration barrier, not a package identity. **Phase two — the plugin face.** 1. The kernel mounts the vendored Loader and injects the module system as `internal` before any entry exists. Ordering matters: `tree.import`'s bare-import fallback must never run in a browser. -2. It creates every graph row uniformly. Importing the modules row returns the memoized bootstrap exports, whose `apply()` provides the closed-over system as `ctx.modules`; rows that require that service remain PENDING until then, so the modules row needs no special creation position. Render assembly is an ordinary host-graph row provided by `dsh-client-ui-renderer`; the kernel appends no assembly pseudo-entry. +2. It creates every graph row uniformly. Importing the modules row returns the memoized bootstrap exports, whose `apply()` reads that tree's `Loader.internal` and provides the same instance as `ctx.modules`; rows that require that service remain PENDING until then, so the modules row needs no special creation position. Render assembly is an ordinary host-graph row provided by `dsh-client-ui-renderer`; the kernel appends no assembly pseudo-entry. 3. Graph order governs synchronous factory availability; Cordis activation remains independent and proceeds through service waiting. 4. `settled` = every entry created + `loader.await()` quiescent + an all-ACTIVE sweep. The sweep lists each import-failed, FAILED, or PENDING fiber with its missing services. It exists because cordis inject waits have no timeout — the sweep is the fail-loud floor. 5. The framework-free loading page projects real fiber states via `internal/status`. After the sweep, the kernel calls `ctx.uiRenderer.mount(container)` and replaces the page with the real UI in one pass. diff --git a/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.zh.md b/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.zh.md index c7d6982c26..27d5026ed0 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.zh.md @@ -58,12 +58,12 @@ Host 会快照每个已构建插件产物,并把每个调度阶段的有序 ro 为什么名册是 yml 行而不是扫描?因为哪些插件组合进一次部署是组合决策,不是包属性——一个在仓库中声明了 dsh.client 的包,不代表这次部署要挂载它,扫描发现无从替人做这个决定;node 半只扫描配置树实际挂载了的东西。 -**第一阶段——模块面。**注入的 HTML 以 queue 模式安装 `window.__ModuleLoader__`,开始预加载所有 application combo URL,以阻塞式 classic script 依次执行所有 bootstrap combo URL,赋值 `window.__DSH_BOOT__`,然后启动 Vite 主模块。内核把原始图和外壳 seed 传给 facade 的 `create()`。Facade 移除 modules registration,用拒绝全部 external 的 bootstrap `require` 将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造系统、记忆化自身 exports、在模块闭包中保留该实例,并把同一 facade 切换到 live registration。随后内核并行预取每个 `immediately` row;同一 application combo 中的 row 共享一次执行,不同 combo 会在 immediate row、被请求依赖或普通 entry import 首次触及时独立加载。预取失败在这里被吞下,因为第二阶段 import 会重试并拥有那次大声失败。`immediately` 仍是 registration barrier,不是包身份。 +**第一阶段——模块面。**注入的 HTML 以 queue 模式安装 `window.__ModuleLoader__`,开始预加载所有 application combo URL,以阻塞式 classic script 依次执行所有 bootstrap combo URL,赋值 `window.__DSH_BOOT__`,然后启动 Vite 主模块。内核把原始图和外壳 seed 传给 facade 的 `create()`。Facade 移除 modules registration,用拒绝全部 external 的 bootstrap `require` 将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造并返回系统、记忆化自身 exports,并把同一 facade 切换到 live registration。随后内核并行预取每个 `immediately` row;同一 application combo 中的 row 共享一次执行,不同 combo 会在 immediate row、被请求依赖或普通 entry import 首次触及时独立加载。预取失败在这里被吞下,因为第二阶段 import 会重试并拥有那次大声失败。`immediately` 仍是 registration barrier,不是包身份。 **第二阶段——插件面。** 1. 内核挂载 vendored Loader,在任何 entry 存在之前就把模块系统注入为 `internal`。顺序有讲究:`tree.import` 的裸 import 兜底分支在浏览器里绝不能跑到。 -2. 它统一创建每个 graph row。Import modules row 会返回记忆化的 bootstrap exports,其 `apply()` 把闭包中的系统提供为 `ctx.modules`;需要该 service 的 row 会保持 PENDING 直至此时,因此 modules row 无需特殊创建位置。渲染组装是由 `dsh-client-ui-renderer` 提供的普通 host graph row;内核不追加组装伪 entry。 +2. 它统一创建每个 graph row。Import modules row 会返回记忆化的 bootstrap exports,其 `apply()` 读取该树的 `Loader.internal`,把同一个实例提供为 `ctx.modules`;需要该 service 的 row 会保持 PENDING 直至此时,因此 modules row 无需特殊创建位置。渲染组装是由 `dsh-client-ui-renderer` 提供的普通 host graph row;内核不追加组装伪 entry。 3. Graph 顺序治理同步 factory 可用性;Cordis 激活与之独立,仍经服务等待推进。 4. `settled` = 每个 entry 已创建 + `loader.await()` 完全停稳 + 一次全 ACTIVE 扫描。扫描列出每个 import 失败、FAILED 或 PENDING 的 fiber 及其缺失的服务。它存在的理由:cordis 的 inject 等待没有超时——这次扫描就是大声失败的兜底线。 5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。检查完成后,内核调用 `ctx.uiRenderer.mount(container)`,一次切换到真实 UI。 diff --git a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.i18n.yaml index 892e3a8fc0..a5d41c6bca 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md -2026-07-29-projected-token-usage-and-request-context.md: f96257243bef91ff6a73418231de5e777d8edb2e -2026-07-29-projected-token-usage-and-request-context.zh.md: 7365d5d816f9f9b324f3e3d3b4db0d3346851bdf +2026-07-29-projected-token-usage-and-request-context.md: 10056ac8ef85562035d4fc4591e90aec603a7dd8 +2026-07-29-projected-token-usage-and-request-context.zh.md: 0e4cd68791f0ee65dd700b64493be3c33b78a920 diff --git a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md index f96257243b..10056ac8ef 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md +++ b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md @@ -58,4 +58,4 @@ Token totals stay stable across pagination, compaction, replay, restart, and rec Occupancy is approximate in the ways documented above. It is available immediately after restore or reconnect, since both fields are durable, at the cost of describing the last recorded request rather than an exact current boundary. -Each session log gains one small `request/context` record per route or advertised-capacity change. Token-meter is the canonical owner of durable usage semantics, including retry-attempt separation in the cumulative projection and the reusable exact attempt/Turn fold; Web Chat only selects a complete loaded Turn and renders the fold result. The TUI retains its live per-step map because it does not mount the generic projection seam, and the standalone browser fixture mirrors the unit. Connection and API Gateway carry no token-specific code, own no per-session metrics cache, and perform no measurement. The browser keeps two generic projection values and no connection-local telemetry; streaming text deltas do not force the stats line to recompute or churn layout-observer subscriptions. +Each session log gains one small `request/context` record per route or advertised-capacity change. Token-meter is the canonical owner of durable usage semantics, including retry-attempt separation in the cumulative projection and the reusable exact attempt/Turn fold; Web Chat only selects a complete loaded Turn and renders the fold result. The TUI retains its live per-step map because it does not mount the generic projection seam, and the assembled RemoteMock scenario supplies the same projection values. Connection and API Gateway carry no token-specific code, own no per-session metrics cache, and perform no measurement. The browser keeps two generic projection values and no connection-local telemetry; streaming text deltas do not force the stats line to recompute or churn layout-observer subscriptions. diff --git a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md index 7365d5d816..0e4cd68791 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md @@ -58,4 +58,4 @@ token 总量在分页、压缩、回放、重启和重连期间保持稳定, 占用率在上文记录的意义上是近似值。由于两个字段都是持久的,它在恢复或重连后立即可用;代价是它描述的是最后一条已记录的请求,而不是精确的当前边界。 -每个会话日志会为每次路由或已公布容量变化增加一条小型 `request/context` 记录。token-meter 是持久用量语义的正典所有方,包括累计投影中的重试 attempt 分离,以及可复用的精确 attempt/Turn fold;Web Chat 只选择已完整加载的 Turn 并渲染 fold 结果。TUI 未挂载通用投影 seam,因此保留自己的实时逐步骤 map,而独立浏览器 fixture(测试前置数据)会镜像该单元。Connection 与 API Gateway 不携带任何 token 专用代码,不拥有逐会话指标缓存,也不执行测量。浏览器只保留两个通用投影值,不保留连接本地的遥测数据;流式文本增量不会迫使统计行重新计算或反复替换布局 observer 订阅。 +每个会话日志会为每次路由或已公布容量变化增加一条小型 `request/context` 记录。token-meter 是持久用量语义的正典所有方,包括累计投影中的重试 attempt 分离,以及可复用的精确 attempt/Turn fold;Web Chat 只选择已完整加载的 Turn 并渲染 fold 结果。TUI 未挂载通用投影 seam,因此保留自己的实时逐步骤 map,而组装 RemoteMock 场景提供相同投影值。Connection 与 API Gateway 不携带任何 token 专用代码,不拥有逐会话指标缓存,也不执行测量。浏览器只保留两个通用投影值,不保留连接本地的遥测数据;流式文本增量不会迫使统计行重新计算或反复替换布局 observer 订阅。 diff --git a/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.i18n.yaml index dde6b822da..aecb82c585 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.md -2026-07-31-goal-owned-durable-events.md: 8f7d3b74a83e063fbfc5a530f9e51a2a3eea8197 -2026-07-31-goal-owned-durable-events.zh.md: 8ee0099209c9c98a88549073b33c98ddb4b12bbd +2026-07-31-goal-owned-durable-events.md: 0d3e632ba275f38e48fdf1f9ee6747f4bc460aab +2026-07-31-goal-owned-durable-events.zh.md: fd29a6f36eed42f5ba26a46a32acb165d1e42873 diff --git a/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.md b/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.md index 8f7d3b74a8..0d3e632ba2 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.md +++ b/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.md @@ -30,4 +30,4 @@ The domain does not automatically project each mutation into model input. Goal t Goal state is independent of inbox placement and admission. Replay has one mutation path, projections advance directly on `goal/change`, and continuation messages carry only round attribution. The model does not receive a mutation-only `` message; model-visible state appears through goal tools and scheduled continuation prompts. Direct session writers remain trusted and can append malformed changes, which the strict fold and invariant companion reject. -Focused goal, goal-round-driver, command, TUI, and client-fixture tests pin durable replay, positive-round accounting, inbox independence, projection updates, and restored-session behavior. The keyless process test inspects the persisted `goal/change` event and verifies that creation alone starts no continuation round. +Focused goal, goal-round-driver, command, TUI, and Client RemoteMock tests pin durable replay, positive-round accounting, inbox independence, projection updates, and restored-session behavior. The keyless process test inspects the persisted `goal/change` event and verifies that creation alone starts no continuation round. diff --git a/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.zh.md b/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.zh.md index 8ee0099209..fd29a6f36e 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-31-goal-owned-durable-events.zh.md @@ -30,4 +30,4 @@ Goal 领域需要持久状态,但不需要拥有待处理的模型输入。继 Goal 状态不依赖 inbox 放置与准入。回放只有一条变更路径,投影直接由 `goal/change` 推进,继续执行消息只携带 Round 归属。模型不会收到仅用于变更的 `` 消息;模型可见状态来自 goal 工具与已调度的继续执行提示词。直接写入会话的写入方仍受信任,并且可以追加畸形变更;严格折叠与 invariant 配套模块会拒绝这些变更。 -聚焦的 goal、goal-round-driver、command、TUI 与 client fixture(测试前置数据)测试固定持久回放、正数 Round 计数、inbox 独立性、投影更新和恢复会话行为。无密钥进程测试检查持久的 `goal/change` 事件,并验证仅创建 goal 不会启动继续执行 Round。 +聚焦的 goal、goal-round-driver、command、TUI 与 Client RemoteMock 测试固定持久回放、正数 Round 计数、inbox 独立性、投影更新和恢复会话行为。无密钥进程测试检查持久的 `goal/change` 事件,并验证仅创建 goal 不会启动继续执行 Round。 diff --git a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml index 9898d344d1..d9bd4a9c76 100644 --- a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md -2026-08-15-client-shells-and-dynamic-packages.md: 1d67c778b6a06849324dd6095a98d57dc41b94f9 -2026-08-15-client-shells-and-dynamic-packages.zh.md: db1e4e7e7b319c283ae39a94535d88d4dc71d60a +2026-08-15-client-shells-and-dynamic-packages.md: 89f89784512b86b70ee1b9460850e6f2f951a082 +2026-08-15-client-shells-and-dynamic-packages.zh.md: 5512d9262e979a94a65c25a6c1271b030e23ec70 diff --git a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md index 1d67c778b6..89f8978451 100644 --- a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md +++ b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md @@ -51,7 +51,7 @@ The modules Node half injects the startup protocol into the served HTML in this 4. Assign `window.__DSH_BOOT__`, including all scheduling descriptors and every row's one-resource HMR combo URL. 5. Execute the Vite main module. -The bootstrap combo currently registers only the modules factory. The startup kernel passes the raw graph and shell seeds to `__ModuleLoader__.create()`. The facade removes the modules registration, materializes it with a `require` function that rejects every external, and invokes its `createClientModuleSystem` export. The modules bundle parses the graph, constructs `ClientModuleSystem`, caches its own exports as the modules row, retains the system in a module closure, and switches the same facade to live mode. The modules client face consequently has a zero-external bootstrap requirement. +The bootstrap combo currently registers only the modules factory. The startup kernel passes the raw graph and shell seeds to `__ModuleLoader__.create()`. The facade removes the modules registration, materializes it with a `require` function that rejects every external, and invokes its `createClientModuleSystem` export. The modules bundle parses the graph, constructs and returns `ClientModuleSystem`, caches its own exports as the modules row, and switches the same facade to live mode. The kernel installs that instance as its Loader's `internal`, and the modules plugin reads it there when it provides `ctx.modules`. The modules client face consequently has a zero-external bootstrap requirement and no module-global system identity. After the `immediately` tier has registered its factories, the kernel creates all Loader entries, awaits Cordis quiescence, and requires every fiber to be ACTIVE. It then calls `ctx.uiRenderer.mount(container)`. The dynamic `ui-renderer` package owns React, slot rendering, hydration of the existing boot DOM, and the React root lifecycle; the startup kernel and failure page remain React-free. diff --git a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md index db1e4e7e7b..5512d9262e 100644 --- a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md @@ -51,7 +51,7 @@ Modules Node 半按以下顺序向实际返回的 HTML 注入启动协议: 4. 赋值 `window.__DSH_BOOT__`,其中包含全部调度描述及每个 row 的单资源 HMR combo URL。 5. 执行 Vite 主模块。 -Bootstrap combo 当前只登记 modules factory。启动内核把原始图与外壳 seed 传给 `__ModuleLoader__.create()`。Facade 移除 modules registration,用拒绝全部 external 的 `require` 函数将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造 `ClientModuleSystem`、把自身 exports 缓存为 modules row、在模块闭包中保留该系统,并把同一 facade 切换到 live 模式。因此 modules client face 必须满足零 external 的自举要求。 +Bootstrap combo 当前只登记 modules factory。启动内核把原始图与外壳 seed 传给 `__ModuleLoader__.create()`。Facade 移除 modules registration,用拒绝全部 external 的 `require` 函数将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造并返回 `ClientModuleSystem`、把自身 exports 缓存为 modules row,并把同一 facade 切换到 live 模式。内核把该实例装成自身 Loader 的 `internal`,modules 插件从这里读取并提供 `ctx.modules`。因此 modules client face 保持零 external 的自举要求,也没有模块级系统身份。 `immediately` 层级完成 factory 注册后,内核创建全部 Loader entry,等待 Cordis 静止,并要求每个 fiber 都进入 ACTIVE。随后调用 `ctx.uiRenderer.mount(container)`。动态 `ui-renderer` 包拥有 React、slot 渲染、已有启动 DOM 的 hydrate 和 React root 生命周期;启动内核与失败页保持 React-free。 diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml index 0e53dfc6d6..fff8e98bc5 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md -2026-08-23-client-derived-tool-presentation.md: 895618099c6669058d23fc0a58335a90d5fbe4ca -2026-08-23-client-derived-tool-presentation.zh.md: 38572f17c41ea9b68dc8c8c8cdc26a74d9dbb757 +2026-08-23-client-derived-tool-presentation.md: 703960cb24f90eb678fda381f929efb3f94bbd28 +2026-08-23-client-derived-tool-presentation.zh.md: 7f161379553f3aca33c72c2edf93e114d27eb7f9 diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md index 895618099c..703960cb24 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md @@ -387,7 +387,7 @@ This change does not add a general tool-side-effect registry. The ability for a ## Fixtures and Test Data -The Client fixture deletes its handwritten `presentCall()`, `presentResult()`, `viewFor()`, and fixture tool-view types. It continues producing the same raw calls, result content, and result metadata as a real log. +The assembled RemoteMock scenario contains no handwritten `presentCall()`, `presentResult()`, `viewFor()`, or tool-view types. It supplies the same raw calls, result content, and result metadata as a real log. | Fixture | Raw facts that must remain | |---|---| @@ -398,7 +398,7 @@ The Client fixture deletes its handwritten `presentCall()`, `presentResult()`, ` | web | result metadata sources/answer or url/statusCode/truncated | | generic/custom | name, argsRaw, content, and error | -The fixture does not import Host tool packages to compute page presentation and retains no presenter mirror. The same raw fixture continues to drive jsdom, built Web snapshots, and the `?fixture` browser path. +The scenario does not import Host tool packages to compute page presentation and retains no presenter mirror. The same raw scenario drives built Web snapshots under jsdom; real-Host browser cases independently cover the network path. ## Presentation-Equivalence Matrix @@ -566,7 +566,7 @@ Changes to this decision use `dsh-pre-push-checks` to select commands for the fi - ui-chat and ui-trajectory Tool Definition tests; - ui-tool terminal, diff, read, search, web, row, tree, and details tests; - ui-deliverables produced-file tests; -- connection fixture and Client runtime tests; +- assembled RemoteMock and Client runtime tests; - affected Host and Client TypeScript faces; - lint and duplication; - per-file 100% coverage for affected source files; diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md index 38572f17c4..7f16137955 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md @@ -387,7 +387,7 @@ Deliverables Definition 按 callId 观察 root `tool/call` 与成功 `tool/resul ## Fixture 与测试数据 -Client fixture 删除手写 `presentCall()`、`presentResult()`、`viewFor()` 与 fixture tool-view 类型。它继续产生与真实日志相同的 raw call、result content 和 result meta。 +组装 RemoteMock 场景不包含手写 `presentCall()`、`presentResult()`、`viewFor()` 或 tool-view 类型。它提供与真实日志相同的 raw call、result content 和 result meta。 | Fixture | 必须保留的原始事实 | |---|---| @@ -398,7 +398,7 @@ Client fixture 删除手写 `presentCall()`、`presentResult()`、`viewFor()` | web | result meta 的 sources/answer 或 url/statusCode/truncated | | generic/custom | name、argsRaw、content、error | -fixture 不导入 Host 工具包来计算页面展示,也不保留 presenter 镜像。同一 raw fixture 继续驱动 jsdom、built Web snapshot 与 `?fixture` 浏览器路径。 +该场景不导入 Host 工具包来计算页面展示,也不保留 presenter 镜像。同一 raw 场景在 jsdom 下驱动 built Web snapshot;真实 Host 浏览器用例独立覆盖网络路径。 ## 展示等价矩阵 @@ -566,7 +566,7 @@ Host registry 允许不同 scope 为同一 tool name 提供不同定义;Sessio - ui-chat 与 ui-trajectory Tool Definition 测试; - ui-tool terminal、diff、read、search、web、row、tree 与 details 测试; - ui-deliverables produced-files 测试; -- connection fixture 与 Client runtime 测试; +- 组装 RemoteMock 与 Client runtime 测试; - 受影响 Host/Client TypeScript face; - lint 与 duplication; - 受影响源文件 per-file 100% coverage; diff --git a/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.i18n.yaml index e78d7bdc51..a5e163f76e 100644 --- a/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.md -2026-09-06-embedded-stream-record-readers.md: 972fee634833cef5fd7b0a54f69780b9370f0cc3 -2026-09-06-embedded-stream-record-readers.zh.md: faba6e179887a9943926f2c73aa8e42a10cee300 +2026-09-06-embedded-stream-record-readers.md: 76e109de577d070093bb7123aa50d9a4ae7fcbe5 +2026-09-06-embedded-stream-record-readers.zh.md: 33f4b31117197c47e7f2ff1637bc13fa2cbf1a8c diff --git a/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.md b/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.md index 972fee6348..76e109de57 100644 --- a/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.md +++ b/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.md @@ -20,7 +20,7 @@ After v2 embedded streams settlement widened with the message content and Chat a - Run readers: `runFirstTokenTime` and `runFirstVisibleTime` reconstruct the first qualifying member's time from `time0` and the `dt` gaps and stop scanning there; a name-bearing Tool-call run yields `time0` without reading a fragment. - Stream readers: `assistantStreamFirstTokenTime`, `assistantStreamHasVisibleContent`, `assistantStreamHasVisibleText`, `lastAssistantStreamChunk(stream, type)` (backward scan), `assistantStreamChunks(stream, type)`, `joinAssistantStreamText`, and `assembleAssistantStream`, which feeds a `BlockAssembler` one joined delta per run (assembly only concatenates, so blocks, usage, finish, and replay state equal the per-member result). `RawStreamChunkType` excludes the delta types, so a raw-chunk lookup can never silently skip packed members. -Session Stats reads `assistantStreamFirstTokenTime`; the token meter reads `lastAssistantStreamChunk(stream, 'usage')` and assembles provider output through `assembleAssistantStream`; the subagent output fold appends `joinAssistantStreamText`; the Session Controller scans `assistantStreamChunks(stream, 'block-end')` for images. +Session Stats, Chat, and Trajectory read `assistantStreamFirstTokenTime` from both `assistant/attempt` and `assistant/message`, retaining the Step's first token across retries. Chat and Trajectory settle content from the assembled message while reading timing independently, so reopening history retains TTFT and decoding metrics without expanding streams. The token meter reads `lastAssistantStreamChunk(stream, 'usage')` and assembles provider output through `assembleAssistantStream`; the subagent output fold appends `joinAssistantStreamText`; the Session Controller scans `assistantStreamChunks(stream, 'block-end')` for images. `expandAssistantStream` keeps its strict validation and its remaining callers, which need every member or validate the stream at a durable boundary: Session restore validation, the v1-to-v2 migration validator and publication Worker replay, the reconnect baseline, and test support. diff --git a/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.zh.md b/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.zh.md index faba6e1798..33f4b31117 100644 --- a/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.zh.md @@ -20,7 +20,7 @@ Session 格式 v2 将每次模型尝试的紧凑流(`AssistantStreamRecord[]` - Run 读取器:`runFirstTokenTime` 与 `runFirstVisibleTime` 从 `time0` 与 `dt` 间隔重建首个合格成员的时间并停止扫描;带名称的 Tool-call run 直接产出 `time0`,不读片段。 - 流读取器:`assistantStreamFirstTokenTime`、`assistantStreamHasVisibleContent`、`assistantStreamHasVisibleText`、`lastAssistantStreamChunk(stream, type)`(逆向扫描)、`assistantStreamChunks(stream, type)`、`joinAssistantStreamText` 与 `assembleAssistantStream`(每个 run 向 `BlockAssembler` 喂入一个拼接后的 delta;组装只做拼接,因此 blocks、usage、finish 与 replay state 与逐成员结果一致)。`RawStreamChunkType` 排除 delta 类型,因此原始 chunk 查找不可能静默跳过打包成员。 -Session Stats 读取 `assistantStreamFirstTokenTime`;token 计量读取 `lastAssistantStreamChunk(stream, 'usage')` 并通过 `assembleAssistantStream` 组装提供商输出;子代理输出折叠追加 `joinAssistantStreamText`;Session Controller 用 `assistantStreamChunks(stream, 'block-end')` 扫描镜像。 +Session Stats、Chat 与 Trajectory 从 `assistant/attempt` 和 `assistant/message` 读取 `assistantStreamFirstTokenTime`,跨重试保留步骤的首个 token。Chat 与 Trajectory 从组装后的消息结算内容,并独立读取计时,因此重新打开历史时无需展开流便能保留 TTFT 与解码指标。token 计量读取 `lastAssistantStreamChunk(stream, 'usage')` 并通过 `assembleAssistantStream` 组装提供商输出;子代理输出折叠追加 `joinAssistantStreamText`;Session Controller 用 `assistantStreamChunks(stream, 'block-end')` 扫描镜像。 `expandAssistantStream` 保留其严格校验与其余调用方(需要每个成员或在持久边界校验流):Session 恢复校验、v1-to-v2 迁移校验器与发布 Worker 重放、重连基线、测试支撑。 diff --git a/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.i18n.yaml new file mode 100644 index 0000000000..3a9f8ed124 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-10-image-offload-events.md +2026-09-10-image-offload-events.md: f540dc98cee8daf3e360b14234a2ac21d0f53aab +2026-09-10-image-offload-events.zh.md: 868fca59ba5fc1941de752127f61b091aedb234f diff --git a/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.md b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.md new file mode 100644 index 0000000000..f540dc98ce --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.md @@ -0,0 +1,49 @@ +# Agent Note: Durable image offload events + +Status: implemented + +English | [中文](2026-09-10-image-offload-events.zh.md) + +## Problem + +A request can exceed an image-capable route's byte or image-count budget. Recomputing the omitted prefix for each request lets a route switch, inline fallback, or compaction restore previously omitted images. The model-visible image set then depends on unlogged request preparation. The [unified image pipeline](../feature/2026-08-20-unified-image-request-pipeline.md) owns attachment normalization and request versions; durable omission needs its own recorded decision. + +## Decision + +`dsh-compaction-image-offload` owns recovery on the `agent/request-error` waterfall. An `IMAGE_OFFLOAD_REQUIRED` failure supplies `offloadImages`; the plugin selects that many oldest retained input-image occurrences in current surface order, appends one `image/offload` event, and returns `retry`. Assistant output images are excluded. If no retained input image remains, the plugin delegates. Other failures do not enter this recovery. Offload does not consume the provider retry budget or emit `llm/retry`. + +Summary requests bypass the agent error waterfall. `dsh-compaction-basic` preserves the full `LlmFailure`, checks cancellation and selection stability, and dispatches synchronous `compaction/summary-error` with the selected event seqs. The same image-offload plugin records omissions only within that selection. Synchronous recovery keeps the stability check and decision adjacent. The backend then re-derives input and pricing, including the shrink baseline. Each retry requires additional omitted occurrences; exhaustion delegates the failure. A later summary failure or cancellation preserves any recorded omissions, so command errors do not claim the conversation is unchanged. + +The event payload is `{ targets: [{ seq, imageIndexes }] }`. Each target identifies a current `user/message` or `tool/result` node. Its strictly increasing, zero-based indexes enumerate every image in depth-first content order, including nested tool results and previously omitted images. Equal attachment ids remain distinct occurrences. Exact selections remain unambiguous after positional replacements reorder the surface; an attachment id or a raw-log prefix cannot identify the selected set. + +The event has no `surfaceOp`. It neither creates a message nor replaces a message node. The plugin's pure projection validates all targets before Session commits the event and rejects missing or shadowed nodes, output images, duplicate targets, invalid indexes, and already omitted occurrences. Reconstruction derives immutable message copies with `ImageBlock.offloaded: true` at the recorded positions. Original events, message ids, sources, and unaffected blocks remain unchanged. Resume, fork, and replay apply the same selections from the log. + +The compaction plugin owns selection, event declaration, image validation and projection, and retry policy. [Plugin-owned message projections](2026-09-11-plugin-owned-message-projections.md) owns the generic Session integration and explicit detached assembly, superseding core-owned interpretation. The instance `deriveEventMessage()` and `deriveMessages()` return projected messages; pure reconstructors pass `foldSurface(events, projections).projectedMessages` to the exported `deriveEventMessage()`. Rewriters preserving images must consume projected messages, including tool-result pruning and summarizer input. + +`contentGeneration` advances for message replacements and image-offload events so cached history and request series are refreshed. `replaceGeneration` advances only for replacements; image omission cannot masquerade as successful text compaction. An unchanged request envelope gets a `request/header` with reason `series`; a simultaneous envelope change carries `startsSeries: true`. + +Adapters prepare only retained images and render marked occurrences as `offloadedImageText` with the current execution-world access path. Route-local budget checks report required counts, including the tighter inline fallback budget, but do not edit history. These checks are provisional: a provider failure that identifies uncacheable images can replace local counting without moving the durable decision into the adapter. + +Token accounting applies selections to the affected nodes' image occurrences without replacing their identities or changing prior usage-anchor snapshots. Route pricing substitutes placeholder text for image cost. The fixed structural heuristic excludes the `offloaded` marker, so image omission does not change that heuristic or require a `compaction/prune` shadow price. The context-pressure and context-breakdown checkpoint versions advance to reject cached estimates that counted the marker. + +`image/offload` is required on read. Its addition changes the recognized event vocabulary, not the Session envelope or operation grammar, so it does not advance `SESSION_FORMAT_VERSION`. A reader without this event type refuses the log instead of silently restoring omitted images. + +## Alternatives considered + +**Reuse `compaction/prune` and `surfaceOp: replace`.** The [archived replacement design](../../archived/architecture/2026-09-02-durable-image-offload.md) records full carrying-message copies and couples omission to node replacement and shadow-price accounting. A dedicated event expresses which images are omitted while retaining message identity and keeping retry policy in the same plugin. + +**Record a request-local projection outcome.** A record that does not affect subsequent derivation cannot prevent omitted images from returning. Logging whole request bodies repeats message history for a decision that needs only occurrence indexes. + +**Use an attachment id or a single watermark.** Attachments can occur repeatedly, and surface order can differ from event-sequence order. Explicit per-node indexes distinguish these cases without redefining a watermark after every replacement. + +**Move selection into the loop or adapters.** The request-error extension already owns repair and retry. The adapter knows the route budget; the plugin knows the session input order. Neither requires route-specific policy in the shared loop. + +## Consequences + +Omission remains effective when a route budget grows, an inline fallback ends, or compaction reduces context. It does not delete attachment bytes. Reading the placeholder's access path creates a new image occurrence that can be retained independently. The execution-world path is still resolved during serialization, as it is for retained image handles; this event records the selected image set, not filesystem mappings. + +Session invokes the registered pure projection and maintains one content-generation signal. Consumers reconstructing model input must use projected messages. Human views can still show original images from immutable events. The plugin requires neither token-meter nor compaction services merely to record omission. + +## Testing + +Plugin projection tests cover nested and repeated occurrences, atomic rejection, cache invalidation, pure folding, restore, fork, and untouched source events. Recovery tests cover consecutive failures, current surface order, exhausted recovery, fresh request headers, and disposal. Token-meter tests preserve node identity and prior usage anchors while repricing only selected occurrences. Tool-result pruning and compaction tests ensure later rewrites consume the projected history. TypeScript and Python SDK expected outputs exercise the required event through shipped profiles. diff --git a/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.zh.md b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.zh.md new file mode 100644 index 0000000000..868fca59ba --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.zh.md @@ -0,0 +1,49 @@ +# Agent Note:持久图片省略事件 + +Status: implemented + +[English](2026-09-10-image-offload-events.md) | 中文 + +## 问题 + +请求可能超过图片路由的字节或图片数量预算。每次请求重新计算省略前缀,会让路由切换、内联回退或压缩恢复先前省略的图片。模型可见的图片集合因此取决于未记录的请求准备过程。[统一图片流水线](../feature/2026-08-20-unified-image-request-pipeline.zh.md)负责附件归一化与请求版本,持久省略需要单独记录决策。 + +## 决策 + +`dsh-compaction-image-offload` 在 `agent/request-error` waterfall 上负责恢复。`IMAGE_OFFLOAD_REQUIRED` 失败提供 `offloadImages`,插件按当前 surface 顺序选择该数量最旧且仍保留的输入图片出现位置,追加一个 `image/offload` 事件,再返回 `retry`。Assistant 输出图片不参与选择。没有保留的输入图片时,插件委托后续处理。其他失败不进入这项恢复。图片省略不消耗提供方重试预算,也不产生 `llm/retry`。 + +摘要请求不经过 agent 错误 waterfall。`dsh-compaction-basic` 保留完整的 `LlmFailure`,检查取消和选区稳定性,再携带所选事件序号触发同步的 `compaction/summary-error`。同一个图片省略插件只在选区内记录省略。同步恢复使稳定性检查与决策相邻执行。后端随后重新派生输入并计价,包括判断摘要是否缩短输入的基准。每次重试都必须继续省略图片,无图可省略时委派失败。后续摘要失败或取消仍保留已经记录的省略,因此命令错误不声称会话未变。 + +事件载荷为 `{ targets: [{ seq, imageIndexes }] }`。每个目标指向当前的 `user/message` 或 `tool/result` 节点。索引从零开始且严格递增,按内容深度优先顺序枚举所有图片,包括嵌套工具结果和先前省略的图片。相同附件 ID 的多次出现分别计数。位置替换改变 surface 顺序后,明确选择仍没有歧义,附件 ID 或原始日志前缀无法标识这个集合。 + +事件不带 `surfaceOp`,不创建消息,也不替换消息节点。插件的纯投影在 Session 提交事件前校验全部目标,拒绝缺失或已被遮蔽的节点、输出图片、重复目标、无效索引和已被省略的位置。重建过程派生不可变消息副本,将记录的位置标为 `ImageBlock.offloaded: true`。原始事件、消息 ID、来源和未受影响的内容块保持不变。恢复、分叉和回放从日志应用相同的选择。 + +compaction 插件拥有选图、事件声明、图片校验与投影,以及重试策略。[插件拥有消息投影](2026-09-11-plugin-owned-message-projections.zh.md)负责通用 Session 集成和显式独立装配,取代由核心负责解释的职责划分。实例方法 `deriveEventMessage()` 与 `deriveMessages()` 返回派生消息,纯重建函数将 `foldSurface(events, projections).projectedMessages` 传给导出的 `deriveEventMessage()`。需要保留图片的改写必须读取派生消息,包括工具结果裁剪和摘要输入。 + +消息替换和图片省略事件都会推进 `contentGeneration`,使历史缓存和请求系列刷新。`replaceGeneration` 只随替换推进,图片省略不能被判断为文本压缩成功。请求配置未变时写入原因是 `series` 的 `request/header`,配置同时变化时携带 `startsSeries: true`。 + +适配器只准备保留的图片,将带标记的位置渲染为包含当前执行环境访问路径的 `offloadedImageText`。路由本地预算检查报告所需数量,包括内联回退的更低预算,但不编辑历史。这些检查是暂时方案,未来提供方报告无法缓存的图片时,可以替代本地计数,无需把持久决策移入适配器。 + +Token 计量将选择应用到受影响节点的图片出现位置,不替换节点身份,也不修改已有用量锚点快照。路由计价以占位文本替代图片费用。固定结构启发式不计 `offloaded` 标记,因此图片省略不改变该启发式,也不需要 `compaction/prune` 的遮蔽价格。上下文压力和分类明细检查点版本递增,拒绝曾将该标记计入的缓存估算。 + +`image/offload` 是读取时必须识别的事件。它增加已知事件类型,不改变 Session 信封或操作语法,因此不推进 `SESSION_FORMAT_VERSION`。不认识该类型的读取器会拒绝日志,避免静默恢复已省略的图片。 + +## 考虑过的替代方案 + +**复用 `compaction/prune` 和 `surfaceOp: replace`。** [已归档的替换设计](../../archived/architecture/2026-09-02-durable-image-offload.md)记录携带图片的完整消息副本,把图片省略与节点替换和遮蔽价格计量绑定。专用事件表达被省略的图片,保留消息身份,重试策略仍留在同一个插件中。 + +**记录单次请求的投影结果。** 不影响后续派生的记录无法阻止省略的图片返回。记录完整请求体会重复消息历史,而决策只需要图片出现位置索引。 + +**使用附件 ID 或单个分界点。** 附件可能重复出现,surface 顺序也可能与事件序号顺序不同。逐节点记录明确索引能够区分这些情况,无需在每次替换后重新定义分界点。 + +**把选择移入循环或适配器。** 请求错误扩展点已经负责修复与重试。适配器知道路由预算,插件知道会话输入顺序,两者都不需要在共享循环中加入路由策略。 + +## 影响 + +路由预算增加、内联回退结束或压缩减少上下文时,省略仍然有效。省略不删除附件字节。读取占位文本中的访问路径会创建新的图片出现位置,可以独立保留。执行环境路径仍在序列化时解析,与保留图片的标识文本一致。这个事件记录选择的图片集合,不记录文件系统映射。 + +Session 调用已注册的纯投影,并维护一个内容版本信号。重建模型输入的消费方必须使用派生消息。面向人的视图仍可从不可变事件显示原始图片。插件记录省略时不需要 token-meter 或 compaction 服务。 + +## 测试 + +插件的投影测试覆盖嵌套和重复图片、原子拒绝、缓存失效、纯折叠、恢复、分叉及原始事件不变。恢复测试覆盖连续失败、当前 surface 顺序、无图可省略时的恢复、新请求头和释放。Token-meter 测试在仅重计所选位置价格时保留节点身份和已有用量锚点。工具结果裁剪和压缩测试保证后续改写读取派生历史。TypeScript 和 Python SDK 预期输出通过发布的 profile 覆盖必需事件。 diff --git a/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.i18n.yaml new file mode 100644 index 0000000000..86af0d0570 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.md +2026-09-11-plugin-owned-message-projections.md: 9e1eca92567fc035d48c1c42ad86d371f2542c5b +2026-09-11-plugin-owned-message-projections.zh.md: b26b5d4549294b37665ac933944b7f51ef7a5717 diff --git a/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.md b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.md new file mode 100644 index 0000000000..9e1eca9256 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.md @@ -0,0 +1,35 @@ +# Agent Note: Plugin-owned message projections + +Status: implemented + +English | [中文](2026-09-11-plugin-owned-message-projections.zh.md) + +## Problem + +The dedicated [image-offload event](2026-09-10-image-offload-events.md) changes derived message content without replacing nodes. Implementing its image traversal and target validation inside Session makes each feature-specific message transformation a core change. The ordinary session-projection registry derives domain state but does not participate in canonical model-history derivation. + +## Decision + +The event-owning plugin supplies a pure `SessionMessageProjection`: one event type and a synchronous interpreter of the preceding history. It validates the complete durable payload and returns immutable updates for existing messages, preserving their identities. Session owns atomic acceptance, shared live and detached folding, and content-generation cache invalidation. It contains no image selection or image projection implementation. + +The `@messageProjection` tag on the owning `SessionEventMap` member generates the required-interpreter inventory. Missing definitions reject append, seeded creation, restore, and pure folding. This inventory does not make unknown third-party event names readable or change `ignorable` compatibility. A projection event cannot also declare a surface operation. + +The compaction-image-offload plugin registers its definition through a fiber-owned `ctx.sessions.registerMessageProjection()` effect. Each event has one registered owner. Removing a definition invalidates pending committed decisions and cached reads that used it; a replacement definition requires restoring the session. Rejected, uncommitted candidates retain no interpreter dependency. + +Detached readers pass definitions explicitly. The current Session format catalog assembles the same browser-safe plugin exports for persistence validation and offline queries. This static assembly does not mount recovery listeners. Live sessions use the registered composition, while compaction invariants borrow that composition's definitions. No process-global registry or import-time registration is involved. + +The image event payload and required-on-read semantics remain owned by the image-offload note. Its indexes, selection policy, retry behavior, and SDK recordings are unchanged; this decision partially supersedes that note's core-owned interpretation. + +## Alternatives considered + +**Keep image interpretation in Session.** That gives detached callers an implicit built-in, but turns a plugin-owned transformation into core event-specific code. Explicit assembly preserves deterministic replay without assigning the algorithm to core. + +**Use the ordinary session-projection registry.** Its folds expose domain state after commit. They cannot veto an invalid append or supply canonical `deriveMessages()` content without another integration point. + +**Transform only the outgoing request.** Compaction, fork, offline replay, and request invariants also derive messages. A send-time transformation does not cover these consumers. + +## Consequences + +Adding a content-changing event requires its declaration, pure interpreter, live registration, and detached assembly entry. Ordinary log-only events need none of this. Consumers of the pure fold use the generic `projectedMessages` result. The event envelope and stored image decisions remain unchanged, so no structural Session format version is added. + +Tests cover missing interpreters, generic atomic projection, loaded windows, restore and fork, duplicate registration, fiber disposal, cached and pending reads, and rejected candidates. Image-specific tests remain with their plugin. The catalog checks that every generated required type has a detached interpreter and rejects invalid image references. Existing TypeScript and Python image SDK recordings exercise the same durable event through shipped profiles. diff --git a/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.zh.md b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.zh.md new file mode 100644 index 0000000000..b26b5d4549 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.zh.md @@ -0,0 +1,35 @@ +# Agent Note: 插件拥有消息投影 + +Status: implemented + +[English](2026-09-11-plugin-owned-message-projections.md) | 中文 + +## 问题 + +独立的[图片省略事件](2026-09-10-image-offload-events.zh.md)修改派生消息内容,不替换节点。如果图片遍历和目标校验在 Session 内实现,每种功能专用的消息转换都需要修改核心代码。普通的 session-projection 注册表派生领域状态,不参与模型历史的标准派生过程。 + +## 决策 + +事件所属的插件提供纯 `SessionMessageProjection`,包含一个事件类型和同步解释此前历史的处理器。它校验完整的持久载荷,返回现有消息的不可变更新,并保留消息身份。Session 负责原子接受、实时与独立回放共用的折叠,以及内容版本驱动的缓存失效。它不包含选图或图片投影实现。 + +所属 `SessionEventMap` 成员上的 `@messageProjection` 标记生成必需处理器清单。缺少处理器时,追加、带历史的创建、恢复和纯折叠都会被拒绝。这份清单不会让未知的第三方事件名变得可读,也不改变 `ignorable` 的兼容规则。投影事件不能同时声明 surface 操作。 + +compaction-image-offload 插件通过 fiber 拥有的 `ctx.sessions.registerMessageProjection()` effect 注册处理器。每种事件只有一个注册者。移除处理器后,尚未应用的已提交决策和使用过它的缓存读取都会失效。替换处理器需要重新恢复会话。被拒绝且未提交的候选事件不会保留对处理器的依赖。 + +独立读取器显式传入处理器。当前 Session 格式目录装配相同的浏览器安全插件导出,供持久数据校验和离线查询使用。这项静态装配不挂载恢复监听器。实时会话使用已注册的组合,压缩 invariant 借用该组合的处理器。不使用进程全局注册表,也不在导入模块时注册。 + +图片事件载荷和读取时必须识别的语义仍由图片省略记录负责。索引、选图策略、重试行为和 SDK 录制保持不变。本决策部分取代该记录中由核心负责解释事件的职责划分。 + +## 考虑过的替代方案 + +**保留 Session 内的图片解释逻辑。** 独立调用者可以隐式获得内置处理器,但插件拥有的转换会变成核心中特定事件的代码。显式装配能够保留确定性回放,无需让核心拥有算法。 + +**使用普通的 session-projection 注册表。** 它的折叠在提交后公开领域状态。没有额外集成点,它无法否决无效追加,也无法提供标准 `deriveMessages()` 内容。 + +**只转换发出的请求。** 压缩、分叉、离线回放和请求 invariant 也会派生消息。发送时转换无法覆盖这些消费方。 + +## 影响 + +增加修改内容的事件,需要事件声明、纯处理器、实时注册和独立装配项。普通的仅记录日志事件不需要这些机制。纯折叠的消费方使用通用的 `projectedMessages` 结果。事件信封和已经存储的图片决策保持不变,因此不增加结构性的 Session 格式版本。 + +测试覆盖缺少处理器、通用原子投影、已加载窗口、恢复与分叉、重复注册、fiber 释放、缓存与待应用读取,以及被拒绝的候选事件。图片专用测试保留在插件包。目录检查每种生成的必需类型都有独立处理器,并拒绝无效图片引用。现有 TypeScript 和 Python 图片 SDK 录制通过发布的 profile 覆盖相同的持久事件。 diff --git a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.i18n.yaml b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.i18n.yaml index 68cd8b91e9..df9b8b6339 100644 --- a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.md -2026-08-20-unified-image-request-pipeline.md: 467c8ccf02221414b4355459115df72cc34cf0ba -2026-08-20-unified-image-request-pipeline.zh.md: 5796f59337d1933b112203673f5c93fd37489cda +2026-08-20-unified-image-request-pipeline.md: 7431a474b55a9dae82b1c9cd36b770b31fe39602 +2026-08-20-unified-image-request-pipeline.zh.md: 247a57b8017f513c552204d951ef6931f7f9d045 diff --git a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.md b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.md index 467c8ccf02..7431a474b5 100644 --- a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.md +++ b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.md @@ -26,7 +26,7 @@ Batch admission prepares and verifies every normalized attachment once before pu The `variantId` and cache path cover the normalized attachment id, transform version, route pixel and byte budgets, and fixed encoder parameters. A new cache entry is fully decoded before publication. Cache hits use a header probe to check format, 8-bit sRGB/sRGBA facts, dimensions, and alpha without decoding the complete raster again; a mismatch regenerates the entry. DeepSeek Files and pi-ai inline base64 therefore use the same deterministic bytes for the same policy. Inline accounting uses the derived byte length after base64 expansion, not the normalized attachment byte count. Equal in-process `variantId` calls share one transform and cache write. Each caller can cancel its own wait; the shared transform is aborted only after every waiter has cancelled. Callers preserve order by applying `Promise.all` to singular `readImageRequest` calls. The local implementation runs normalization and request transforms through one FIFO limiter; `imageCompressionConcurrency` is configurable from 1 through 8 and defaults to 2. Batch publication remains sequential after every normalized attachment has been prepared. -Request-size offload is a deterministic oldest-first projection. Before reading attachments, each route uses `min(attachmentBytes, requestVersionMaxBytes)` as a conservative upper bound and removes the oldest over-budget prefix. Only retained attachments are read and transformed, so an omitted missing or corrupt object cannot block the request. A second projection uses exact derived lengths without bringing omitted images back. DeepSeek defaults to 128MiB and 600 referenced images. Its removed prefix advances past successive 64MiB byte boundaries and in 20-image count quanta, so 129 one-megabyte images remove the oldest 65, retain 64MiB, and keep that prefix stable until total history passes 192MiB. Pi-ai retains a configurable base64 request bound. Each omitted image becomes a per-image placeholder that retains its identity and access resolved for the current tool execution world, including nested tool-result images, while append-only session history keeps the original references. +Request-size offload follows the [durable image-offload decision](../architecture/2026-09-10-image-offload-events.md). Routes check retained occurrences against their request-version byte and image-count budgets and report `IMAGE_OFFLOAD_REQUIRED` with a removal count. The compaction plugin records exact oldest-first occurrences in `image/offload`; Session derivation applies the marks. Adapters prepare only retained attachments and render each marked occurrence as placeholder text with its identity and current execution-world access path, including nested tool-result images. Original message events retain the attachment references. ### Stable handles @@ -62,7 +62,7 @@ Historical attachment objects that later disappear or fail integrity verificatio ## Verification -Package tests generate 16-bit RGB and RGBA PNG fixtures, prove 8-bit conversion and clean 8-bit passthrough, retain alpha under byte pressure, stop lazy encoding after the first fitting candidate, keep the smallest ladder output above an unreachable byte target, cover square and wide 640,000-pixel projections, singleflight equal variants and uploads without shared-cancellation leaks, bound transform concurrency, preserve cache and upload identity, skip attachment reads for conservatively offloaded history, prepare batches once, reject inconsistent Files responses, refresh near-expiry ids without retrieve, recover once from single-id, multiple-id, and ambiguous stale responses, fall back to bounded all-inline requests after file resolution failure, paginate before quota deletion, normalize provider diagnostics, project text-only history, and share normal/compaction request bytes. Keyless assembled snapshots cover the real tool schemas and image request path. A credentialed test uses the built-in `deepseek-official` route and its configured endpoint, never a custom provider entry. +Package tests generate 16-bit RGB and RGBA PNG fixtures, prove 8-bit conversion and clean 8-bit passthrough, retain alpha under byte pressure, stop lazy encoding after the first fitting candidate, keep the smallest ladder output above an unreachable byte target, cover square and wide 640,000-pixel projections, singleflight equal variants and uploads without shared-cancellation leaks, bound transform concurrency, preserve cache and upload identity, skip attachment reads for durably offloaded history, prepare batches once, reject inconsistent Files responses, refresh near-expiry ids without retrieve, recover once from single-id, multiple-id, and ambiguous stale responses, fall back to bounded all-inline requests after file resolution failure, paginate before quota deletion, normalize provider diagnostics, project text-only history, and share normal/compaction request bytes. Keyless assembled snapshots cover the real tool schemas and image request path. A credentialed test uses the built-in `deepseek-official` route and its configured endpoint, never a custom provider entry. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.zh.md b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.zh.md index 5796f59337..247a57b801 100644 --- a/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.zh.md +++ b/.agents/notes/implemented/feature/2026-08-20-unified-image-request-pipeline.zh.md @@ -26,7 +26,7 @@ Status: implemented `variantId` 和缓存路径覆盖规范化附件 ID、变换策略版本、路由像素和字节预算及固定编码参数。新缓存条目在发布前会完整解码。缓存命中只探测文件头,校验格式、8-bit sRGB/sRGBA、尺寸和透明通道,不会再次完整解码光栅;不匹配时会重新生成。因此,同一策略下的 DeepSeek Files 和 pi-ai 内联 base64 使用相同的确定性字节。内联计量使用派生字节经过 base64 膨胀后的长度,不使用规范化附件字节数。同一进程内相同 `variantId` 的调用共享一次变换和缓存写入。每个调用方可以取消自己的等待;只有全部等待方都取消时,共享变换才会中止。调用方对单数 `readImageRequest` 使用 `Promise.all` 保持结果顺序。本地实现通过一个 FIFO 限流器运行规范化和请求变换,`imageCompressionConcurrency` 的可配置范围为 1 至 8,默认值为 2。全部规范化附件准备完成后,批次仍按顺序发布。 -请求大小 offload 是确定性的从旧到新投影。读取附件前,每条路由先以 `min(附件字节数, 请求版本字节上限)` 作为保守上界,移除超出预算的最旧前缀。系统只读取并转换保留的附件,因此已省略的缺失或损坏对象不会阻塞请求。第二次投影使用确切派生长度,但不会重新加入已省略图片。DeepSeek 默认上限为 128MiB 和 600 张引用图片。被移除前缀会越过连续的 64MiB 字节边界,并按 20 张图片数量步长递增,因此 129 张 1MiB 图片会移除最旧的 65 张并保留 64MiB;持久历史超过 192MiB 前,该前缀保持不变。Pi-ai 保留可配置的 base64 请求上限。每张省略图片都会变成逐图占位文本,保留自己的身份和本次工具执行环境解析出的访问方式,嵌套工具结果图片也使用相同规则;追加式会话历史继续保留原始引用。 +请求大小 offload 遵循[持久图片省略决策](../architecture/2026-09-10-image-offload-events.zh.md)。路由根据请求版本字节和图片数量预算检查保留的位置,通过 `IMAGE_OFFLOAD_REQUIRED` 报告所需移除数量。Compaction 插件在 `image/offload` 中记录明确的最旧图片出现位置,Session 派生时应用标记。适配器只准备保留的附件,将每个带标记的位置渲染为包含其身份和当前执行环境访问路径的占位文本,嵌套工具结果图片也适用。原始消息事件保留附件引用。 ### 稳定句柄 @@ -62,7 +62,7 @@ Status: implemented ## Verification -包测试会生成 16-bit RGB 和 RGBA PNG fixture,验证 8-bit 转换与干净 8-bit 字节直通、字节压力下保留透明通道、首个候选合规后停止编码、字节目标不可达时保留最小阶梯产物、正方形和宽屏 640,000 像素投影、相同变体与上传 singleflight 且不会共享取消、变换并发上限、缓存与上传身份、跳过已保守 offload 的历史附件读取、批量只准备一次、Files 响应不一致、进入刷新余量时不查询远端并更新 ID、单个 ID、多个 ID 和模糊失效响应只恢复一次、文件解析失败后回退到有界全内联请求、删除配额文件前完成分页、规范化提供方诊断、纯文本投影,以及普通请求与压缩共享请求字节。无需密钥的组装快照覆盖真实工具 schema 和图片请求路径。使用凭据的测试只使用内置 `deepseek-official` 路由及其已配置端点,不使用自定义提供方条目。 +包测试会生成 16-bit RGB 和 RGBA PNG fixture,验证 8-bit 转换与干净 8-bit 字节直通、字节压力下保留透明通道、首个候选合规后停止编码、字节目标不可达时保留最小阶梯产物、正方形和宽屏 640,000 像素投影、相同变体与上传 singleflight 且不会共享取消、变换并发上限、缓存与上传身份、跳过已持久省略的历史附件读取、批量只准备一次、Files 响应不一致、进入刷新余量时不查询远端并更新 ID、单个 ID、多个 ID 和模糊失效响应只恢复一次、文件解析失败后回退到有界全内联请求、删除配额文件前完成分页、规范化提供方诊断、纯文本投影,以及普通请求与压缩共享请求字节。无需密钥的组装快照覆盖真实工具 schema 和图片请求路径。使用凭据的测试只使用内置 `deepseek-official` 路由及其已配置端点,不使用自定义提供方条目。 ## Consequences diff --git a/.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.i18n.yaml b/.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.i18n.yaml new file mode 100644 index 0000000000..c9f4dad8fa --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md +2026-09-09-ptc-trajectory-code-inspection.md: 477d199f519b5d515e5d58430bd902d9d209e46b +2026-09-09-ptc-trajectory-code-inspection.zh.md: f305569176214c63ac549b0ec5103289e3e57a88 diff --git a/.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md b/.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md new file mode 100644 index 0000000000..477d199f51 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md @@ -0,0 +1,29 @@ +# Agent Note: Inspect recorded PTC source in Trajectory + +Status: implemented + +English | [中文](2026-09-09-ptc-trajectory-code-inspection.zh.md) + +## Problem + +PTC programs arrive as JSON string arguments. Escaping makes long programs difficult to read in a generic argument tree, while historical calls may use a different runtime language from the current deployment. Recorded result text can contain both printed output and a returned value without retaining their separation. + +## Decision + +[Trajectory](../../../../packages/client/ui-trajectory/README.md) identifies calls by their recorded tool name and derives a code inspector from validated `run_code` arguments. The validated program carries the original JSON text; copying preserves source and argument bytes and exposes the original JSON through a separate toggle. Syntax highlighting requires an unambiguous TypeScript or Python hint in that call's recorded parameter description; unknown or conflicting hints leave plain text. Unsupported arguments retain the generic inspector. + +The result view preserves recorded text and uses a tree only for complete JSON objects or arrays. Errors retain captured output. Code views sample the shared wrapping preference when opened and keep their own choice while mounted. + +The [PTC runtime decision](2026-06-15-ptc.md) still owns execution and settlement; the [client presentation decision](../architecture/2026-08-23-client-derived-tool-presentation.md) still owns deriving UI from recorded facts. This inspector adds no Session events or host presentation fields. + +## Alternatives considered + +**Keep source inside the argument tree.** JSON escaping obscures program structure and makes copying executable source cumbersome. + +**Use the active runtime language or infer it from source.** Either can mislabel a historical program. Recorded schema hints constrain highlighting without changing the recording. + +**Split printed output from return values.** Historical rendered text does not establish that distinction; a parser could invent a separation the producer never recorded. + +## Consequences + +Readers can inspect and copy recorded programs without changing replay data. Schemas with no recognizable language hint receive no syntax highlighting. Component tests cover recorded-name recognition, schema fallback, exact source and argument copying, output states, and independent wrapping. JSON-tree tests cover clipping geometry, missing `ResizeObserver`, clipboard settlement after hover changes or unmount, and value-read counts during hover. Thinking tests cover body arrival, manual disclosure, and switching records; the [PTC browser scenario](../../../../apps/web/tests/ptc-round.e2e.ts) pins the assembled inspector and verifies overflow and the original-JSON round trip. diff --git a/.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md b/.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md new file mode 100644 index 0000000000..f305569176 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md @@ -0,0 +1,29 @@ +# Agent Note: 在 Trajectory 中检查已记录的 PTC 源码 + +Status: implemented + +[English](2026-09-09-ptc-trajectory-code-inspection.md) | 中文 + +## 问题 + +PTC 程序以 JSON 字符串参数传入。转义使长程序在通用参数树中难以阅读,而历史调用使用的运行时语言可能不同于当前部署。记录的结果文本可以同时包含打印输出与返回值,却未保留二者的分界。 + +## 决策 + +[Trajectory](../../../../packages/client/ui-trajectory/README.zh.md) 按已记录的工具名识别调用,并从已验证的 `run_code` 参数派生代码检查器。验证后的程序携带原始 JSON 文本;复制时保留源码与参数字节,并通过独立切换按钮展示原始 JSON。语法高亮要求该调用记录的参数说明包含明确且无冲突的 TypeScript 或 Python 提示;未知或冲突的提示使用纯文本。不支持的参数保留通用检查器。 + +结果视图保留记录的文本,只对完整 JSON 对象或数组使用树形展示。错误保留已捕获的输出。代码视图在打开时读取共享换行偏好,并在挂载期间保留自身的选择。 + +[PTC 运行时决策](2026-06-15-ptc.zh.md) 仍负责执行与结算;[客户端展示决策](../architecture/2026-08-23-client-derived-tool-presentation.zh.md) 仍负责从已记录事实派生 UI。此检查器不增加 Session 事件或宿主展示字段。 + +## 考虑过的替代方案 + +**把源码保留在参数树中。** JSON 转义遮蔽程序结构,也使复制可执行源码变得繁琐。 + +**使用当前运行时语言,或从源码推断语言。** 两种方式都可能错误标注历史程序。已记录 Schema 的提示约束高亮,无需修改记录。 + +**拆分打印输出与返回值。** 历史渲染文本无法确定二者的分界;解析器可能凭空添加生产方从未记录的分隔。 + +## 后果 + +读者可以检查和复制已记录的程序,无需修改回放数据。Schema 没有可识别的语言提示时不提供语法高亮。组件测试覆盖记录工具名识别、Schema 回退、源码与参数原样复制、输出状态及独立换行。JSON 树测试覆盖裁剪几何、缺少 `ResizeObserver`、悬停切换或卸载后剪贴板写入落定,以及悬停期间读取值的次数。思考测试覆盖正文到达、手动展开折叠及记录切换;[PTC 浏览器场景](../../../../apps/web/tests/ptc-round.e2e.ts) 固定组装后的检查器展示,并验证溢出和原始 JSON 的往返切换。 diff --git a/.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.i18n.yaml b/.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.i18n.yaml new file mode 100644 index 0000000000..21ad4429b4 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md +2026-09-09-web-sidebar-terminal.md: ce1162a84d3f96336cbb217dbdfe62d12ac23280 +2026-09-09-web-sidebar-terminal.zh.md: 51e0f374b5fde389323868e56fbd86d3bc5720b3 diff --git a/.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md b/.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md new file mode 100644 index 0000000000..ce1162a84d --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md @@ -0,0 +1,45 @@ +# Agent Note: Web sidebar terminals + +Status: implemented + +English | [中文](2026-09-09-web-sidebar-terminal.zh.md) + +## Problem + +Web users need an interactive shell beside a Session to inspect the workspace and run commands. The Agent's persistent terminal tools control prompts and wait for semantic results; a human terminal instead needs raw keyboard input, normal shell configuration and a full screen. Browser rendering and transport can disappear while a command is still running. + +## Decision + +`api-terminal-controller` owns user terminals by Session and exposes the `terminal` Remote namespace. `ui-sidebar-terminal` registers native right-sidebar tabs, xterm.js rendering and FitAddon sizing. A new terminal offers installed shells and waits for Start. Host discovery verifies the configured candidates, with the execution default first; creation accepts only a currently discovered path. The browser remembers the last selected shell path in origin-scoped localStorage and falls back to the current default if that path is unavailable. The terminal type declares independent instances, so ordinary page deduplication cannot collapse separate processes when opening or docking tabs. The existing sidebar controls open additional tabs; double-clicking a tab title renames its terminal. Terminal processes use the composed subprocess provider and Session sandbox policy. Shell resolution occurs during discovery and creation; reading limits and reconnecting an existing process do not depend on the default executable remaining available. Interactive shell configuration supplies Tab completion and optional inline suggestions. + +Close and replacement remove the tab synchronously and run process cleanup in the background. The Client first records the unfinished close request under a terminal-specific localStorage key; success removes it, and startup retries requests that remain. A cleanup failure produces a lightweight notification with a retry action without reopening the tab. Independent keys prevent another window from overwriting unrelated cleanup requests. Collapse, tab/Session switching, floating, fullscreen and browser disconnection preserve the process. Component cleanup and `TabDomain.signal` only detach browser work because the same lifetime can end during plugin reload. Failed process cleanup retains ownership, including failures after allocation but before create publication. Session owner disposal and Host plugin disposal also clean up terminals. A definitive missing-Session response retires its saved close request because the Session owns process cleanup; transport failures remain retryable. Client plugin disposal awaits every detached stream so a replacement plugin does not inherit unfinished Client cleanup. + +Sidebar layout, open-tab mappings, selection and process PIDs are not persisted. When the Session header mounts, the Client queries `terminal.list` and opens retained Host terminals as new tabs. Listing takes the Session ID directly because history can outlive its Agent and terminal owner; an offline Session has no retained terminals to restore. Their `params.terminalId` association exists only in the current page. New and recovered views use different `createWhenMissing` values: only a new view may allocate a process; a recovered target that disappears reports an error. Host state supplies recovery identities and titles, so the browser does not maintain a second active-terminal registry. + +The caller retains a terminal ID in memory before creating it. Repeating create with the same Session and open ID does not allocate a second process. Closing an uncertain create uses that ID even when no creation response arrived. The Host records closed IDs before awaiting allocation, preventing a delayed create from reviving a closed terminal. Each attachment begins with a consistent, bounded serialized xterm screen; ordered output follows through the Gateway's existing multiplexed Remote stream. Output and screen snapshots share one operation queue. Followers retain final output on normal closure and fail explicitly on buffer overflow. Browser render acknowledgement prevents React batching from dropping increments. + +The latest attachment controls input and dimensions; other attachments remain read-only. Every physical stream opening gets a fresh input attachment identity, including automatic transport recovery. Input RPCs are serialized, and results from a superseded attachment cannot downgrade a replacement connection. Terminal output creates no model input, Agent tool result or Session event. The existing [persistent Agent terminal decision](2026-07-16-persistent-pty-sessions.md) continues to govern model-owned sessions; this feature extends the [portable subprocess provider](../architecture/2026-07-28-portable-execution-world-consumers.md) with terminal environment facts and resize. Control-transfer and process-exit refusals only disable input, preserving the healthy output view. Client-owned error identifiers are translated by the terminal UI, including guidance to close retained exited terminals when the quota is full. + +## Alternatives considered + +**Persist complete shell profiles in the browser.** Only the selected path is a user preference. Arguments and availability belong to Host discovery; persisting them would allow stale profiles to bypass current execution configuration. The sidebar continues to own the terminal list and tab controls. + +**Keep the tab visible until process cleanup finishes.** A slow or failed termination would delay the user's close action. Saving the cleanup intent allows immediate removal while preserving failure reporting and retry. + +**Persist sidebar layout or an active tab-to-process registry.** Layout persistence is outside this feature. An additional active registry duplicates Host state and can restore stale associations. Only an unfinished close is an independent user request that must survive page reload. + +**Share the Agent terminal registry.** Its controlled prompts and semantic send/wait behavior would change human shell configuration and blur process ownership. User terminals share the subprocess capability instead. + +**Add a dedicated terminal WebSocket.** The existing Remote stream transport already owns authentication, cancellation and reconnection. A second carrier would duplicate those responsibilities. + +**Replay only a bounded raw byte tail.** A tail can begin inside an escape sequence or omit an alternate-screen transition. A serialized terminal screen provides a consistent recovery point with bounded history. + +**Kill when a React body or tab signal is disposed.** Unmount, Session switching and plugin reload can end those lifetimes without an explicit close. Cleanup must follow the user's close operation. + +**Build a Web completion engine.** Native shell completion already handles commands, paths and configured plugins through ordinary PTY input. An independent completion UI adds shell-specific parsing and synchronization; it is outside this feature. + +## Consequences + +A kept-open terminal retains a process and bounded screen memory. Reload restores Host-retained terminals rather than the previous sidebar layout; Host restart does not restore processes. An exited shell remains visible without automatic respawn. Background cleanup may outlive its tab, and unavailable browser storage limits retry recovery to the current page. Native PTY support and descendant cleanup guarantees remain provider-specific. One writable attachment avoids competing resize and input streams, while explicit takeover permits recovery from another page. Changing sandbox mode requires closing retained terminals first. + +The implementation retains the Agent-terminal and portable-execution notes because their ownership and provider decisions remain independently useful; neither is superseded by browser terminals. diff --git a/.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md b/.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md new file mode 100644 index 0000000000..51e0f374b5 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md @@ -0,0 +1,45 @@ +# Agent Note: Web sidebar terminals + +Status: implemented + +[English](2026-09-09-web-sidebar-terminal.md) | 中文 + +## 问题 + +Web 用户需要在 Session 旁使用交互式 shell 检查工作区和运行命令。Agent 的持久终端工具控制提示符并等待语义结果;人工终端需要原始键盘输入、正常 shell 配置和完整屏幕。命令仍在运行时,浏览器渲染和网络连接可能消失。 + +## 决定 + +`api-terminal-controller` 按 Session 管理用户终端并提供 `terminal` Remote namespace。`ui-sidebar-terminal` 注册原生右侧栏标签页,使用 xterm.js 渲染和 FitAddon 测量尺寸。新终端提供已安装 shell 的选择并等待用户启动。Host 探测会验证配置的候选,并把执行环境默认项放在首位;创建只接受当前探测返回的路径。浏览器在当前站点 localStorage 中记住上次选择的 shell 路径,该路径不可用时回到当前默认项。终端类型声明独立实例,因此打开或停靠标签页时,普通页面的去重规则不会合并不同进程。已有侧栏控件负责打开更多标签页,双击标签页标题可重命名终端。终端进程使用组合的 subprocess provider 和 Session sandbox policy。shell 在探测和创建时解析;读取限制和重新连接已有进程不依赖默认可执行文件仍然可用。交互式 shell 配置提供 Tab 补全和可选的内联建议。 + +关闭和替换会同步移除标签页,并在后台清理进程。Client 先以终端独立的 localStorage key 保存未完成的关闭请求;成功后删除,启动时重试剩余请求。清理失败时显示带重试操作的轻量通知,不重新打开标签页。独立 key 避免其他窗口覆盖无关的清理请求。折叠、切换标签页或 Session、浮动、全屏和浏览器断线均保留进程。组件清理和 `TabDomain.signal` 只停止浏览器工作,因为插件重新加载也会结束这些生命周期。进程清理失败时保留所有权,包括分配完成但 create 尚未发布时的失败。Session owner 和 Host 插件卸载也会清理终端。 明确的 Session 不存在响应会清除已保存的关闭请求,因为进程清理由 Session 负责;传输失败仍可重试。Client 插件卸载等待所有断开的流结束,避免替换插件继承未完成的 Client 清理。 + +侧栏布局、打开标签页映射、选中项和进程 PID 不持久化。Session header 挂载时,Client 查询 `terminal.list`,把 Host 保留的终端打开为新标签页。列表直接使用 Session ID,因为历史记录可以比 Agent 和终端 owner 存活更久;离线 Session 没有需要恢复的保留终端。`params.terminalId` 关联只在当前页面中保留。新视图与恢复视图使用不同的 `createWhenMissing`:只有新视图可以分配进程,恢复目标消失时显示错误。恢复标识和标题来自 Host 状态,浏览器不维护第二份活跃终端注册表。 + +调用者在创建之前把终端 ID 保留在内存中。相同 Session 和未关闭 ID 的重复 create 不再分配进程。创建结果不确定时,关闭仍使用该 ID,即使没有收到创建响应。Host 在等待分配完成前记录已关闭 ID,防止迟到的 create 复活已关闭终端。每次连接先接收一致、有界的 xterm 序列化屏幕,后续有序输出使用 Gateway 已有的复用 Remote stream。输出和屏幕快照共享操作队列。订阅者正常关闭时保留末尾输出,缓存超限时明确失败。浏览器在完成渲染后确认帧,避免 React 批处理丢失增量。 + +最新连接控制输入和尺寸,其他连接保持只读。每次物理流建立都创建新的输入连接标识,包括传输自动恢复。输入 RPC 按序发送,旧连接的结果不能把新连接降级为失败。终端输出不产生模型输入、Agent 工具结果或 Session 事件。[Agent 持久终端决策](2026-07-16-persistent-pty-sessions.zh.md)仍约束模型拥有的终端;此功能为[可移植 subprocess provider](../architecture/2026-07-28-portable-execution-world-consumers.zh.md)增加执行环境事实和 resize。 控制权转移和进程退出导致的拒绝只禁用输入,保留健康的输出视图。Client 自产错误标识由终端 UI 翻译,包括名额用满时关闭已保留的退出终端的提示。 + +## 考虑过的替代方案 + +**在浏览器保存完整 shell profile。** 用户偏好只包含选中的路径。参数和可用性由 Host 探测决定;持久化这些信息会让过期 profile 绕过当前执行配置。终端列表和标签页控件仍由侧栏负责。 + +**进程清理完成前保留标签页。** 缓慢或失败的终止会拖延用户关闭操作。保存清理意图后,可以立即移除标签页,同时保留错误反馈与重试。 + +**持久化侧栏布局或活跃标签页到进程的注册表。** 布局持久化不属于此功能。额外的活跃注册表重复 Host 状态,可能恢复过期关联。只有未完成的关闭操作是需要跨页面刷新保留的独立用户请求。 + +**共用 Agent 终端注册表。** 受控提示符和语义化 send/wait 会改变人工 shell 配置并混淆进程所有权。用户终端只共享 subprocess 能力。 + +**增加专用终端 WebSocket。** 现有 Remote stream 已管理认证、取消和重连。第二条传输通道会重复这些职责。 + +**只重放有界原始字节尾部。** 字节尾部可能从转义序列中间开始,或缺失备用屏幕切换。序列化终端屏幕能在保留有界历史的同时提供一致恢复点。 + +**React 正文或 tab signal 卸载时 kill。** 组件卸载、Session 切换和插件重载都可能结束这些生命周期,而用户并未关闭终端。清理必须跟随显式关闭操作。 + +**自建 Web 补全引擎。** shell 原生补全通过普通 PTY 输入处理命令、路径和已配置插件。独立补全界面需要针对 shell 解析和同步,不属于此功能。 + +## 影响 + +保留终端会保留进程和有界屏幕内存。刷新恢复的是 Host 保留的终端,不是此前的侧栏布局;Host 重启不恢复进程。shell 退出后保持可见,不自动重启。后台清理可能比标签页存活更久,浏览器存储不可用时只能在当前页面保留重试能力。原生 PTY 支持和后代进程清理保证仍由 provider 决定。单一可写连接避免竞争的输入和尺寸流,显式接管允许从另一页面恢复操作。改变 sandbox mode 前需要关闭保留的终端。 + +Agent 终端和可移植执行环境两篇记录仍保留,其所有权与 provider 决策继续独立有效,不被浏览器终端取代。 diff --git a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.i18n.yaml b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.i18n.yaml index 2bfae42d7b..8d3f060672 100644 --- a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-20-gui-testing-system.md -2026-07-20-gui-testing-system.md: fa38b6e57fc0ff48f2604b000f6b9131897f2e4c -2026-07-20-gui-testing-system.zh.md: 06ff9b81aad0d13abf51b2db093670d5e0ca57fb +2026-07-20-gui-testing-system.md: 3fdb9a596dd18861ea8b765da129656942778f96 +2026-07-20-gui-testing-system.zh.md: e49d5a2f848e7f4847c7761ee0a487ce6fd3220e diff --git a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.md b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.md index fa38b6e57f..3fdb9a596d 100644 --- a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.md +++ b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.md @@ -20,12 +20,12 @@ Cut along the architecture's natural test hooks into three tiers, bottom-up: |---|---|---|---| | 1 Protocol isomorphism | Generated Typert Remote descriptors + `ApiGateway` + the Connection RPC carrier (arguments / results / errors / streams / cancellation) | **The full chain at the isomorphic point**: gateway host/client suites validate descriptor codecs and Remote dispatch in process; Connection host suites exercise the same `/api` carrier framing and trust checks without a browser | `packages/api/gateway/tests/`, `packages/client/connection/tests/` | | 2 Object-layer orchestration | `Session`/`SessionManager`/`ConnectionController` (state machines and timing: stitching / dedup / paging / optimistic draft clearing / pendingBuffers / reconnect / backoff) | **The "event sequence in → snapshot out" golden path**: programmable fakes + deferreds controlling timing + fake timers controlling backoff | `packages/client/{runtime,connection}/tests/` | -| 3 Assembled presentation | Built artifacts × the real client loader and plugin composition | App-owned semantic snapshots boot all eight built client plugins under jsdom for deterministic cross-plugin state changes; bare Playwright smoke separately proves the real browser/carrier boundary, with real-host cases self-skipping without a key; the keyless browser e2e lane disables the shipped model-adapter row and replays recorded session fixtures through `dsh-llm-replay` in the real in-process web assembly against conversation aria goldens ([web e2e lane](../testing/2026-07-24-web-gui-browser-e2e-lane.md), [required CI gate](../testing/2026-07-30-web-browser-snapshot-ci-gate.md)) | `apps/web/tests/*.snapshot.ts`, `apps/web/tests/smoke-{fixture,real}.e2e.ts`, `apps/web/tests/{replay-round-trip,seeded-history}.e2e.ts` | +| 3 Assembled presentation | Built artifacts × the real client loader and plugin composition | App-owned semantic snapshots boot the built client graph under jsdom with a test-owned `RemoteMock`; Playwright cases separately exercise the real browser and Host carriers, with recorded model sessions replayed through `dsh-llm-replay` ([whole-client tier](../testing/2026-09-06-client-assembly-test-line.md), [web e2e lane](../testing/2026-07-24-web-gui-browser-e2e-lane.md)) | `apps/web/tests/*.expected.e2e.ts`, `apps/web/tests/*.e2e.ts`, `apps/web/tests/*.snapshot.ts` | Inter-tier discipline: **each tier tests its own layer, upper tiers never re-test lower ones** — an app semantic snapshot pins only user-visible projection across the assembled plugin boundary, while Playwright smoke proves browser and carrier liveness; wire semantics belong to tier 1 and data semantics to tier 2. Pure-function layers (lineage/partial/notifier/transcript-adapter) are tested directly with zero fakes in the same package's tests/ alongside tier 2. - **Host and client source** are under the repo-wide per-file 100% coverage gate except the narrow browser-grade exclusions annotated in `vitest.config.ts`; component suites use per-file jsdom pragmas and Testing Library without changing Node suites. -- **App-owned semantic snapshots** read built client bundles, execute them through the real loader, and drive only deterministic fixture hooks. They own stable visible state such as sidebar labels, breadcrumbs, and `document.title`, not CSS pixels or lower-layer state-machine details. +- **App-owned semantic snapshots** read built client bundles, execute them through the real loader, and drive deterministic RemoteMock scenarios. They own stable visible state such as sidebar labels, breadcrumbs, and `document.title`, not CSS pixels or lower-layer state-machine details. ## Lane map @@ -33,7 +33,7 @@ Inter-tier discipline: **each tier tests its own layer, upper tiers never re-tes |---|---|---|---| | Baseline | `pnpm run test:gui` | Tier 1+2 vitest (`packages/client packages/host`), seconds-fast, no browser, no server | Casually, after touching any GUI source | | Semantic snapshot | `DSH_EXAMPLE_MODE=lib pnpm run test:snapshot` | Keyless assembled-application semantics plus the repo's transport-specific expected outputs | After a human-visible GUI change; before delivery | -| Browser end-to-end | `pnpm run test:web` | Rebuilds the front-end dist first, then runs the tier-3 browser set: the two-level smoke (fixture level + real-host level self-skip) plus the keyless replayed e2e scenarios (`DSH_SNAPSHOT=record`/`refresh` re-record fixtures / rewrite goldens) | After touching the build surface/boot/carriage; before delivery | +| Browser end-to-end | `pnpm run test:web` | Rebuilds the front-end dist first, then runs built-client RemoteMock cases and real-Host browser scenarios, including keyless recorded-session replay (`DSH_SNAPSHOT=record`/`refresh` re-record fixtures / rewrite goldens) | After touching the build surface/boot/carriage; before delivery | | Browser expected-output gate | `DSH_SNAPSHOT=replay pnpm run test:web:built` | Reuses CI-built artifacts and compares every committed browser golden without writing | Every Linux pull request | | Gate | `pnpm run test:coverage` | The repo-wide gate (host and client GUI packages included, except annotated browser-grade exclusions) | The PR window | @@ -42,7 +42,7 @@ Inter-tier discipline: **each tier tests its own layer, upper tiers never re-tes ## Anti-regression discipline - **Every bug fix pins an assertion**: a browser-visible bug is pinned into its owning browser spec (smoke or e2e scenario); a data-layer bug is pinned into the matching spec (precedent: the res-close misjudgment pinned in the webserver bridge suite — pure Node, reproduces in seconds, no longer needs the 12s browser sentinel as the only defense). -- **All-green on fixture is not done, the real wire must pass too**: what the fixture short-circuits is exactly the wire carriage chain (node:http bridge close semantics, real network timing); both empirically confirmed bugs hid there. Changes touching connection/bridge/handler/SSE must run the browser lane (`pnpm run test:web`) — its keyless e2e scenarios drive the real HTTP/SSE carriage, and the with-key real-host smoke remains the live-model complement. +- **All-green on RemoteMock is not done, the real wire must pass too**: a decoded in-process carrier deliberately omits the HTTP/WebSocket chain and its network timing. Changes touching connection, bridge, handler, or streaming transport must run the browser lane (`pnpm run test:web`), whose keyless e2e scenarios drive the real carrier; the with-key real-Host smoke remains the live-model complement. - The code-on-disk-is-the-answer reconciliation workflow: when a behavior change lands and turns existing cases red, reconcile on the spot (fix the test or fix the code, with the RFC/contract as arbiter); no red left hanging. ## Consequences @@ -55,6 +55,6 @@ Each lane tests its own tier: touching any GUI source gets seconds-fast `test:gu |---|---| | Single e2e (everything through the browser) | Browser startup is seconds × N slower and timing is uncontrollable; wire/object-layer invariants can be fully asserted in milliseconds in node env | | Migrating the verify scripts to vitest | An ordered script shares one browser session; splitting the cases either formalizes it (sequential + shared page) or re-runs the preamble × N; streaming PASS/FAIL output is exactly the agent's locating interface | -| Reusing FixtureApiClient in tests | The demo script runs on a real clock, tests need deferred hand-controlled timing — orthogonal purposes; forced reuse chains the tests to the demo's rhythm | +| Keeping a production client fixture for tests | It couples shipped code to scenario data and a query-selected transport; RemoteMock and real-Host tests own the two required tiers directly | | A standalone vitest config for GUI packages (once designed as vitest.gui.config.ts) | Package-level tests/ are already scanned by the root include; `vitest run packages/client packages/host` path filtering is the tight loop — zero new config | | Deferring hooks/component-layer unit tests | jsdom remains the coverage mainline because it gives fast per-file component behavior; the required browser replay gate complements it at the assembled tier rather than replacing it ([CI gate decision](../testing/2026-07-30-web-browser-snapshot-ci-gate.md)) | diff --git a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.zh.md b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.zh.md index 06ff9b81aa..e49d5a2f84 100644 --- a/.agents/notes/implemented/process/2026-07-20-gui-testing-system.zh.md +++ b/.agents/notes/implemented/process/2026-07-20-gui-testing-system.zh.md @@ -20,12 +20,12 @@ GUI 栈需要考虑多种应用形态,同应用形态内的不同运行环境 |---|---|---|---| | 1 协议同构层 | 生成的 Typert Remote 描述符 + `ApiGateway` + Connection RPC 承载(参数/结果/错误/流/取消) | **同构点全链**:gateway host/client 套件在进程内验证描述符 codec 与 Remote 分派;Connection host 套件不经浏览器即可运行相同的 `/api` 承载帧与信任检查 | `packages/api/gateway/tests/`、`packages/client/connection/tests/` | | 2 对象层编排 | `Session`/`SessionManager`/`ConnectionController`(状态机与时序:缝合/去重/翻页/乐观清稿/pendingBuffers/重连/退避) | **「事件序列进→快照出」黄金路径**:可编程假体 + deferred 控时序 + fake timers 控退避 | `packages/client/{runtime,connection}/tests/` | -| 3 组装呈现层 | 构建产物 × 真实 client loader 与插件组合 | 归应用所有的语义快照会在 jsdom 下启动全部 8 个已构建的 client 插件,以确定性方式驱动跨插件状态变化;另有最简 Playwright 冒烟测试负责验证真实浏览器/承载层边界,真 host 用例在无密钥时自行跳过;无密钥浏览器 e2e 车道会禁用交付配置中的模型适配器行,并通过 `dsh-llm-replay` 在真实进程内 web 组装中回放录制的会话 fixture(测试前置数据),与会话区 aria 预期输出比对([web e2e 车道](../testing/2026-07-24-web-gui-browser-e2e-lane.zh.md)、[必需 CI 门禁](../testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md)) | `apps/web/tests/*.snapshot.ts`、`apps/web/tests/smoke-{fixture,real}.e2e.ts`、`apps/web/tests/{replay-round-trip,seeded-history}.e2e.ts` | +| 3 组装呈现层 | 构建产物 × 真实 client loader 与插件组合 | 归应用所有的语义快照在 jsdom 下用测试持有的 `RemoteMock` 启动构建后 Client 图;Playwright 用例分别验证真实浏览器与 Host 载体,并通过 `dsh-llm-replay` 回放已录制的模型会话([整机客户端测试档](../testing/2026-09-06-client-assembly-test-line.zh.md)、[web e2e 车道](../testing/2026-07-24-web-gui-browser-e2e-lane.zh.md)) | `apps/web/tests/*.expected.e2e.ts`、`apps/web/tests/*.e2e.ts`、`apps/web/tests/*.snapshot.ts` | 层间纪律:**各层各测各的,上层不重测下层**:应用语义快照只固定组装后插件边界上的用户可见投影,Playwright 冒烟测试负责验证浏览器与承载层是否存活;wire 语义归 1 层,数据语义归 2 层。纯函数层(lineage/partial/notifier/transcript-adapter)随 2 层同包 tests/ 零假体直测。 - **host 与 client 源码**均纳入全仓 per-file 100% 覆盖率门禁,仅排除 `vitest.config.ts` 中带注释的少量浏览器级例外;组件套件通过逐文件 jsdom pragma 和 Testing Library 运行,不会改变 Node 套件。 -- **归应用所有的语义快照**读取已构建的 client bundle,通过真实 loader 执行它们,并且只驱动确定性的 fixture 钩子。它们负责固定侧边栏标签、面包屑和 `document.title` 等稳定可见状态,而不固定 CSS 像素或下层状态机细节。 +- **归应用所有的语义快照**读取已构建的 client bundle,通过真实 loader 执行它们,并驱动确定性的 RemoteMock 场景。它们负责固定侧边栏标签、面包屑和 `document.title` 等稳定可见状态,而不固定 CSS 像素或下层状态机细节。 ## 车道地图 @@ -33,7 +33,7 @@ GUI 栈需要考虑多种应用形态,同应用形态内的不同运行环境 |---|---|---|---| | 基础 | `pnpm run test:gui` | 1+2 层 vitest(`packages/client packages/host`),秒级、无浏览器、无 server | 改 GUI 任意源码后随手跑 | | 语义快照 | `DSH_EXAMPLE_MODE=lib pnpm run test:snapshot` | 无需密钥的组装应用语义,以及仓库按传输形态划分的预期输出 | 用户可见的 GUI 变更后;交付前 | -| 浏览器端到端 | `pnpm run test:web` | 先重建前端 dist,再跑 3 层浏览器全集:双级冒烟测试(fixture 级 + 真 host 级 self-skip)加上无密钥回放 e2e 场景(`DSH_SNAPSHOT=record`/`refresh` 重录 fixture / 重写期望输出) | 改构建面/boot/承载后;交付前 | +| 浏览器端到端 | `pnpm run test:web` | 先重建前端 dist,再运行 built-client RemoteMock 用例与真实 Host 浏览器场景,其中包括无密钥的录制会话回放(`DSH_SNAPSHOT=record`/`refresh` 重录 fixture/重写预期输出) | 改构建面/boot/承载后;交付前 | | 浏览器预期输出门禁 | `DSH_SNAPSHOT=replay pnpm run test:web:built` | 复用 CI 构建的产物,并在不写入的情况下比较每份已提交的浏览器预期输出 | 每个 Linux 拉取请求 | | 门禁 | `pnpm run test:coverage` | 全仓门禁(host 与 client GUI 包均纳入,仅排除带注释的浏览器级例外) | PR(Pull Request)窗口 | @@ -42,7 +42,7 @@ GUI 栈需要考虑多种应用形态,同应用形态内的不同运行环境 ## 防回归纪律 - **修一个 bug 钉一条断言**:浏览器可见的 bug 钉进所属浏览器 spec(冒烟测试或 e2e 场景);数据层 bug 钉进对应 spec(先例:res-close 误判钉在 webserver 桥 suite——纯 Node 秒级复现,不再需要 12s 浏览器哨兵作唯一防线)。 -- **fixture 全绿不算完,真 wire 也要过**:fixture 短路的恰是 wire 承载链(node:http 桥 close 语义、真网络时序),两次实证 bug 都藏在那里。改动触及连接/桥/handler/SSE 的,浏览器车道(`pnpm run test:web`)必跑——其无密钥 e2e 场景驱动真实 HTTP/SSE 承载,带密钥的真 host 冒烟测试仍是真模型侧的补充。 +- **RemoteMock 全绿不算完,真 wire 也要过**:已解码的进程内载体会刻意绕过 HTTP/WebSocket 链及其网络时序。改动触及 connection、bridge、handler 或流式 transport 时必须运行浏览器车道(`pnpm run test:web`),由其中的无密钥 e2e 场景驱动真实载体;带密钥的真实 Host 冒烟仍是真模型侧的补充。 - 落盘代码即答案的对表工作流:行为改动落盘打红既有用例时,当场对表校准(改测试还是改代码以 RFC/约定为裁),不留悬红。 ## Consequences @@ -55,6 +55,6 @@ GUI 栈需要考虑多种应用形态,同应用形态内的不同运行环境 |---|---| | 单一 e2e(全走浏览器) | 浏览器起步秒级×N 倍慢+时序不可控;wire/对象层不变量在 node env 可毫秒级全断言 | | verify 脚本迁 vitest | 有序脚本共享浏览器会话,拆 case 要么形式化(sequential+共享 page)要么重走前置×N;PASS/FAIL 流式输出正是 agent(智能体)定位接口 | -| 测试复用 FixtureApiClient | 演示脚本走真实时钟,测试需要 deferred 手控时序——用途正交,硬复用把测试绑死在演示节奏上 | +| 为测试保留生产 Client fixture | 它把交付代码耦合到场景数据和 query 选择的 transport;RemoteMock 与真实 Host 测试分别直接持有两个所需层级 | | GUI 包独立 vitest config(曾设计 vitest.gui.config.ts) | 包级 tests/ 本就被根 include 扫到,`vitest run packages/client packages/host` 路径过滤即窄循环——零新 config | | 钩子/组件层暂缓单测 | jsdom 仍是覆盖率主线,因为它能快速验证逐文件组件行为;必需的浏览器回放门禁在组装层与之互补,而非取代它([CI 门禁决策](../testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md)) | diff --git a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml index 5e7ef2db63..dbe621f214 100644 --- a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md -2026-07-24-web-gui-browser-e2e-lane.md: 8276ed1982a7472ba1b825837a933058c32bf840 -2026-07-24-web-gui-browser-e2e-lane.zh.md: 3a7e5ea5547cf5584a2ad3f61b90c91326379de5 +2026-07-24-web-gui-browser-e2e-lane.md: b08d99a7d750bdeabac3dcbb7007e922d32dbdcd +2026-07-24-web-gui-browser-e2e-lane.zh.md: fbb92a094bbd14042d33e5ebd20d2d3ffd490d43 diff --git a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md index 8276ed1982..b08d99a7d7 100644 --- a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md +++ b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md @@ -6,7 +6,7 @@ English | [中文](2026-07-24-web-gui-browser-e2e-lane.zh.md) ## Problem -The web GUI ships as a real assembled chain — chromium page → client plugin bundles → HTTP unary RPC + two SSE streams → `toFetchHandler`/apiproxy → the host agent loop, tools, and JSONL persistence — and no test exercised that chain keylessly and deterministically. The [GUI testing system](../process/2026-07-20-gui-testing-system.md) covers tier 1 (wire isomorphism in node), tier 2 (object-layer state machines), and tier-3 smokes, but the keyless smoke drives `FixtureApiClient` — no host, no wire, no agent loop — while the full-chain smoke needs `DEEPSEEK_API_KEY` and a live model, so it is nondeterministic and self-skips in keyless CI. The snapshot philosophy of [docs/testing.md](../../../../docs/testing.md) — record once with a key, replay forever keyless, refresh on format churn — already covers the ACP, headless `stream-json`, and TUI transcript surfaces; the web surface was the one assembled product shape without it. The gap is exactly where the two confirmed GUI P0s hid: the wire carriage chain the fixture client short-circuits. +The web GUI ships as a real assembled chain — chromium page → client plugin bundles → HTTP unary RPC + two SSE streams → `toFetchHandler`/apiproxy → the host agent loop, tools, and JSONL persistence — and no test exercised that chain keylessly and deterministically. The [GUI testing system](../process/2026-07-20-gui-testing-system.md) covers tier 1 (wire isomorphism in node), tier 2 (object-layer state machines), and tier-3 smokes, but the former keyless smoke drove a standalone client fixture — no host, no wire, no agent loop — while the full-chain smoke needed `DEEPSEEK_API_KEY` and a live model, so it was nondeterministic and self-skipped in keyless CI. The snapshot philosophy of [docs/testing.md](../../../../docs/testing.md) — record once with a key, replay forever keyless, refresh on format churn — already covered the ACP, headless `stream-json`, and TUI transcript surfaces; the web surface was the one assembled product shape without it. The gap is exactly where the two confirmed GUI P0s hid: the wire carriage chain the standalone fixture short-circuited. ## Decision @@ -64,7 +64,7 @@ Surveyed AI-chat/agent web UIs and mocking layers (LibreChat, vercel/ai-chatbot **Mock HTTP provider at `DEEPSEEK_BASE_URL`.** Rejected as the lane's mechanism (kept for the one existing workspace-probe smoke): fixtures become hand-authored OpenAI SSE byte scripts, a second fixture format that drifts from the session-log format the rest of the repo records and replays; the adapter's real HTTP path is with-key e2e's job. -**Growing the `?fixture` client.** Rejected: tier separation — `FixtureApiClient` exists to test the client shell without a server; everything below the client API boundary stays untested by construction. +**Keeping or growing a `?fixture` client.** Rejected: client-only assembly tests inject `RemoteMock`, while behavior that depends on a Host runs through the real scaffold. A production query switch would retain a second application backend and still leave everything below the Client API untested. **Placeholder `DEEPSEEK_API_KEY` + replay interception instead of disabling the adapter row.** Rejected despite zero composition change and two in-tree precedents: it satisfies `llm-deepseek`'s fail-loud key check with a lie and leaves a dead adapter mounted-but-intercepted; the disabled row (the ACP overlay's move) is honest keylessness and fails loud at the earliest resolvable point. diff --git a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md index 3a7e5ea554..fbb92a094b 100644 --- a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md +++ b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md @@ -6,7 +6,7 @@ Status: implemented ## 问题 -Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bundle → HTTP 单次 RPC + 两条 SSE(Server-Sent Events)流 → `toFetchHandler`/apiproxy → host 端的 agent loop(智能体循环)、工具与 JSONL 持久化——却没有任何测试无密钥且确定性地检验这条链。[GUI 测试体系](../process/2026-07-20-gui-testing-system.zh.md)覆盖第 1 层(Node 中的协议同构)、第 2 层(对象层状态机)与第 3 层冒烟测试,但无密钥冒烟驱动的是 `FixtureApiClient`——没有 host、没有 wire、没有 agent loop——而全链路冒烟需要 `DEEPSEEK_API_KEY` 和真实模型,因此不确定、在无密钥 CI 中自行跳过。[docs/testing.md](../../../../docs/testing.zh.md) 的快照哲学——带密钥录制一次、永久无密钥回放、格式变动时刷新——已覆盖 ACP(Agent Client Protocol)、headless `stream-json` 与 TUI 三个 transcript(文本记录)表面;web 表面是唯一没有这层保障的组装形态。而缺口恰恰是两起已实证 GUI P0 藏身之处:fixture(测试前置数据)客户端短路掉的 wire 承载链。 +Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bundle → HTTP 单次 RPC + 两条 SSE(Server-Sent Events)流 → `toFetchHandler`/apiproxy → host 端的 agent loop(智能体循环)、工具与 JSONL 持久化——却没有任何测试无密钥且确定性地检验这条链。[GUI 测试体系](../process/2026-07-20-gui-testing-system.zh.md)覆盖第 1 层(Node 中的协议同构)、第 2 层(对象层状态机)与第 3 层冒烟测试,但原有无密钥冒烟驱动的是独立 Client fixture——没有 host、没有 wire、没有 agent loop——而全链路冒烟需要 `DEEPSEEK_API_KEY` 和真实模型,因此不确定、在无密钥 CI 中自行跳过。[docs/testing.md](../../../../docs/testing.zh.md) 的快照哲学——带密钥录制一次、永久无密钥回放、格式变动时刷新——已覆盖 ACP(Agent Client Protocol)、headless `stream-json` 与 TUI 三个 transcript(文本记录)表面;web 表面是唯一没有这层保障的组装形态。而缺口恰恰是两起已实证 GUI P0 藏身之处:独立 fixture 短路掉的 wire 承载链。 ## 决策 @@ -64,7 +64,7 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu **`DEEPSEEK_BASE_URL` 处的 mock HTTP 提供方。** 作为本车道机制已否决(仅保留给既有的工作区探针冒烟):fixture 会变成手写的 OpenAI SSE 字节脚本,一种与仓库其余部分录制回放的会话日志格式渐行渐远的第二 fixture 格式;适配器的真实 HTTP 路径归带密钥 e2e 管。 -**扩展 `?fixture` 客户端。** 已否决:分层纪律——`FixtureApiClient` 的存在意义就是脱离服务器测试客户端 shell;client API 边界以下按构造即失测。 +**保留或扩展 `?fixture` 客户端。** 已否决:仅客户端的组装测试注入 `RemoteMock`,依赖 Host 行为的测试则运行真实 scaffold。生产 query 开关会保留第二套应用后端,而且 Client API 以下仍然按构造失测。 **用占位 `DEEPSEEK_API_KEY` + 回放拦截替代禁用适配器行。** 尽管零组合改动且树内有两处先例仍被否决:它用谎言满足 `llm-deepseek` 的快速失败密钥检查,还留下一个挂载却被拦截的死适配器;禁用行(ACP overlay 的同款做法)是诚实的无密钥,并在最早可解析点快速失败。 diff --git a/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.i18n.yaml b/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.i18n.yaml index c345ae674c..e8edd6c33e 100644 --- a/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.md -2026-08-03-opt-in-reasoning-chunk-browser-stress.md: bc5d31d80e5fb8aebcd9988dcb1f051d113cbf63 -2026-08-03-opt-in-reasoning-chunk-browser-stress.zh.md: 105c8d69c37a18fd18aed3cc9f6f716e38b952d7 +2026-08-03-opt-in-reasoning-chunk-browser-stress.md: 5b679ce2682743531bd3ff48621cbde462a335aa +2026-08-03-opt-in-reasoning-chunk-browser-stress.zh.md: 3d730a4505351be968c71092c65a9b9c6b69818c diff --git a/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.md b/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.md index bc5d31d80e..5b679ce268 100644 --- a/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.md +++ b/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.md @@ -8,7 +8,7 @@ English | [中文](2026-08-03-opt-in-reasoning-chunk-browser-stress.zh.md) Long reasoning streams continuously produce large numbers of process-local `assistant/live-chunk` updates before one durable settlement. Each update must remain ordered and be folded into the Assistant Definition to preserve live completeness, while the settlement embeds the exact stream for replay; React, however, needs only the current accumulated result, not every intermediate state within one browser frame. -Each `yield` in an async stream can create a new microtask boundary, so `Notifier.markDirty()` backed only by microtask batching degrades into rebuilding a `ConversationSnapshot`, notifying `useSyncExternalStore`, and running a React render for every chunk. Even with the live Think row collapsed, 100,000 reasoning chunks can overwhelm the main thread with reconciliation, commit, and layout work. The performance boundary must sit between session ingestion and React publication; it cannot hide the problem by slowing the producer or discarding raw events. +Each `yield` in an async stream can create a new microtask boundary, so `Notifier.markDirty()` backed only by microtask batching degrades into rebuilding a `ConversationSnapshot`, notifying `useSyncExternalStore`, and running a React render for every chunk. Even with the live Think row collapsed, 100,000 reasoning chunks can overwhelm the main thread with reconciliation, commit, and layout work. The performance boundary must sit between session ingestion and React publication; the test must preserve every raw event and distinguish one-batch rendering cost from an unbounded transport backlog. ## Decision @@ -18,9 +18,9 @@ The Session Controller appends every Client-only live chunk to its event source, Keeping the live Think row horizontally pinned to the end of the accumulated text is purely visual alignment and does not require synchronous layout reads on every React commit. An in-component scheduler coalesces consecutive requests into one update every three frames, reads `scrollWidth` and `clientWidth` from the latest DOM, and updates `scrollLeft` directly to the latest position; the fixed visual cadence keeps summary changes readable without allowing browser smooth-scroll animations to accumulate. This throttling applies only to Think's horizontal summary and does not delay Chat body scrolling, history-prepend anchoring, or user-triggered `scrollIntoView`. -`pnpm run test:web:stress` remains keyless, opt-in browser performance evidence. The deterministic `?fixture` session emits 100,000 `reasoning-delta` events at a cadence independent of painting, and a terminal marker proves that the events cross production session reduction and reach the live Think row; a 50-millisecond heartbeat and a pre-scheduled DOM event measure main-thread stalls and interaction latency, respectively, with a 250-millisecond budget for identifying clear regressions. `DSH_WEB_STRESS_HEADFUL=1` lets developers profile the same scenario in a visible browser with the Performance panel. The stress lane is evidence for manual performance diagnosis and fix acceptance, not a default CI gate or a substitute for deterministic scheduling unit tests. +`pnpm run test:web:stress` remains keyless, opt-in browser performance evidence. A test-owned model adapter emits 100,000 `reasoning-delta` chunks through the real Host, Gateway, WebSocket, Session reduction, and live Think row. Each 128-chunk batch waits for one browser timer turn before the next batch, so a 50-millisecond heartbeat and a pre-scheduled DOM event measure one bounded ingestion/render interval instead of accumulated socket backlog against a 250-millisecond budget. A terminal marker proves that all chunks reached the UI. `DSH_WEB_STRESS_HEADFUL=1` lets developers profile the same scenario in a visible browser with the Performance panel. The stress lane is evidence for manual performance diagnosis and fix acceptance, not a default CI gate or a substitute for deterministic scheduling unit tests. -Focused tests pin `Notifier`'s per-frame coalescing, structural-event preemption, invalidated callbacks, and no-rAF fallback, and prove at the `Session` layer that a frame publishes the latest accumulated text only once and that finalization is not followed by a duplicate notification from a stale frame callback. Small fixture unit tests continue to pin input validation, external arrival pacing, concurrency rejection, exact event count, and terminal-marker delivery without bringing the 100,000-chunk workload into the default test suites. +Focused tests pin `Notifier`'s per-frame coalescing, structural-event preemption, invalidated callbacks, and no-rAF fallback, and prove at the `Session` layer that a frame publishes the latest accumulated text only once and that finalization is not followed by a duplicate notification from a stale frame callback. The opt-in browser case owns adapter pacing, exact event count, and terminal-marker delivery without bringing the 100,000-chunk workload into the default test suites. ## Alternatives considered @@ -32,7 +32,9 @@ Focused tests pin `Notifier`'s per-frame coalescing, structural-event preemption **Pacing the test producer by animation frames.** Rejected: the producer would slow whenever rendering slowed, giving the page implicit backpressure absent from a real network stream and masking main-thread starvation. -**A live model or recorded HTTP byte stream.** Rejected: live models are nondeterministic, and an HTTP/SSE recording would not improve the target assertion. The in-memory fixture preserves individual asynchronous session events, production client reduction, and the React rendering path while controlling the workload and arrival cadence. +**Sending the former browser-local rate through the WebSocket without an acknowledgment.** Rejected: queued transport work accumulates faster than a model can produce it and turns the result into a socket-backlog measurement. A browser timer acknowledgment bounds each batch while the heartbeat still detects a batch that starves the page. + +**A live external model or recorded HTTP byte stream.** Rejected: an external model is nondeterministic, and an HTTP recording would add a second fixture format without improving the target assertion. The test-owned adapter preserves individual chunks through the real Host and browser transport while controlling the workload. ## Consequences @@ -40,4 +42,4 @@ The publication rate of streaming `ConversationSnapshot` objects is bounded by t Horizontal layout reads and writes for the collapsed Think summary run at most once every three frames, and each update moves the summary directly to the latest position; React still commits accumulated snapshots normally, and the summary returns to the first line at finalization. This local visual policy does not change the immediacy of body scrolling or user interactions. -The browser stress lane continues to provide a responsiveness signal from the real assembled application and an entry point for visible profiling, but hardware and scheduling differences make it suitable only as explicit performance evidence. Deterministic focused tests guard publication counts, accumulated content, and preemption order, while the default test lanes remain fast. +The browser stress lane continues to provide a responsiveness signal from the real assembled application and carrier plus an entry point for visible profiling, but hardware and scheduling differences make it suitable only as explicit performance evidence. Deterministic focused tests guard publication counts, accumulated content, and preemption order, while the default test lanes remain fast. diff --git a/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.zh.md b/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.zh.md index 105c8d69c3..3d730a4505 100644 --- a/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.zh.md +++ b/.agents/notes/implemented/testing/2026-08-03-opt-in-reasoning-chunk-browser-stress.zh.md @@ -8,7 +8,7 @@ Status: implemented 长 reasoning stream 会在一个持久 settlement 前连续产生大量进程本地 `assistant/live-chunk` update。每个 update 都必须保持有序并折叠进 Assistant Definition,以保留实时完整性;settlement 则嵌入精确 stream 供 replay。React 只需要看到当前累计结果,不需要观察同一浏览器帧内每个中间态。 -异步流的每次 `yield` 都可能形成新的微任务边界,因此仅靠微任务合批的 `Notifier.markDirty()` 会退化为每个分片重建一次 `ConversationSnapshot`、通知一次 `useSyncExternalStore` 并运行一次 React render。即使实时 Think 行保持折叠,100,000 个推理分片仍会让协调、提交和布局工作压住主线程。性能边界必须位于会话接收与 React 发布之间,不能通过减慢生产方或丢弃原始事件来掩盖问题。 +异步流的每次 `yield` 都可能形成新的微任务边界,因此仅靠微任务合批的 `Notifier.markDirty()` 会退化为每个分片重建一次 `ConversationSnapshot`、通知一次 `useSyncExternalStore` 并运行一次 React render。即使实时 Think 行保持折叠,100,000 个推理分片仍会让协调、提交和布局工作压住主线程。性能边界必须位于会话接收与 React 发布之间;测试必须保留每个原始事件,并区分单批渲染成本与无限累积的 transport backlog。 ## 决策 @@ -18,9 +18,9 @@ Session Controller 把每个 Client-only live chunk 追加到 event source,Con 实时 Think 行对累计文本的横向跟尾属于纯视觉对齐,不需要在每次 React 提交中同步读取布局。组件内调度器将连续请求合并为每三帧一次,从最新 DOM 读取 `scrollWidth` 和 `clientWidth` 并将 `scrollLeft` 直接更新到最新位置;固定的视觉节奏让摘要变化可读,又不会积压浏览器平滑滚动动画。该节流只作用于 Think 的横向摘要,不延迟 Chat 正文滚动、历史 prepend 锚定或用户触发的 `scrollIntoView`。 -`pnpm run test:web:stress` 保留为无密钥、需显式启用的浏览器性能证据。确定性的 `?fixture` 会话以独立于绘制的节奏发出 100,000 个 `reasoning-delta`,结尾标记证明事件经过生产会话归并并到达实时 Think 行;50 毫秒心跳和预先调度的 DOM 事件分别测量主线程停顿与交互延迟,250 毫秒预算用于识别明显回归。`DSH_WEB_STRESS_HEADFUL=1` 允许开发者在可见浏览器中使用 Performance 面板分析同一场景。该压力车道是手动性能诊断与修复验收的证据,不是默认 CI 门禁,也不替代确定性的调度单元测试。 +`pnpm run test:web:stress` 保留为无密钥、需显式启用的浏览器性能证据。测试持有的模型 adapter 通过真实 Host、Gateway、WebSocket、Session 归并和实时 Think 行发出 100,000 个 `reasoning-delta` chunk。每批 128 个 chunk 会等待一次浏览器 timer 轮次后再发下一批,因此 50 毫秒心跳与预先调度的 DOM 事件按 250 毫秒预算测量一个有界接收/渲染区间,而不是累积的 socket backlog。结尾标记证明所有 chunk 都已到达 UI。`DSH_WEB_STRESS_HEADFUL=1` 允许开发者在可见浏览器中使用 Performance 面板分析同一场景。该压力车道是手动性能诊断与修复验收的证据,不是默认 CI 门禁,也不替代确定性的调度单元测试。 -聚焦测试固定 `Notifier` 的逐帧合并、结构事件抢占、失效回调和无 rAF 回退,并在 `Session` 层证明一帧只发布一次最新累计文本且定稿不会被旧帧回调重复通知。fixture(测试前置数据)的小型单元测试继续固定输入校验、外部到达节奏、并发拒绝、精确事件数和结尾标记交付,无需把 100,000 分片工作负载带入默认测试套件。 +聚焦测试固定 `Notifier` 的逐帧合并、结构事件抢占、失效回调和无 rAF 回退,并在 `Session` 层证明一帧只发布一次最新累计文本且定稿不会被旧帧回调重复通知。按需启用的浏览器用例持有 adapter 节奏、精确事件数和结尾标记交付,不把 100,000 分片工作负载带入默认测试套件。 ## 曾考虑的替代方案 @@ -32,7 +32,9 @@ Session Controller 把每个 Client-only live chunk 追加到 event source,Con **按动画帧控制测试生产方节奏。** 不予采纳:生产方会在渲染变慢时同步减速,使页面获得真实网络流不存在的隐式背压,并掩盖主线程饥饿。 -**真实模型或录制的 HTTP 字节流。** 不予采纳:实时模型不具确定性,HTTP/SSE(Server-Sent Events)录制也不会改进目标断言。内存 fixture 保留逐个异步会话事件、生产客户端归并和 React 渲染路径,同时控制工作负载与到达节奏。 +**不等待确认就把原有浏览器本地速率发入 WebSocket。** 不予采纳:transport 工作的排队速度会超过模型可能达到的产出速度,使结果变成 socket backlog 测量。浏览器 timer 确认限制每批工作量,同时 heartbeat 仍能发现一批数据使页面饥饿。 + +**真实外部模型或录制的 HTTP 字节流。** 不予采纳:外部模型不具确定性,HTTP 录制还会增加第二种 fixture 格式,却不会改进目标断言。测试持有的 adapter 让每个 chunk 经过真实 Host 与浏览器 transport,同时控制工作负载。 ## 后果 @@ -40,4 +42,4 @@ Session Controller 把每个 Client-only live chunk 追加到 event source,Con 折叠 Think 摘要的横向布局读写最多每三帧执行一次,并直接追上该时刻的最新位置;React 仍按累计快照正常提交,定稿时摘要恢复到首行。该局部视觉策略不会改变正文滚动和用户交互的即时性。 -浏览器压力车道继续提供真实组装应用上的响应性信号和可见 profiling 入口,但硬件与调度差异使其只适合作为显式性能证据。确定性的 focused tests 负责守住发布次数、累计内容与抢占顺序,默认测试车道保持快速。 +浏览器压力车道继续提供真实组装应用与载体上的响应性信号和可见 profiling 入口,但硬件与调度差异使其只适合作为显式性能证据。确定性的 focused tests 负责守住发布次数、累计内容与抢占顺序,默认测试车道保持快速。 diff --git a/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.i18n.yaml b/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.i18n.yaml index e5675ab2e6..1e0850bf40 100644 --- a/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.md -2026-09-06-client-assembly-test-line.md: 0d7ed86e4f0c8898441737c8d6a4c631586268a2 -2026-09-06-client-assembly-test-line.zh.md: 8b4381dbfa24819e1b1f086b7c318d388b681ef5 +2026-09-06-client-assembly-test-line.md: d2515c0689b9ea39bf14e4ce653a26d9a81ebfb8 +2026-09-06-client-assembly-test-line.zh.md: 90b5768db4307c551b895d7101324f329f42c955 diff --git a/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.md b/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.md index 0d7ed86e4f..d2515c0689 100644 --- a/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.md +++ b/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.md @@ -18,21 +18,25 @@ A whole-client tier lives in `@deepseek-ai/dsh-client-test-runtime` under the de **The roster is read from the bundles, never copied.** `bundleRoster(bundles)` parses each bundle's `dsh.bundle.patch` with the include plugin's own YAML dialect (`entryListSchema`, which carries `!!js`) and composes the layers with its `applyEntryPatches`, then keeps every enabled row whose package declares `dsh.client.platform === 'web'`, carrying that declaration's `inject` and `immediately`. `webApp` is the `web` profile's roster (`dsh-base`, then `dsh-web-app`), computed at import. A spec names what it tests and derives the rest: `webApp.closure([row])` keeps a row plus its transitive `inject` cone; `pick` and `without` exist for deliberate cuts. The test runtime stays a Client-face package: it imports no Host module, uses no dynamic import for this, and its client-face `types` adds `node` beside `client-build-environment` so the reader can use `node:fs`. `closure` treats the shell's static platform modules (`PLATFORM_MODULES`) as satisfied without a row. -**The production boot path runs unchanged.** `TestClient.start(plan, mock)` installs the mock as the Connection carrier (`__DSH_TRANSPORT__ = { rpc: mock.rpc }`), loads each roster row's `/client` module, registers its factory through the production module facade's `pendingQueue`, boots through `bootClient`, optionally mounts through `mountClient`, and waits for `connected`. `reload(name)` rebuilds a Loader entry through client-hmr's exported `tearDownEntryFiber`; `unload(name)` removes it; `dispose()` tears down and then fails the test on any endpoint that had no rule. jsdom lacks `EventSource` and `ResizeObserver`; `start` installs inert stand-ins only where the global is absent. Boots and entry rebuilds run one at a time per worker, since the `connection` plugin reads the transport global at apply, and each installs the acting client's transport first; the transport and shims are held by reference count, the first client installing them and the last dispose restoring them, so overlapping clients in one test each connect to their own mock, also after a `reload` of the `connection` row. +**The production boot path runs with one Connection substitution.** `TestClient.start(plan, mock)` loads each roster row's `/client` module, replaces the Connection row's page-global adapter with `installConnection(ctx, { transport: { rpc: mock.rpc } })`, registers the resulting factories through the production module facade's `pendingQueue`, boots through `bootClient`, optionally mounts through `mountClient`, and waits for `connected`. `reload(name)` rebuilds a Loader entry through client-hmr's exported `tearDownEntryFiber`; `unload(name)` removes it; `dispose()` tears down and then fails the test on any endpoint that had no rule. jsdom lacks `EventSource` and `ResizeObserver`; `start` reference-counts inert stand-ins only where the global is absent. Module systems, Connection carriers, Remote mocks, reloads, and disposal are instance-owned, so clients start concurrently and do not coordinate through page globals. + +The modules plugin reads the module system from its own `ctx.loader.internal` when it activates. Reloading the bootstrap row republishes that same instance without invalidating its bootstrap code, while another client's Loader and `ctx.modules` remain independent. **`remote.` is a contract-free proxy, not the generated client.** The `@deepseek-ai/dsh-api-remotes` row is dropped because its generated clients exist only in built `lib/`. For every `remote.` a roster row injects, plus every namespace the mock has a rule for, the tier provides one Proxy: `ctx.remote..(...args)` calls the endpoint `/` over the roster's own Connection with the positional args, as a stream when the mock registered a stream script for it and as a unary call otherwise. Cordis resolves `ctx.remote.` to the service `remote.`, so the Gateway client itself is untouched. A unary answer returns unchanged; a unary rejection folds the way the generated client folds a carrier throw, through the Gateway client's exported `carrierFailure` and `cancelledFailure`, so product code that fires a Remote call without awaiting sees no rejection. Stream items and failures pass through as the stream yields them. **Native mocks own response configuration and call assertions.** Tests use `mock.remote..` with `mockResolvedValue`, `mockResolvedValueOnce`, `mockReturnValueOnce`, or `mockImplementation`; the generated API supplies the signatures. Each mock instance owns its native response queue. Reusable tables register only default values or positional handlers, and each endpoint retains only its latest default. Stateful callbacks and deferred promises belong to individual tests. `ok` builds the success envelope. Streams require explicit declarations and can receive scripts over the opening args and a handle (`push`, `end`, `fail`); a declaration without a script produces a stream miss, while undeclared endpoints default to unary calls. Values are not validated. `mock.streams` controls scripted streams and exposes readiness/drain waits; `mock.log` records carrier calls (`pending`, `answered`, `failed`), scripted-stream state, first-argument `requests(endpoint?)`, and unmatched requests. `RemoteMock.create()` answers `$events` with a ready frame so the client can connect. -**Vitest owns each test's mock and client lifetime.** `createClientTest(plan, options)` adds native `mock`, `remote`, and `start` fixtures. The mock is fresh and carries the default responses; `remote` is its namespace proxy, and explicit `start()` leaves startup responses configurable and shares one startup promise within the test. Teardown waits for startup, disposes the successful client even after an assertion failure, checks missing responses, and rejects later starts. Callers await startup failures. Independently owned clients still use `TestClient.start`. Scenario data configures native mocks directly; returning a mutation response and updating subsequent describe responses remain separate actions. +**Vitest owns each test's mock and client lifetime.** `createClientTest(plan, options)` adds native `mock`, `remote`, and `start` fixtures. The mock is fresh and carries the default responses; `remote` is its namespace proxy, and explicit `start()` leaves startup responses configurable and shares one startup promise within the test. Object-layer specs use `remote` directly when they do not need the Connection; cases requiring Gateway `$stream` call `start()`. Teardown waits for startup, disposes the successful client even after an assertion failure, checks missing responses, and rejects later starts. Callers await startup failures. Independently owned clients still use `TestClient.start`. Scenario data configures native mocks directly; returning a mutation response and updating subsequent describe responses remain separate actions. **`remoteDefaultResponses` holds default responses for the boot-time Remote endpoints.** The table lists exactly the endpoints the `web` roster calls while booting and rendering with no sessions, no workspaces, and default settings, each row commented with its caller. A spec layers its own `RemoteTable` on top; a new boot-time call fails the spec at `dispose()`. +App-owned built-bundle tests install a fresh `RemoteMock` through `__DSH_TRANSPORT__` before `AppWebEntry` starts. Their scenario data stays under `apps/web/tests`; the production Connection package contains no scenario dataset or query-selected test transport. Browser cases that need Host behavior use `launchWebScaffold()` and the real HTTP/WebSocket path. + `mock.remote` uses native `@vitest/spy.fn` functions shared by direct callers and Connection dispatch. `MockedRemote` applies Vitest's deep mock type transformation to the entire generated namespace map; an empty map weakens only this proxy to `any`. Production `Context` and Remote declarations remain strict, with no namespace-specific type copies or compiler flags. The [proxy typing guidance](../../../../packages/test-support/remote-mock/README.md#remote-proxy) requires build-backed local type checking even when unbuilt tests pass. ## Product exports added for the tier -- `client/connection`: `ClientTransportHooks.rpc?` publishes the already decoded carrier the `?fixture` path used internally; `fetch` becomes optional. +- `client/connection`: `ClientTransportHooks.rpc?` accepts an already decoded carrier owned by an in-process composition; `fetch` is optional. `installConnection(ctx, options)` installs the same production service from instance-local transport, recovery, and location inputs. - `client/hmr`: `tearDownEntryFiber(entry)` is the registry-first fiber teardown `reload` already performed. - `client/modules`: `parseDshClient` and `exactPackageSpecifier` are exported from the client face and shared by the Host and roster reader. Test factories use the existing registration queue. The roster-to-boot-graph synthesis has only test consumers and lives in the tier. - `client/web`: `bootClient` and `mountClient` are extracted from `AppWebEntry.run()`, which now calls them. @@ -48,7 +52,11 @@ A whole-client tier lives in `@deepseek-ai/dsh-client-test-runtime` under the de **A hand-written test-side YAML and patch parser.** Rejected: `entryListSchema` and `applyEntryPatches` are the launcher's own and carry no Host Context merge; the tier writes only file reading, package.json location, and the web-row filter. -**A mock module standing in for the Gateway client.** Rejected: the mock must not interfere with Gateway internals; installing it on the Connection carrier keeps retry, folding, and stream semantics real. +**A mock module standing in for the Gateway client.** Rejected: the mock must not interfere with Gateway internals; passing it to the production Connection installer keeps retry, folding, and stream semantics real. + +**A query-selected fixture transport and scenario dataset in the production Connection package.** Rejected: it couples shipped code to mutable application-test data and makes a URL flag a hidden transport selector. Client-only assembled tests inject `RemoteMock`; browser tests that own Host behavior use the real Host scaffold. + +**A worker-level boot turn around a page-global Connection carrier.** Rejected: clients would share mutable process state, and every start or Connection reload would need serialization to install the acting client's carrier. Instance-local Connection installation lets independent plugin trees start and reload concurrently. **A second typed Gateway implementation with an `Api` generic, envelope and error classes, and a fixtures directory.** Rejected: it duplicates Gateway declarations and encoding. The mock derives method types from the generated namespace map and declares only unary or stream behavior at runtime. @@ -87,4 +95,4 @@ Product facts the tier surfaced and leaves as they are: ## Testing -`packages/test-support/remote-mock/tests/` covers rules, streams, the log, and the carrier face; the `assembly-` specs under `packages/test-support/client-runtime/tests/` cover the roster reader on the real bundles and on a scratch installation, module loading, the proxies including their fold, and `TestClient` under jsdom and plain Node. Seven converted specs use the tier. In `packages/client/ui-settings-general/tests/`, the shell and apply specs boot the whole `web` roster; the apply spec reads its Chinese copy from the Host settings document the mock answers and reconfigures the jsdom page URL for the off-loopback branch. In `packages/api/session-controller/tests/`, the Session, queue-store, and pending-submission specs drive their objects over the roster's real Connection through the `remote.` proxies, with the Gateway client's own `$stream` retry loop, over the gateway's dependency cone, and the client-apply spec boots the plugin's dependency cone, delivering Remote events as emit frames on `$events`. In `packages/api/workspace-controller/tests/`, the transport spec boots the plugin's cone for apply cases and the gateway cone for hand-built stream and controller cases, since a rostered plugin would share the follow endpoint. Each package keeps a `tests/remote/` module with its default responses and frame builders. The fixture tests include an expected assertion failure and independently observe completed client cleanup; settings reload tests observe replaced registration identities, and write tests assert every mutation argument. Teardown-failure tests execute the real tree disposer before reporting the injected failure and observe the `$events` stream's cancelled state. +`packages/test-support/remote-mock/tests/` covers rules, streams, the log, and the carrier face; the `assembly-` specs under `packages/test-support/client-runtime/tests/` cover the roster reader on the real bundles and on a scratch installation, module loading, the proxies including their fold, parallel client startup, instance-local module and Connection reload, and `TestClient` under jsdom and plain Node. App-owned built-client specs under `apps/web/tests/` inject a fresh RemoteMock and keep their stateful scenario data beside those specs. In `packages/client/ui-settings-general/tests/`, the shell and apply specs boot the whole `web` roster; the apply spec reads its Chinese copy from the Host settings document the mock answers and reconfigures the jsdom page URL for the off-loopback branch. In `packages/client/ui-message-feedback/tests/`, object-layer specs pass the `messageFeedback` RemoteMock namespace directly. In `packages/api/session-controller/tests/`, Session, queue-store, pending-submission, projection tail-page, sessions-service, and the manager cases that exercise streams start the Gateway dependency cone; the other manager cases and projection-store's manager-routing cases use the same per-test Remote fixtures without starting a client; and client-apply boots the plugin's dependency cone, delivering Remote events as emit frames on `$events`. In `packages/api/workspace-controller/tests/`, the transport spec boots the plugin's cone for apply cases and the gateway cone for hand-built stream and controller cases, since a rostered plugin would share the follow endpoint. Each package keeps a `tests/remote/` module with its default responses and frame builders. The fixture tests include an expected assertion failure and independently observe completed client cleanup; settings reload tests observe replaced registration identities, and write tests assert every mutation argument. Teardown-failure tests execute the real tree disposer before reporting the injected failure and observe the `$events` stream's cancelled state. diff --git a/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.zh.md b/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.zh.md index 8b4381dbfa..90b5768db4 100644 --- a/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.zh.md +++ b/.agents/notes/implemented/testing/2026-09-06-client-assembly-test-line.zh.md @@ -18,21 +18,25 @@ API 客户端 spec 用一个可编程的 Remote 面假件驱动对象。这个 **roster 从 bundle 现读,绝不拷贝。** `bundleRoster(bundles)` 用 include 插件自己的 YAML 方言(带 `!!js` 的 `entryListSchema`)解析每个 bundle 的 `dsh.bundle.patch`,用它的 `applyEntryPatches` 合成各层,再保留每个未禁用且其包声明 `dsh.client.platform === 'web'` 的行,带上该声明的 `inject` 与 `immediately`。`webApp` 是 `web` profile 的 roster(先 `dsh-base`、再 `dsh-web-app`),import 时算出。spec 点名它要测的东西,其余推导:`webApp.closure([row])` 保留一行及其传递 `inject` 锥;`pick` 与 `without` 留给刻意裁剪。测试运行时仍是 Client 面的包:它不 import 任何 Host 模块,此处不用动态 import,其 client 面的 `types` 在 `client-build-environment` 之外加了 `node`,好让读取器使用 `node:fs`。`closure` 把 shell 静态种入的平台模块(`PLATFORM_MODULES`)视为无需行即已满足。 -**生产启动路径原样运行。** `TestClient.start(plan, mock)` 把 mock 装成 Connection 载体(`__DSH_TRANSPORT__ = { rpc: mock.rpc }`),加载每个 roster 行的 `/client` 模块,经生产模块 facade 的 `pendingQueue` 登记其工厂,经 `bootClient` 启动,可选经 `mountClient` 挂载,然后等 `connected`。`reload(name)` 经 client-hmr 导出的 `tearDownEntryFiber` 重建一个 Loader entry;`unload(name)` 移除它;`dispose()` 拆掉一切,然后对任何没有规则的端点让测试失败。jsdom 没有 `EventSource` 与 `ResizeObserver`;`start` 只在全局缺失处装惰性替身。每个 worker 内启动与 entry 重建逐个进行,因为 `connection` 插件在 apply 时读传输全局,每次都先装上当事客户端的传输;传输与替身按引用计数持有,第一个客户端安装、最后一次 dispose 恢复,因此同一测试里重叠的客户端各连各的 mock,`reload` 了 `connection` 行之后也是。 +**生产启动路径只替换 Connection 输入适配。** `TestClient.start(plan, mock)` 加载每个 roster 行的 `/client` 模块,用 `installConnection(ctx, { transport: { rpc: mock.rpc } })` 替换 Connection 行的页面全局适配,经生产模块 facade 的 `pendingQueue` 登记这些工厂,经 `bootClient` 启动,可选经 `mountClient` 挂载,然后等 `connected`。`reload(name)` 经 client-hmr 导出的 `tearDownEntryFiber` 重建一个 Loader entry;`unload(name)` 移除它;`dispose()` 拆掉一切,然后对任何没有规则的端点让测试失败。jsdom 没有 `EventSource` 与 `ResizeObserver`;`start` 只对全局缺失的惰性替身做引用计数。模块系统、Connection 载体、Remote mock、重载和销毁都归实例所有,因此客户端可以并行启动,不通过页面全局变量协调。 + +modules 插件激活时从自己的 `ctx.loader.internal` 读取模块系统。重载 bootstrap 行会重新发布同一个实例而不让 bootstrap 代码失效,另一个客户端的 Loader 与 `ctx.modules` 不受影响。 **`remote.` 是无契约代理,不是生成客户端。** `@deepseek-ai/dsh-api-remotes` 行被去掉,因为它生成的客户端只存在于构建后的 `lib/`。对 roster 行注入的每个 `remote.`,加上 mock 有规则的每个命名空间,本档各提供一个 Proxy:`ctx.remote..(...args)` 经 roster 自己的 Connection 用位置参数调用端点 `/`,mock 为它登记了流脚本就走流、否则走一元。Cordis 把 `ctx.remote.` 解析到服务 `remote.`,所以 Gateway 客户端本身不动。一元应答原样返回;一元拒绝按生成客户端折叠载体抛错的方式折叠,经 Gateway 客户端导出的 `carrierFailure` 与 `cancelledFailure`,因此不等待就发出 Remote 调用的产品代码看不到任何 reject。流的项与失败按流吐出的样子直传。 **原生 mock 负责响应配置和调用断言。** 测试通过 `mock.remote..` 使用 `mockResolvedValue`、`mockResolvedValueOnce`、`mockReturnValueOnce` 或 `mockImplementation`,签名由生成的 API 提供。每个 mock 实例独立持有原生响应队列。可复用的表只登记默认值或位置参数 handler,每个端点仅保留最新默认响应。有状态回调和 deferred promise 归各测试所有。`ok` 构造成功信封。流需要显式声明,可提供接收打开参数与句柄(`push`、`end`、`fail`)的脚本;无脚本的声明产生流漏配,未声明端点默认走一元。值不校验。`mock.streams` 控制脚本流并提供打开/排空等待;`mock.log` 记录载体调用(`pending`、`answered`、`failed`)、脚本流状态、首参数 `requests(endpoint?)` 和未匹配请求。`RemoteMock.create()` 为 `$events` 应答 ready 帧,让客户端可以连接。 -**Vitest 拥有每条测试的 mock 和客户端生命周期。** `createClientTest(plan, options)` 增加原生 `mock`、`remote` 与 `start` fixture。mock 每次新建并携带默认响应;`remote` 是它的命名空间 Proxy,显式 `start()` 留出配置启动期应答的时机,同一测试共用一个启动 Promise。收尾等待启动,即使断言失败也销毁成功创建的客户端、检查漏配,并拒绝后续启动。启动错误由调用方 await 观察。分别拥有多个客户端时仍用 `TestClient.start`。场景数据直接配置原生 mock;返回 mutation 应答与更新后续 describe 应答仍是两个独立操作。 +**Vitest 拥有每条测试的 mock 和客户端生命周期。** `createClientTest(plan, options)` 增加原生 `mock`、`remote` 与 `start` fixture。mock 每次新建并携带默认响应;`remote` 是它的命名空间 Proxy,显式 `start()` 留出配置启动期应答的时机,同一测试共用一个启动 Promise。不需要 Connection 的对象层 spec 直接使用 `remote`;需要 Gateway `$stream` 的用例调用 `start()`。收尾等待启动,即使断言失败也销毁成功创建的客户端、检查漏配,并拒绝后续启动。启动错误由调用方 await 观察。分别拥有多个客户端时仍用 `TestClient.start`。场景数据直接配置原生 mock;返回 mutation 应答与更新后续 describe 应答仍是两个独立操作。 **`remoteDefaultResponses` 是启动期 Remote 端点的默认响应。** 这张表恰好列出 `web` roster 在没有 session、没有 workspace、默认设置下启动并渲染时会打的端点,每行注明调用方。spec 在其上叠加自己的 `RemoteTable`;新的启动期调用会在 `dispose()` 时让 spec 失败。 +归应用所有的 built-bundle 测试会在 `AppWebEntry` 启动前通过 `__DSH_TRANSPORT__` 安装一份新的 `RemoteMock`。场景数据留在 `apps/web/tests` 下;生产 Connection 包不包含场景数据集或由 query 选择的测试 transport。需要 Host 行为的浏览器用例使用 `launchWebScaffold()` 和真实 HTTP/WebSocket 路径。 + `mock.remote` 使用直接调用方与 Connection 分发共用的原生 `@vitest/spy.fn` 函数。`MockedRemote` 对完整生成的命名空间映射应用 Vitest 深层 mock 类型转换;映射为空时仅这个 Proxy 弱化为 `any`。生产 `Context` 与 Remote 声明保持严格,不需要命名空间专属类型副本或编译器 Flag。[Proxy 类型指引](../../../../packages/test-support/remote-mock/README.zh.md#remote-proxy)要求即使无构建测试通过,本地也必须执行构建后的类型检查。 ## 为本档新增的产品导出 -- `client/connection`:`ClientTransportHooks.rpc?` 公开 `?fixture` 路径内部已在用的已解码载体;`fetch` 变为可选。 +- `client/connection`:`ClientTransportHooks.rpc?` 接受由进程内组合持有的已解码载体;`fetch` 可选。`installConnection(ctx, options)` 从实例局部的 transport、recovery 与 location 输入安装同一个生产服务。 - `client/hmr`:`tearDownEntryFiber(entry)` 就是 `reload` 本来执行的 registry 先行的 fiber 拆除。 - `client/modules`:`parseDshClient` 与 `exactPackageSpecifier` 从 client 面导出,由 Host 和 roster 读取器共用。测试工厂使用已有注册队列。roster 行到 boot graph 的合成只有测试消费者,放在本档里。 - `client/web`:`bootClient` 与 `mountClient` 从 `AppWebEntry.run()` 抽出,后者现在调用它们。 @@ -48,7 +52,11 @@ API 客户端 spec 用一个可编程的 Remote 面假件驱动对象。这个 **自写一套测试侧的 YAML 与补丁解析器。** 否决:`entryListSchema` 与 `applyEntryPatches` 就是启动器自己的,不带 Host Context 合并;本档只写读文件、定位 package.json 和 web 行过滤。 -**用一个 mock 模块替代 Gateway 客户端。** 否决:mock 不得干涉 Gateway 内部;装在 Connection 载体上让重试、折叠与流语义都保持真实。 +**用一个 mock 模块替代 Gateway 客户端。** 否决:mock 不得干涉 Gateway 内部;把它传给生产 Connection 安装函数会让重试、折叠与流语义都保持真实。 + +**在生产 Connection 包中保留由 query 选择的 fixture transport 与场景数据集。** 否决:这会把交付代码耦合到易变的应用测试数据,并让 URL 标记成为隐藏的 transport 选择器。仅客户端的组装测试注入 `RemoteMock`;拥有 Host 行为的浏览器测试使用真实 Host scaffold。 + +**用 worker 级启动轮次包住页面全局 Connection 载体。** 否决:客户端会共享可变进程状态,每次启动或 Connection 重载都必须串行安装当事客户端的载体。实例局部的 Connection 安装让独立插件树可以并行启动和重载。 **第二套带类型的 Gateway 实现,包含 `Api` 泛型、信封与错误类以及 fixtures 目录。** 否决:它重复 Gateway 声明与编解码。mock 从生成的命名空间映射派生方法类型,运行时只声明一元或流行为。 @@ -87,4 +95,4 @@ spec 起的是真插件:整个 `web` roster 冷启动约五秒、热启动远 ## 测试 -`packages/test-support/remote-mock/tests/` 覆盖规则、流、日志与载体面;`packages/test-support/client-runtime/tests/` 下的 `assembly-` 系列 spec 覆盖在真 bundle 与临时安装上的 roster 读取器、模块加载、含折叠的代理,以及 jsdom 与纯 Node 下的 `TestClient`。七条改造后的 spec 使用本档。`packages/client/ui-settings-general/tests/` 下,shell 与 apply 两条起整个 `web` roster;apply 从 mock 应答的 Host settings 文档读它的中文文案,并为 off-loopback 分支重配 jsdom 页面 URL。`packages/api/session-controller/tests/` 下,Session、queue-store、pending-submission 三条在 gateway 依赖锥上经 `remote.` 代理走 roster 的真 Connection 驱动对象(`$stream` 的重试循环仍是 Gateway 客户端自己的),client-apply 起插件的依赖锥,把 Remote 事件作为 `$events` 上的 emit 帧投递。`packages/api/workspace-controller/tests/` 下,transport 的 apply 用例起插件锥、手工构造流与 controller 的用例起 gateway 锥,因为进了 roster 的插件会共用 follow 端点。每个包在 `tests/remote/` 保有自己的默认响应与帧构造。fixture 测试包含预期的断言失败,并独立观察客户端清理完成;settings 重载测试观察注册身份被替换,写入测试断言全部 mutation 参数。teardown 失败测试先执行真实树清理,再报告注入的失败,并观察 `$events` 流的取消状态。 +`packages/test-support/remote-mock/tests/` 覆盖规则、流、日志与载体面;`packages/test-support/client-runtime/tests/` 下的 `assembly-` 系列 spec 覆盖在真 bundle 与临时安装上的 roster 读取器、模块加载、含折叠的代理、并行客户端启动、实例局部的模块与 Connection 重载,以及 jsdom 与纯 Node 下的 `TestClient`。`apps/web/tests/` 下归应用所有的 built-client spec 注入一份新的 RemoteMock,并将有状态场景数据留在这些 spec 旁边。`packages/client/ui-settings-general/tests/` 下,shell 与 apply 两条起整个 `web` roster;apply 从 mock 应答的 Host settings 文档读它的中文文案,并为 off-loopback 分支重配 jsdom 页面 URL。`packages/client/ui-message-feedback/tests/` 下,对象层 spec 直接传入 RemoteMock 的 `messageFeedback` 命名空间。`packages/api/session-controller/tests/` 下,Session、queue-store、pending-submission、projection tail-page、sessions-service 以及验证流的 manager 用例启动 Gateway 依赖锥;其余 manager 用例与 projection-store 的 manager-routing 用例直接使用同一组逐测试 Remote fixture,不启动客户端;client-apply 则启动插件的依赖锥,把 Remote 事件作为 `$events` 上的 emit 帧投递。`packages/api/workspace-controller/tests/` 下,transport 的 apply 用例起插件锥、手工构造流与 controller 的用例起 gateway 锥,因为进了 roster 的插件会共用 follow 端点。每个包在 `tests/remote/` 保有自己的默认响应与帧构造。fixture 测试包含预期的断言失败,并独立观察客户端清理完成;settings 重载测试观察注册身份被替换,写入测试断言全部 mutation 参数。teardown 失败测试先执行真实树清理,再报告注入的失败,并观察 `$events` 流的取消状态。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index e2b79c08ab..0b5075db85 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -61,7 +61,10 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`@tanstack/react-virtual`](https://github.com/TanStack/virtual) | MIT | | [`@trycua/cua-driver`](https://github.com/trycua/cua) | MIT | | [`@vscode/ripgrep`](https://github.com/microsoft/vscode-ripgrep) | MIT | +| [`@xterm/addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit) | MIT | +| [`@xterm/addon-serialize`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize) | MIT | | [`@xterm/headless`](https://github.com/xtermjs/xterm.js) | MIT | +| [`@xterm/xterm`](https://github.com/xtermjs/xterm.js) | MIT | | [`@yarnpkg/parsers`](https://github.com/yarnpkg/berry) | BSD-2-Clause | | [`acorn`](https://github.com/acornjs/acorn) | MIT | | [`anser`](https://github.com/IonicaBizau/anser) | MIT | diff --git a/apps/cli/composition.md b/apps/cli/composition.md index 353ffecd30..5307252167 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -152,6 +152,8 @@ flowchart LR cfg --> plugin_dsh_base_session_checkpoint_policy plugin_dsh_base_tool_result_pruner["tool-result-pruner
@deepseek-ai/dsh-compaction-tool-result-pruner"] cfg --> plugin_dsh_base_tool_result_pruner + plugin_dsh_base_image_offload["image-offload
@deepseek-ai/dsh-compaction-image-offload"] + cfg --> plugin_dsh_base_image_offload plugin_dsh_base_tool_todo["tool-todo
@deepseek-ai/dsh-tool-todo"] cfg --> plugin_dsh_base_tool_todo plugin_dsh_base_tool_goal["tool-goal
@deepseek-ai/dsh-tool-goal"] @@ -256,6 +258,7 @@ flowchart LR | `spill-policy` | `@deepseek-ai/dsh-spill-policy` | | `session-checkpoint-policy` | `@deepseek-ai/dsh-session-checkpoint-policy` | | `tool-result-pruner` | `@deepseek-ai/dsh-compaction-tool-result-pruner` | +| `image-offload` | `@deepseek-ai/dsh-compaction-image-offload` | | `tool-todo` | `@deepseek-ai/dsh-tool-todo` | | `tool-goal` | `@deepseek-ai/dsh-tool-goal` | | `tool-ralph` | `@deepseek-ai/dsh-tool-ralph` | diff --git a/apps/web/package.json b/apps/web/package.json index 456b264094..598b4c9e7f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -33,6 +33,7 @@ "@deepseek-ai/cordis-plugin-group": "workspace:^", "@deepseek-ai/dsh-client-modules": "workspace:^", "@deepseek-ai/dsh-client-store": "workspace:^", + "@deepseek-ai/dsh-client-test-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-dockkit": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", @@ -41,6 +42,7 @@ "@deepseek-ai/dsh-pwsh-local": "workspace:^", "@deepseek-ai/dsh-experimental-webworker-packer": "workspace:^", "@deepseek-ai/dsh-experimental-webworker-runtime": "workspace:^", + "@deepseek-ai/dsh-remote-mock": "workspace:^", "@types/node": "^22.0.0", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", @@ -56,6 +58,7 @@ "vitest": "^4.1.8", "ws": "8.21.0", "@deepseek-ai/dsh-launch-environment": "workspace:^", + "@deepseek-ai/dsh-subprocess-local": "workspace:^", "@deepseek-ai/dsh-experimental-auto-review": "workspace:^" } } diff --git a/apps/web/stress-tests/reasoning-chunks.stress.ts b/apps/web/stress-tests/reasoning-chunks.stress.ts index 2eb1aa29ef..7f4969b456 100644 --- a/apps/web/stress-tests/reasoning-chunks.stress.ts +++ b/apps/web/stress-tests/reasoning-chunks.stress.ts @@ -1,22 +1,29 @@ /** * Opt-in browser stress reproduction for reasoning-stream renderer stalls. - * The fixture emits 100,000 individual chunks through the normal async - * carrier; the test measures event-loop and scheduled-interaction delay while - * the assembled React surface keeps a collapsed Think row live. + * A test-owned model adapter emits 100,000 individual chunks through the real + * Host, Gateway, and browser carriers; the test measures event-loop and + * scheduled-interaction delay while the assembled React surface keeps a + * collapsed Think row live. */ import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { expect, it, onTestFailed } from 'vitest' +import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm' +import type {} from '@deepseek-ai/dsh-agent-default-model' import { launchWebScaffold, watchConsole, type WebScaffold } from '../tests/scaffold.ts' -import { newEnglishPage, saveFailureShot } from '../tests/support.ts' +import { + connectFreshWorkspace, newEnglishPage, saveFailureShot, writeComposerDraft, +} from '../tests/support.ts' const CHUNK_COUNT = 100_000 const CHUNKS_PER_INTERVAL = 128 const CHUNK_INTERVAL_MS = 16 const MAIN_THREAD_DELAY_BUDGET_MS = 250 +const PROVIDER = 'reasoning-stress-test' +const MODEL = 'reasoning-storm' +const MARKER = `REASONING_STRESS_COMPLETE:${String(CHUNK_COUNT)}` interface ReasoningChunkStormState { - sessionId: string chunkCount: number chunksPerInterval: number intervalMs: number @@ -25,6 +32,86 @@ interface ReasoningChunkStormState { emitting: boolean } +/** Externally paced model stream whose counters are observed outside the browser. */ +class ReasoningStressAdapter extends LlmAdapter { + private finishStream!: () => void + private resolveStarted!: () => void + private resolveCompleted!: () => void + private releaseBatch: (() => void) | undefined + private resolveBatch: (() => void) | undefined + private batchInFlight = false + private finishRequested = false + private readonly finishGate = new Promise((resolve) => { this.finishStream = resolve }) + readonly started = new Promise((resolve) => { this.resolveStarted = resolve }) + readonly completed = new Promise((resolve) => { this.resolveCompleted = resolve }) + private emitted = 0 + private emitting = false + + finish(): void { + this.finishRequested = true + this.releaseBatch?.() + this.finishStream() + } + + async emitNextBatch(): Promise { + if (!this.emitting || this.releaseBatch === undefined) { + throw new Error('reasoning stress adapter has no batch awaiting release') + } + if (this.batchInFlight) throw new Error('reasoning stress adapter accepts one batch release at a time') + this.batchInFlight = true + const delivered = new Promise((resolve) => { this.resolveBatch = resolve }) + const release = this.releaseBatch + this.releaseBatch = undefined + release() + await delivered + } + + state(): ReasoningChunkStormState { + return { + chunkCount: CHUNK_COUNT, + chunksPerInterval: CHUNKS_PER_INTERVAL, + intervalMs: CHUNK_INTERVAL_MS, + emitted: this.emitted, + marker: MARKER, + emitting: this.emitting, + } + } + + override async *stream(options: GenerateOptions): AsyncIterable { + if (this.emitting || this.emitted !== 0) throw new Error('reasoning stress adapter accepts one model call') + this.emitting = true + const parts: string[] = [] + yield { type: 'block-start', index: 0, blockType: 'reasoning' } + while (this.emitted < CHUNK_COUNT) { + await new Promise((resolve) => { + this.releaseBatch = resolve + if (this.emitted === 0) this.resolveStarted() + if (this.finishRequested) resolve() + }) + if (this.finishRequested) break + options.signal?.throwIfAborted() + const end = Math.min(this.emitted + CHUNKS_PER_INTERVAL, CHUNK_COUNT) + while (this.emitted < end) { + const text = this.emitted === CHUNK_COUNT - 1 + ? `\n${MARKER}` + : this.emitted % 64 === 63 ? '推理\n' : '推理' + parts.push(text) + yield { type: 'reasoning-delta', index: 0, text } + this.emitted += 1 + } + this.batchInFlight = false + this.resolveBatch?.() + this.resolveBatch = undefined + } + this.emitting = false + if (this.emitted === CHUNK_COUNT) this.resolveCompleted() + await this.finishGate + options.signal?.throwIfAborted() + yield { type: 'block-end', index: 0, block: { type: 'reasoning', text: parts.join('') } } + yield { type: 'finish', reason: { kind: 'stop' } } + } +} + interface StressProbe { intervalId: number intervalMs: number @@ -36,10 +123,6 @@ interface StressProbe { } interface StressWindow extends Window { - __fxTiming?: { - startReasoningChunkStorm(id: string, chunkCount: number, chunksPerInterval: number, intervalMs: number): string - reasoningChunkStormState(): ReasoningChunkStormState | null - } __reasoningStressProbe?: StressProbe } @@ -47,23 +130,22 @@ it('keeps the browser responsive while rendering 100,000 reasoning chunks', asyn let scaffold: WebScaffold | undefined let browser: Browser | undefined let page: Page | undefined + const adapter = new ReasoningStressAdapter() try { scaffold = await launchWebScaffold() + scaffold.ctx.effect( + () => scaffold!.ctx.llm.registerAdapter([PROVIDER], adapter), + 'reasoning stress adapter', + ) + await scaffold.ctx.agentDefaultModel.saveSelection({ provider: PROVIDER, model: MODEL }) browser = await chromium.launch({ headless: process.env.DSH_WEB_STRESS_HEADFUL !== '1' }) page = await newEnglishPage(browser) const activePage = page - await activePage.addInitScript(() => { - localStorage.setItem('dsh.sessions.current', JSON.stringify({ sessionId: 'fx-alpha' })) - }) const tripwire = watchConsole(activePage) onTestFailed(() => saveFailureShot(activePage, 'web-stress-reasoning-chunks')) - await activePage.goto(`${scaffold.baseUrl}?fixture`, { waitUntil: 'load' }) + await activePage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await activePage.waitForSelector('[class*="frame"]', { timeout: 30_000 }) - // Fixture settings deliberately reject writes, so its welcome notice - // cannot acknowledge. Hide only that test overlay; the assembled chat - // tree beneath it remains mounted and exercises the production renderer. - await activePage.addStyleTag({ content: '[class*="onboardingOverlay"] { display: none !important; }' }) - await activePage.locator('[data-sample="bash"]').first().waitFor({ timeout: 30_000 }) + await connectFreshWorkspace(activePage, scaffold.workspaceCwd) await activePage.evaluate(() => { const intervalMs = 50 @@ -92,45 +174,42 @@ it('keeps the browser responsive while rendering 100,000 reasoning chunks', asyn ;(window as StressWindow).__reasoningStressProbe = probe }) - const marker = await activePage.evaluate(({ chunkCount, chunksPerInterval, intervalMs }) => { - const hooks = (window as StressWindow).__fxTiming - if (hooks === undefined) throw new Error('reasoning stress fixture hooks unavailable') - return hooks.startReasoningChunkStorm('fx-alpha', chunkCount, chunksPerInterval, intervalMs) - }, { - chunkCount: CHUNK_COUNT, - chunksPerInterval: CHUNKS_PER_INTERVAL, - intervalMs: CHUNK_INTERVAL_MS, - }) + const settled = scaffold.whenTurnSettled(540_000) + const input = activePage.locator('[data-composer-input]').first() + await writeComposerDraft(activePage, input, `Render ${String(CHUNK_COUNT)} reasoning chunks.`) + await input.press('Enter') + await adapter.started + await adapter.emitNextBatch() const liveThink = activePage.locator('[data-variant="think"][data-state="running"]').last() await liveThink.waitFor({ timeout: 60_000 }) - await expect.poll(async () => await activePage.evaluate(() => { - const hooks = (window as StressWindow).__fxTiming - return hooks?.reasoningChunkStormState()?.emitted ?? 0 - }), { timeout: 540_000, interval: 100 }).toBe(CHUNK_COUNT) - await expect.poll(() => liveThink.textContent(), { timeout: 60_000, interval: 100 }).toContain(marker) + await activePage.evaluate(intervalMs => new Promise((resolve) => { + window.setTimeout(resolve, intervalMs) + }), CHUNK_INTERVAL_MS) + while (adapter.state().emitted < CHUNK_COUNT) { + await adapter.emitNextBatch() + await activePage.evaluate(intervalMs => new Promise((resolve) => { + window.setTimeout(resolve, intervalMs) + }), CHUNK_INTERVAL_MS) + } + await adapter.completed + await expect.poll(() => liveThink.textContent(), { timeout: 60_000, interval: 100 }).toContain(MARKER) - const report = await activePage.evaluate(() => { + const browserReport = await activePage.evaluate(() => { const win = window as StressWindow const probe = win.__reasoningStressProbe - const state = win.__fxTiming?.reasoningChunkStormState() - if (probe === undefined || state === undefined || state === null) { - throw new Error('reasoning stress metrics unavailable') - } + if (probe === undefined) throw new Error('reasoning stress metrics unavailable') window.clearInterval(probe.intervalId) const interactionDelayMs = probe.interactionHandledAt === null ? null : probe.interactionHandledAt - probe.interactionDueAt return { - chunkCount: state.chunkCount, - chunksPerInterval: state.chunksPerInterval, - intervalMs: state.intervalMs, - emitted: state.emitted, maxMainThreadDelayMs: Math.max(0, probe.maxDelayMs), interactionDelayMs, heartbeatSamples: probe.samples, } }) + const report = { ...adapter.state(), ...browserReport } process.stdout.write(`reasoning-chunk stress report: ${JSON.stringify(report)}\n`) expect(report).toMatchObject({ @@ -146,7 +225,10 @@ it('keeps the browser responsive while rendering 100,000 reasoning chunks', asyn expect(interactionDelayMs, JSON.stringify(report)).toBeLessThan(MAIN_THREAD_DELAY_BUDGET_MS) expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) + adapter.finish() + await settled } finally { + adapter.finish() await browser?.close() await scaffold?.close() } diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml index 4a06bf19ed..cf707e6cb3 100644 --- a/apps/web/tests/README.i18n.yaml +++ b/apps/web/tests/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/web/tests/README.md -README.md: 883d93d746afa6bac0b67f2c2d6bc6c03b31553a -README.zh.md: 7a232329b890a669240272aada3a5bafff9efd99 +README.md: b2fa27bb7a60ed78309bf81e77b9cd5217bbee2a +README.zh.md: 29ae2e86018257a4fc2379ea2563c8b5e5e9036f diff --git a/apps/web/tests/README.md b/apps/web/tests/README.md index 883d93d746..b2fa27bb7a 100644 --- a/apps/web/tests/README.md +++ b/apps/web/tests/README.md @@ -18,6 +18,6 @@ Importing a Client package — a value or a type — pulls its whole TypeScript When a scenario needs a Client-owned constant or pure function, mirror it here instead, next to the commented-out import that names the source module. A drift then surfaces as a missed selector or a stale mirrored value — a loud failure, never a silent pass. `scaffold.ts` follows this rule for the welcome-notice namespace, acknowledgement field, version, and asserted Chinese copy. -One kind of Client import stands. `assembled-boot.ts` drives the shell itself, so it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest type from `@deepseek-ai/dsh-client-modules/client`: booting the real shell is what that harness is for, and both packages are already in the Host graph. The chat scenarios mirror `conversationContextKey` in `support.ts` instead of importing its Client owner. +The built-client harness is the exception. `assembled-boot.ts` imports `AppWebEntry`, the boot-manifest type, and `RemoteMock`; `assembled-remote.ts` imports the Client test runtime's default responses and `RemoteMock`. These packages are explicit project references for booting the real shell against a test-owned carrier. The chat scenarios mirror `conversationContextKey` in `support.ts` instead of importing its Client owner. Nothing mechanically enforces this rule; keep it in review. diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md index 7a232329b8..29ae2e8601 100644 --- a/apps/web/tests/README.zh.md +++ b/apps/web/tests/README.zh.md @@ -18,6 +18,6 @@ import 一个 Client 包——无论值还是类型——都会把它整个 Type 当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的 import 点明源模块。这样漂移会表现为选择器未命中或镜像值陈旧——是响亮的失败,绝不会是静默通过。`scaffold.ts` 按此规则镜像 welcome-notice 的 namespace、确认字段、版本和被断言的中文文案。 -有一类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从 `@deepseek-ai/dsh-client-web` import `AppWebEntry`、从 `@deepseek-ai/dsh-client-modules/client` import boot manifest(元数据清单)类型:启动真实 shell 正是该 harness 的用途,且这两个包本来就在 Host 图中。chat 场景则在 `support.ts` 中镜像 `conversationContextKey`,而不 import 其 Client owner。 +built-client harness 是例外。`assembled-boot.ts` import `AppWebEntry`、boot manifest(元数据清单)类型与 `RemoteMock`;`assembled-remote.ts` import Client test runtime 的默认响应与 `RemoteMock`。这些包是显式的工程引用,用于通过测试持有的 carrier 启动真实 shell。chat 场景仍在 `support.ts` 中镜像 `conversationContextKey`,而不 import 其 Client owner。 没有任何机制强制这条规则;靠 review 守住它。 diff --git a/apps/web/tests/assembled-boot.ts b/apps/web/tests/assembled-boot.ts index a18893d82b..90acb73bec 100644 --- a/apps/web/tests/assembled-boot.ts +++ b/apps/web/tests/assembled-boot.ts @@ -1,6 +1,6 @@ // Shared scaffolding for the assembled-jsdom snapshots: the real built // workspace `lib/client.js` artifacts booted through AppWebEntry's -// ModuleLoader path (loadBundle) against the keyless fixture Connection RPC +// ModuleLoader path (loadBundle) against a test-owned RemoteMock carrier // transport. Every file that mounts this graph needs the same boot entry list, // the same bundle map, the same jsdom globals, and the same mount call, and // differs only in what it asserts afterwards, so the scaffolding lives here. @@ -15,7 +15,11 @@ import { act, cleanup } from '@testing-library/react' import { afterEach, beforeEach, vi } from 'vitest' import { bootInjections, orderByModuleGraph } from '@deepseek-ai/dsh-client-modules' import type { ClientModuleLoaderTarget, WebBootEntry, WebBootGraph } from '@deepseek-ai/dsh-client-modules/client' +import type { RemoteMock } from '@deepseek-ai/dsh-remote-mock' import { AppWebEntry } from '@deepseek-ai/dsh-client-web' +import { + createAssembledRemote, type AssembledRemote, type AssembledRemoteOptions, +} from './assembled-remote.ts' interface AssembledPlugin extends WebBootEntry { /** Absolute path to the built client artifact declared by this package. */ @@ -25,6 +29,8 @@ interface AssembledPlugin extends WebBootEntry { interface AssembledBootOptions { /** Package ids omitted from this mounted composition. */ readonly exclude?: readonly string[] + /** Remote answers owned by this assembled case. */ + readonly remote?: AssembledRemoteOptions } interface ClientPackageManifest { @@ -174,6 +180,7 @@ function bundleTable(graph: WebBootGraph, plugins: readonly AssembledPlugin[]): interface FixtureWindow extends Window { __DSH_BOOT__?: WebBootGraph __ModuleLoader__?: ClientModuleLoaderTarget + __DSH_TRANSPORT__?: { readonly rpc: RemoteMock['rpc'] } } class ResizeObserverStub { @@ -189,6 +196,7 @@ class EventSourceStub { const win = window as FixtureWindow let unmount: (() => Promise) | undefined +let mountedRemote: RemoteMock | undefined /** * Register the per-test jsdom setup and teardown the assembled boot needs: @@ -214,9 +222,8 @@ export function installAssembledBootEnv(): void { beforeEach(() => { localStorage.clear() // The locale service derives its provisional locale from the browser and - // takes an explicit choice only from Host settings, which this lane's - // fixture transport does not serve; pinning the navigator is what selects - // English here. + // takes an explicit choice only from Host settings. This scenario serves no + // locale setting, so pinning the navigator selects English. Object.defineProperty(navigator, 'languages', { value: ['en-US'], configurable: true }) Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true }) document.title = 'DeepSeek Harness' @@ -228,11 +235,23 @@ export function installAssembledBootEnv(): void { }) afterEach(async () => { - await act(async () => { await unmount?.() }) + const failures: unknown[] = [] + try { + await act(async () => { await unmount?.() }) + } catch (error) { + failures.push(error) + } + try { + mountedRemote?.assertNoUnmatched() + } catch (error) { + failures.push(error) + } unmount = undefined + mountedRemote = undefined cleanup() delete win.__DSH_BOOT__ delete win.__ModuleLoader__ + delete win.__DSH_TRANSPORT__ document.body.innerHTML = '' document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() }) document.title = '' @@ -243,19 +262,23 @@ export function installAssembledBootEnv(): void { delete ownNavigator.languages delete ownNavigator.language vi.unstubAllGlobals() + if (failures.length > 0) throw new AggregateError(failures, 'assembled boot teardown failed') }) } /** - * Mount the assembled application on the fixture transport; the teardown + * Mount the assembled application on an isolated RemoteMock transport; the teardown * registered by installAssembledBootEnv disposes it. - * @param search - fixture query string used to select deterministic host behavior. * @param options - composition changes applied to this mount. + * @returns the test-owned RemoteMock world. */ -export function mountAssembledApp(search = '?fixture', options: AssembledBootOptions = {}): void { +export function mountAssembledApp(options: AssembledBootOptions = {}): AssembledRemote { const excluded = new Set(options.exclude) const plugins = PLUGINS.filter(plugin => !excluded.has(plugin.id)) - history.replaceState(null, '', `/${search}`) + const remote = createAssembledRemote(options.remote) + mountedRemote = remote.mock + win.__DSH_TRANSPORT__ = { rpc: remote.mock.rpc } + history.replaceState(null, '', '/') const root = document.createElement('div') root.id = 'root' document.body.appendChild(root) @@ -281,6 +304,7 @@ export function mountAssembledApp(search = '?fixture', options: AssembledBootOpt void entry.run() unmount = () => entry.dispose() }) + return remote } /** diff --git a/apps/web/tests/assembled-remote.spec.ts b/apps/web/tests/assembled-remote.spec.ts new file mode 100644 index 0000000000..6e5ade036e --- /dev/null +++ b/apps/web/tests/assembled-remote.spec.ts @@ -0,0 +1,44 @@ +/** Stateful behavior of the app-owned RemoteMock scenario. */ + +import { expect, it } from 'vitest' +import { createAssembledRemote } from './assembled-remote.ts' + +it('advances turns and updates the Session summary after accepted prompts', async () => { + const { mock } = createAssembledRemote() + await mock.dispatch('session/create', [{ request: { cwd: '/work' } }]) + await mock.dispatch('session/prompt', [{ + request: { sessionId: 'fx-1', requestId: 'request-1', mode: 'queue', content: [] }, + }]) + await mock.dispatch('session/prompt', [{ + request: { sessionId: 'fx-1', requestId: 'request-2', mode: 'queue', content: [] }, + }]) + + const listed = await mock.dispatch('session/list', []) as { + readonly ok: true + readonly value: { + readonly items: readonly { readonly sessionId: string; readonly blank: boolean; readonly running: boolean }[] + } + } + expect(listed.ok).toBe(true) + expect(listed.value.items.find(item => item.sessionId === 'fx-1')).toMatchObject({ + blank: false, + running: true, + }) + + const iterator = mock.open('session/follow', [{ + request: { address: { kind: 'session', sessionId: 'fx-1' } }, + }], new AbortController().signal)[Symbol.asyncIterator]() + await expect(iterator.next()).resolves.toMatchObject({ + done: false, + value: { + records: [ + { event: { type: 'turn/start', data: { turn: 0 } } }, + { event: { type: 'user/message' } }, + { event: { type: 'turn/start', data: { turn: 1 } } }, + { event: { type: 'user/message' } }, + ], + }, + }) + await iterator.return?.() + mock.assertNoUnmatched() +}) diff --git a/apps/web/tests/assembled-remote.ts b/apps/web/tests/assembled-remote.ts new file mode 100644 index 0000000000..69739dc7a7 --- /dev/null +++ b/apps/web/tests/assembled-remote.ts @@ -0,0 +1,399 @@ +/** + * RemoteMock scenario for built-client tests that do not own a Host. + * The adjacent JSON is maintained with this module when Remote responses or + * the current Session header version change. + * Fixture Sessions have no retained Host terminals to restore. + */ + +import { readFileSync } from 'node:fs' +import { randomUUID } from 'node:crypto' +import { join } from 'node:path' +import { ok, RemoteMock } from '@deepseek-ai/dsh-remote-mock' +import { remoteDefaultResponses } from '@deepseek-ai/dsh-client-test-runtime/src/assembly/remote-default-responses.ts' + +interface SessionSummary { + readonly sessionId: string + updatedAt: number + running: boolean + blank: boolean + readonly parentSessionId?: string + readonly origin?: 'subagent' + readonly cwd?: string + readonly projections?: { + readonly asOfSeq: number + readonly values: Readonly> + } +} + +interface WorkspaceView { + readonly workspaceId: string + readonly path: string + readonly title: string + sessionIds: string[] + readonly createdAt: string + updatedAt: string +} + +interface EventRecord { + readonly type: 'event' + readonly event: { + readonly seq: number + readonly time: number + readonly type: string + readonly data: unknown + readonly surfaceOp?: string + } +} + +interface FollowSnapshot { + readonly type: 'snapshot' + readonly header: Readonly> + readonly cursor: number + readonly records: readonly EventRecord[] + readonly hasMore: boolean + readonly projections: Readonly> + readonly assistantStream?: Readonly> +} + +interface ControlBaseline { + readonly type: 'baseline' + readonly value: { + readonly queues: Readonly> + readonly jobs: Readonly> + readonly approvals: readonly unknown[] + readonly questions: readonly unknown[] + readonly projections: Readonly> + }>> + } +} + +interface CapturedFixture { + readonly sessionList: { readonly ok: true; readonly value: { readonly items: readonly SessionSummary[] } } + readonly settingsDescribe: unknown + readonly credentialsDescribe: unknown + readonly modelCatalog: unknown + readonly agentPresets: unknown + readonly commands: unknown + readonly workspace: { + readonly type: 'baseline' + readonly value: { + readonly items: readonly WorkspaceView[] + readonly archivedSessionIds: readonly string[] + } + } + readonly control: ControlBaseline + readonly remoteEvents: readonly unknown[] + readonly follow: FollowSnapshot + readonly attachment: unknown +} + +export interface AssembledRemoteOptions { + /** Return the fixture's image-dimension admission error from Session prompt. */ + readonly rejectPrompt?: boolean +} + +export interface AssembledRemote { + readonly mock: RemoteMock +} + +const fixtureSource = readFileSync( + join(process.cwd(), 'apps/web/tests/fixtures/assembled-remote.fixture.json'), + 'utf8', +) + +/** Create one isolated RemoteMock world for an assembled built-client case. */ +export function createAssembledRemote(options: AssembledRemoteOptions = {}): AssembledRemote { + const fixture = JSON.parse(fixtureSource) as CapturedFixture + const sessions = [...structuredClone(fixture.sessionList.value.items)] + const workspaces = [...structuredClone(fixture.workspace.value.items)] + const records = new Map([[ + 'fx-alpha', + structuredClone(fixture.follow.records) as EventRecord[], + ]]) + const nextTurns = new Map([...records].map(([sessionId, sessionRecords]) => { + let next = 0 + for (const { event } of sessionRecords) { + if (event.type !== 'turn/start' || !isRecord(event.data)) continue + const turn = event.data['turn'] + if (typeof turn === 'number') next = Math.max(next, turn + 1) + } + return [sessionId, next] as const + })) + const attachments = new Map([['fixture:image', structuredClone(fixture.attachment)]]) + const blankSessionProjections = fixture.control.value.projections['fx-gamma'] + if (blankSessionProjections === undefined) { + throw new Error('assembled fixture: blank Session projections missing') + } + let nextSession = 1 + + const mock = RemoteMock.create().load(remoteDefaultResponses) + mock.load({ + unary: { + 'settings/describe': structuredClone(fixture.settingsDescribe), + 'credentials/describe': structuredClone(fixture.credentialsDescribe), + 'session/modelCatalog': structuredClone(fixture.modelCatalog), + 'agentPresets/list': structuredClone(fixture.agentPresets), + 'commands/list': structuredClone(fixture.commands), + 'settings/canOpenAgentPresetDirectory': ok(true), + 'settings/openSettingsDocument': ok({ opened: true }), + 'settings/openAgentPresetDirectory': ok({ opened: true }), + 'subagents/list': ok({ entries: [], parentAvailable: true }), + 'terminal/list': ok([]), + 'skills/list': ok({ skills: [] }), + 'session/canOpenWorkspacePath': ok(true), + 'session/openWorkspacePath': ok({ opened: true }), + 'session/updateQueue': { + ok: false, + error: { + code: 'session/queue-item-not-found', + message: 'assembled fixture has no pending queue item', + details: {}, + }, + }, + 'session/cancel': ok({ accepted: true }), + }, + }) + + mock.stream('$events', (_args, stream) => { + for (const frame of fixture.remoteEvents) stream.push(structuredClone(frame)) + }) + mock.stream('session/control', (_args, stream) => { + stream.push(structuredClone(fixture.control)) + }) + mock.stream('workspace/follow', (_args, stream) => { + stream.push({ + type: 'baseline', + value: { + items: structuredClone(workspaces), + archivedSessionIds: structuredClone(fixture.workspace.value.archivedSessionIds), + }, + }) + }) + mock.stream('session/follow', ([args], stream) => { + const request = recordValue(args, 'request') + const sessionId = followedSessionId(request) + if (sessionId === 'fx-alpha') { + stream.push(structuredClone(fixture.follow)) + return + } + const summary = sessions.find(candidate => candidate.sessionId === sessionId) + if (summary === undefined) throw new Error(`assembled fixture: no Session ${sessionId}`) + const sessionRecords = records.get(sessionId) ?? [] + stream.push({ + type: 'snapshot', + header: { + version: 3, + id: sessionId, + createdAt: summary.updatedAt, + cwd: summary.cwd, + isSeeded: false, + }, + cursor: sessionRecords.at(-1)?.event.seq ?? -1, + records: structuredClone(sessionRecords), + hasMore: false, + projections: structuredClone(blankSessionProjections), + ...isAssistantStreamRequested(request) ? { assistantStream: { revision: 0 } } : {}, + }) + }) + + mock.unary('$events/result', (result: unknown) => { + const eventId = recordString(result, 'eventId') + mock.streams.push('$events', { type: 'cancel', eventId }) + return ok(undefined) + }) + mock.unary('session/list', () => ok({ items: structuredClone(sessions) })) + mock.unary('workspace/create', (request: unknown) => { + const path = recordString(recordValue(request, 'request'), 'path') + const existing = workspaces.find(workspace => workspace.path === path) + if (existing !== undefined) return ok({ workspace: structuredClone(existing), created: false }) + const now = new Date().toISOString() + const workspace = { + workspaceId: `fx-ws-${String(workspaces.length + 1)}`, + path, + title: path.split('/').filter(Boolean).at(-1) ?? path, + sessionIds: [], + createdAt: now, + updatedAt: now, + } + workspaces.unshift(workspace) + mock.streams.push('workspace/follow', { type: 'upsert', workspace: structuredClone(workspace) }) + return ok({ workspace: structuredClone(workspace), created: true }) + }) + mock.unary('session/create', (request: unknown) => { + request = recordValue(request, 'request') + const requestedId = optionalRecordString(request, 'sessionId') + const sessionId = requestedId ?? `fx-${String(nextSession++)}` + const existing = sessions.find(candidate => candidate.sessionId === sessionId) + if (existing !== undefined) return ok({ sessionId }) + const workspaceId = optionalRecordString(request, 'workspaceId') + const workspace = workspaces.find(candidate => candidate.workspaceId === workspaceId) + const cwd = workspace?.path ?? optionalRecordString(request, 'cwd') ?? '/tmp/fixture' + const summary: SessionSummary = { + sessionId, + updatedAt: Date.now(), + running: false, + blank: true, + cwd, + projections: structuredClone(blankSessionProjections), + } + sessions.push(summary) + records.set(sessionId, []) + nextTurns.set(sessionId, 0) + if (workspace !== undefined && !workspace.sessionIds.includes(sessionId)) { + workspace.sessionIds = [sessionId, ...workspace.sessionIds] + workspace.updatedAt = new Date().toISOString() + mock.streams.push('workspace/follow', { + type: 'upsert', + workspace: structuredClone(workspace), + }) + } + mock.streams.push('$events', { type: 'emit', event: 'api-session/added', args: [structuredClone(summary)] }) + return ok({ sessionId }) + }) + mock.unary('session/attachment', (request: unknown) => { + request = recordValue(request, 'request') + const attachmentId = recordString(request, 'attachmentId') + return attachments.get(attachmentId) ?? { + ok: false, + error: { + code: 'session/attachment-invalid', + message: `assembled fixture attachment ${attachmentId} is missing`, + details: { reason: 'ATTACHMENT_NOT_FOUND' }, + }, + } + }) + mock.unary('session/prompt', (request: unknown) => { + request = recordValue(request, 'request') + if (options.rejectPrompt === true) { + return { + ok: false, + error: { + code: 'session/attachment-invalid', + message: 'assembled fixture: image side exceeds the deployment limit', + details: { reason: 'IMAGE_DIMENSION_TOO_LARGE' }, + }, + } + } + const sessionId = recordString(request, 'sessionId') + const requestId = recordString(request, 'requestId') + const sessionRecords = records.get(sessionId) ?? [] + const summary = sessions.find(candidate => candidate.sessionId === sessionId) + if (summary === undefined) throw new Error(`assembled fixture: no Session ${sessionId}`) + const content = recordArray(request, 'content').map((part) => { + if (!isRecord(part) || part.type !== 'image') return part + const attachmentId = `assembled:${randomUUID()}` + const data = recordString(part, 'data') + const attachment = { + attachmentId, + mediaType: recordString(part, 'mediaType'), + bytes: Math.max(1, Math.floor(data.length * 3 / 4)), + width: 160, + height: 90, + ...optionalRecordString(part, 'name') === undefined + ? {} + : { name: optionalRecordString(part, 'name') }, + } + attachments.set(attachmentId, ok({ attachment, data })) + return { type: 'image', attachment } + }) + const turn = nextTurns.get(sessionId) ?? 0 + nextTurns.set(sessionId, turn + 1) + summary.updatedAt = Date.now() + summary.blank = false + if (!summary.running) { + summary.running = true + mock.streams.push('$events', { type: 'emit', event: 'api-session/status', args: [sessionId, true] }) + } + const turnEvent = eventOf(sessionRecords.length, 'turn/start', { turn }) + const userEvent = eventOf(sessionRecords.length + 1, 'user/message', { + content, + source: { kind: 'user', rpcId: requestId }, + role: 'user', + id: randomUUID(), + }, 'append') + sessionRecords.push(turnEvent, userEvent) + records.set(sessionId, sessionRecords) + mock.streams.push('session/follow', turnEvent, follows(sessionId)) + mock.streams.push('session/follow', userEvent, follows(sessionId)) + return ok({ accepted: true }) + }) + mock.unary('commands/execute', (args: unknown) => { + const line = optionalRecordString(args, 'line') ?? '' + const name = /^\/(\S+)/u.exec(line.trim())?.[1] + if (name === undefined || !['compact', 'echo', 'goal', 'permission', 'plan'].includes(name)) { + return ok(undefined) + } + return ok({ + commandId: `assembled-command-${randomUUID()}`, + result: { + kind: 'success', + ...name === 'echo' ? { text: line.replace(/^\/echo\s*/u, '') } : {}, + }, + }) + }) + + return { mock } +} + +function eventOf( + seq: number, + type: string, + data: unknown, + surfaceOp?: string, +): EventRecord { + return { + type: 'event', + event: { + seq, + time: Date.now(), + type, + data, + ...(surfaceOp === undefined ? {} : { surfaceOp }), + }, + } +} + +function follows(sessionId: string): (args: readonly unknown[]) => boolean { + return ([args]) => followedSessionId(recordValue(args, 'request')) === sessionId +} + +function followedSessionId(value: unknown): string { + if (!isRecord(value) || !isRecord(value.address)) throw new TypeError('assembled fixture follow request is invalid') + return value.address.kind === 'session' + ? recordString(value.address, 'sessionId') + : recordString(value.address, 'childSessionId') +} + +function isAssistantStreamRequested(value: unknown): boolean { + return isRecord(value) && value.assistantStream === true +} + +function recordString(value: unknown, key: string): string { + const selected = isRecord(value) ? value[key] : undefined + if (typeof selected !== 'string') throw new TypeError(`assembled fixture ${key} must be a string`) + return selected +} + +function optionalRecordString(value: unknown, key: string): string | undefined { + const selected = isRecord(value) ? value[key] : undefined + if (selected === undefined) return undefined + if (typeof selected !== 'string') throw new TypeError(`assembled fixture ${key} must be a string`) + return selected +} + +function recordArray(value: unknown, key: string): readonly unknown[] { + const selected = isRecord(value) ? value[key] : undefined + if (!Array.isArray(selected)) throw new TypeError(`assembled fixture ${key} must be an array`) + return selected +} + +function recordValue(value: unknown, key: string): unknown { + if (!isRecord(value) || !(key in value)) throw new TypeError(`assembled fixture ${key} is missing`) + return value[key] +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} diff --git a/apps/web/tests/built-boot.expected.e2e.ts b/apps/web/tests/built-boot.expected.e2e.ts index e30ac22630..17a741dac4 100644 --- a/apps/web/tests/built-boot.expected.e2e.ts +++ b/apps/web/tests/built-boot.expected.e2e.ts @@ -4,7 +4,7 @@ // reach a surface only the built bundles expose; this one asserts that the // graph assembles at all — staged activation across the immediately tier and // the inject layers, per-plugin CSS injection, and a rendered journey reaching -// chat content from the keyless fixture Connection RPC. +// chat content from the keyless RemoteMock scenario. // // Component behavior remains owned by per-package suites (SlotTestRuntime // benches over src). This smoke additionally pins the resident interaction @@ -92,7 +92,7 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn expect(waitingRow.querySelector('[data-state="ongoing"]')).toBeNull() within(waitingRow).getByText('Waiting for answer') - // Opening a session reaches chat content through the fixture transport. + // Opening a session reaches chat content through the RemoteMock transport. fireEvent.click(waitingTitle) await waitFor(() => { expect(document.querySelector('[data-sample="bash"]')).not.toBeNull() @@ -162,7 +162,7 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn }) it('boots without ui-chat and does not select another conversation view implicitly', async () => { - mountAssembledApp('?fixture', { exclude: ['@deepseek-ai/dsh-client-ui-chat'] }) + mountAssembledApp({ exclude: ['@deepseek-ai/dsh-client-ui-chat'] }) const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 }) const boot = Reflect.get(window, '__DSH_BOOT__') as { entries: Array<{ id: string }> } | undefined diff --git a/apps/web/tests/clickable-links-gallery.e2e.ts b/apps/web/tests/clickable-links-gallery.e2e.ts index 081d183c5b..48285d1c65 100644 --- a/apps/web/tests/clickable-links-gallery.e2e.ts +++ b/apps/web/tests/clickable-links-gallery.e2e.ts @@ -45,6 +45,8 @@ const OVERLAY = fileURLToPath(new URL('./produced-files.overlay.yml', import.met const MODE = webSnapshotMode() const SEED_ID = 'clickable-links-gallery-web-e2e' const DONE = 'LINK_GALLERY_DONE' +// Seeded events and browser time share a day independently of host timezones. +const GALLERY_TIME = Date.UTC(2026, 0, 15, 12) const GUIDE_URL = 'https://docs.example.test/guide' const API_URL = 'https://docs.example.test/api' @@ -192,7 +194,6 @@ const CALLS: GalleryCall[] = [ */ function galleryFixture(imageUrl: string): string { const session = Session.create(SessionId('clickable-links-gallery-source')) - const eventTimeOrigin = new Date().setHours(12, 0, 0, 0) session.append('turn/start', { turn: 1 }) const user = session.append('user/message', createUserMessage({ content: text('Assemble the link gallery: write the report and styles, inspect the sources, and summarize.'), @@ -289,7 +290,7 @@ function galleryFixture(imageUrl: string): string { }), ...session.snapshotEvents().map(event => JSON.stringify({ ...event, - time: eventTimeOrigin + event.seq * 1_000, + time: GALLERY_TIME + event.seq * 1_000, })), '', ].join('\n') @@ -305,9 +306,10 @@ describe('web e2e: clickable links gallery', () => { beforeAll(async () => { scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY }) imageUrl = new URL('/favicon.svg', scaffold.baseUrl).toString() - await seedSession(scaffold, galleryFixture(imageUrl), SEED_ID) + await seedSession(scaffold, galleryFixture(imageUrl), SEED_ID, undefined, { createdAt: GALLERY_TIME }) browser = await chromium.launch() page = await newEnglishPage(browser) + await page.clock.setFixedTime(GALLERY_TIME + 60_000) tripwire = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) diff --git a/apps/web/tests/command-image-envelope.expected.e2e.ts b/apps/web/tests/command-image-envelope.expected.e2e.ts index 565eaa3524..ef288f5818 100644 --- a/apps/web/tests/command-image-envelope.expected.e2e.ts +++ b/apps/web/tests/command-image-envelope.expected.e2e.ts @@ -1,6 +1,6 @@ // @vitest-environment jsdom // The command attachment envelope over the BUILT client graph (real -// bundles via AppWebEntry, keyless fixture Connection RPC): an enter +// bundles via AppWebEntry, keyless RemoteMock transport): an enter // submission carrying composer attachments resolves only through a command whose // descriptor declares `input.attachments`. A non-declaring command refuses with // one composer error banner and everything retained; a declaring command diff --git a/apps/web/tests/details-session-lifecycle.e2e.ts b/apps/web/tests/details-session-lifecycle.e2e.ts index 14245fafa0..a02bafac00 100644 --- a/apps/web/tests/details-session-lifecycle.e2e.ts +++ b/apps/web/tests/details-session-lifecycle.e2e.ts @@ -243,6 +243,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S await select(original, 'LIGHTHOUSE') await open() + await column.locator('[data-sidebar-right-guide-entry="files"]').click() // The content-box panel adds its one rendered border pixel outside the // CSS width assigned by the grid solver. await expect.poll(() => sidebarSnapshot(page), { timeout: 5_000 }) @@ -256,6 +257,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S await expect.poll(() => split.isDisabled()).toBe(false) await split.click() await expect.poll(() => panes.count()).toBe(2) + await panes.last().locator('[data-sidebar-right-guide-entry="files"]').click() await panes.first().locator('[data-dockkit-tab]').filter({ hasText: 'Files' }).click() await expect.poll(() => panes.first().locator('[data-files-state="tree"]').count()).toBe(1) const retainedA = await paneSnapshot(page) @@ -278,6 +280,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S await expect.poll(() => detailsTrack(page)).toBe(0) await open() expect(await panel.getAttribute('data-sidebar-right-panel')).toBe('push') + await column.locator('[data-sidebar-right-guide-entry="files"]').click() await column.locator('[data-files-state="tree"]').waitFor({ timeout: 15_000 }) const workspaceDirectory = column.locator('[data-files-entry="directory"] > button').filter({ hasText: /^workspace$/ }) await workspaceDirectory.waitFor({ timeout: 15_000 }) diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index 8715b98d3d..0e1c570f2a 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -134,6 +134,7 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () const column = page.locator('[data-rightbar-col]') await page.locator('[data-sidebar-right-expand]').click() + await column.locator('[data-sidebar-right-guide-entry="files"]').click() await column.locator('[data-files-state="tree"]').waitFor({ state: 'visible' }) await column.locator('[data-files-reload]').click() const filesTab = column.locator('[data-dockkit-tab]').filter({ has: page.getByText('Files', { exact: true }) }) diff --git a/apps/web/tests/expected/sidebar-terminal/limit.expected.md b/apps/web/tests/expected/sidebar-terminal/limit.expected.md new file mode 100644 index 0000000000..845c392f43 --- /dev/null +++ b/apps/web/tests/expected/sidebar-terminal/limit.expected.md @@ -0,0 +1 @@ +- alert: "Terminal error: The terminal limit has been reached. Close unused terminals and try again. Exited terminals also count toward the limit." diff --git a/apps/web/tests/expected/sidebar-terminal/running.expected.md b/apps/web/tests/expected/sidebar-terminal/running.expected.md new file mode 100644 index 0000000000..91f8d58a99 --- /dev/null +++ b/apps/web/tests/expected/sidebar-terminal/running.expected.md @@ -0,0 +1 @@ +- textbox "Terminal" diff --git a/apps/web/tests/expected/sidebar-terminal/selection.expected.md b/apps/web/tests/expected/sidebar-terminal/selection.expected.md new file mode 100644 index 0000000000..21961c3c4d --- /dev/null +++ b/apps/web/tests/expected/sidebar-terminal/selection.expected.md @@ -0,0 +1,3 @@ +- text: Shell +- button "Shell": bash — /bin/bash +- button "Start terminal" diff --git a/apps/web/tests/expected/sidebar-terminal/shell-menu.expected.md b/apps/web/tests/expected/sidebar-terminal/shell-menu.expected.md new file mode 100644 index 0000000000..50be37e43b --- /dev/null +++ b/apps/web/tests/expected/sidebar-terminal/shell-menu.expected.md @@ -0,0 +1,5 @@ +- menu: + - menuitem "bash — /bin/bash": + - text: bash — /bin/bash + - img + - menuitem "sh — /bin/sh" diff --git a/apps/web/tests/fixtures/assembled-remote.fixture.json b/apps/web/tests/fixtures/assembled-remote.fixture.json new file mode 100644 index 0000000000..34d3112dfe --- /dev/null +++ b/apps/web/tests/fixtures/assembled-remote.fixture.json @@ -0,0 +1,5859 @@ +{ + "sessionList": { + "ok": true, + "value": { + "items": [ + { + "sessionId": "fx-alpha", + "updatedAt": 1789268875874, + "running": true, + "blank": false, + "cwd": "/tmp/fixture" + }, + { + "sessionId": "fx-beta", + "updatedAt": 1789268815874, + "running": false, + "blank": false, + "parentSessionId": "fx-alpha", + "cwd": "/tmp/fixture" + }, + { + "sessionId": "fx-gamma", + "updatedAt": 1789268755874, + "running": false, + "blank": false, + "cwd": "/tmp/fixture" + } + ] + } + }, + "settingsDescribe": { + "ok": true, + "value": { + "writable": true, + "hasDocument": true, + "namespaces": [ + { + "ns": "llm-deepseek", + "schema": {}, + "value": { + "apiKeyEnv": "DEEPSEEK_API_KEY" + }, + "applies": "live", + "secrets": [ + { + "path": [ + "apiKey" + ], + "set": false + } + ], + "revision": 0 + } + ] + } + }, + "credentialsDescribe": { + "ok": true, + "value": { + "DEEPSEEK_API_KEY": { + "configured": true, + "source": "file", + "writable": true + } + } + }, + "modelCatalog": { + "ok": true, + "value": { + "default": { + "provider": "deepseek-official", + "model": "deepseek-v4-flash" + }, + "routableProviders": [ + "deepseek-official", + "openai", + "acme-gateway" + ], + "groups": [ + { + "id": "deepseek-official", + "name": "DeepSeek", + "models": [ + { + "id": "deepseek-v4-flash", + "name": "DeepSeek-V4-Flash", + "description": "快速响应", + "reasoning": { + "efforts": [ + { + "id": "off", + "name": "Off" + }, + { + "id": "high", + "name": "High" + }, + { + "id": "max", + "name": "Max" + } + ], + "defaultEffort": "high" + } + }, + { + "id": "deepseek-v4-pro", + "name": "DeepSeek-V4-Pro", + "description": "复杂任务", + "reasoning": { + "efforts": [ + { + "id": "off", + "name": "Off" + }, + { + "id": "high", + "name": "High" + }, + { + "id": "max", + "name": "Max" + } + ], + "defaultEffort": "high" + } + } + ] + }, + { + "id": "openai", + "name": "OpenAI", + "models": [ + { + "id": "gpt-5", + "name": "GPT-5", + "reasoning": { + "efforts": [ + { + "id": "off", + "name": "Off" + }, + { + "id": "medium", + "name": "Medium" + }, + { + "id": "high", + "name": "High" + }, + { + "id": "max", + "name": "Max" + } + ], + "defaultEffort": "medium" + } + } + ] + } + ], + "failures": [] + } + }, + "agentPresets": { + "ok": true, + "value": { + "presets": [ + { + "id": "standard", + "trust": "system", + "isDefault": true + }, + { + "id": "minimal", + "trust": "system", + "isDefault": false + }, + { + "id": "my-agent", + "trust": "user", + "isDefault": false + } + ], + "authorable": true, + "modeSelectionEnabled": true + } + }, + "commands": { + "ok": true, + "value": [ + { + "name": "compact", + "description": "fixture:压缩当前会话上下文" + }, + { + "name": "echo", + "description": "fixture:回显参数", + "input": { + "hint": "text to echo" + } + }, + { + "definitionId": "@deepseek-ai/dsh-command-goal", + "name": "goal", + "description": "Set or view the goal for a long-running task", + "input": { + "hint": "", + "attachments": true + } + }, + { + "definitionId": "@deepseek-ai/dsh-permission-presets", + "name": "permission", + "description": "Switch the permission preset (sandbox mode + approval policy)", + "input": { + "hint": "" + } + }, + { + "definitionId": "@deepseek-ai/dsh-plan-mode", + "name": "plan", + "description": "Enter or leave plan mode", + "input": { + "hint": "[off|message]", + "attachments": true + } + } + ] + }, + "workspace": { + "type": "baseline", + "value": { + "items": [ + { + "workspaceId": "fx-ws-fixture", + "path": "/tmp/fixture", + "title": "fixture", + "sessionIds": [ + "fx-alpha", + "fx-beta", + "fx-gamma" + ], + "createdAt": "2026-09-13T03:02:55.884Z", + "updatedAt": "2026-09-13T03:02:55.884Z" + }, + { + "workspaceId": "fx-ws-home", + "path": "/home/fixture/Documents/project", + "title": "project", + "sessionIds": [], + "createdAt": "2026-09-13T03:02:55.884Z", + "updatedAt": "2026-09-13T03:02:55.884Z" + } + ], + "archivedSessionIds": [] + } + }, + "control": { + "type": "baseline", + "value": { + "queues": { + "fx-alpha": [], + "fx-beta": [], + "fx-gamma": [] + }, + "jobs": { + "fx-alpha": [], + "fx-beta": [], + "fx-gamma": [] + }, + "approvals": [], + "questions": [], + "projections": { + "fx-alpha": { + "asOfSeq": 552, + "values": { + "modelSelection": { + "lastUsed": null, + "next": null + }, + "title": "Fixture 历史会话", + "todos": [ + { + "content": "梳理需求", + "status": "completed" + }, + { + "content": "实现 fixture 样本", + "status": "in_progress" + }, + { + "content": "跑后台构建", + "status": "in_progress" + }, + { + "content": "浏览器验收", + "status": "pending" + } + ], + "permissions": { + "options": [ + { + "value": "workspace-write", + "name": "workspace-write", + "description": "Write inside the workspace and permitted temporary directories; wider retries require approval." + }, + { + "value": "danger-full-access", + "name": "danger-full-access", + "description": "Full file access without approval prompts." + } + ], + "currentValue": "workspace-write" + }, + "plan": { + "active": false, + "pending": false + }, + "goal": null, + "tokenUsage": { + "uncachedInputTokens": 1914, + "outputTokens": 708, + "cacheReadTokens": 6880, + "cacheWriteTokens": 32 + }, + "contextPressure": { + "pressureTokens": 104, + "contextWindow": 128000 + }, + "contextBreakdown": { + "systemTokens": 17, + "toolsTokens": 0, + "messageTokens": 4427 + }, + "sessionStats": { + "turns": 75, + "steps": 87, + "llmMs": 69600, + "toolMs": 24800, + "ttftMs": 0, + "ttftSteps": 0, + "decodeMs": 0, + "decodeTokens": 0 + }, + "imageLimits": { + "maxImageBytes": 5242880, + "maxImagesPerMessage": 20, + "maxMessageImageBytes": 104857600, + "maxImagePixels": 40000000, + "maxImageDimension": 2000, + "mediaTypes": [ + "image/png", + "image/jpeg", + "image/webp", + "image/gif" + ] + } + } + }, + "fx-beta": { + "asOfSeq": -1, + "values": { + "modelSelection": { + "lastUsed": null, + "next": null + }, + "todos": null, + "permissions": { + "options": [ + { + "value": "workspace-write", + "name": "workspace-write", + "description": "Write inside the workspace and permitted temporary directories; wider retries require approval." + }, + { + "value": "danger-full-access", + "name": "danger-full-access", + "description": "Full file access without approval prompts." + } + ], + "currentValue": "workspace-write" + }, + "plan": { + "active": false, + "pending": false + }, + "goal": null, + "tokenUsage": { + "uncachedInputTokens": 0, + "outputTokens": 0, + "cacheReadTokens": 0, + "cacheWriteTokens": 0 + }, + "contextPressure": {}, + "contextBreakdown": { + "systemTokens": 0, + "toolsTokens": 0, + "messageTokens": 0 + }, + "sessionStats": { + "turns": 0, + "steps": 0, + "llmMs": 0, + "toolMs": 0, + "ttftMs": 0, + "ttftSteps": 0, + "decodeMs": 0, + "decodeTokens": 0 + }, + "imageLimits": { + "maxImageBytes": 5242880, + "maxImagesPerMessage": 20, + "maxMessageImageBytes": 104857600, + "maxImagePixels": 40000000, + "maxImageDimension": 2000, + "mediaTypes": [ + "image/png", + "image/jpeg", + "image/webp", + "image/gif" + ] + } + } + }, + "fx-gamma": { + "asOfSeq": -1, + "values": { + "modelSelection": { + "lastUsed": null, + "next": null + }, + "todos": null, + "permissions": { + "options": [ + { + "value": "workspace-write", + "name": "workspace-write", + "description": "Write inside the workspace and permitted temporary directories; wider retries require approval." + }, + { + "value": "danger-full-access", + "name": "danger-full-access", + "description": "Full file access without approval prompts." + } + ], + "currentValue": "workspace-write" + }, + "plan": { + "active": false, + "pending": false + }, + "goal": null, + "tokenUsage": { + "uncachedInputTokens": 0, + "outputTokens": 0, + "cacheReadTokens": 0, + "cacheWriteTokens": 0 + }, + "contextPressure": {}, + "contextBreakdown": { + "systemTokens": 0, + "toolsTokens": 0, + "messageTokens": 0 + }, + "sessionStats": { + "turns": 0, + "steps": 0, + "llmMs": 0, + "toolMs": 0, + "ttftMs": 0, + "ttftSteps": 0, + "decodeMs": 0, + "decodeTokens": 0 + }, + "imageLimits": { + "maxImageBytes": 5242880, + "maxImagesPerMessage": 20, + "maxMessageImageBytes": 104857600, + "maxImagePixels": 40000000, + "maxImageDimension": 2000, + "mediaTypes": [ + "image/png", + "image/jpeg", + "image/webp", + "image/gif" + ] + } + } + } + } + } + }, + "remoteEvents": [ + { + "type": "ready", + "clientId": "36b204ff-b72f-4720-b58a-c86bebca8c42", + "host": { + "home": "/home/fixture" + } + }, + { + "type": "waterfall", + "event": "approval/request", + "eventId": "fx-interaction-approval", + "agentId": "fx-alpha", + "request": { + "toolName": "dangerous_tool", + "reason": "fixture 常驻审批(可答:批准/拒绝后消失)" + } + }, + { + "type": "waterfall", + "event": "user-questions/request", + "eventId": "fx-interaction-question", + "agentId": "fx-alpha", + "request": { + "questions": [ + { + "id": "harness-profile", + "header": "偏好", + "question": "你现在更想招哪类 Agent/Harness 候选人?", + "options": [ + { + "label": "工程落地型 (Recommended)", + "description": "更看重能直接做 runtime、tool executor、sandbox、trace 和线上问题排查。" + }, + { + "label": "研究潜力型", + "description": "更看重 Agent 理解、训练评测思路和长期成长空间。" + }, + { + "label": "均衡型", + "description": "同时要求工程能力和 Agent 认知,但可能筛选门槛更高。" + } + ] + }, + { + "id": "work-mode", + "header": "方式", + "question": "你希望候选人优先展示哪种工作方式?", + "options": [ + { + "label": "先做小型原型 (Recommended)", + "description": "用可运行结果尽快验证关键假设。" + }, + { + "label": "先写完整设计", + "description": "先收敛边界、协议和风险,再开始实现。" + } + ] + }, + { + "id": "signals", + "header": "信号", + "question": "哪些面试信号最重要?", + "detail": "按当前招聘目标选择;跳过则视为不设偏好。", + "multiSelect": true, + "options": [ + { + "label": "系统设计" + }, + { + "label": "代码质量" + }, + { + "label": "Agent 产品判断" + } + ] + } + ] + } + } + ], + "follow": { + "type": "snapshot", + "header": { + "version": 3, + "id": "fx-alpha", + "createdAt": 1789268875874, + "cwd": "/tmp/fixture", + "isSeeded": false + }, + "cursor": 552, + "records": [ + { + "type": "event", + "event": { + "seq": 365, + "time": 1789265568675, + "type": "turn/start", + "data": { + "turn": 51 + } + } + }, + { + "type": "event", + "event": { + "seq": 366, + "time": 1789265569475, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 51:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "781d1628-a401-4577-9608-fad94469115f" + } + } + }, + { + "type": "event", + "event": { + "seq": 367, + "time": 1789265570275, + "type": "step/start", + "data": { + "turn": 51, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 368, + "time": 1789265571075, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 51, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "回答 51:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "41db22fd-d174-446f-8d4f-dd0769071b92" + }, + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265571075, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265571075, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "回答 51:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265571075, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265571075, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 369, + "time": 1789265571875, + "type": "step/end", + "data": { + "turn": 51, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 370, + "time": 1789265572675, + "type": "turn/end", + "data": { + "turn": 51, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 371, + "time": 1789265573475, + "type": "turn/start", + "data": { + "turn": 52 + } + } + }, + { + "type": "event", + "event": { + "seq": 372, + "time": 1789265574275, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 52:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "ca2e9326-9086-477e-b80d-32005b85cbc6" + } + } + }, + { + "type": "event", + "event": { + "seq": 373, + "time": 1789265575075, + "type": "step/start", + "data": { + "turn": 52, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 374, + "time": 1789265575875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 52, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "reasoning", + "text": "思考过程 52:这是一段可折叠的 reasoning 内容。" + }, + { + "type": "text", + "text": "回答 52:这是 fixture 生成的历史回复正文。" + }, + { + "type": "tool-call", + "id": "fx-call-52", + "name": "echo", + "arguments": "{\"text\":\"turn 52\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "38c0ab81-0fc5-401d-a876-f0d1e766de03" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "reasoning" + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "reasoning", + "text": "思考过程 52:这是一段可折叠的 reasoning 内容。" + } + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-start", + "index": 1, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-end", + "index": 1, + "block": { + "type": "text", + "text": "回答 52:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-start", + "index": 2, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-end", + "index": 2, + "block": { + "type": "tool-call", + "id": "fx-call-52", + "name": "echo", + "arguments": "{\"text\":\"turn 52\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 375, + "time": 1789265576675, + "type": "tool/call", + "data": { + "turn": 52, + "step": 0, + "callId": "fx-call-52", + "name": "echo", + "arguments": "{\"text\":\"turn 52\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 376, + "time": 1789265577475, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 52, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-52" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-52", + "content": [ + { + "type": "text", + "text": "ECHO: TURN 52" + } + ], + "isError": false + } + ], + "role": "user", + "id": "e9bbfe6c-96f3-44b6-b65c-89997783be1e" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 377, + "time": 1789265578275, + "type": "step/end", + "data": { + "turn": 52, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 378, + "time": 1789265579075, + "type": "step/start", + "data": { + "turn": 52, + "step": 1 + } + } + }, + { + "type": "event", + "event": { + "seq": 379, + "time": 1789265579875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 52, + "step": 1, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "工具结果已消化(turn 52)。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "891ea3be-04ca-4adc-a1b5-70c311b5b0fd" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 9, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265579875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265579875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "工具结果已消化(turn 52)。" + } + } + }, + { + "type": "chunk", + "time": 1789265579875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 9, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265579875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 380, + "time": 1789265580675, + "type": "step/end", + "data": { + "turn": 52, + "step": 1 + } + } + }, + { + "type": "event", + "event": { + "seq": 381, + "time": 1789265581475, + "type": "turn/end", + "data": { + "turn": 52, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 382, + "time": 1789265582275, + "type": "turn/start", + "data": { + "turn": 53 + } + } + }, + { + "type": "event", + "event": { + "seq": 383, + "time": 1789265583075, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 53:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "c26142ac-056b-47cb-bf77-6e554e565484" + } + } + }, + { + "type": "event", + "event": { + "seq": 384, + "time": 1789265583875, + "type": "step/start", + "data": { + "turn": 53, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 385, + "time": 1789265584675, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 53, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "回答 53:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "cc41f0c0-2a74-4bc9-b5a2-f02f4c455569" + }, + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265584675, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265584675, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "回答 53:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265584675, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265584675, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 386, + "time": 1789265585475, + "type": "step/end", + "data": { + "turn": 53, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 387, + "time": 1789265586275, + "type": "turn/end", + "data": { + "turn": 53, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 388, + "time": 1789265587075, + "type": "turn/start", + "data": { + "turn": 54 + } + } + }, + { + "type": "event", + "event": { + "seq": 389, + "time": 1789265587875, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 54:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "eadaefc3-d83e-471e-ad64-8df6ae40c1eb" + } + } + }, + { + "type": "event", + "event": { + "seq": 390, + "time": 1789265588675, + "type": "step/start", + "data": { + "turn": 54, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 391, + "time": 1789265589475, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 54, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "回答 54:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "b8f5e883-ce1f-42ef-8106-dfa130620801" + }, + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265589475, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265589475, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "回答 54:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265589475, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265589475, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 392, + "time": 1789265590275, + "type": "step/end", + "data": { + "turn": 54, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 393, + "time": 1789265591075, + "type": "turn/end", + "data": { + "turn": 54, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 394, + "time": 1789265591875, + "type": "turn/start", + "data": { + "turn": 55 + } + } + }, + { + "type": "event", + "event": { + "seq": 395, + "time": 1789265592675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 55:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "116535cf-1160-4aff-bd8f-f89e4fc6c41f" + } + } + }, + { + "type": "event", + "event": { + "seq": 396, + "time": 1789265593475, + "type": "step/start", + "data": { + "turn": 55, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 397, + "time": 1789265594275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 55, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "reasoning", + "text": "思考过程 55:这是一段可折叠的 reasoning 内容。" + }, + { + "type": "text", + "text": "回答 55:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "6a80e9b4-ca99-40ef-8de8-facbc8c27b76" + }, + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "reasoning" + } + }, + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "reasoning", + "text": "思考过程 55:这是一段可折叠的 reasoning 内容。" + } + } + }, + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "block-start", + "index": 1, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "block-end", + "index": 1, + "block": { + "type": "text", + "text": "回答 55:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 398, + "time": 1789265595075, + "type": "step/end", + "data": { + "turn": 55, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 399, + "time": 1789265595875, + "type": "turn/end", + "data": { + "turn": 55, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 400, + "time": 1789265596675, + "type": "turn/start", + "data": { + "turn": 56 + } + } + }, + { + "type": "event", + "event": { + "seq": 401, + "time": 1789265597475, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 56:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "093791f8-cb5b-4106-853d-306ed532feec" + } + } + }, + { + "type": "event", + "event": { + "seq": 402, + "time": 1789265598275, + "type": "step/start", + "data": { + "turn": 56, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 403, + "time": 1789265599075, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 56, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "回答 56:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "bc0ebb05-d212-4f30-a3eb-c4c209ff7103" + }, + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265599075, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265599075, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "回答 56:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265599075, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265599075, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 404, + "time": 1789265599875, + "type": "step/end", + "data": { + "turn": 56, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 405, + "time": 1789265600675, + "type": "turn/end", + "data": { + "turn": 56, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 406, + "time": 1789265601475, + "type": "turn/start", + "data": { + "turn": 57 + } + } + }, + { + "type": "event", + "event": { + "seq": 407, + "time": 1789265602275, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 57:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "4196af12-5f06-4244-a83e-281b0e2e2565" + } + } + }, + { + "type": "event", + "event": { + "seq": 408, + "time": 1789265603075, + "type": "step/start", + "data": { + "turn": 57, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 409, + "time": 1789265603875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 57, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "回答 57:这是 fixture 生成的历史回复正文。" + }, + { + "type": "tool-call", + "id": "fx-call-57", + "name": "echo", + "arguments": "{\"text\":\"turn 57\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "dc114329-e3f4-43a4-bbd7-3524203a99e9" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "回答 57:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "block-start", + "index": 1, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "block-end", + "index": 1, + "block": { + "type": "tool-call", + "id": "fx-call-57", + "name": "echo", + "arguments": "{\"text\":\"turn 57\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 410, + "time": 1789265604675, + "type": "tool/call", + "data": { + "turn": 57, + "step": 0, + "callId": "fx-call-57", + "name": "echo", + "arguments": "{\"text\":\"turn 57\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 411, + "time": 1789265605475, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 57, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-57" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-57", + "content": [ + { + "type": "text", + "text": "ECHO: TURN 57" + } + ], + "isError": false + } + ], + "role": "user", + "id": "652931ce-6eb0-4400-8173-9735b82fdf8e" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 412, + "time": 1789265606275, + "type": "step/end", + "data": { + "turn": 57, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 413, + "time": 1789265607075, + "type": "step/start", + "data": { + "turn": 57, + "step": 1 + } + } + }, + { + "type": "event", + "event": { + "seq": 414, + "time": 1789265607875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 57, + "step": 1, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "工具结果已消化(turn 57)。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "02030b82-e0d9-4e8f-a17c-bfeaa78a74d0" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 9, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265607875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265607875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "工具结果已消化(turn 57)。" + } + } + }, + { + "type": "chunk", + "time": 1789265607875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 9, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265607875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 415, + "time": 1789265608675, + "type": "step/end", + "data": { + "turn": 57, + "step": 1 + } + } + }, + { + "type": "event", + "event": { + "seq": 416, + "time": 1789265609475, + "type": "turn/end", + "data": { + "turn": 57, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 417, + "time": 1789265610275, + "type": "turn/start", + "data": { + "turn": 58 + } + } + }, + { + "type": "event", + "event": { + "seq": 418, + "time": 1789265611075, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 58:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "d08478d2-30ac-49fa-b3ca-f71e8d4517ee" + } + } + }, + { + "type": "event", + "event": { + "seq": 419, + "time": 1789265611875, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "[fixture] 上下文注入(turn 58)" + } + ], + "source": { + "kind": "plugin", + "plugin": "fixture" + }, + "role": "user", + "id": "cbb94cfa-48f7-453b-9825-e61432572399" + } + } + }, + { + "type": "event", + "event": { + "seq": 420, + "time": 1789265612675, + "type": "step/start", + "data": { + "turn": 58, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 421, + "time": 1789265613475, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 58, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "reasoning", + "text": "思考过程 58:这是一段可折叠的 reasoning 内容。" + }, + { + "type": "text", + "text": "回答 58:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "271e5020-8deb-418e-a4c2-d6609f31aa60" + }, + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "reasoning" + } + }, + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "reasoning", + "text": "思考过程 58:这是一段可折叠的 reasoning 内容。" + } + } + }, + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "block-start", + "index": 1, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "block-end", + "index": 1, + "block": { + "type": "text", + "text": "回答 58:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 422, + "time": 1789265614275, + "type": "step/end", + "data": { + "turn": 58, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 423, + "time": 1789265615075, + "type": "turn/end", + "data": { + "turn": 58, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 424, + "time": 1789265615875, + "type": "turn/start", + "data": { + "turn": 59 + } + } + }, + { + "type": "event", + "event": { + "seq": 425, + "time": 1789265616675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "用户字面量:# 不渲染 `code` [link](https://example.com)" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "14861e74-05a9-4e2b-b98a-c907999daa42" + } + } + }, + { + "type": "event", + "event": { + "seq": 426, + "time": 1789265617475, + "type": "step/start", + "data": { + "turn": 59, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 427, + "time": 1789265618275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 59, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "# Markdown fixture\n\nAssistant output renders **strong text**, *emphasis*, and `inline code`.\n\n- first item\n - nested item\n\n| Area | State |\n| --- | --- |\n| history | rendered |\n| streaming | stable |\n\n[DeepSeek](https://www.deepseek.com)\n\n```ts\nconst markdown = true\n```" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "07a55bdb-ff08-40b3-8922-0422a2f04a87" + }, + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265618275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265618275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "# Markdown fixture\n\nAssistant output renders **strong text**, *emphasis*, and `inline code`.\n\n- first item\n - nested item\n\n| Area | State |\n| --- | --- |\n| history | rendered |\n| streaming | stable |\n\n[DeepSeek](https://www.deepseek.com)\n\n```ts\nconst markdown = true\n```" + } + } + }, + { + "type": "chunk", + "time": 1789265618275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265618275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 428, + "time": 1789265619075, + "type": "step/end", + "data": { + "turn": 59, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 429, + "time": 1789265619875, + "type": "turn/end", + "data": { + "turn": 59, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 430, + "time": 1789265620675, + "type": "turn/start", + "data": { + "turn": 60 + } + } + }, + { + "type": "event", + "event": { + "seq": 431, + "time": 1789265621475, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 60:bash 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "de206a9a-a621-435f-88f0-9613ff152eb5" + } + } + }, + { + "type": "event", + "event": { + "seq": 432, + "time": 1789265622275, + "type": "step/start", + "data": { + "turn": 60, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 433, + "time": 1789265623075, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 60, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-60", + "name": "bash", + "arguments": "{\"command\":\"ls -la\\necho done\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "4d338006-4348-4002-8a03-c730802f6f51" + }, + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 4 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265623075, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265623075, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-60", + "name": "bash", + "arguments": "{\"command\":\"ls -la\\necho done\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265623075, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 4 + } + } + }, + { + "type": "chunk", + "time": 1789265623075, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 434, + "time": 1789265623875, + "type": "tool/call", + "data": { + "turn": 60, + "step": 0, + "callId": "fx-call-60", + "name": "bash", + "arguments": "{\"command\":\"ls -la\\necho done\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 435, + "time": 1789265624675, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 60, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-60" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-60", + "content": [ + { + "type": "text", + "text": "total 2\ndrwxr-xr-x fixture\n-rw-r--r-- demo.txt" + } + ], + "isError": false + } + ], + "role": "user", + "id": "aa4c1898-7bec-4831-8ba1-0700f4ae897e" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 436, + "time": 1789265625475, + "type": "step/end", + "data": { + "turn": 60, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 437, + "time": 1789265626275, + "type": "turn/end", + "data": { + "turn": 60, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 438, + "time": 1789265627075, + "type": "turn/start", + "data": { + "turn": 61 + } + } + }, + { + "type": "event", + "event": { + "seq": 439, + "time": 1789265627875, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 61:write 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "6fd7a2c3-2ead-41a0-b954-21883b4bc03d" + } + } + }, + { + "type": "event", + "event": { + "seq": 440, + "time": 1789265628675, + "type": "step/start", + "data": { + "turn": 61, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 441, + "time": 1789265629475, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 61, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-61", + "name": "write", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"content\":\"hello fixture\\n\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "a3c1b7d4-200f-49b7-b550-bbc5ad98dbc9" + }, + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265629475, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265629475, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-61", + "name": "write", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"content\":\"hello fixture\\n\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265629475, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265629475, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 442, + "time": 1789265630275, + "type": "tool/call", + "data": { + "turn": 61, + "step": 0, + "callId": "fx-call-61", + "name": "write", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"content\":\"hello fixture\\n\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 443, + "time": 1789265631075, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 61, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-61" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-61", + "content": [ + { + "type": "text", + "text": "wrote notes/demo.txt" + } + ], + "isError": false + } + ], + "role": "user", + "id": "4eedd943-773b-43d3-a104-e13550015670" + }, + "meta": { + "diffs": [ + { + "path": "notes/demo.txt", + "oldText": null, + "newText": "hello fixture\n" + } + ] + } + } + } + }, + { + "type": "event", + "event": { + "seq": 444, + "time": 1789265631875, + "type": "step/end", + "data": { + "turn": 61, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 445, + "time": 1789265632675, + "type": "turn/end", + "data": { + "turn": 61, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 446, + "time": 1789265633475, + "type": "turn/start", + "data": { + "turn": 62 + } + } + }, + { + "type": "event", + "event": { + "seq": 447, + "time": 1789265634275, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 62:edit 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "8f4e6682-2963-4daa-a912-84ff053ac32e" + } + } + }, + { + "type": "event", + "event": { + "seq": 448, + "time": 1789265635075, + "type": "step/start", + "data": { + "turn": 62, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 449, + "time": 1789265635875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 62, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-62", + "name": "edit", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"old_string\":\"hello\",\"new_string\":\"hello fixture\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "f1b3b777-d4ec-4b33-80ac-fa4e9d87885a" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265635875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265635875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-62", + "name": "edit", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"old_string\":\"hello\",\"new_string\":\"hello fixture\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265635875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265635875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 450, + "time": 1789265636675, + "type": "tool/call", + "data": { + "turn": 62, + "step": 0, + "callId": "fx-call-62", + "name": "edit", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"old_string\":\"hello\",\"new_string\":\"hello fixture\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 451, + "time": 1789265637475, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 62, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-62" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-62", + "content": [ + { + "type": "text", + "text": "已编辑" + } + ], + "isError": false + } + ], + "role": "user", + "id": "37d72542-2cbc-4821-8897-329da97fe462" + }, + "meta": { + "diffs": [ + { + "path": "notes/demo.txt", + "oldText": "hello", + "newText": "hello fixture" + } + ] + } + } + } + }, + { + "type": "event", + "event": { + "seq": 452, + "time": 1789265638275, + "type": "step/end", + "data": { + "turn": 62, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 453, + "time": 1789265639075, + "type": "turn/end", + "data": { + "turn": 62, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 454, + "time": 1789265639875, + "type": "turn/start", + "data": { + "turn": 63 + } + } + }, + { + "type": "event", + "event": { + "seq": 455, + "time": 1789265640675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 63:write 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "8e09ed22-fb26-4b3a-8ef5-1e2ac363e0f8" + } + } + }, + { + "type": "event", + "event": { + "seq": 456, + "time": 1789265641475, + "type": "step/start", + "data": { + "turn": 63, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 457, + "time": 1789265642275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 63, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-63", + "name": "write", + "arguments": "{\"file_path\":\"notes/new-demo.txt\",\"content\":\"hello fixture\\n\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "65f7aa11-c436-4b13-8715-63437a194d29" + }, + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265642275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265642275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-63", + "name": "write", + "arguments": "{\"file_path\":\"notes/new-demo.txt\",\"content\":\"hello fixture\\n\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265642275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265642275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 458, + "time": 1789265643075, + "type": "tool/call", + "data": { + "turn": 63, + "step": 0, + "callId": "fx-call-63", + "name": "write", + "arguments": "{\"file_path\":\"notes/new-demo.txt\",\"content\":\"hello fixture\\n\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 459, + "time": 1789265643875, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 63, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-63" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-63", + "content": [ + { + "type": "text", + "text": "已写入" + } + ], + "isError": false + } + ], + "role": "user", + "id": "0d0c76cf-f5b2-4f77-97cb-b0b66a67d135" + }, + "meta": { + "diffs": [ + { + "path": "notes/new-demo.txt", + "oldText": null, + "newText": "hello fixture\n" + } + ] + } + } + } + }, + { + "type": "event", + "event": { + "seq": 460, + "time": 1789265644675, + "type": "step/end", + "data": { + "turn": 63, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 461, + "time": 1789265645475, + "type": "turn/end", + "data": { + "turn": 63, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 462, + "time": 1789265646275, + "type": "turn/start", + "data": { + "turn": 64 + } + } + }, + { + "type": "event", + "event": { + "seq": 463, + "time": 1789265647075, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 64:edit 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "99fcaad3-6e35-4e4f-a359-f97c673c72a2" + } + } + }, + { + "type": "event", + "event": { + "seq": 464, + "time": 1789265647875, + "type": "step/start", + "data": { + "turn": 64, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 465, + "time": 1789265648675, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 64, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-64", + "name": "edit", + "arguments": "{\"file_path\":\"src/config.ts\",\"old_string\":\"const timeout = 30\",\"new_string\":\"const timeout = 60\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "e0ae34f7-02f8-4093-a01b-e92d87fcbc3c" + }, + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265648675, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265648675, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-64", + "name": "edit", + "arguments": "{\"file_path\":\"src/config.ts\",\"old_string\":\"const timeout = 30\",\"new_string\":\"const timeout = 60\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265648675, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265648675, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 466, + "time": 1789265649475, + "type": "tool/call", + "data": { + "turn": 64, + "step": 0, + "callId": "fx-call-64", + "name": "edit", + "arguments": "{\"file_path\":\"src/config.ts\",\"old_string\":\"const timeout = 30\",\"new_string\":\"const timeout = 60\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 467, + "time": 1789265650275, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 64, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-64" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-64", + "content": [ + { + "type": "text", + "text": "已编辑" + } + ], + "isError": false + } + ], + "role": "user", + "id": "11a489c7-466c-45ba-a5de-228fecd455f2" + }, + "meta": { + "diffs": [ + { + "path": "src/config.ts", + "oldText": "const timeout = 30", + "newText": "const timeout = 60" + }, + { + "path": "src/config.ts", + "oldText": "retries: 1", + "newText": "retries: 3" + } + ] + } + } + } + }, + { + "type": "event", + "event": { + "seq": 468, + "time": 1789265651075, + "type": "step/end", + "data": { + "turn": 64, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 469, + "time": 1789265651875, + "type": "turn/end", + "data": { + "turn": 64, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 470, + "time": 1789265652675, + "type": "turn/start", + "data": { + "turn": 65 + } + } + }, + { + "type": "event", + "event": { + "seq": 471, + "time": 1789265653475, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 65:run_code 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "350becdf-2087-439b-be5e-cb19abf1dfff" + } + } + }, + { + "type": "event", + "event": { + "seq": 472, + "time": 1789265654275, + "type": "step/start", + "data": { + "turn": 65, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 473, + "time": 1789265655075, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 65, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-65", + "name": "run_code", + "arguments": "{\"code\":\"const listing = await tools.bash({ command: \\\"ls notes\\\", description: \\\"List notes\\\" })\\nconst demo = await tools.read({ file_path: \\\"notes/demo.txt\\\" })\\nawait tools.read({ file_path: \\\"notes/missing.txt\\\" }).catch(() => \\\"tolerated\\\")\\nreturn { listing, demo }\",\"description\":\"Read the notes files and summarize\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "4af251a1-bd9f-4e7a-a7e8-9d4ac6f0f8e5" + }, + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265655075, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265655075, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-65", + "name": "run_code", + "arguments": "{\"code\":\"const listing = await tools.bash({ command: \\\"ls notes\\\", description: \\\"List notes\\\" })\\nconst demo = await tools.read({ file_path: \\\"notes/demo.txt\\\" })\\nawait tools.read({ file_path: \\\"notes/missing.txt\\\" }).catch(() => \\\"tolerated\\\")\\nreturn { listing, demo }\",\"description\":\"Read the notes files and summarize\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265655075, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265655075, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 474, + "time": 1789265655875, + "type": "tool/call", + "data": { + "turn": 65, + "step": 0, + "callId": "fx-call-65", + "name": "run_code", + "arguments": "{\"code\":\"const listing = await tools.bash({ command: \\\"ls notes\\\", description: \\\"List notes\\\" })\\nconst demo = await tools.read({ file_path: \\\"notes/demo.txt\\\" })\\nawait tools.read({ file_path: \\\"notes/missing.txt\\\" }).catch(() => \\\"tolerated\\\")\\nreturn { listing, demo }\",\"description\":\"Read the notes files and summarize\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 475, + "time": 1789265656675, + "type": "tool/ptc-dispatch-start", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:1", + "name": "bash", + "arguments": { + "command": "ls notes", + "description": "List notes" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 476, + "time": 1789265657475, + "type": "tool/ptc-dispatch", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:1", + "name": "bash", + "arguments": { + "command": "ls notes", + "description": "List notes" + }, + "isError": false, + "content": [ + { + "type": "text", + "text": "demo.txt\nnew-demo.txt" + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 477, + "time": 1789265658275, + "type": "tool/ptc-dispatch-start", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:2", + "name": "read", + "arguments": { + "file_path": "notes/demo.txt" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 478, + "time": 1789265659075, + "type": "tool/ptc-dispatch", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:2", + "name": "read", + "arguments": { + "file_path": "notes/demo.txt" + }, + "isError": false, + "content": [ + { + "type": "text", + "text": "hello fixture\n" + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 479, + "time": 1789265659875, + "type": "tool/ptc-dispatch-start", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:3", + "name": "read", + "arguments": { + "file_path": "notes/missing.txt" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 480, + "time": 1789265660675, + "type": "tool/ptc-dispatch", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:3", + "name": "read", + "arguments": { + "file_path": "notes/missing.txt" + }, + "isError": true, + "content": [ + { + "type": "text", + "text": "Error: ENOENT: notes/missing.txt not found" + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 481, + "time": 1789265661475, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 65, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-65" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-65", + "content": [ + { + "type": "text", + "text": "{\"listing\":\"demo.txt\\nnew-demo.txt\",\"demo\":\"hello fixture\\n\"}" + } + ], + "isError": false + } + ], + "role": "user", + "id": "ca689e1a-ebfb-4465-b187-43c3490abb9b" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 482, + "time": 1789265662275, + "type": "step/end", + "data": { + "turn": 65, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 483, + "time": 1789265663075, + "type": "turn/end", + "data": { + "turn": 65, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 484, + "time": 1789265663875, + "type": "turn/start", + "data": { + "turn": 66 + } + } + }, + { + "type": "event", + "event": { + "seq": 485, + "time": 1789265664675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 66:bash 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "4a5bb1d5-3698-407a-bde1-5f8acd048eab" + } + } + }, + { + "type": "event", + "event": { + "seq": 486, + "time": 1789265665475, + "type": "step/start", + "data": { + "turn": 66, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 487, + "time": 1789265666275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 66, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-66", + "name": "bash", + "arguments": "{\"command\":\"pnpm run check\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture/deep/nested\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "68d3d448-1cac-44fd-8a6d-5446684ac98a" + }, + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265666275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265666275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-66", + "name": "bash", + "arguments": "{\"command\":\"pnpm run check\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture/deep/nested\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265666275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265666275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 488, + "time": 1789265667075, + "type": "tool/call", + "data": { + "turn": 66, + "step": 0, + "callId": "fx-call-66", + "name": "bash", + "arguments": "{\"command\":\"pnpm run check\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture/deep/nested\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 489, + "time": 1789265667875, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 66, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-66" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-66", + "content": [ + { + "type": "text", + "text": "\u001b[1mRunning 4 checks\u001b[0m\n\u001b[32m✓\u001b[0m typecheck 1.82s\n\u001b[32m✓\u001b[0m lint 0.94s\n\u001b[32m✓\u001b[0m duplication 2.10s\n\u001b[31m✗\u001b[0m unit 8.41s\n\n\u001b[90mpackages/client/ui-primitives/tests/terminal-block.client.spec.tsx\u001b[0m\n \u001b[31mFAIL\u001b[0m caps output at the configured line budget\n expected 16 lines, received 24\n\nNAME LINES BRANCHES FUNCTIONS UNCOVERED\nTerminalBlock.tsx 100% 100% 100% -\nansi.ts 100% 100% 100% -\nclipboard.ts 100% 100% 100% -\nCodeBlock.tsx 98.4% 96.2% 100% 41-43\nhighlight.ts 100% 100% 100% -\nPill.tsx 100% 100% 100% -\nStateDot.tsx 100% 100% 100% -\nmarkdown/Markdown.tsx 100% 100% 100% -\n\n\u001b[31m1 of 4 checks failed\u001b[0m\n[exit code: 1]" + } + ], + "isError": false + } + ], + "role": "user", + "id": "0da2afe6-5036-4d36-b9f3-ff562585507f" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 490, + "time": 1789265668675, + "type": "step/end", + "data": { + "turn": 66, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 491, + "time": 1789265669475, + "type": "turn/end", + "data": { + "turn": 66, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 492, + "time": 1789265670275, + "type": "turn/start", + "data": { + "turn": 67 + } + } + }, + { + "type": "event", + "event": { + "seq": 493, + "time": 1789265671075, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 67:grep 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "8dab4a18-bae1-492c-a43a-8337ae80abc3" + } + } + }, + { + "type": "event", + "event": { + "seq": 494, + "time": 1789265671875, + "type": "step/start", + "data": { + "turn": 67, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 495, + "time": 1789265672675, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 67, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-67", + "name": "grep", + "arguments": "{\"pattern\":\"SEARCH_MAX_LINES\",\"path\":\"packages/client\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "53469dfd-6af1-4eb2-9273-53ea475b6e94" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265672675, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265672675, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-67", + "name": "grep", + "arguments": "{\"pattern\":\"SEARCH_MAX_LINES\",\"path\":\"packages/client\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265672675, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265672675, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 496, + "time": 1789265673475, + "type": "tool/call", + "data": { + "turn": 67, + "step": 0, + "callId": "fx-call-67", + "name": "grep", + "arguments": "{\"pattern\":\"SEARCH_MAX_LINES\",\"path\":\"packages/client\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 497, + "time": 1789265674275, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 67, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-67" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-67", + "content": [ + { + "type": "text", + "text": "Found 9 of 42 matches\n\npackages/client/ui-primitives/src/SearchBlock.tsx\nLine 16: export const DEFAULT_SEARCH_MAX_LINES = 16\nLine 138: export function SearchBlock(props: SearchBlockProps) {\nLine 141: const [collapsed, setCollapsed] = useState>(() => new Set())\npackages/client/ui-tool/src/client/tool/models/search-card-model.ts\nLine 45: export const CHAT_SEARCH_MAX_LINES = 8\nLine 130: export function searchCardModel(block: ToolCallBlock): SearchCardModel | null {\npackages/client/ui-tool/src/client/tool/toolviews/search-row.tsx\nLine 34: export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) {\nLine 36: const search = searchCardModel(block)\nLine 56: search={search}\nLine 78: yield ctx.slots.register({ name: 'tool.call.toolview', key: 'grep', locale: NS }, SearchRow)\n\n(Full grep result stored at: fixture://spill/grep-66. Read it to see every match.)" + } + ], + "isError": false + } + ], + "role": "user", + "id": "9a3df388-356c-4440-b0bf-b09e0dda0387" + }, + "meta": { + "shape": "matches", + "files": [ + { + "path": "packages/client/ui-primitives/src/SearchBlock.tsx", + "matches": [ + { + "lineNumber": 16, + "line": "export const DEFAULT_SEARCH_MAX_LINES = 16" + }, + { + "lineNumber": 138, + "line": "export function SearchBlock(props: SearchBlockProps) {" + }, + { + "lineNumber": 141, + "line": " const [collapsed, setCollapsed] = useState>(() => new Set())" + } + ] + }, + { + "path": "packages/client/ui-tool/src/client/tool/models/search-card-model.ts", + "matches": [ + { + "lineNumber": 45, + "line": "export const CHAT_SEARCH_MAX_LINES = 8" + }, + { + "lineNumber": 130, + "line": "export function searchCardModel(block: ToolCallBlock): SearchCardModel | null {" + } + ] + }, + { + "path": "packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx", + "matches": [ + { + "lineNumber": 34, + "line": "export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) {" + }, + { + "lineNumber": 36, + "line": " const search = searchCardModel(block)" + }, + { + "lineNumber": 56, + "line": " search={search}" + }, + { + "lineNumber": 78, + "line": " yield ctx.slots.register({ name: 'tool.call.toolview', key: 'grep', locale: NS }, SearchRow)" + } + ] + } + ], + "truncated": true, + "total": 42 + } + } + } + }, + { + "type": "event", + "event": { + "seq": 498, + "time": 1789265675075, + "type": "step/end", + "data": { + "turn": 67, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 499, + "time": 1789265675875, + "type": "turn/end", + "data": { + "turn": 67, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 500, + "time": 1789265676675, + "type": "turn/start", + "data": { + "turn": 68 + } + } + }, + { + "type": "event", + "event": { + "seq": 501, + "time": 1789265677475, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 68:glob 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "728f42ae-723b-485f-bf63-11b28892a89d" + } + } + }, + { + "type": "event", + "event": { + "seq": 502, + "time": 1789265678275, + "type": "step/start", + "data": { + "turn": 68, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 503, + "time": 1789265679075, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 68, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-68", + "name": "glob", + "arguments": "{\"pattern\":\"**/SearchBlock*\",\"path\":\"packages/client\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "b6c6950d-d67b-45eb-a718-1a4fdb241e05" + }, + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265679075, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265679075, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-68", + "name": "glob", + "arguments": "{\"pattern\":\"**/SearchBlock*\",\"path\":\"packages/client\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265679075, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265679075, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 504, + "time": 1789265679875, + "type": "tool/call", + "data": { + "turn": 68, + "step": 0, + "callId": "fx-call-68", + "name": "glob", + "arguments": "{\"pattern\":\"**/SearchBlock*\",\"path\":\"packages/client\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 505, + "time": 1789265680675, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 68, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-68" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-68", + "content": [ + { + "type": "text", + "text": "packages/client/ui-primitives/src/SearchBlock.tsx\npackages/client/ui-primitives/src/SearchBlock.module.css\npackages/client/ui-tool/src/client/tool/models/search-card-model.ts\npackages/client/ui-tool/src/client/tool/toolviews/search-row.tsx\npackages/client/ui-tool/tests/search-card.client.spec.tsx\n\n(Showing 5 of 23 paths. Full sorted result stored at: fixture://spill/glob-67. Read it to see every path.)" + } + ], + "isError": false + } + ], + "role": "user", + "id": "e6d88943-d180-472b-9c1b-d59a21a50ab9" + }, + "meta": { + "shape": "paths", + "paths": [ + "packages/client/ui-primitives/src/SearchBlock.tsx", + "packages/client/ui-primitives/src/SearchBlock.module.css", + "packages/client/ui-tool/src/client/tool/models/search-card-model.ts", + "packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx", + "packages/client/ui-tool/tests/search-card.client.spec.tsx" + ], + "truncated": true, + "total": 23 + } + } + } + }, + { + "type": "event", + "event": { + "seq": 506, + "time": 1789265681475, + "type": "step/end", + "data": { + "turn": 68, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 507, + "time": 1789265682275, + "type": "turn/end", + "data": { + "turn": 68, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 508, + "time": 1789265683075, + "type": "turn/start", + "data": { + "turn": 69 + } + } + }, + { + "type": "event", + "event": { + "seq": 509, + "time": 1789265683875, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 69:read 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "f0564417-a09b-4e3a-8dd3-c0bd41604c23" + } + } + }, + { + "type": "event", + "event": { + "seq": 510, + "time": 1789265684675, + "type": "step/start", + "data": { + "turn": 69, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 511, + "time": 1789265685475, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 69, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-69", + "name": "read", + "arguments": "{\"file_path\":\"packages/client/ui-primitives/src/ReadBlock.tsx\",\"offset\":41}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "bfbff068-730d-4cf8-b647-6086e170221a" + }, + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265685475, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265685475, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-69", + "name": "read", + "arguments": "{\"file_path\":\"packages/client/ui-primitives/src/ReadBlock.tsx\",\"offset\":41}" + } + } + }, + { + "type": "chunk", + "time": 1789265685475, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265685475, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 512, + "time": 1789265686275, + "type": "tool/call", + "data": { + "turn": 69, + "step": 0, + "callId": "fx-call-69", + "name": "read", + "arguments": "{\"file_path\":\"packages/client/ui-primitives/src/ReadBlock.tsx\",\"offset\":41}" + } + } + }, + { + "type": "event", + "event": { + "seq": 513, + "time": 1789265687075, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 69, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-69" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-69", + "content": [ + { + "type": "text", + "text": "packages/client/ui-primitives/src/ReadBlock.tsx\nfile\n\n41: export interface ReadBlockProps {\n42: label?: string | undefined\n43: lines: readonly ReadBlockLine[]\n44: totalLines: number\n45: lang?: string | undefined\n46: maxLines?: number | undefined\n47: className?: string | undefined\n48: }\n49: \n50: // A windowed read keeps the file line numbers in the gutter.\n51: const marker = \"fixture read sample\"\n\n(Showing lines 41-51 of 180. Use offset=52 to continue.)\n" + } + ], + "isError": false + } + ], + "role": "user", + "id": "937c02dd-63d8-4106-9310-a9df2055f0e9" + }, + "meta": { + "path": "packages/client/ui-primitives/src/ReadBlock.tsx", + "offset": 41, + "lines": [ + { + "number": 41, + "text": "export interface ReadBlockProps {" + }, + { + "number": 42, + "text": " label?: string | undefined" + }, + { + "number": 43, + "text": " lines: readonly ReadBlockLine[]" + }, + { + "number": 44, + "text": " totalLines: number" + }, + { + "number": 45, + "text": " lang?: string | undefined" + }, + { + "number": 46, + "text": " maxLines?: number | undefined" + }, + { + "number": 47, + "text": " className?: string | undefined" + }, + { + "number": 48, + "text": "}" + }, + { + "number": 49, + "text": "" + }, + { + "number": 50, + "text": "// A windowed read keeps the file line numbers in the gutter." + }, + { + "number": 51, + "text": "const marker = \"fixture read sample\"" + } + ], + "totalLines": 180, + "lang": "ts" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 514, + "time": 1789265687875, + "type": "step/end", + "data": { + "turn": 69, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 515, + "time": 1789265688675, + "type": "turn/end", + "data": { + "turn": 69, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 516, + "time": 1789265689475, + "type": "turn/start", + "data": { + "turn": 70 + } + } + }, + { + "type": "event", + "event": { + "seq": 517, + "time": 1789265690275, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 70:web_search 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "c7ba3fc7-e437-4d77-b212-e766b1ca1c47" + } + } + }, + { + "type": "event", + "event": { + "seq": 518, + "time": 1789265691075, + "type": "step/start", + "data": { + "turn": 70, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 519, + "time": 1789265691875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 70, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-70", + "name": "web_search", + "arguments": "{\"queries\":[\"deepseek harness architecture\"]}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "70f9aaef-f006-4b53-986d-631458679ef6" + }, + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 4 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265691875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265691875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-70", + "name": "web_search", + "arguments": "{\"queries\":[\"deepseek harness architecture\"]}" + } + } + }, + { + "type": "chunk", + "time": 1789265691875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 4 + } + } + }, + { + "type": "chunk", + "time": 1789265691875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 520, + "time": 1789265692675, + "type": "tool/call", + "data": { + "turn": 70, + "step": 0, + "callId": "fx-call-70", + "name": "web_search", + "arguments": "{\"queries\":[\"deepseek harness architecture\"]}" + } + } + }, + { + "type": "event", + "event": { + "seq": 521, + "time": 1789265693475, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 70, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-70" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-70", + "content": [ + { + "type": "text", + "text": "Search results for deepseek harness architecture." + } + ], + "isError": false + } + ], + "role": "user", + "id": "c2ddb04d-74c9-4a2f-83f6-47a48fc6fbc2" + }, + "meta": { + "answer": "DeepSeek Harness is a plugin-based agent harness on vendored Cordis where **every capability is a plugin**.", + "sources": [ + { + "url": "https://github.com/deepseek-ai/deepseek-harness", + "title": "DeepSeek Harness — plugin-based agent harness", + "snippet": "Everything is a plugin: session, tools, agent-loop, and LLM adapters all mount on the same Cordis context.", + "publishedAt": "2026-07-01" + }, + { + "url": "https://www.deepseek.com/blog/harness-architecture", + "snippet": "The capability-seam pattern splits each capability into interface, implementation, and consumer packages." + }, + { + "url": "https://docs.deepseek.com/harness/plugins", + "title": "Writing a harness plugin", + "publishedAt": "2026-06-15" + } + ], + "truncated": true + } + } + } + }, + { + "type": "event", + "event": { + "seq": 522, + "time": 1789265694275, + "type": "step/end", + "data": { + "turn": 70, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 523, + "time": 1789265695075, + "type": "turn/end", + "data": { + "turn": 70, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 524, + "time": 1789265695875, + "type": "turn/start", + "data": { + "turn": 71 + } + } + }, + { + "type": "event", + "event": { + "seq": 525, + "time": 1789265696675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 71:web_fetch 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "b164bd50-e038-4ada-a92e-42b483efab9a" + } + } + }, + { + "type": "event", + "event": { + "seq": 526, + "time": 1789265697475, + "type": "step/start", + "data": { + "turn": 71, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 527, + "time": 1789265698275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 71, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-71", + "name": "web_fetch", + "arguments": "{\"url\":\"https://www.deepseek.com/blog/harness-architecture\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "72a35941-d5a4-44ec-9b4d-f14303cf165b" + }, + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265698275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265698275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-71", + "name": "web_fetch", + "arguments": "{\"url\":\"https://www.deepseek.com/blog/harness-architecture\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265698275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265698275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 528, + "time": 1789265699075, + "type": "tool/call", + "data": { + "turn": 71, + "step": 0, + "callId": "fx-call-71", + "name": "web_fetch", + "arguments": "{\"url\":\"https://www.deepseek.com/blog/harness-architecture\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 529, + "time": 1789265699875, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 71, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-71" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-71", + "content": [ + { + "type": "text", + "text": "# Harness architecture\n\nEverything is a plugin." + } + ], + "isError": false + } + ], + "role": "user", + "id": "57cf5cd3-3daf-4e2c-bb16-f8695bc2ab8c" + }, + "meta": { + "url": "https://www.deepseek.com/blog/harness-architecture", + "statusCode": 200, + "truncated": false + } + } + } + }, + { + "type": "event", + "event": { + "seq": 530, + "time": 1789265700675, + "type": "step/end", + "data": { + "turn": 71, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 531, + "time": 1789265701475, + "type": "turn/end", + "data": { + "turn": 71, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 532, + "time": 1789265702275, + "type": "turn/start", + "data": { + "turn": 72 + } + } + }, + { + "type": "event", + "event": { + "seq": 533, + "time": 1789265703075, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 72:请完整列出全部一百条条目。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "35a4608a-bd98-46a2-af6a-233b28470390" + } + } + }, + { + "type": "event", + "event": { + "seq": 534, + "time": 1789265703875, + "type": "step/start", + "data": { + "turn": 72, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 535, + "time": 1789265704675, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 72, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "条目 1:第一条。条目 2:第二条。条目 3:这一条写到一半被" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "006a595a-cd1e-4137-af5c-e36dc98f354e" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265704675, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265704675, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "条目 1:第一条。条目 2:第二条。条目 3:这一条写到一半被" + } + } + }, + { + "type": "chunk", + "time": 1789265704675, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265704675, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 536, + "time": 1789265705475, + "type": "step/end", + "data": { + "turn": 72, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 537, + "time": 1789265706275, + "type": "turn/end", + "data": { + "turn": 72, + "reason": { + "kind": "max-tokens" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 538, + "time": 1789265707075, + "type": "turn/start", + "data": { + "turn": 73 + } + } + }, + { + "type": "event", + "event": { + "seq": 539, + "time": 1789265707875, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "image", + "attachment": { + "attachmentId": "fixture:image", + "mediaType": "image/png", + "bytes": 247, + "width": 160, + "height": 90, + "name": "fixture-image.png" + } + }, + { + "type": "text", + "text": "历史用户图片" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "f7aa4d0e-5b76-4ccd-a566-150a227b0c27" + } + } + }, + { + "type": "event", + "event": { + "seq": 540, + "time": 1789265708675, + "type": "step/start", + "data": { + "turn": 73, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 541, + "time": 1789265709475, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 73, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "结构化模型图片:" + }, + { + "type": "image", + "attachment": { + "attachmentId": "fixture:image", + "mediaType": "image/png", + "bytes": 247, + "width": 160, + "height": 90, + "name": "fixture-image.png" + } + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-vision" + }, + "id": "35080466-4027-4eba-95b8-538f1447867b" + }, + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "结构化模型图片:" + } + } + }, + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "block-start", + "index": 1, + "blockType": "image" + } + }, + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "block-end", + "index": 1, + "block": { + "type": "image", + "attachment": { + "attachmentId": "fixture:image", + "mediaType": "image/png", + "bytes": 247, + "width": 160, + "height": 90, + "name": "fixture-image.png" + } + } + } + }, + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 542, + "time": 1789265710275, + "type": "step/end", + "data": { + "turn": 73, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 543, + "time": 1789265711075, + "type": "turn/end", + "data": { + "turn": 73, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 544, + "time": 1789265711875, + "type": "turn/start", + "data": { + "turn": 74 + } + } + }, + { + "type": "event", + "event": { + "seq": 545, + "time": 1789265712675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 74:todo_write 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "df61981f-bcff-4984-9d9e-de8754da7bc8" + } + } + }, + { + "type": "event", + "event": { + "seq": 546, + "time": 1789265713475, + "type": "step/start", + "data": { + "turn": 74, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 547, + "time": 1789265714275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 74, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-74", + "name": "todo_write", + "arguments": "{\"todos\":[{\"content\":\"梳理需求\",\"status\":\"completed\"},{\"content\":\"实现 fixture 样本\",\"status\":\"in_progress\"},{\"content\":\"跑后台构建\",\"status\":\"in_progress\"},{\"content\":\"浏览器验收\",\"status\":\"pending\"}]}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "c346330a-d740-45f6-9cbe-fb7b20d9f741" + }, + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265714275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265714275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-74", + "name": "todo_write", + "arguments": "{\"todos\":[{\"content\":\"梳理需求\",\"status\":\"completed\"},{\"content\":\"实现 fixture 样本\",\"status\":\"in_progress\"},{\"content\":\"跑后台构建\",\"status\":\"in_progress\"},{\"content\":\"浏览器验收\",\"status\":\"pending\"}]}" + } + } + }, + { + "type": "chunk", + "time": 1789265714275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265714275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 548, + "time": 1789265715075, + "type": "tool/call", + "data": { + "turn": 74, + "step": 0, + "callId": "fx-call-74", + "name": "todo_write", + "arguments": "{\"todos\":[{\"content\":\"梳理需求\",\"status\":\"completed\"},{\"content\":\"实现 fixture 样本\",\"status\":\"in_progress\"},{\"content\":\"跑后台构建\",\"status\":\"in_progress\"},{\"content\":\"浏览器验收\",\"status\":\"pending\"}]}" + } + } + }, + { + "type": "event", + "event": { + "type": "todo/write", + "time": 1789265715475, + "data": { + "todos": [ + { + "content": "梳理需求", + "status": "completed" + }, + { + "content": "实现 fixture 样本", + "status": "in_progress" + }, + { + "content": "跑后台构建", + "status": "in_progress" + }, + { + "content": "浏览器验收", + "status": "pending" + } + ] + }, + "seq": 549 + } + }, + { + "type": "event", + "event": { + "seq": 550, + "time": 1789265715875, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 74, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-74" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-74", + "content": [ + { + "type": "text", + "text": "Updated todo list: 1 pending, 2 in progress, 1 completed." + } + ], + "isError": false + } + ], + "role": "user", + "id": "499968fd-a83f-45ad-80fe-b1d73651ff13" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 551, + "time": 1789265716675, + "type": "step/end", + "data": { + "turn": 74, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 552, + "time": 1789265717475, + "type": "turn/end", + "data": { + "turn": 74, + "reason": { + "kind": "completed" + } + } + } + } + ], + "hasMore": true, + "projections": { + "asOfSeq": 552, + "values": { + "modelSelection": { + "lastUsed": null, + "next": null + }, + "title": "Fixture 历史会话", + "todos": [ + { + "content": "梳理需求", + "status": "completed" + }, + { + "content": "实现 fixture 样本", + "status": "in_progress" + }, + { + "content": "跑后台构建", + "status": "in_progress" + }, + { + "content": "浏览器验收", + "status": "pending" + } + ], + "permissions": { + "options": [ + { + "value": "workspace-write", + "name": "workspace-write", + "description": "Write inside the workspace and permitted temporary directories; wider retries require approval." + }, + { + "value": "danger-full-access", + "name": "danger-full-access", + "description": "Full file access without approval prompts." + } + ], + "currentValue": "workspace-write" + }, + "plan": { + "active": false, + "pending": false + }, + "goal": null, + "tokenUsage": { + "uncachedInputTokens": 1914, + "outputTokens": 708, + "cacheReadTokens": 6880, + "cacheWriteTokens": 32 + }, + "contextPressure": { + "pressureTokens": 104, + "contextWindow": 128000 + }, + "contextBreakdown": { + "systemTokens": 17, + "toolsTokens": 0, + "messageTokens": 4427 + }, + "sessionStats": { + "turns": 75, + "steps": 87, + "llmMs": 69600, + "toolMs": 24800, + "ttftMs": 0, + "ttftSteps": 0, + "decodeMs": 0, + "decodeTokens": 0 + }, + "imageLimits": { + "maxImageBytes": 5242880, + "maxImagesPerMessage": 20, + "maxMessageImageBytes": 104857600, + "maxImagePixels": 40000000, + "maxImageDimension": 2000, + "mediaTypes": [ + "image/png", + "image/jpeg", + "image/webp", + "image/gif" + ] + } + } + }, + "assistantStream": { + "revision": 0 + } + }, + "attachment": { + "ok": true, + "value": { + "attachment": { + "attachmentId": "fixture:image", + "mediaType": "image/png", + "bytes": 247, + "width": 160, + "height": 90, + "name": "fixture-image.png" + }, + "data": "iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAYAAAA/xl1SAAAAvklEQVR42u3SMQ0AAAjAMIyhELM4AAe8PD1qYFlk9cCXEAEDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGxIBCYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIAYEAyIAcGAGBAMiAHBgBgQDIgBwYAYEAyIAcGAGBAMiAHBgBgQDIgB4bYWLb6pnOb1xAAAAABJRU5ErkJggg==" + } + } +} diff --git a/apps/web/tests/fixtures/sidebar-terminal.patch.yml b/apps/web/tests/fixtures/sidebar-terminal.patch.yml new file mode 100644 index 0000000000..8b96197c79 --- /dev/null +++ b/apps/web/tests/fixtures/sidebar-terminal.patch.yml @@ -0,0 +1,8 @@ +- id: terminal-controller + config: + maxTerminals: 2 + shellCandidates: [/bin/bash, /bin/sh] + shell: + path: /bin/bash + name: bash + args: [--noprofile, --norc, -i] diff --git a/apps/web/tests/goal-bar.e2e.ts b/apps/web/tests/goal-bar.e2e.ts index e1d77f0cb6..177a16d8a4 100644 --- a/apps/web/tests/goal-bar.e2e.ts +++ b/apps/web/tests/goal-bar.e2e.ts @@ -1,18 +1,19 @@ -// Keyless assembled-browser coverage for the goal bar over the shipped Web -// bundles and the fixture Connection RPC. The command creates a real projected -// goal in the fixture session; the golden pins the active strip, while the -// clear gesture proves the acknowledged tombstone leaves neither stale chrome -// nor a duplicate-mutation error. +// Keyless browser coverage for the goal bar over the shipped Web composition. +// The command creates a real projected goal in a real Host session. The +// goldens pin the active and disarmed strips, while the clear gesture proves +// the acknowledged tombstone leaves neither stale chrome nor a +// duplicate-mutation error. import { fileURLToPath } from 'node:url' import { join } from 'node:path' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import type {} from '@deepseek-ai/dsh-goal' import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { newEnglishPage, saveFailureShot } from './support.ts' +import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/goal-bar', import.meta.url)) const ACTIVE_EXPECTED = join(SNAPSHOT_DIR, 'active.expected.md') @@ -27,14 +28,13 @@ describe('web e2e: goal bar clear convergence', () => { let tripwire: ReturnType beforeAll(async () => { - scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, welcomeNoticePending: true }) + scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY }) browser = await chromium.launch() page = await newEnglishPage(browser) tripwire = watchConsole(page) - const login = await page.context().request.get(scaffold.authenticatedUrl, { maxRedirects: 0 }) - expect(login.status()).toBe(303) - await page.goto(`${scaffold.baseUrl}?fixture`, { waitUntil: 'load' }) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + await connectFreshWorkspace(page, scaffold.workspaceCwd) }, 120_000) afterAll(async () => { @@ -44,8 +44,6 @@ describe('web e2e: goal bar clear convergence', () => { it('renders one active goal and clears it without exposing a stale error', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-bar-clear')) - // Startup reuses the fixture workspace's blank session, keeping this - // command independent of alpha's running replay and pending question. const input = page.locator('[data-composer-input][data-placeholder="Describe what you want to build, / commands, @ files or sessions"]') await input.waitFor({ timeout: 10_000 }) await input.fill('/goal guard rapid clear clicks') @@ -53,15 +51,16 @@ describe('web e2e: goal bar clear convergence', () => { const bar = page.locator('[data-goal-bar]') await bar.waitFor({ timeout: 10_000 }) - await expect.poll(() => bar.getByRole('button', { name: 'Pause goal' }).count(), { + const pause = bar.getByRole('button', { name: 'Pause goal' }) + await expect.poll(() => pause.count(), { timeout: 10_000, }).toBe(1) const snapshot = await captureStableAria(page, '[data-goal-bar]', scaffold.workspaceCwd) await compareOrRefreshGolden(ACTIVE_EXPECTED, snapshot, MODE) - await page.evaluate(() => { - (globalThis as unknown as { __fxTiming?: { disarmOnlyGoal(): void } }).__fxTiming?.disarmOnlyGoal() - }) + const agents = scaffold.ctx.agents.list() + expect(agents).toHaveLength(1) + scaffold.ctx.goals.disarm(agents[0]!) await expect.poll(() => bar.getByRole('button', { name: 'Resume goal' }).count(), { timeout: 10_000, }).toBe(1) diff --git a/apps/web/tests/goal-bar.overlay.yml b/apps/web/tests/goal-bar.overlay.yml index 4e9e382bb8..7e7fb66cf5 100644 --- a/apps/web/tests/goal-bar.overlay.yml +++ b/apps/web/tests/goal-bar.overlay.yml @@ -1,5 +1,4 @@ -# The client-side FixtureApiClient intentionally rejects settings traffic, so -# this goal-only scenario omits the settings shell and the onboarding steps it -# would mount. Onboarding owns separate assembled-browser coverage. -- id: ui-settings-general +# This scenario owns Goal bar actions, not autonomous Goal rounds. Keeping the +# driver out prevents an active Goal from issuing an unrelated model request. +- id: goal-round-driver disabled: true diff --git a/apps/web/tests/image-display.expected.e2e.ts b/apps/web/tests/image-display.expected.e2e.ts index e35486abfe..adbb742d0b 100644 --- a/apps/web/tests/image-display.expected.e2e.ts +++ b/apps/web/tests/image-display.expected.e2e.ts @@ -1,6 +1,6 @@ // @vitest-environment jsdom // Multimodal image surfaces over the BUILT client graph (the ptc-fixture -// idiom: real bundles via AppWebEntry, keyless fixture Connection RPC). +// idiom: real bundles via AppWebEntry, keyless RemoteMock transport). // Opens the fixture history session whose turn 73 carries an image in BOTH a // user message and an assistant message, and pins the product surfaces: the // history ImageGallery loading real fixture bytes through the authorized @@ -169,7 +169,7 @@ it('accepts a whole-page drop under the limits-labeled overlay and refuses an ov // A file drag anywhere over the page raises the full-viewport overlay whose // desc line carries the projected limits — copy that can only render after - // the imageLimits projection crossed the real fixture transport. + // the imageLimits projection crossed the RemoteMock transport. const image = new File([new Uint8Array([137, 80, 78, 71])], 'dropped.png', { type: 'image/png' }) const dataTransfer = { types: ['Files'], files: [image], dropEffect: 'none' } fireEvent.dragEnter(document.body, { dataTransfer }) @@ -207,7 +207,7 @@ it('accepts a whole-page drop under the limits-labeled overlay and refuses an ov }) it('renders a host dimension rejection with the projected 2000px limit', async () => { - mountAssembledApp('?fixture&fixturePrompt=reject') + mountAssembledApp({ remote: { rejectPrompt: true } }) const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 }) const start = tree.querySelector('button[aria-label="New session in fixture"]') diff --git a/apps/web/tests/lifecycle-chrome.e2e.ts b/apps/web/tests/lifecycle-chrome.e2e.ts index 7ad725a827..4af796b3ef 100644 --- a/apps/web/tests/lifecycle-chrome.e2e.ts +++ b/apps/web/tests/lifecycle-chrome.e2e.ts @@ -290,7 +290,12 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', () try { await input.press('Enter') if (MODE !== 'record') { - const liveTail = page.locator('[data-variant="think"][data-state="running"] [data-follow-end]') + const thinking = page.locator('[data-variant="think"][data-state="running"]') + const disclosure = thinking.getByRole('button') + await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('true') + await disclosure.click() + await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('false') + const liveTail = thinking.locator('[data-follow-end]') await expect.poll(async () => { if (await liveTail.count() !== 1) return false return await liveTail.evaluate((element) => { diff --git a/apps/web/tests/markdown-wide-table.e2e.ts b/apps/web/tests/markdown-wide-table.e2e.ts index 09f980ab5c..45d889c6d4 100644 --- a/apps/web/tests/markdown-wide-table.e2e.ts +++ b/apps/web/tests/markdown-wide-table.e2e.ts @@ -56,6 +56,7 @@ const TAIL_MARKER = 'MWT_TABLES_DONE' const FILL_MARKER = 'MWT_FILL_C1' const WIDE_MARKER = 'MWT_WIDE_C01' const LONG_CELL_MARKER = 'MWT_LONGCELL_F1' +const SHORT_MARKER = 'MWT_SHORT_C1' const MARKERS = [FILL_MARKER, WIDE_MARKER, LONG_CELL_MARKER] /** Golden-facing names, in {@link MARKERS} order. */ const TABLE_NAMES = ['fill', 'wide', 'long-cell'] @@ -76,13 +77,20 @@ const SENTENCE = 'This cell carries one full sentence so the unwrapped table is const LONG_TOKEN = 'workspace/deepseek-harness/packages/client/ui-primitives/src/markdown/render.tsx/'.repeat(3) const CJK_SENTENCE = '这个单元格包含一段较长的中文说明,用来验证长内容在窄列宽下按最小可读宽度换行而不是把列压缩到无法阅读。' -/** The assistant markdown: one 3-column fill, one 12-column wide, one long-cell table. */ +/** The assistant markdown includes fitting and overflowing wide tables. */ function tablesMarkdown(): string { const wideHeader = [WIDE_MARKER, ...Array.from({ length: 11 }, (_, i) => `C${String(i + 2).padStart(2, '0')}`)] const wideRow = (row: number): string[] => Array.from({ length: 12 }, (_, i) => `v${String(row)}${String(i + 1).padStart(2, '0')}`) return [ - 'Three markdown tables exercise the wide-table layout rules.', + 'Markdown tables exercise the wide-table layout rules.', + '', + `| ${SHORT_MARKER} | C2 | C3 | C4 |`, + '| --- | --- | --- | --- |', + '| 1 | 2 | 3 | 4 |', + '| 5 | 6 | 7 | 8 |', + '', + 'The paragraph after the short table stays in place.', '', `| ${FILL_MARKER} | Current approach | Proposed approach |`, '| --- | --- | --- |', @@ -103,7 +111,7 @@ function tablesMarkdown(): string { ].join('\n') } -/** Build one closed, invariant-checked session fixture carrying the three tables. */ +/** Build one closed, invariant-checked session fixture carrying the tables. */ function wideTableFixture(): string { const session = Session.create(SessionId('markdown-wide-table-source')) const eventTimeOrigin = new Date().setHours(12, 0, 0, 0) @@ -257,7 +265,8 @@ describe('web e2e: markdown tables fill the column, wide ones break out and scro beforeAll(async () => { scaffold = await launchWebScaffold({}) await seedSession(scaffold, wideTableFixture(), SEED_ID) - browser = await chromium.launch() + // The geometry assertions include the space occupied by native scrollbars. + browser = await chromium.launch({ ignoreDefaultArgs: ['--hide-scrollbars'] }) page = await newEnglishPage(browser) tripwire = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) @@ -386,13 +395,42 @@ describe('web e2e: markdown tables fill the column, wide ones break out and scro // Resting hidden overflow keeps the scroll position reachable and intact. expect(await wide.evaluate(element => element.scrollLeft)).toBeGreaterThanOrEqual(0) await wide.hover() - await expect.poll(overflowState, { timeout: 5_000 }).toBe('auto 0px') + await expect.poll(overflowState, { timeout: 5_000 }).toBe('scroll 0px') // Pointer leaves: the bar rests hidden again. await page.mouse.move(4, 4) await expect.poll(overflowState, { timeout: 5_000 }).toBe('hidden 8px') expect(tripwire.pageErrors).toEqual([]) }, 120_000) + it('keeps a fitting wide table and its following paragraph stationary during interaction', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-short-table-height')) + await settleAt(1680) + const short = page.locator('[class*="tableScroll"]', { hasText: SHORT_MARKER }) + await short.evaluate((element) => { element.scrollIntoView({ block: 'center', behavior: 'instant' }) }) + await page.mouse.move(4, 4) + await short.evaluate((element) => { element.blur() }) + expect(await short.evaluate(element => element.classList.contains('md-table-wide'))).toBe(true) + expect(await short.evaluate(element => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1) + const position = () => short.evaluate((element) => { + const following = element.nextElementSibling + if (following === null) throw new Error('short table has no following paragraph') + return { + height: element.getBoundingClientRect().height, + followingTop: following.getBoundingClientRect().top, + } + }) + const resting = await position() + await short.hover() + await expect.poll(position).toEqual(resting) + await page.mouse.move(4, 4) + await short.focus() + expect(await short.evaluate(element => document.activeElement === element)).toBe(true) + await expect.poll(position).toEqual(resting) + await short.evaluate((element) => { element.blur() }) + await expect.poll(position).toEqual(resting) + expect(tripwire.pageErrors).toEqual([]) + }, 120_000) + it('keeps the fill/scroll relations under page zoom', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-wide-table-zoom')) await sweep() diff --git a/apps/web/tests/max-tokens-notice.expected.e2e.ts b/apps/web/tests/max-tokens-notice.expected.e2e.ts index 5634315a32..38d23ee815 100644 --- a/apps/web/tests/max-tokens-notice.expected.e2e.ts +++ b/apps/web/tests/max-tokens-notice.expected.e2e.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom // Assembled max-tokens snapshot: boots the real built `packages/client/*/lib/ // client.js` bundles through AppWebEntry's ModuleLoader path against the -// keyless fixture Connection RPC, opens the fixture session, and pins the +// keyless RemoteMock transport, opens the fixture session, and pins the // surface its max-tokens turn (72) reaches — the turn-end notice row that a // provider output-cap truncation must render instead of ending silently. // diff --git a/apps/web/tests/navigation-panes.e2e.ts b/apps/web/tests/navigation-panes.e2e.ts index a76b88bfb2..22d35431e1 100644 --- a/apps/web/tests/navigation-panes.e2e.ts +++ b/apps/web/tests/navigation-panes.e2e.ts @@ -24,6 +24,7 @@ import { expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './supp const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/navigation-panes', import.meta.url)) const SEED = join(SNAPSHOT_DIR, 'session.v3.jsonl') const TRAJECTORY_EXPECTED = join(SNAPSHOT_DIR, 'trajectory.expected.md') +const TIMING_EXPECTED = join(SNAPSHOT_DIR, 'timing.expected.md') const SEARCH_EXPECTED = join(SNAPSHOT_DIR, 'search-results.expected.md') const TERMINAL_EXPECTED = join(SNAPSHOT_DIR, 'terminal-card.expected.md') const MODE = webSnapshotMode() @@ -97,7 +98,8 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { const raw = await readFile(SEED, 'utf8') expect(fixtureUserPrompts(raw), 'seed fixture must carry exactly the two drive prompts') .toEqual([PROMPT_TURN1, PROMPT_TURN2]) - await seedSession(scaffold, raw, SEED_ID) + // The inspector's calendar date must not depend on the day the test runs. + await seedSession(scaffold, raw, SEED_ID, undefined, { createdAt: Date.UTC(2026, 0, 1) }) } browser = await chromium.launch() }, 120_000) @@ -264,13 +266,30 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { await page.evaluate(() => { document.body.removeAttribute('data-ds-dark-theme') }) await page.getByRole('tab', { name: 'Result' }).click() await expect.poll(() => page.getByText('NAVIGATION_OK', { exact: false }).count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(1) - expect(await page.locator('[data-timeline-span="message"][data-assistant-timing="true"]').count()).toBe(0) + expect(await page.locator('[data-timeline-span="message"][data-assistant-timing="true"]').count()).toBeGreaterThan(0) const snapshot = (await captureStableAria(page, '[class*="viewArea"]', scaffold.workspaceCwd)) .split(SEED_ID).join('{{seededId}}') await compareOrRefreshGolden(TRAJECTORY_EXPECTED, snapshot, MODE) await details.getByRole('button', { name: 'Close details' }).click() }, 60_000) + it.skipIf(MODE === 'record')('restores Assistant timing from recorded history', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-timing')) + await ensureSeedOpen(page) + await page.getByRole('tab', { name: 'Trajectory' }).click() + await page.getByRole('button', { name: 'Request #1', exact: true }).click() + const details = page.getByRole('complementary', { name: 'Event details' }) + await details.getByRole('tab', { name: 'Timing', exact: true }).click() + const panel = details.getByRole('tabpanel', { name: 'Timing' }) + for (const metric of ['TTFT', 'Generation', 'Throughput']) { + const value = panel.getByText(metric, { exact: true }).locator('..').locator('dd') + await expect.poll(() => value.textContent()).toMatch(/^\d/) + } + expect(await panel.getByText('First token unavailable', { exact: true }).count()).toBe(0) + const snapshot = await captureStableAria(page, '#trajectory-detail-panel', scaffold.workspaceCwd) + await compareOrRefreshGolden(TIMING_EXPECTED, snapshot, MODE) + }, 60_000) + it.skipIf(MODE === 'record')('downloads through the Session Header and /export with one dialog', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-export')) await ensureSeedOpen(page) @@ -504,7 +523,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { it.skipIf(MODE === 'record')('keeps the recorded fixture inventory exact', async () => { await assertFixtureInventory(SNAPSHOT_DIR, [ 'session.v3.jsonl', 'search-results.expected.md', 'trajectory.expected.md', - 'terminal-card.expected.md', + 'terminal-card.expected.md', 'timing.expected.md', ]) }) }) diff --git a/apps/web/tests/ptc-round.e2e.ts b/apps/web/tests/ptc-round.e2e.ts index 045c475bb6..54bcc461c9 100644 --- a/apps/web/tests/ptc-round.e2e.ts +++ b/apps/web/tests/ptc-round.e2e.ts @@ -8,13 +8,15 @@ import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import type { SessionEvent } from '@deepseek-ai/dsh-session' import { - acknowledgeReloadConnectionLoss, captureExpandedTurnProcessAria, compareOrRefreshGolden, fixtureUserPrompts, + acknowledgeReloadConnectionLoss, captureExpandedTurnProcessAria, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' import { connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts' const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/ptc-round/session.v3.jsonl', import.meta.url)) const UI_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/ptc-round/ui.expected.md', import.meta.url)) +const TRAJECTORY_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/ptc-round/trajectory.expected.md', import.meta.url)) +const CODE_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/ptc-round/code.expected.md', import.meta.url)) const MODE = webSnapshotMode() // Elicits the successful and failed sub-rows this scenario asserts. @@ -151,6 +153,47 @@ describe('web e2e: PTC mode round renders nested sub-calls', () => { await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE) }) + it.skipIf(MODE === 'record')('inspects recorded PTC source, wrapping, and original JSON in the trajectory', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-ptc-inspector')) + const call = sessionEvents.find(event => event.type === 'tool/call' && event.data.name === 'run_code') + if (call?.type !== 'tool/call') throw new Error('recorded PTC call missing') + const args = JSON.parse(call.data.arguments) as { code: string; description: string } + await page.getByRole('tab', { name: 'Trajectory', exact: true }).click() + const row = page.locator('tr[data-kind="tool"]').filter({ hasText: 'run_code' }).first() + await row.click() + await page.getByRole('tab', { name: 'Code', exact: true }).waitFor() + expect(await page.getByRole('tabpanel').textContent()).toContain(args.description) + expect(await page.getByRole('tabpanel').locator('dl').first().locator('dt').allTextContents()) + .toEqual(['Hierarchy', 'Status']) + const overview = await Promise.all([1, 2].map(index => captureStableAria( + page, `[role="tabpanel"] [class*="overviewSections"] > section:nth-child(${index})`, scaffold.workspaceCwd, + ))) + await compareOrRefreshGolden(TRAJECTORY_EXPECTED, overview.join('\n'), MODE) + + await page.getByRole('button', { name: 'Code', exact: true }).click() + const source = page.locator('[data-line-numbers] pre') + await source.waitFor() + expect(await source.textContent()).toBe(args.code.endsWith('\n') ? args.code.slice(0, -1) : args.code) + const wrap = page.getByRole('button', { name: 'Wrap lines', exact: true }) + expect(await wrap.getAttribute('aria-pressed')).toBe('false') + const content = page.locator('[data-wrap]').filter({ has: source }) + expect(await content.evaluate(element => element.scrollWidth > element.clientWidth)).toBe(true) + await wrap.click() + expect(await wrap.getAttribute('aria-pressed')).toBe('true') + await expect.poll(() => content.evaluate(element => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1) + const code = await captureStableAria(page, '[role="tabpanel"]', scaffold.workspaceCwd) + await compareOrRefreshGolden(CODE_EXPECTED, code, MODE) + + await page.getByRole('button', { name: 'Original JSON' }).click() + const json = page.getByRole('tree', { name: 'parameters JSON' }) + await json.waitFor() + expect(await json.textContent()).toContain(args.description) + await page.getByRole('button', { name: 'Original JSON' }).click() + expect(await source.textContent()).toBe(args.code.endsWith('\n') ? args.code.slice(0, -1) : args.code) + await page.getByRole('tab', { name: 'Summary', exact: true }).click() + expect(await wrap.getAttribute('aria-pressed')).toBe('true') + }) + it.skipIf(MODE === 'record')('stayed clean: no page errors, no reconnect churn', () => { expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) diff --git a/apps/web/tests/replay-round-trip.e2e.ts b/apps/web/tests/replay-round-trip.e2e.ts index db60b35753..fce61a7bb3 100644 --- a/apps/web/tests/replay-round-trip.e2e.ts +++ b/apps/web/tests/replay-round-trip.e2e.ts @@ -213,7 +213,7 @@ describe('web e2e: fresh round trip through the real assembly', () => { it.skipIf(MODE === 'record')('expands and collapses the reasoning fold from its click target', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-round-trip-think')) // Interaction over the REAL wire-delivered transcript (the fixture-client - // tier pins the same gesture against the fixture Connection RPC; this one runs on + // tier pins the same gesture against RemoteMock; this one runs on // follow-stream-fed state). Runs after the golden capture so the committed // aria surface stays the untouched settled state. await expandTurnProcesses(page) diff --git a/apps/web/tests/search-card.expected.e2e.ts b/apps/web/tests/search-card.expected.e2e.ts index b54d14e8e7..181a6448cb 100644 --- a/apps/web/tests/search-card.expected.e2e.ts +++ b/apps/web/tests/search-card.expected.e2e.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom // Assembled search-card snapshot: boots the real built workspace client bundles // through AppWebEntry's ModuleLoader path against the keyless -// fixture Connection RPC (no API key, no model round), opens the fixture +// RemoteMock transport (no API key, no model round), opens the fixture // session, and pins the search card the `grep` turn (fixture turn 67) renders in // the assembled application. The built-boot smoke proves the graph boots but // intentionally carries no behavior assertions; this is the assembled-output check diff --git a/apps/web/tests/sidebar-right.e2e.ts b/apps/web/tests/sidebar-right.e2e.ts index d1387af303..3ec5f5d309 100644 --- a/apps/web/tests/sidebar-right.e2e.ts +++ b/apps/web/tests/sidebar-right.e2e.ts @@ -132,6 +132,7 @@ async function resetSidebar(page: Page): Promise { const column = page.locator('[data-rightbar-col]') await expandOf(page).waitFor({ timeout: 15_000 }) await ensureExpanded(page, column) + await column.locator('[data-sidebar-right-guide-entry="files"]').click() await expect.poll(async () => await tabTitles(column)).toEqual(['Files']) await width(column) return column @@ -390,6 +391,10 @@ describe('web e2e: shipped right Sidebar', () => { expect(await centreY(selector), selector).toBe(textLine) } + await expect.poll(async () => await tabTitles(column)).toEqual(['Start']) + await expect.poll(async () => await column.locator('[data-sidebar-right-guide-entry]').count()).toBe(2) + await column.locator('[data-sidebar-right-guide-entry="files"]').click() + // A manual guide is closable beside Files and suppresses another add // control in its pane until it is closed. const addTab = column.locator('[data-dockkit-add-tab]') @@ -761,10 +766,13 @@ describe('web e2e: shipped right Sidebar', () => { ) await expect.poll(async () => await panes.count()).toBe(2) - const splitFiles = panes.nth(1).locator('[data-dockkit-tab]').filter({ hasText: 'Files' }) - expect(await splitFiles.locator('[data-dockkit-tab-close]').count()).toBe(1) - await dragTo(page, splitFiles, await pointIn(panes.first(), 0.5, 0.5)) + const splitGuide = panes.nth(1).locator('[data-dockkit-tab]').filter({ hasText: 'Start' }) + expect(await splitGuide.locator('[data-dockkit-tab-close]').count()).toBe(1) + await dragTo(page, splitGuide, await pointIn(panes.first(), 0.5, 0.5)) await expect.poll(async () => await tabTitles(panes.nth(1))).toEqual([SAMPLE_NAME]) + const movedGuide = panes.first().locator('[data-dockkit-tab]').filter({ hasText: 'Start' }) + await movedGuide.hover() + await movedGuide.locator('[data-dockkit-tab-close]').click() // Neither pane holds a guide, so both offer an add control. const filePane = panes.filter({ has: page.locator('[data-dockkit-tab-title]', { hasText: SAMPLE_NAME }) }) @@ -901,7 +909,7 @@ describe('web e2e: shipped right Sidebar', () => { expect(await panes.count()).toBe(2) expect(await splitButtons.count()).toBe(0) - // 5. A manual guide and the document float while Files stays docked. + // 5. The split's guide and the document float while Files stays docked. const floatOne = panes.last().locator('[data-dockkit-tab]').filter({ hasText: SAMPLE_NAME }) await floatByDrag(page, floatOne) await expect.poll(async () => await floats.count()).toBe(1) @@ -910,8 +918,7 @@ describe('web e2e: shipped right Sidebar', () => { await dragElement(page, floats.first().locator('[data-dockkit-float-grip]'), { x: box.x + 140, y: box.y + 90 }) await expect.poll(async () => (await floats.first().boundingBox())?.x ?? box.x).not.toBe(box.x) - await panes.last().locator('[data-dockkit-add-tab]').click() - await expect.poll(async () => await tabTitles(panes.last())).toEqual(['Files', 'Start']) + await expect.poll(async () => await tabTitles(panes.last())).toEqual(['Start']) const second = panes.last().locator('[data-dockkit-tab]').filter({ hasText: 'Start' }) await floatByDrag(page, second) await expect.poll(async () => await floats.count()).toBe(2) @@ -966,7 +973,7 @@ describe('web e2e: shipped right Sidebar', () => { // Any other tab standing alone closes together with the column. Open the // sample file, close the guide (an ordinary close with two tabs), then // close the file: the column collapses in the same gesture, and the - // settle rule reseeds the current default, so reopening shows Files. + // settle rule reseeds the current default, so reopening shows Start. await page.getByRole('button', { name: `Open ${SAMPLE_NAME}` }).click() await expect.poll(async () => await tabTitles(column)).toEqual(['Start', SAMPLE_NAME]) await column.locator('[data-dockkit-tab]').first().hover() @@ -976,8 +983,8 @@ describe('web e2e: shipped right Sidebar', () => { await column.locator('[data-dockkit-tab-close]').first().click() await expect.poll(async () => await column.locator('[data-sidebar-right-open]').count()).toBe(0) await expandOf(page).click() - await expect.poll(async () => await tabTitles(column)).toEqual(['Files']) - expect(await column.locator('[data-files-state="tree"]').count()).toBe(1) + await expect.poll(async () => await tabTitles(column)).toEqual(['Start']) + expect(await column.locator('[data-sidebar-right-guide]').count()).toBe(1) expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) @@ -1003,6 +1010,7 @@ describe('web e2e: shipped right Sidebar', () => { await ensureExpanded(page, column) await expect.poll(async () => await column.locator('[data-dockkit-tab]').count()).toBeGreaterThan(0) + await column.locator('[data-sidebar-right-guide-entry="files"]').click() await column.locator('[data-dockkit-add-tab]').click() await expect.poll(async () => await tabTitles(column)).toEqual(['Files', 'Start']) // No "more" control on the chip: the chip carries its close, and the menu @@ -1050,6 +1058,8 @@ describe('web e2e: shipped right Sidebar', () => { const column = zhPage.locator('[data-rightbar-col]') await expandOf(zhPage).waitFor({ timeout: 20_000 }) await expandOf(zhPage).click() + await expect.poll(async () => await tabTitles(column)).toEqual(['开始']) + await column.locator('[data-sidebar-right-guide-entry="files"]').click() await expect.poll(async () => await tabTitles(column)).toEqual(['文件']) await column.locator('[data-dockkit-add-tab]').click() diff --git a/apps/web/tests/sidebar-terminal.e2e.ts b/apps/web/tests/sidebar-terminal.e2e.ts new file mode 100644 index 0000000000..ba1ee4ca94 --- /dev/null +++ b/apps/web/tests/sidebar-terminal.e2e.ts @@ -0,0 +1,227 @@ +/** Shipped sidebar terminal over the real Loader, Remote mux, Chromium and local PTY. */ +import { mkdir } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { chromium, type Browser, type Page } from 'playwright' +import { afterEach, beforeEach, describe, expect, it, onTestFailed, vi } from 'vitest' +import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import type {} from '@deepseek-ai/dsh-api-terminal-controller' +import type { SubprocessTerminalHandle } from '@deepseek-ai/dsh-subprocess' +import { createProcessInspector, type ProcessIdentity } from '@deepseek-ai/dsh-subprocess-local/src/process-inspector.ts' +import { compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts' +import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' + +const expected = fileURLToPath(new URL('./expected/sidebar-terminal/running.expected.md', import.meta.url)) +const shots = fileURLToPath(new URL('../../../.artifacts/screenshots/sidebar-terminal/', import.meta.url)) + +async function openTerminal(page: Page, waitForShell = true): Promise { + const expand = page.locator('[data-sidebar-right-expand]') + if (await expand.isVisible()) await expand.click() + const entry = page.locator('[data-sidebar-right-guide-entry="terminal"]') + if (!await entry.isVisible()) await page.locator('[data-dockkit-add-tab]').click() + await entry.click() + await page.getByRole('button', { name: 'Start terminal', exact: true }).click() + if (waitForShell) await expect.poll(async () => await page.locator('.xterm-rows:visible').innerText()).toContain('bash-') +} + +async function command(page: Page, text: string): Promise { + await page.locator('.xterm-helper-textarea:visible').click() + await page.keyboard.insertText(text) + await page.keyboard.press('Enter') +} + +describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + let handles: SubprocessTerminalHandle[] + const inspector = createProcessInspector() + const alive = (identity: ProcessIdentity) => inspector.isAlive(identity) + const processIdentity = (index: number): ProcessIdentity => { + const pid = handles[index]!.pid + const identity = inspector.snapshot().tree(pid).find(member => member.pid === pid) + if (identity === undefined) throw new Error(`Terminal process ${pid} is missing`) + return identity + } + + beforeEach(async () => { + scaffold = await launchWebScaffold({ extraOverlayPath: fileURLToPath(new URL('./fixtures/sidebar-terminal.patch.yml', import.meta.url)) }) + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + await connectFreshWorkspace(page, scaffold.workspaceCwd) + const agent = scaffold.ctx.agents.list()[0] + if (agent === undefined) throw new Error('Workspace did not create a Session') + handles = [] + const subprocess = agent.ctx.get('subprocess') + if (subprocess === undefined) throw new Error('Session subprocess provider is missing') + const spawn = subprocess.spawnTerminal.bind(subprocess) + vi.spyOn(subprocess, 'spawnTerminal').mockImplementation(async (spec) => { + const handle = await spawn(spec) + handles.push(handle) + return handle + }) + agent.session.append('turn/start', { turn: 1 }) + agent.session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Open a terminal.' }], source: { kind: 'user' } }), { surfaceOp: 'append' }) + agent.session.append('step/start', { turn: 1, step: 1 }) + agent.session.append('assistant/message', { stream: [], turn: 1, step: 1, message: createMessage({ role: 'assistant', content: [{ type: 'text', text: 'Ready for terminal input.' }], source: { kind: 'model', provider: 'fixture', model: 'fixture' } }) }, { surfaceOp: 'append' }) + agent.session.append('step/end', { turn: 1, step: 1 }) + agent.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + await scaffold.ctx.sessions.flush(agent.session) + await page.getByText('Ready for terminal input.').waitFor() + await mkdir(shots, { recursive: true }) + }, 180_000) + + afterEach(async () => { + try { await browser?.close() } finally { + try { await scaffold?.close() } finally { vi.restoreAllMocks() } + } + }) + + it('completes commands, preserves the process through collapse and reload, resizes, and kills on tab close', async () => { + onTestFailed(() => saveFailureShot(page, 'sidebar-terminal')) + await openTerminal(page) + const terminal = page.locator('[data-sidebar-terminal]') + await command(page, "PS1=''; printf '\\033cTERMINAL_READY\\n'") + const screen = page.locator('.xterm-rows:visible') + await expect.poll(async () => await screen.innerText()).toContain('TERMINAL_READY') + const aria = await terminal.ariaSnapshot() + await compareOrRefreshGolden(expected, aria, webSnapshotMode()) + await command(page, "printf 'DSH_PID:%s\\n' \"$$\"") + await expect.poll(async () => await screen.innerText()).toMatch(/DSH_PID:\d+/u) + const pid = Number((await screen.innerText()).match(/DSH_PID:(\d+)/u)?.[1]) + const firstProcess = processIdentity(0) + expect(alive(firstProcess)).toBe(true) + await command(page, 'dsh_terminal_completion_probe(){ printf "completed_from_shell\\n"; }') + await page.keyboard.press('Control+l') + await page.keyboard.insertText('dsh_terminal_completion_pro') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await expect.poll(async () => await screen.innerText()).toContain('completed_from_shell') + await command(page, "printf 'PERSIST:%s\\n' \"$TERM\"") + await expect.poll(async () => await screen.innerText()).toContain('PERSIST:xterm-256color') + await page.locator('[data-dockkit-tab-title]').getByText('bash', { exact: true }).dblclick() + await page.getByRole('textbox', { name: 'Terminal name', exact: true }).fill('Development') + await page.getByRole('textbox', { name: 'Terminal name', exact: true }).press('Enter') + await expect.poll(async () => await page.locator('[data-dockkit-tab-title]').allInnerTexts()).toContain('Development') + await openTerminal(page) + await command(page, "printf 'SECOND_PID:%s\\n' \"$$\"") + await expect.poll(async () => await screen.innerText()).toMatch(/SECOND_PID:\d+/u) + const secondPid = Number((await screen.innerText()).match(/SECOND_PID:(\d+)/u)?.[1]) + // Shell PIDs belong to the sandbox namespace; process liveness uses Host identities. + const secondProcess = processIdentity(1) + expect(secondProcess.pid).not.toBe(firstProcess.pid) + await page.locator('[data-dockkit-tab]').filter({ hasText: 'Development' }).click() + await expect.poll(async () => await screen.innerText()).toContain('PERSIST:xterm-256color') + expect(alive(secondProcess)).toBe(true) + await page.getByRole('button', { name: 'Collapse right sidebar', exact: true }).click() + expect(alive(firstProcess)).toBe(true) + await page.locator('[data-sidebar-right-expand]').click() + const terminals = () => scaffold.ctx.terminalController.list(scaffold.ctx.agents.list()[0]!.id) + const dockedCols = terminals()[0]!.cols + await page.getByRole('button', { name: 'Fullscreen', exact: true }).click() + await expect.poll(() => terminals()[0]!.cols).toBeGreaterThan(dockedCols) + await command(page, "printf 'SIZE:'; stty size") + await expect.poll(async () => await screen.innerText()).toContain(`SIZE:${terminals()[0]!.rows} ${terminals()[0]!.cols}`) + await page.screenshot({ path: `${shots}/fullscreen.png`, fullPage: true }) + await page.reload({ waitUntil: 'load' }) + await page.locator('[data-dockkit-tab]').filter({ hasText: 'Development' }).waitFor({ timeout: 15_000 }) + await expect.poll(async () => await page.locator('[data-dockkit-tab-title]').allInnerTexts()).toEqual(['Development', 'bash']) + expect(terminals()).toHaveLength(2) + expect(alive(firstProcess)).toBe(true) + expect(alive(secondProcess)).toBe(true) + await expect.poll(async () => await screen.innerText()).toContain(`SECOND_PID:${secondPid}`) + const secondTab = page.locator('[data-dockkit-tab]').filter({ hasText: 'bash' }) + await secondTab.hover() + await secondTab.locator('[data-dockkit-tab-close]').click() + await expect.poll(async () => await secondTab.count()).toBe(0) + await expect.poll(() => alive(secondProcess), { timeout: 10_000 }).toBe(false) + await expect.poll(async () => await screen.innerText()).toContain('PERSIST:xterm-256color') + await command(page, "printf 'RECOVERED_PID:%s\\n' \"$$\"") + await expect.poll(async () => await screen.innerText()).toContain(`RECOVERED_PID:${pid}`) + await page.screenshot({ path: `${shots}/recovered.png`, fullPage: true }) + const previousMembers = new Set(inspector.snapshot().tree(firstProcess.pid).map(member => member.pid)) + await command(page, "sleep 120 & printf 'CHILD_PID:%s\\n' $!") + await expect.poll(async () => await screen.innerText()).toMatch(/CHILD_PID:\d+/u) + const descendants = inspector.snapshot().tree(firstProcess.pid).filter(member => member.pid !== firstProcess.pid) + expect(descendants.some(member => !previousMembers.has(member.pid))).toBe(true) + expect(descendants.every(alive)).toBe(true) + const tab = page.locator('[data-dockkit-tab]').filter({ hasText: 'Development' }) + await tab.hover() + await tab.locator('[data-dockkit-tab-close]').click() + await expect.poll(() => alive(firstProcess), { timeout: 10_000 }).toBe(false) + await expect.poll(() => descendants.some(alive), { timeout: 10_000 }).toBe(false) + await expect.poll(() => scaffold.ctx.terminalController.list(scaffold.ctx.agents.list()[0]!.id).length).toBe(0) + expect(tripwire.pageErrors).toEqual([]) + }) + + it('offers installed shells, remembers the choice after reload, and restores without a picker', async () => { + onTestFailed(() => saveFailureShot(page, 'sidebar-terminal-shell-choice')) + await page.locator('[data-sidebar-right-expand]').click() + const entry = page.locator('[data-sidebar-right-guide-entry="terminal"]') + expect(await entry.innerText()).toBe('New terminal\nRun commands in the Session workspace') + await page.locator('[data-sidebar-right-guide]').screenshot({ path: `${shots}/terminal-guide.png`, animations: 'disabled' }) + await entry.click() + const selector = page.getByRole('button', { name: 'Shell', exact: true }) + await selector.waitFor() + expect(await selector.innerText()).toContain('bash — /bin/bash') + expect(handles).toHaveLength(0) + await compareOrRefreshGolden(fileURLToPath(new URL('./expected/sidebar-terminal/selection.expected.md', import.meta.url)), + await page.locator('[data-sidebar-terminal]').ariaSnapshot(), webSnapshotMode()) + await selector.click() + await compareOrRefreshGolden(fileURLToPath(new URL('./expected/sidebar-terminal/shell-menu.expected.md', import.meta.url)), + await page.getByRole('menu').ariaSnapshot(), webSnapshotMode()) + await page.screenshot({ path: `${shots}/shell-menu.png`, fullPage: true }) + await page.getByRole('menuitem', { name: 'sh — /bin/sh', exact: true }).click() + expect(await page.evaluate(() => localStorage.getItem('dsh.terminal.shell'))).toBe('/bin/sh') + await page.screenshot({ path: `${shots}/shell-choice.png`, fullPage: true }) + await page.getByRole('button', { name: 'Start terminal', exact: true }).click() + await command(page, "printf 'CHOSEN_SHELL:%s\\n' \"$0\"") + const screen = page.locator('.xterm-rows:visible') + await expect.poll(() => screen.innerText()).toContain('CHOSEN_SHELL:/bin/sh') + expect(await page.evaluate(() => localStorage.getItem('dsh.terminal.shell'))).toBe('/bin/sh') + const retained = processIdentity(0) + await page.reload({ waitUntil: 'load' }) + await page.locator('.xterm-helper-textarea:visible').waitFor() + expect(await selector.count()).toBe(0) + expect(alive(retained)).toBe(true) + await page.locator('[data-dockkit-add-tab]').click() + await page.locator('[data-sidebar-right-guide-entry="terminal"]').click() + await selector.waitFor() + expect(await selector.innerText()).toContain('sh — /bin/sh') + expect(handles).toHaveLength(1) + await page.getByRole('button', { name: 'Start terminal', exact: true }).click() + await expect.poll(() => handles.length).toBe(2) + expect(scaffold.ctx.terminalController.list(scaffold.ctx.agents.list()[0]!.id).map(info => info.shell.path)).toEqual(['/bin/sh', '/bin/sh']) + expect(tripwire.pageErrors).toEqual([]) + }) + + it('explains that exited terminals count toward the quota and permits creation after closing one', async () => { + onTestFailed(() => saveFailureShot(page, 'sidebar-terminal-quota')) + const terminals = () => scaffold.ctx.terminalController.list(scaffold.ctx.agents.list()[0]!.id) + for (let count = 1; count <= 2; count++) { + await openTerminal(page) + await command(page, 'exit') + await expect.poll(() => terminals().filter(info => info.state === 'exited').length).toBe(count) + } + await openTerminal(page, false) + const alert = page.getByRole('alert') + await expect.poll(async () => await alert.innerText()).toContain('Exited terminals also count toward the limit.') + const expectedLimit = fileURLToPath(new URL('./expected/sidebar-terminal/limit.expected.md', import.meta.url)) + await compareOrRefreshGolden(expectedLimit, await alert.ariaSnapshot(), webSnapshotMode()) + const failed = page.locator('[data-dockkit-tab][aria-selected="true"]') + await failed.hover() + await failed.locator('[data-dockkit-tab-close]').click() + const exited = page.locator('[data-dockkit-tab]').filter({ hasText: 'bash' }).first() + await exited.hover() + await exited.locator('[data-dockkit-tab-close]').click() + await expect.poll(() => terminals().length).toBe(1) + await openTerminal(page) + await expect.poll(() => terminals().filter(info => info.state === 'running').length).toBe(1) + expect(tripwire.pageErrors).toEqual([]) + }) + +}) diff --git a/apps/web/tests/submission-echo.e2e.ts b/apps/web/tests/submission-echo.e2e.ts index 7693f3dc87..239c2c86e0 100644 --- a/apps/web/tests/submission-echo.e2e.ts +++ b/apps/web/tests/submission-echo.e2e.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -// Local submission echo over the BUILT client graph (keyless fixture Connection RPC +// Local submission echo over the BUILT client graph (keyless RemoteMock // transport): a text-plus-image send paints its echo bubble synchronously on // the submit keystroke — before serialization, transport, or the fixture's // durable admission — with the composer already cleared and editable, and the diff --git a/apps/web/tests/todo-row.expected.e2e.ts b/apps/web/tests/todo-row.expected.e2e.ts index 21fa70d9bf..8bc45f4dfb 100644 --- a/apps/web/tests/todo-row.expected.e2e.ts +++ b/apps/web/tests/todo-row.expected.e2e.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom // Assembled todo snapshot: boots the real built `packages/client/*/lib/ // client.js` bundles through AppWebEntry's ModuleLoader path against the -// keyless fixture Connection RPC, opens the fixture session, and pins the +// keyless RemoteMock transport, opens the fixture session, and pins the // two surfaces the fixture's parallel plan (turn 74, two items `in_progress`) // reaches — the `todo_write` tool row and the dock's plan strip. // diff --git a/apps/web/tests/trajectory-image-display.expected.e2e.ts b/apps/web/tests/trajectory-image-display.expected.e2e.ts index 315d3baa1e..855e798e55 100644 --- a/apps/web/tests/trajectory-image-display.expected.e2e.ts +++ b/apps/web/tests/trajectory-image-display.expected.e2e.ts @@ -1,6 +1,6 @@ // @vitest-environment jsdom // Trajectory image surfaces over the BUILT client graph (the ptc-fixture -// idiom: real bundles via AppWebEntry, keyless fixture Connection RPC). +// idiom: real bundles via AppWebEntry, keyless RemoteMock transport). // Opens the fixture history session whose turn 73 carries an image in BOTH a // user message and an assistant message, and pins the Trajectory surfaces: // selecting the ledger record renders the shared ui-attachment gallery from diff --git a/apps/web/tests/turn-tail-actions.e2e.ts b/apps/web/tests/turn-tail-actions.e2e.ts index 0a176a9af0..fdc1ef26f5 100644 --- a/apps/web/tests/turn-tail-actions.e2e.ts +++ b/apps/web/tests/turn-tail-actions.e2e.ts @@ -201,8 +201,8 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => { await timeTrigger.click() const timeDialog = page.getByRole('dialog', { name: 'Turn time and speed' }) expect(await timeDialog.count()).toBe(1) - expect(await timeDialog.getByText(/tok\/s/).count()).toBe(0) - expect(await timeDialog.getByText('Time to first token (TTFT)', { exact: true }).count()).toBe(0) + expect(await timeDialog.getByText(/tok\/s/).count()).toBeGreaterThan(0) + expect(await timeDialog.getByText('Time to first token (TTFT)', { exact: true }).count()).toBe(1) await page.keyboard.press('Escape') await trigger.click() diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 606d227b8f..2c1a38c447 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -101,6 +101,7 @@ "tests/feedback-release.e2e.ts", "tests/agent-team-panel.e2e.ts", "tests/sidebar-right.e2e.ts", + "tests/sidebar-terminal.e2e.ts", "tests/startup-auto-selection.e2e.ts", "tests/produced-files.e2e.ts", "tests/produced-file-mentions.e2e.ts", @@ -135,6 +136,12 @@ }, { "path": "../../packages/client/modules" + }, + { + "path": "../../packages/test-support/client-runtime" + }, + { + "path": "../../packages/test-support/remote-mock" } ] } diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 799699d836..c3956d0da9 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/architecture.md -architecture.md: 4c8b78554cd744838a26da9bde39a405b275b739 -architecture.zh.md: 0eea56b18ae79b297178dfb831ce78e5aed66699 +architecture.md: 3addf70ab997590d97a2581ea0a459e6d4e86911 +architecture.zh.md: 078d99dc06cf49233d0ca4c036bab89a3f664d57 diff --git a/docs/architecture.md b/docs/architecture.md index 4c8b78554c..3addf70ab9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -106,7 +106,7 @@ turn/end Input reaches the driver through one inbox. Some messages wake it immediately; injected context waits in the inbox until another message does. -`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. The prepared call capability governs prompt admission, not the preceding `request/context`. Every attempt synchronously reconciles the same rendered assembly, appends users only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes can append non-empty updates after the cached prefix; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)). +`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. The prepared call capability governs prompt admission, not the preceding `request/context`. Every attempt synchronously reconciles the same rendered assembly, appends users only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes can append non-empty updates after the cached prefix; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)). The loop sends immutable requests while keeping cancellation live. It reuses message-freeze evidence only for identities it has fully frozen; [agent-loop](../packages/core/agent-loop/README.md) owns the request construction rules. @@ -118,7 +118,7 @@ The session log is the source of the context the model sees. `deriveMessages()` Session consumers know only the current logical format. Header-only `stat` and `list` rescan each Session directory, select its numerically highest canonical generation, and translate a supported historical header without loading events or publishing a successor. A stored-session `open` selects that same generation, refuses a future version, or decodes and composes the static adjacent migration chain once before returning validated current logical events. A read open uses that in-memory result without publishing a successor; a write open first encodes, verifies, and exclusively publishes the final version-named successor beside the unchanged source. Ordinary repair of an unsealed interrupted tail remains a handle consumer responsibility; migration inserts a missing interrupted `turn/end` only for the bounded released restart already sealed by a later `turn/start`. JSONL v0 uses `session.jsonl[.zstd]`, v1 and later use lowercase `session.vN.jsonl[.zstd]`, and committed generation paths are never renamed, replaced, or deleted. The JSONL provider owns physical framing, compression, generation selection, and exclusive publication, while each adjacent migration package owns exactly one `vN -> vN+1` step ([decision](../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md)). -**Model-visible means logged.** Anything that reaches a model request must be reconstructable from the log, and a runtime invariant asserts it. This is why a new model-visible input requires a new session event: extend `SessionEventMap` and render from the log. +**Model-visible means logged.** Anything that reaches a model request must be reconstructable from the log, and a runtime invariant asserts it. A new model-visible input requires a session event. Plugins that change existing message content register [pure message projections](subsystems/session.md#plugin-owned-message-projections); detached readers supply the same definitions explicitly. **Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, host consumers read one typed state with `stateOf()`, and carriers batch cropped client views with `snapshot()`. A host reader either requires this service during activation or fails explicitly when the registry or required key is absent. Contributors may retain `ctx.inject(['sessionProjections'], ...)` registration without silently defaulting a missing host value. The agent loop registers shared `turnBoundary` state for its readers ([decision](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md)). diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 0eea56b18a..078d99dc06 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -110,7 +110,7 @@ turn/end 输入通过同一个 inbox 到达驱动器。有些消息会立即唤醒它;注入的上下文会留在 inbox 中,直到另一条消息将其唤醒。 -`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试同步协调同一份已渲染组装结果、仅在首次尝试追加用户消息、按需记录 header/context、派生并冻结请求,再通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。 +`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试同步协调同一份已渲染组装结果、仅在首次尝试追加用户消息、按需记录 header/context、派生并冻结请求,再通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。 循环发送不可变请求,同时保留实时取消能力。只有已由该循环完整冻结的消息对象身份才能复用冻结证明;[agent-loop](../packages/core/agent-loop/README.zh.md)拥有请求构造规则。 @@ -122,7 +122,7 @@ turn/end Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list` 会重新扫描每个 Session 目录,选择数值最高的规范 generation,并在不加载事件或发布后继的情况下转换受支持的历史 header。已存储 Session 的 `open` 选择同一 generation,拒绝未来版本,或只 Decode 并组合一次构建时静态确定的相邻迁移链,再返回经过校验的当前逻辑事件。只读 open 直接使用这份内存结果,不发布后继;写 open 则先编码、校验并在未改变源的旁边排他发布最终版本命名的后继。未被后续事件封住的普通中断尾部仍由句柄消费方修复;只有在后续 `turn/start` 已经封住一种有限的已发布 restart 时,migration 才会插入缺失的 interrupted `turn/end`。JSONL v0 使用 `session.jsonl[.zstd]`,v1 及后续版本使用小写 `session.vN.jsonl[.zstd]`;已提交 generation 路径绝不重命名、替换或删除。JSONL provider 负责物理 framing、压缩、generation 选择与排他发布,每个相邻迁移包只负责一个 `vN -> vN+1` 步骤([决策](../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md))。 -**模型可见即已记录。** 抵达模型请求的一切都必须能从日志重建,并由一项运行时不变量断言这一点。因此,新增一项模型可见输入就需要新增一个会话事件:扩展 `SessionEventMap` 并从日志渲染。 +**模型可见即已记录。** 抵达模型请求的一切都必须能从日志重建,并由一项运行时不变量断言这一点。新增模型可见输入需要一个会话事件。修改现有消息内容的插件注册[纯消息投影](subsystems/session.zh.md#plugin-owned-message-projections),独立读取器显式传入相同的处理器。 **投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,host 消费方通过 `stateOf()` 读取单个类型化状态,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。host 读取方要么在激活时要求该服务,要么在注册表或必需 key 缺席时明确失败。贡献方可以保留 `ctx.inject(['sessionProjections'], ...)` 注册,但不能为缺失的 host 值静默提供默认值。agent loop 为读取方注册共享的 `turnBoundary` 状态([决策](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md))。 diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index 92b53a72ee..eb4ffabf6a 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/capability-seams.md -capability-seams.md: 009f2247bfb91f22ac87e9e67ce46ba4fd11aee5 -capability-seams.zh.md: 07172bdca05eb5cca9442af90686b17481f1261b +capability-seams.md: 839d241869f8f88125cd7db6ad75646f25b4460d +capability-seams.zh.md: eb6088bf63e51e6a4bb1c41453dd8a2af102b610 diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 009f2247bf..839d241869 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -58,6 +58,8 @@ flowchart LR svc_settingsController["ctx.settingsController
Host settings-surface Remote controller"] pkg_api_workspace_files["api-workspace-files"] svc_workspaceFiles["ctx.workspaceFiles
Host workspace file Remote service"] + pkg_api_terminal_controller["api-terminal-controller"] + svc_terminalController["ctx.terminalController
Session interactive terminal Remote controller"] pkg_api_workspace_controller["api-workspace-controller"] svc_workspaceController["ctx.workspaceController
Host Workspace Remote controller"] svc_directoryPickerController["ctx.directoryPickerController
Host directory-picking Remote controller"] @@ -247,6 +249,7 @@ flowchart LR pkg_api_session_controller --> svc_sessionSkillCatalog pkg_api_settings_controller --> svc_credentialsController pkg_api_settings_controller --> svc_settingsController + pkg_api_terminal_controller --> svc_terminalController pkg_api_workspace_controller --> svc_directoryPickerController pkg_api_workspace_controller --> svc_workspaceController pkg_api_workspace_files --> svc_workspaceFiles @@ -521,6 +524,7 @@ flowchart LR | `ctx.credentialsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | Projects the credential-reference seam onto the generated Remote namespace: batch fan-out, view projection, and refusal mapping live here, not on the seam Definition. | | `ctx.settingsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | Projects the user-settings seam onto the generated Remote namespace: the read is always redacted and every refusal is classified here, not on the seam Definition. | | `ctx.workspaceFiles` | `core` | [`api-workspace-files`](../packages/api/workspace-files) | - | - | - | Serves stat, paged text, byte windows, directory listings, and the change feed for files inside a Session's workspace root, confined by lstat, containment, and a stat re-check. | +| `ctx.terminalController` | `core` | [`api-terminal-controller`](../packages/api/terminal-controller) | - | - | - | Owns user terminal processes, default shell resolution and bounded screen recovery through the subprocess provider and typed Remote transport. | | `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Owns Workspace commands and reconnect-safe Workspace state delivery through the generated Remote namespace. | | `ctx.directoryPickerController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Carries the picking seam onto the wire: capability gating, cancellation, and the seam-coded failures a browser directory flow discriminates on. | | `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index 07172bdca0..eb6088bf63 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -60,6 +60,8 @@ flowchart LR svc_settingsController["ctx.settingsController
Host settings-surface Remote controller"] pkg_api_workspace_files["api-workspace-files"] svc_workspaceFiles["ctx.workspaceFiles
Host workspace file Remote service"] + pkg_api_terminal_controller["api-terminal-controller"] + svc_terminalController["ctx.terminalController
Session interactive terminal Remote controller"] pkg_api_workspace_controller["api-workspace-controller"] svc_workspaceController["ctx.workspaceController
Host Workspace Remote controller"] svc_directoryPickerController["ctx.directoryPickerController
Host directory-picking Remote controller"] @@ -249,6 +251,7 @@ flowchart LR pkg_api_session_controller --> svc_sessionSkillCatalog pkg_api_settings_controller --> svc_credentialsController pkg_api_settings_controller --> svc_settingsController + pkg_api_terminal_controller --> svc_terminalController pkg_api_workspace_controller --> svc_directoryPickerController pkg_api_workspace_controller --> svc_workspaceController pkg_api_workspace_files --> svc_workspaceFiles @@ -523,6 +526,7 @@ flowchart LR | `ctx.credentialsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | 把凭据引用 seam 投影到生成的 Remote namespace:批量扇出、视图投影与拒绝映射都在这里,而不在 seam Definition 上。 | | `ctx.settingsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | 把用户设置 seam 投影到生成的 Remote namespace:读取一律脱敏,所有拒绝在这里分类,而不在 seam Definition 上。 | | `ctx.workspaceFiles` | `core` | [`api-workspace-files`](../packages/api/workspace-files) | - | - | - | 为会话工作区根内的文件提供 stat、分页文本、字节窗口、目录列举与变更流,经 lstat、包含关系与 stat 重检限定。 | +| `ctx.terminalController` | `core` | [`api-terminal-controller`](../packages/api/terminal-controller) | - | - | - | 通过子进程提供方与类型化 Remote 传输管理用户终端进程、解析默认 shell,并恢复有界终端屏幕。 | | `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 通过生成的 Remote namespace 负责 Workspace 命令和可在重连后收敛的 Workspace 状态投递。 | | `ctx.directoryPickerController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 把选目录 seam 送上线:能力门禁、取消传播,以及浏览器目录流程用于分支判断的 seam 错误码。 | | `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | 配套子路径注册所属包本地的检查;该服务负责选择、唯一性、子 fiber,以及标明所属包的失败。 | diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 2b89efbdd1..3cde1b9e33 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 8949c8bb942416761f79fae692225af53274f11f -config-catalog.zh.md: 0285b00ac21cdac04e91cc19a7707b720d614dac +config-catalog.md: ad1d7d977e0cea23c76256fca626988d51429fb6 +config-catalog.zh.md: 4a3d1692355c0a6ff7306a929fd87050f61ed06b diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 8949c8bb94..ad1d7d977e 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -229,6 +229,45 @@ export interface Config { Source: [`packages/api/settings-controller/src/index.ts:36`](../packages/api/settings-controller/src/index.ts) + + +## `@deepseek-ai/dsh-api-terminal-controller` + +Requires: `subprocess` · `sandboxPolicy` · `sessionProjections` · `typert` + +```ts config-catalog +/** Deployment limits and an optional shell profile. */ +export interface Config { + /** Explicit shell profile; omission uses the execution environment's default shell. */ + readonly shell?: { + /** Executable path or PATH name, verified by the subprocess provider. */ + path: string + /** User-visible profile name. */ + name: string + /** Arguments passed to the interactive shell. */ + args: string[] + } | undefined + /** Executable names or paths checked for the new-terminal shell selector. */ + readonly shellCandidates: string[] + /** Maximum retained terminals and pending allocations per Session. */ + readonly maxTerminals: number + /** Maximum terminal width in columns. */ + readonly maxCols: number + /** Maximum terminal height in rows. */ + readonly maxRows: number + /** Screen history rows retained for reconnecting clients. */ + readonly scrollback: number + /** Maximum queued UTF-8 frame bytes per output follower before disconnection. */ + readonly maxBufferedBytes: number + /** Maximum UTF-8 bytes in one input request. */ + readonly maxInputBytes: number + /** Provider process-termination grace period in milliseconds. */ + readonly disposeGraceMs: number +} +``` + +Source: [`packages/api/terminal-controller/src/index.ts:28`](../packages/api/terminal-controller/src/index.ts) + ## `@deepseek-ai/dsh-api-workspace-files` @@ -1489,7 +1528,7 @@ export interface ReplayModelConfig { Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) -Source: [`packages/test-support/llm-replay/src/index.ts:1123`](../packages/test-support/llm-replay/src/index.ts) +Source: [`packages/test-support/llm-replay/src/index.ts:1122`](../packages/test-support/llm-replay/src/index.ts) @@ -3580,6 +3619,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` ([`packages/client/ui-sidebar-documentpreview/src/index.ts`](../packages/client/ui-sidebar-documentpreview/src/index.ts)) - `@deepseek-ai/dsh-client-ui-sidebar-files` ([`packages/client/ui-sidebar-files/src/index.ts`](../packages/client/ui-sidebar-files/src/index.ts)) - `@deepseek-ai/dsh-client-ui-sidebar-right` ([`packages/client/ui-sidebar-right/src/index.ts`](../packages/client/ui-sidebar-right/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-sidebar-terminal` ([`packages/client/ui-sidebar-terminal/src/index.ts`](../packages/client/ui-sidebar-terminal/src/index.ts)) - `@deepseek-ai/dsh-client-ui-skill` ([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts)) - `@deepseek-ai/dsh-client-ui-subagent` ([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts)) - `@deepseek-ai/dsh-client-ui-theme` ([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts)) @@ -3592,6 +3632,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-command-feedback` — requires `commands` ([`packages/feedback/command-feedback/src/index.ts`](../packages/feedback/command-feedback/src/index.ts)) - `@deepseek-ai/dsh-command-goal` — requires `commands` · `goals` ([`packages/goal/command-goal/src/index.ts`](../packages/goal/command-goal/src/index.ts)) - `@deepseek-ai/dsh-commands` ([`packages/interaction/commands/src/index.ts`](../packages/interaction/commands/src/index.ts)) +- `@deepseek-ai/dsh-compaction-image-offload` — requires `agents` · `sessions` ([`packages/compaction/compaction-image-offload/src/index.ts`](../packages/compaction/compaction-image-offload/src/index.ts)) - `@deepseek-ai/dsh-computer-use` ([`packages/computer-use/computer-use/src/index.ts`](../packages/computer-use/computer-use/src/index.ts)) - `@deepseek-ai/dsh-cordis-client-runner` ([`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts)) - `@deepseek-ai/dsh-deepseek-llm-api-extensions` ([`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 0285b00ac2..4a3d169235 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -231,6 +231,45 @@ export interface Config { 来源:[`packages/api/settings-controller/src/index.ts:36`](../packages/api/settings-controller/src/index.ts) + + +## `@deepseek-ai/dsh-api-terminal-controller` + +Requires: `subprocess` · `sandboxPolicy` · `sessionProjections` · `typert` + +```ts config-catalog +/** Deployment limits and an optional shell profile. */ +export interface Config { + /** Explicit shell profile; omission uses the execution environment's default shell. */ + readonly shell?: { + /** Executable path or PATH name, verified by the subprocess provider. */ + path: string + /** User-visible profile name. */ + name: string + /** Arguments passed to the interactive shell. */ + args: string[] + } | undefined + /** Executable names or paths checked for the new-terminal shell selector. */ + readonly shellCandidates: string[] + /** Maximum retained terminals and pending allocations per Session. */ + readonly maxTerminals: number + /** Maximum terminal width in columns. */ + readonly maxCols: number + /** Maximum terminal height in rows. */ + readonly maxRows: number + /** Screen history rows retained for reconnecting clients. */ + readonly scrollback: number + /** Maximum queued UTF-8 frame bytes per output follower before disconnection. */ + readonly maxBufferedBytes: number + /** Maximum UTF-8 bytes in one input request. */ + readonly maxInputBytes: number + /** Provider process-termination grace period in milliseconds. */ + readonly disposeGraceMs: number +} +``` + +来源: [`packages/api/terminal-controller/src/index.ts:28`](../packages/api/terminal-controller/src/index.ts) + ## `@deepseek-ai/dsh-api-workspace-files` @@ -1491,7 +1530,7 @@ export interface ReplayModelConfig { 依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) -来源:[`packages/test-support/llm-replay/src/index.ts:1123`](../packages/test-support/llm-replay/src/index.ts) +来源:[`packages/test-support/llm-replay/src/index.ts:1122`](../packages/test-support/llm-replay/src/index.ts) @@ -3582,6 +3621,7 @@ export interface Config { - `@deepseek-ai/dsh-client-ui-sidebar-documentpreview`([`packages/client/ui-sidebar-documentpreview/src/index.ts`](../packages/client/ui-sidebar-documentpreview/src/index.ts)) - `@deepseek-ai/dsh-client-ui-sidebar-files`([`packages/client/ui-sidebar-files/src/index.ts`](../packages/client/ui-sidebar-files/src/index.ts)) - `@deepseek-ai/dsh-client-ui-sidebar-right`([`packages/client/ui-sidebar-right/src/index.ts`](../packages/client/ui-sidebar-right/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-sidebar-terminal`([`packages/client/ui-sidebar-terminal/src/index.ts`](../packages/client/ui-sidebar-terminal/src/index.ts)) - `@deepseek-ai/dsh-client-ui-skill`([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts)) - `@deepseek-ai/dsh-client-ui-subagent`([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts)) - `@deepseek-ai/dsh-client-ui-theme`([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts)) @@ -3594,6 +3634,7 @@ export interface Config { - `@deepseek-ai/dsh-command-feedback` — 需要 `commands`([`packages/feedback/command-feedback/src/index.ts`](../packages/feedback/command-feedback/src/index.ts)) - `@deepseek-ai/dsh-command-goal` — 需要 `commands` · `goals`([`packages/goal/command-goal/src/index.ts`](../packages/goal/command-goal/src/index.ts)) - `@deepseek-ai/dsh-commands`([`packages/interaction/commands/src/index.ts`](../packages/interaction/commands/src/index.ts)) +- `@deepseek-ai/dsh-compaction-image-offload`,需要 `agents` 和 `sessions`([`packages/compaction/compaction-image-offload/src/index.ts`](../packages/compaction/compaction-image-offload/src/index.ts)) - `@deepseek-ai/dsh-computer-use` ([`packages/computer-use/computer-use/src/index.ts`](../packages/computer-use/computer-use/src/index.ts)) - `@deepseek-ai/dsh-cordis-client-runner`([`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts)) - `@deepseek-ai/dsh-deepseek-llm-api-extensions`([`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts)) diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index b8e9b6bc25..5a3ebe74e6 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/event-producer-consumer.md -event-producer-consumer.md: d58336c9d782a4d61a9df295ac64623e1b533408 -event-producer-consumer.zh.md: 2a97fdae3fd629e210cd80aeeb7968606292dc57 +event-producer-consumer.md: b727570dff92aba1bfa0cbb1fba86f9ffe9d9bff +event-producer-consumer.zh.md: 8fc9c6263e133b4f1fd8703167d44ee530f8b079 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index d58336c9d7..b727570dff 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -18,7 +18,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | | `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:330`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:347`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:316`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:277`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:391`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | @@ -30,6 +30,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) | | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` | +| `compaction/summary-error` | `waterfall` | [`packages/compaction/compaction/src/index.ts:99`](../packages/compaction/compaction/src/index.ts) | [`compaction-basic`](../packages/compaction/compaction-basic) (`waterfall`) | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) | | `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:380`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` | | `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:386`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` | | `cordis/inspect-query` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:392`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` | @@ -81,7 +82,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event string | Dispatchers | Listeners | | --- | --- | --- | -| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) | +| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), `terminal-controller`, [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) | | `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` | | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent), `inspector` | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 2a97fdae3f..8fc9c6263e 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -20,7 +20,7 @@ | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | | `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:330`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:347`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:316`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:277`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:391`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | @@ -32,6 +32,7 @@ | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) | | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` | +| `compaction/summary-error` | `waterfall` | [`packages/compaction/compaction/src/index.ts:99`](../packages/compaction/compaction/src/index.ts) | [`compaction-basic`](../packages/compaction/compaction-basic) (`waterfall`) | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) | | `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:380`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` | | `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:386`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` | | `cordis/inspect-query` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:392`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` | @@ -83,7 +84,7 @@ | Event string | Dispatchers | Listeners | | --- | --- | --- | -| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) | +| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), `terminal-controller`, [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) | | `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` | | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent), `inspector` | diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index e9a4dc0900..e0d157bab7 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: 1a81dbf19ce2cc51a91c89ae310229954fd7c0b5 -module-graph.zh.md: 4f609375c3ada6fb857021a4248e16d64cd4c28a +module-graph.md: 4478407e3c4f8f839c341c41dd2c7b4bb0ab0b22 +module-graph.zh.md: 8cf238d9c49e436cf2738ebffcd26111277c5574 diff --git a/docs/module-graph.md b/docs/module-graph.md index 1a81dbf19c..4478407e3c 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -115,6 +115,7 @@ flowchart TD pkg_api_remotes["api-remotes"] pkg_api_session_controller["api-session-controller"] pkg_api_settings_controller["api-settings-controller"] + pkg_api_terminal_controller["api-terminal-controller"] pkg_api_workspace_controller["api-workspace-controller"] pkg_api_workspace_files["api-workspace-files"] end @@ -180,6 +181,7 @@ flowchart TD pkg_client_ui_sidebar_documentpreview["client-ui-sidebar-documentpreview"] pkg_client_ui_sidebar_files["client-ui-sidebar-files"] pkg_client_ui_sidebar_right["client-ui-sidebar-right"] + pkg_client_ui_sidebar_terminal["client-ui-sidebar-terminal"] pkg_client_ui_skill["client-ui-skill"] pkg_client_ui_slots["client-ui-slots"] pkg_client_ui_subagent["client-ui-subagent"] @@ -195,6 +197,7 @@ flowchart TD pkg_command_compact["command-compact"] pkg_compaction["compaction"] pkg_compaction_basic["compaction-basic"] + pkg_compaction_image_offload["compaction-image-offload"] pkg_compaction_tool_result_pruner["compaction-tool-result-pruner"] end subgraph group_computer_use["packages/computer-use"] @@ -433,6 +436,7 @@ flowchart TD pkg_web_search_perplexity --> pkg_launch_environment pkg_web_search_perplexity --> pkg_web pkg_api_remotes --> pkg_scope + pkg_api_terminal_controller --> pkg_subprocess pkg_attachment_local --> pkg_attachment pkg_attachment_local --> pkg_home_paths pkg_client_file_upload --> pkg_scope @@ -715,11 +719,6 @@ flowchart TD pkg_terminal_bash --> pkg_session_projection pkg_terminal_bash --> pkg_subprocess pkg_terminal_bash --> pkg_terminal - pkg_token_meter --> pkg_compaction - pkg_token_meter --> pkg_llm - pkg_token_meter --> pkg_llm_retry - pkg_token_meter --> pkg_session - pkg_token_meter --> pkg_session_projection pkg_agent_loop --> pkg_agent pkg_agent_loop --> pkg_invariants pkg_agent_loop --> pkg_llm @@ -800,6 +799,10 @@ flowchart TD pkg_hooks_codex --> pkg_tools pkg_command_compact --> pkg_commands pkg_command_compact --> pkg_compaction + pkg_compaction_image_offload --> pkg_agent + pkg_compaction_image_offload --> pkg_compaction + pkg_compaction_image_offload --> pkg_llm + pkg_compaction_image_offload --> pkg_session pkg_agent_instructions --> pkg_agent pkg_agent_instructions --> pkg_fs pkg_agent_instructions --> pkg_home_paths @@ -943,6 +946,12 @@ flowchart TD pkg_plugin_package_inventory_deepseek --> pkg_agent_presets pkg_plugin_package_inventory_deepseek --> pkg_deepseek_llm_api_extensions pkg_plugin_package_inventory_deepseek --> pkg_session + pkg_token_meter --> pkg_compaction + pkg_token_meter --> pkg_compaction_image_offload + pkg_token_meter --> pkg_llm + pkg_token_meter --> pkg_llm_retry + pkg_token_meter --> pkg_session + pkg_token_meter --> pkg_session_projection pkg_session_query --> pkg_brand pkg_session_query --> pkg_llm pkg_session_query --> pkg_session @@ -959,10 +968,6 @@ flowchart TD pkg_api_settings_controller --> pkg_typert_protocol pkg_web_app --> pkg_shell_env pkg_web_app --> pkg_system_prompt - pkg_compaction_tool_result_pruner --> pkg_compaction - pkg_compaction_tool_result_pruner --> pkg_llm - pkg_compaction_tool_result_pruner --> pkg_session - pkg_compaction_tool_result_pruner --> pkg_token_meter pkg_experimental_auto_review --> pkg_agent pkg_experimental_auto_review --> pkg_agent_instructions pkg_experimental_auto_review --> pkg_llm @@ -1083,13 +1088,10 @@ flowchart TD pkg_headless --> pkg_llm pkg_headless --> pkg_session pkg_headless --> pkg_session_query - pkg_compaction_basic --> pkg_agent - pkg_compaction_basic --> pkg_commands - pkg_compaction_basic --> pkg_compaction - pkg_compaction_basic --> pkg_compaction_tool_result_pruner - pkg_compaction_basic --> pkg_llm - pkg_compaction_basic --> pkg_session - pkg_compaction_basic --> pkg_token_meter + pkg_compaction_tool_result_pruner --> pkg_compaction + pkg_compaction_tool_result_pruner --> pkg_llm + pkg_compaction_tool_result_pruner --> pkg_session + pkg_compaction_tool_result_pruner --> pkg_token_meter pkg_session_reference --> pkg_agent pkg_session_reference --> pkg_compaction pkg_session_reference --> pkg_llm @@ -1178,6 +1180,13 @@ flowchart TD pkg_api_session_controller --> pkg_util_values pkg_api_session_controller --> pkg_util_workspace_path pkg_api_session_controller --> pkg_workspace + pkg_compaction_basic --> pkg_agent + pkg_compaction_basic --> pkg_commands + pkg_compaction_basic --> pkg_compaction + pkg_compaction_basic --> pkg_compaction_tool_result_pruner + pkg_compaction_basic --> pkg_llm + pkg_compaction_basic --> pkg_session + pkg_compaction_basic --> pkg_token_meter pkg_experimental_agent_team --> pkg_agent pkg_experimental_agent_team --> pkg_invariants pkg_experimental_agent_team --> pkg_llm @@ -1331,6 +1340,7 @@ flowchart TD | [`client-ui-sidebar-documentpreview`](../packages/client/ui-sidebar-documentpreview) | `client` | — | | [`client-ui-sidebar-files`](../packages/client/ui-sidebar-files) | `client` | — | | [`client-ui-sidebar-right`](../packages/client/ui-sidebar-right) | `client` | — | +| [`client-ui-sidebar-terminal`](../packages/client/ui-sidebar-terminal) | `client` | — | | [`client-ui-skill`](../packages/client/ui-skill) | `client` | — | | [`client-ui-slots`](../packages/client/ui-slots) | `client` | — | | [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | — | @@ -1387,6 +1397,7 @@ flowchart TD | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) | | [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) | | [`api-remotes`](../packages/api/remotes) | `api` | [`scope`](../packages/core/scope) | +| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) | | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) | | [`client-file-upload`](../packages/client/file-upload) | `client` | [`scope`](../packages/core/scope) | | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) | @@ -1459,7 +1470,6 @@ flowchart TD | [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | | [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | | [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal) | -| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`tools`](../packages/core/tools) | | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | @@ -1474,6 +1484,7 @@ flowchart TD | [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | | [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction) | +| [`compaction-image-offload`](../packages/compaction/compaction-image-offload) | `compaction` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | | [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | @@ -1502,10 +1513,10 @@ flowchart TD | [`llm-replay`](../packages/test-support/llm-replay) | `test-support` | [`compaction`](../packages/compaction/compaction), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) | +| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) | | [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) | | [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) | -| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) | @@ -1523,7 +1534,7 @@ flowchart TD | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) | | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) | -| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | +| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | | [`webhook-github`](../packages/webhook/webhook-github) | `webhook` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`session`](../packages/core/session), [`webhook`](../packages/webhook/webhook) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | @@ -1534,6 +1545,7 @@ flowchart TD | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) | +| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 4f609375c3..8cf238d9c4 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -117,6 +117,7 @@ flowchart TD pkg_api_remotes["api-remotes"] pkg_api_session_controller["api-session-controller"] pkg_api_settings_controller["api-settings-controller"] + pkg_api_terminal_controller["api-terminal-controller"] pkg_api_workspace_controller["api-workspace-controller"] pkg_api_workspace_files["api-workspace-files"] end @@ -182,6 +183,7 @@ flowchart TD pkg_client_ui_sidebar_documentpreview["client-ui-sidebar-documentpreview"] pkg_client_ui_sidebar_files["client-ui-sidebar-files"] pkg_client_ui_sidebar_right["client-ui-sidebar-right"] + pkg_client_ui_sidebar_terminal["client-ui-sidebar-terminal"] pkg_client_ui_skill["client-ui-skill"] pkg_client_ui_slots["client-ui-slots"] pkg_client_ui_subagent["client-ui-subagent"] @@ -197,6 +199,7 @@ flowchart TD pkg_command_compact["command-compact"] pkg_compaction["compaction"] pkg_compaction_basic["compaction-basic"] + pkg_compaction_image_offload["compaction-image-offload"] pkg_compaction_tool_result_pruner["compaction-tool-result-pruner"] end subgraph group_computer_use["packages/computer-use"] @@ -435,6 +438,7 @@ flowchart TD pkg_web_search_perplexity --> pkg_launch_environment pkg_web_search_perplexity --> pkg_web pkg_api_remotes --> pkg_scope + pkg_api_terminal_controller --> pkg_subprocess pkg_attachment_local --> pkg_attachment pkg_attachment_local --> pkg_home_paths pkg_client_file_upload --> pkg_scope @@ -717,11 +721,6 @@ flowchart TD pkg_terminal_bash --> pkg_session_projection pkg_terminal_bash --> pkg_subprocess pkg_terminal_bash --> pkg_terminal - pkg_token_meter --> pkg_compaction - pkg_token_meter --> pkg_llm - pkg_token_meter --> pkg_llm_retry - pkg_token_meter --> pkg_session - pkg_token_meter --> pkg_session_projection pkg_agent_loop --> pkg_agent pkg_agent_loop --> pkg_invariants pkg_agent_loop --> pkg_llm @@ -802,6 +801,10 @@ flowchart TD pkg_hooks_codex --> pkg_tools pkg_command_compact --> pkg_commands pkg_command_compact --> pkg_compaction + pkg_compaction_image_offload --> pkg_agent + pkg_compaction_image_offload --> pkg_compaction + pkg_compaction_image_offload --> pkg_llm + pkg_compaction_image_offload --> pkg_session pkg_agent_instructions --> pkg_agent pkg_agent_instructions --> pkg_fs pkg_agent_instructions --> pkg_home_paths @@ -945,6 +948,12 @@ flowchart TD pkg_plugin_package_inventory_deepseek --> pkg_agent_presets pkg_plugin_package_inventory_deepseek --> pkg_deepseek_llm_api_extensions pkg_plugin_package_inventory_deepseek --> pkg_session + pkg_token_meter --> pkg_compaction + pkg_token_meter --> pkg_compaction_image_offload + pkg_token_meter --> pkg_llm + pkg_token_meter --> pkg_llm_retry + pkg_token_meter --> pkg_session + pkg_token_meter --> pkg_session_projection pkg_session_query --> pkg_brand pkg_session_query --> pkg_llm pkg_session_query --> pkg_session @@ -961,10 +970,6 @@ flowchart TD pkg_api_settings_controller --> pkg_typert_protocol pkg_web_app --> pkg_shell_env pkg_web_app --> pkg_system_prompt - pkg_compaction_tool_result_pruner --> pkg_compaction - pkg_compaction_tool_result_pruner --> pkg_llm - pkg_compaction_tool_result_pruner --> pkg_session - pkg_compaction_tool_result_pruner --> pkg_token_meter pkg_experimental_auto_review --> pkg_agent pkg_experimental_auto_review --> pkg_agent_instructions pkg_experimental_auto_review --> pkg_llm @@ -1085,13 +1090,10 @@ flowchart TD pkg_headless --> pkg_llm pkg_headless --> pkg_session pkg_headless --> pkg_session_query - pkg_compaction_basic --> pkg_agent - pkg_compaction_basic --> pkg_commands - pkg_compaction_basic --> pkg_compaction - pkg_compaction_basic --> pkg_compaction_tool_result_pruner - pkg_compaction_basic --> pkg_llm - pkg_compaction_basic --> pkg_session - pkg_compaction_basic --> pkg_token_meter + pkg_compaction_tool_result_pruner --> pkg_compaction + pkg_compaction_tool_result_pruner --> pkg_llm + pkg_compaction_tool_result_pruner --> pkg_session + pkg_compaction_tool_result_pruner --> pkg_token_meter pkg_session_reference --> pkg_agent pkg_session_reference --> pkg_compaction pkg_session_reference --> pkg_llm @@ -1180,6 +1182,13 @@ flowchart TD pkg_api_session_controller --> pkg_util_values pkg_api_session_controller --> pkg_util_workspace_path pkg_api_session_controller --> pkg_workspace + pkg_compaction_basic --> pkg_agent + pkg_compaction_basic --> pkg_commands + pkg_compaction_basic --> pkg_compaction + pkg_compaction_basic --> pkg_compaction_tool_result_pruner + pkg_compaction_basic --> pkg_llm + pkg_compaction_basic --> pkg_session + pkg_compaction_basic --> pkg_token_meter pkg_experimental_agent_team --> pkg_agent pkg_experimental_agent_team --> pkg_invariants pkg_experimental_agent_team --> pkg_llm @@ -1333,6 +1342,7 @@ flowchart TD | [`client-ui-sidebar-documentpreview`](../packages/client/ui-sidebar-documentpreview) | `client` | — | | [`client-ui-sidebar-files`](../packages/client/ui-sidebar-files) | `client` | — | | [`client-ui-sidebar-right`](../packages/client/ui-sidebar-right) | `client` | — | +| [`client-ui-sidebar-terminal`](../packages/client/ui-sidebar-terminal) | `client` | — | | [`client-ui-skill`](../packages/client/ui-skill) | `client` | — | | [`client-ui-slots`](../packages/client/ui-slots) | `client` | — | | [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | — | @@ -1389,6 +1399,7 @@ flowchart TD | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) | | [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) | | [`api-remotes`](../packages/api/remotes) | `api` | [`scope`](../packages/core/scope) | +| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) | | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) | | [`client-file-upload`](../packages/client/file-upload) | `client` | [`scope`](../packages/core/scope) | | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) | @@ -1461,7 +1472,6 @@ flowchart TD | [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | | [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | | [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal) | -| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`tools`](../packages/core/tools) | | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | @@ -1476,6 +1486,7 @@ flowchart TD | [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | | [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction) | +| [`compaction-image-offload`](../packages/compaction/compaction-image-offload) | `compaction` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | | [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | @@ -1504,10 +1515,10 @@ flowchart TD | [`llm-replay`](../packages/test-support/llm-replay) | `test-support` | [`compaction`](../packages/compaction/compaction), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) | +| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) | | [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) | | [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) | -| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) | @@ -1525,7 +1536,7 @@ flowchart TD | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) | | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) | -| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | +| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | | [`webhook-github`](../packages/webhook/webhook-github) | `webhook` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`session`](../packages/core/session), [`webhook`](../packages/webhook/webhook) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | @@ -1536,6 +1547,7 @@ flowchart TD | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) | +| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 31176dde51..633ce43a70 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: f745f42fc34b2aa65d650e3f5ee1f2901907769d -persistence-catalog.zh.md: c4da4dd7c0a220896343c04873b56a90f5aad612 +persistence-catalog.md: b3667b1f07b708753ba6aea645276784bab31a29 +persistence-catalog.zh.md: b8be658b11e8d00043f2d6a043c70b28c16de081 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index f745f42fc3..b3667b1f07 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -21,20 +21,20 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`packages/session/session-persistence-jsonl/src/format.ts#HeaderLine`](#persistence-type-packagessessionsession-persistence-jsonlsrcformattsheaderline) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`SessionEventEnvelope`](#persistence-type-sessioneventenvelope) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`event:agent-preset/selected`](#persistence-type-eventagent-presetselected) | -| `event:agent/inbox/spliced` | event | `f757322e25914d63799c6235af4ed4b4f36d7d083fef40d6873a45e94fe6ad65` | [`event:agent/inbox/spliced`](#persistence-type-eventagentinboxspliced) | +| `event:agent/inbox/spliced` | event | `646c2f1d243d3a78c5bc9305786fb340f5209c48a97e65f2a186cc5195491f3f` | [`event:agent/inbox/spliced`](#persistence-type-eventagentinboxspliced) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`event:approval/asked`](#persistence-type-eventapprovalasked) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`event:approval/decided`](#persistence-type-eventapprovaldecided) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`event:approval/policy`](#persistence-type-eventapprovalpolicy) | -| `event:assistant/attempt` | event | `3516f54b427775f483febe8e938e4e10f9afd3583610861547bbe2999ed4d8d9` | [`event:assistant/attempt`](#persistence-type-eventassistantattempt) | -| `event:assistant/message` | event | `a58ca1bed502d425a8b65cac02f59717498cd296de9513c6f81b83b791c20e9b` | [`event:assistant/message`](#persistence-type-eventassistantmessage) | -| `event:browser-use/stagehand-llm-request` | event | `7cf11fcc829f21289bb7311562c6601499368464cce8ee678f80500e6a448b01` | [`event:browser-use/stagehand-llm-request`](#persistence-type-eventbrowser-usestagehand-llm-request) | -| `event:browser-use/stagehand-llm-result` | event | `05d3b279f065b23cfb5d192a9a95016fe23642d47dbd90378c81be939e8e1b09` | [`event:browser-use/stagehand-llm-result`](#persistence-type-eventbrowser-usestagehand-llm-result) | +| `event:assistant/attempt` | event | `c80c89da83c46db7a454f034c10f969e03cfb574859c7316e5bff683f5a14b0e` | [`event:assistant/attempt`](#persistence-type-eventassistantattempt) | +| `event:assistant/message` | event | `1169b301aaabcd992657b93ec93750c43175dda81ada5cac086f14f2eaaeed6d` | [`event:assistant/message`](#persistence-type-eventassistantmessage) | +| `event:browser-use/stagehand-llm-request` | event | `92cda1e2c0e9f57fdcd167a861b1296ec4cb5ac3ae127733c375e108335b32ac` | [`event:browser-use/stagehand-llm-request`](#persistence-type-eventbrowser-usestagehand-llm-request) | +| `event:browser-use/stagehand-llm-result` | event | `0e0e81784bc85e1e6554b7ff50ab341169a1a3d32149ae49abe8dddca5ce6efe` | [`event:browser-use/stagehand-llm-result`](#persistence-type-eventbrowser-usestagehand-llm-result) | | `event:command/done` | event | `15196447222782e773eb943c92b18316ce96b9af0f0cfddb6e57ba8274ecc5ff` | [`event:command/done`](#persistence-type-eventcommanddone) | | `event:command/run` | event | `37184378c6439257d105c4e2022d80fc9c3a3f7c7f6ac661b00bc9f18d871006` | [`event:command/run`](#persistence-type-eventcommandrun) | | `event:compaction/end` | event | `b0127044ab31a702bddfd785d345f5abd7a70876746e895ce443afa3e60ddf2d` | [`event:compaction/end`](#persistence-type-eventcompactionend) | | `event:compaction/prune` | event | `7f7fd5a6b0064f597534b29ff62ef26e786dffccf5e14f654a7d4fcea2c35f04` | [`event:compaction/prune`](#persistence-type-eventcompactionprune) | | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`event:compaction/start`](#persistence-type-eventcompactionstart) | -| `event:compaction/summary` | event | `eb1d32dcd3d76833392f351e039dcbd40686a1ab24b252a7a5003426f9cf4c89` | [`event:compaction/summary`](#persistence-type-eventcompactionsummary) | +| `event:compaction/summary` | event | `f6f3f30109e9008fccf7da2a3268a63da8646b59dd7cf7b75bcd9cf08ac08a59` | [`event:compaction/summary`](#persistence-type-eventcompactionsummary) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`event:deliverables/presented`](#persistence-type-eventdeliverablespresented) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`event:feedback/message-delete`](#persistence-type-eventfeedbackmessage-delete) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`event:feedback/message-put`](#persistence-type-eventfeedbackmessage-put) | @@ -42,7 +42,8 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:goal/change` | event | `763c8a20af487263a0080548274f7437ef4a86e0ba8769127d1ced10a72c664d` | [`event:goal/change`](#persistence-type-eventgoalchange) | | `event:hook/invoked` | event | `8a6e1ec9e8db346b0e02f027db73c07a94f067a26d40c1aef1abd09c47ce7ba0` | [`event:hook/invoked`](#persistence-type-eventhookinvoked) | | `event:hook/result` | event | `e75916628f3f10c2d50658bd143052a46285fbf1a9a700ba54947614603d26b4` | [`event:hook/result`](#persistence-type-eventhookresult) | -| `event:llm/retry` | event | `91c397f8f870e812e1dc5ac69c2745f9f105be9dc97dadc980d19ef415a65145` | [`event:llm/retry`](#persistence-type-eventllmretry) | +| `event:image/offload` | event | `b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e` | [`event:image/offload`](#persistence-type-eventimageoffload) | +| `event:llm/retry` | event | `525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336` | [`event:llm/retry`](#persistence-type-eventllmretry) | | `event:llm/retry-started` | event | `48e5c9861f16ac07e78cb7b5ae9dabdf7bb85c58baed5a51b4ad275050ea58e3` | [`event:llm/retry-started`](#persistence-type-eventllmretry-started) | | `event:model/selection` | event | `35203ba7ad5ef6f97d556b85df20ae98f04f09c65748cecdf8eefdb8b6405ffc` | [`event:model/selection`](#persistence-type-eventmodelselection) | | `event:permission/preset` | event | `5c45bf4c544a7211dcd8ba6ba7e5f1bc39b49e7a9df9d5cbdc8e87c22771b37b` | [`event:permission/preset`](#persistence-type-eventpermissionpreset) | @@ -54,16 +55,16 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`event:session-log-deepseek/delivery-accepted`](#persistence-type-eventsession-log-deepseekdelivery-accepted) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`event:session/end-seed`](#persistence-type-eventsessionend-seed) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`event:session/title`](#persistence-type-eventsessiontitle) | -| `event:session/title-llm-request` | event | `6091f64d426ada2adc646094a6fe662a1b81d9ee5770929fe76800524e2c89a8` | [`event:session/title-llm-request`](#persistence-type-eventsessiontitle-llm-request) | +| `event:session/title-llm-request` | event | `e0b5bf44c27bbfc6ab144239e3e4169c4d21146645e0e3121bbb359c3bc8591d` | [`event:session/title-llm-request`](#persistence-type-eventsessiontitle-llm-request) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`event:step/end`](#persistence-type-eventstepend) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`event:step/start`](#persistence-type-eventstepstart) | | `event:subagent/catalog` | event | `ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871` | [`event:subagent/catalog`](#persistence-type-eventsubagentcatalog) | | `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`event:subagent/descriptor`](#persistence-type-eventsubagentdescriptor) | | `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`event:subagent/model-selection-policy`](#persistence-type-eventsubagentmodel-selection-policy) | -| `event:system/message` | event | `0ae7ccdc1cc5d9d43772ebd5c7357ff509011c3a6ba188d9c274d33df2542c42` | [`event:system/message`](#persistence-type-eventsystemmessage) | +| `event:system/message` | event | `69becfb6b2d3fd5da91518089454cae8ef33f1835637ec44dde35dd077fd4bae` | [`event:system/message`](#persistence-type-eventsystemmessage) | | `event:team/member` | event | `4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc` | [`event:team/member`](#persistence-type-eventteammember) | | `event:team/message/delivered` | event | `48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb` | [`event:team/message/delivered`](#persistence-type-eventteammessagedelivered) | -| `event:team/message/queued` | event | `2ffcad547cb840f462eb1849fa5f6cb0e17eb93f5d07cc88b61d749b1b78d632` | [`event:team/message/queued`](#persistence-type-eventteammessagequeued) | +| `event:team/message/queued` | event | `443371ec07a03a82a0e93d93abca3e70b03bca55ba5b01d507030fcb66e8fbb4` | [`event:team/message/queued`](#persistence-type-eventteammessagequeued) | | `event:team/task` | event | `d595ec73b32b016a6055333c67a5d646032b22e672da1d4a09c5b1ae398a093a` | [`event:team/task`](#persistence-type-eventteamtask) | | `event:todo/write` | event | `b978cff734e62143eb56c9125423ec275405eda969802d42aaf73ecb987d3b26` | [`event:todo/write`](#persistence-type-eventtodowrite) | | `event:tool-workflow/agent-end` | event | `babf9ee4d1af62bf6c3a8103737f7a5e4e78ce179be835ce05a38803e15884b7` | [`event:tool-workflow/agent-end`](#persistence-type-eventtool-workflowagent-end) | @@ -71,12 +72,12 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:tool-workflow/run-end` | event | `42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f` | [`event:tool-workflow/run-end`](#persistence-type-eventtool-workflowrun-end) | | `event:tool-workflow/run-start` | event | `c1f9e0405de6d18cabb9ee70782a027f9bbdc57e5abec9dcccdd56119e2e9058` | [`event:tool-workflow/run-start`](#persistence-type-eventtool-workflowrun-start) | | `event:tool/call` | event | `3b1be838223869fe0a08210db85bf773796ed3f2373ac16555dff227cade0c48` | [`event:tool/call`](#persistence-type-eventtoolcall) | -| `event:tool/ptc-dispatch` | event | `b5d66eaebed4da391b13498623b11142222149fbf5e025975dbc6d94f0d06796` | [`event:tool/ptc-dispatch`](#persistence-type-eventtoolptc-dispatch) | +| `event:tool/ptc-dispatch` | event | `b33142af8176323ffbb20762541be81c1d51b60a60887803836e0b146e5bef0c` | [`event:tool/ptc-dispatch`](#persistence-type-eventtoolptc-dispatch) | | `event:tool/ptc-dispatch-start` | event | `ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93` | [`event:tool/ptc-dispatch-start`](#persistence-type-eventtoolptc-dispatch-start) | -| `event:tool/result` | event | `3a803805bdeb805f32b229e399fb7258be8e32a89f89063a7c99957cfea942f7` | [`event:tool/result`](#persistence-type-eventtoolresult) | -| `event:turn/end` | event | `84c24f1209fc3e0153de6ac85d58fd6968b851f955e0e761b92321053956609e` | [`event:turn/end`](#persistence-type-eventturnend) | +| `event:tool/result` | event | `29af48b840d0cd9e48b6f50bf3b354f8f6340607c5b99220a48e74f60beac9e2` | [`event:tool/result`](#persistence-type-eventtoolresult) | +| `event:turn/end` | event | `bab768260853e13a7cf2e22e65af572a1e76a5b2c01f3cdc2ce09d39b87fb70b` | [`event:turn/end`](#persistence-type-eventturnend) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`event:turn/start`](#persistence-type-eventturnstart) | -| `event:user/message` | event | `2e49ede7fb3a592cce8919ebe982f6ca1c1f67abe4def359ffc028c85e6d1ad8` | [`event:user/message`](#persistence-type-eventusermessage) | +| `event:user/message` | event | `314765bdff29c7862fb6ce820f1773563ba3094a680d163ea21180a2591b8578` | [`event:user/message`](#persistence-type-eventusermessage) | | `event:web/deepseek-search-llm-request` | event | `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331` | [`event:web/deepseek-search-llm-request`](#persistence-type-eventwebdeepseek-search-llm-request) | ## Event envelope @@ -608,6 +609,25 @@ Source: [`packages/hooks/hook-protocol/src/types.ts:19`](../packages/hooks/hook- Source: [`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook-protocol/src/types.ts) +### `image/*` + + + +#### `image/offload` — log-only + +```ts persistence-catalog +/** + * Permanently omit selected input-image occurrences from subsequent model requests. + * Targets name unique current user/message or tool/result nodes. Nonempty, strictly + * increasing indexes count all images in depth-first order, including nested tool + * results and already omitted images. Message nodes and identities remain unchanged. + * @messageProjection + */ +'image/offload': { targets: ImageOffloadTarget[] } +``` + +Source: [`packages/compaction/compaction-image-offload/src/projection.ts:25`](../packages/compaction/compaction-image-offload/src/projection.ts) + ### `llm/*` @@ -1736,7 +1756,7 @@ Sources: [`packages/preset/agent-presets/src/session.ts:28`](../packages/preset/ ### `event:agent/inbox/spliced` -SHA-256: `f757322e25914d63799c6235af4ed4b4f36d7d083fef40d6873a45e94fe6ad65` +SHA-256: `646c2f1d243d3a78c5bc9305786fb340f5209c48a97e65f2a186cc5195491f3f` | Property | Presence | Type | |---|---|---| @@ -1750,7 +1770,7 @@ SHA-256: `f757322e25914d63799c6235af4ed4b4f36d7d083fef40d6873a45e94fe6ad65` ### `event:agent/inbox/spliced.data` -SHA-256: `7115142af1bff1ad9b52c3c785109bf5c989e77e3ddfe899014e213731352042` +SHA-256: `a6a195c1b4a906d953d24ab5864c9f1881d545460e080a51144adfc9cb058cfe` Sources: [`packages/core/agent/src/types.ts:87`](../packages/core/agent/src/types.ts) @@ -1766,7 +1786,7 @@ Sources: [`packages/core/agent/src/types.ts:87`](../packages/core/agent/src/type ### `event:agent/inbox/spliced.data.inserted` -SHA-256: `86560bf0779bb2d93975405fe895d9d42c1020ab49fbbd2a06e1b3c74fbbc257` +SHA-256: `300341f890f7b9506a1749fff4fa5239d16ea1c63ff6b15851acb03c20f3b48d` Array of [`packages/llm/llm/src/message.ts#UserMessage`](#persistence-type-packagesllmllmsrcmessagetsusermessage). @@ -1774,7 +1794,7 @@ Array of [`packages/llm/llm/src/message.ts#UserMessage`](#persistence-type-packa ### `event:agent/inbox/spliced.data.inserted[0].content` -SHA-256: `0e560aad8fa9f338a60e76cbce6593ce93486ab30d6dd7833b8b5b07c6e6963d` +SHA-256: `bb72d86bc9ef0b3908bfa57ef98ccccae24d67d945c9f6869ca3cb8577889e75` Array of [`packages/llm/llm/src/types.ts#ContentBlock`](#persistence-type-packagesllmllmsrctypestscontentblock). @@ -2086,7 +2106,7 @@ Sources: [`packages/interaction/user-approval/src/index.ts:33`](../packages/inte ### `event:assistant/attempt` -SHA-256: `3516f54b427775f483febe8e938e4e10f9afd3583610861547bbe2999ed4d8d9` +SHA-256: `c80c89da83c46db7a454f034c10f969e03cfb574859c7316e5bff683f5a14b0e` | Property | Presence | Type | |---|---|---| @@ -2100,7 +2120,7 @@ SHA-256: `3516f54b427775f483febe8e938e4e10f9afd3583610861547bbe2999ed4d8d9` ### `event:assistant/attempt.data` -SHA-256: `8725743fcd3db5ddaf3e0aed550cd5d95785d8e30c9d4cbec11b7c90198d3603` +SHA-256: `0c6412d3ba5e41adfaa6929c7d4093f2d52d8598c03f35844fc0d55e3070a3b1` Sources: [`packages/core/session/src/types.ts:335`](../packages/core/session/src/types.ts) @@ -2114,7 +2134,7 @@ Sources: [`packages/core/session/src/types.ts:335`](../packages/core/session/src ### `event:assistant/attempt.data.stream` -SHA-256: `86755151e6c4694ce5ae9511313e1e50f9fb6ead49c67d0a93c056e8650396c4` +SHA-256: `26f3bb7529cfce1e3a0f1ededa365d8b426758ca0feeb0beabeee805f8bb8346` Array of [`packages/llm/llm/src/assistant-stream.ts#AssistantStreamRecord`](#persistence-type-packagesllmllmsrcassistant-streamtsassistantstreamrecord). @@ -2128,7 +2148,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:21`](../packages/llm/llm/src | Property | Presence | Type | |---|---|---| -| `dt` | required | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `dt` | required | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `index` | required | `number` | | `texts` | required | [`event:assistant/attempt.data.stream[0][0].texts`](#persistence-type-eventassistantattemptdatastream00texts) | | `time0` | required | `number` | @@ -2152,7 +2172,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:28`](../packages/llm/llm/src | Property | Presence | Type | |---|---|---| -| `dt` | required | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `dt` | required | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `index` | required | `number` | | `texts` | required | [`event:assistant/attempt.data.stream[0][0].texts`](#persistence-type-eventassistantattemptdatastream00texts) | | `time0` | required | `number` | @@ -2169,7 +2189,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:35`](../packages/llm/llm/src | Property | Presence | Type | |---|---|---| | `args` | required | [`event:assistant/attempt.data.stream[0][0].texts`](#persistence-type-eventassistantattemptdatastream00texts) | -| `dt` | required | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `dt` | required | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `id` | required | `string` | | `index` | required | `number` | | `name` | optional | `string` | @@ -2180,7 +2200,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:35`](../packages/llm/llm/src ### `event:assistant/attempt.data.stream[0][3]` -SHA-256: `3a896379a74576c5a527e4720be2fe6807d3225c27134e978a2b89a94068368e` +SHA-256: `c93b6f51b399607349b4bcc66b58e848a44250ffc1e2537207bd092dfff94192` Sources: [`packages/llm/llm/src/assistant-stream.ts:44`](../packages/llm/llm/src/assistant-stream.ts) @@ -2196,7 +2216,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:44`](../packages/llm/llm/src SHA-256: `647cb2e3aeb58e27d1d4d60011f208bb66dce257e8654193c533b47c4c22c253` -Sources: [`packages/llm/llm/src/types.ts:391`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:425`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2210,7 +2230,7 @@ Sources: [`packages/llm/llm/src/types.ts:391`](../packages/llm/llm/src/types.ts) SHA-256: `95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65` -Sources: [`packages/llm/llm/src/types.ts:392`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:426`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2224,7 +2244,7 @@ Sources: [`packages/llm/llm/src/types.ts:392`](../packages/llm/llm/src/types.ts) SHA-256: `2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095` -Sources: [`packages/llm/llm/src/types.ts:393`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:427`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2238,7 +2258,7 @@ Sources: [`packages/llm/llm/src/types.ts:393`](../packages/llm/llm/src/types.ts) SHA-256: `88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a` -Sources: [`packages/llm/llm/src/types.ts:394`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:428`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2252,9 +2272,9 @@ Sources: [`packages/llm/llm/src/types.ts:394`](../packages/llm/llm/src/types.ts) ### `event:assistant/attempt.data.stream[0][3].chunk[4]` -SHA-256: `6326a7ecfed005f3d54b737ab9d702da4daddb75cfc635d9096e9abb4788024e` +SHA-256: `90c811b8c2668898515e69a24e82caffeb08517f9c7aa193867ff0380f3b535d` -Sources: [`packages/llm/llm/src/types.ts:395`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:429`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2268,7 +2288,7 @@ Sources: [`packages/llm/llm/src/types.ts:395`](../packages/llm/llm/src/types.ts) SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515` -Sources: [`packages/llm/llm/src/types.ts:396`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:430`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2279,9 +2299,9 @@ Sources: [`packages/llm/llm/src/types.ts:396`](../packages/llm/llm/src/types.ts) ### `event:assistant/attempt.data.stream[0][3].chunk[6]` -SHA-256: `a4ed3446d32625902170fce527fb64a0283d6f4817930f54a16fdce3a482b36a` +SHA-256: `a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1` -Sources: [`packages/llm/llm/src/types.ts:397`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:431`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2293,9 +2313,9 @@ Sources: [`packages/llm/llm/src/types.ts:397`](../packages/llm/llm/src/types.ts) ### `event:assistant/attempt.data.stream[0][3].chunk[6].reason[0]` -SHA-256: `64cfc4dbd3739f0ee040689482cb27058890f1d6113abb1de756836318aeebbc` +SHA-256: `8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0` -Sources: [`packages/llm/llm/src/types.ts:134`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:147`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2306,9 +2326,9 @@ Sources: [`packages/llm/llm/src/types.ts:134`](../packages/llm/llm/src/types.ts) ### `event:assistant/attempt.data.stream[0][3].chunk[6].reason[1]` -SHA-256: `ca13866102dfc53a503fccddeebc9caf667b8a06ef27217dd65de29f0495e65d` +SHA-256: `693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057` -Sources: [`packages/llm/llm/src/types.ts:135`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:148`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2321,7 +2341,7 @@ Sources: [`packages/llm/llm/src/types.ts:135`](../packages/llm/llm/src/types.ts) SHA-256: `ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7` -Sources: [`packages/core/session/src/types.ts:213`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:133`](../packages/llm/llm/src/types.ts) +Sources: [`packages/core/session/src/types.ts:213`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:146`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2333,7 +2353,7 @@ Sources: [`packages/core/session/src/types.ts:213`](../packages/core/session/src SHA-256: `1d76406cbb2ddadb0dbbb51e9d8bc6cf678fed455cf8eb512f36f7c86d37a62f` -Sources: [`packages/llm/llm/src/types.ts:131`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:144`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2345,7 +2365,7 @@ Sources: [`packages/llm/llm/src/types.ts:131`](../packages/llm/llm/src/types.ts) SHA-256: `287211846beea1eccb5f9acee00d20388a38fcb2e95858fa2332d6f58529379a` -Sources: [`packages/llm/llm/src/types.ts:132`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:145`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -2363,7 +2383,7 @@ Array of `unknown` (opaque). ### `event:assistant/message` -SHA-256: `a58ca1bed502d425a8b65cac02f59717498cd296de9513c6f81b83b791c20e9b` +SHA-256: `1169b301aaabcd992657b93ec93750c43175dda81ada5cac086f14f2eaaeed6d` | Property | Presence | Type | |---|---|---| @@ -2378,7 +2398,7 @@ SHA-256: `a58ca1bed502d425a8b65cac02f59717498cd296de9513c6f81b83b791c20e9b` ### `event:assistant/message.data` -SHA-256: `a2418dd9dd938f04a3eaf1f27694b33facfb931b6e495dcfb4d5fcfa2ef82b28` +SHA-256: `77b8491f946edfc8cc77ebae3ba49183759da31ba2dfa83d4c9e9e618c4b91d2` Sources: [`packages/core/session/src/types.ts:321`](../packages/core/session/src/types.ts) @@ -2395,7 +2415,7 @@ Sources: [`packages/core/session/src/types.ts:321`](../packages/core/session/src ### `event:browser-use/stagehand-llm-request` -SHA-256: `7cf11fcc829f21289bb7311562c6601499368464cce8ee678f80500e6a448b01` +SHA-256: `92cda1e2c0e9f57fdcd167a861b1296ec4cb5ac3ae127733c375e108335b32ac` | Property | Presence | Type | |---|---|---| @@ -2409,7 +2429,7 @@ SHA-256: `7cf11fcc829f21289bb7311562c6601499368464cce8ee678f80500e6a448b01` ### `event:browser-use/stagehand-llm-request.data.request.messages` -SHA-256: `f3cfc98798d8b0e726ee22b05b13ada5cb19e9cdbe105ef3529699de99ce357a` +SHA-256: `478738df7e7d2b103b09fb937817a9302c81076809f2b341169e00a9419c5665` Array of [`packages/llm/llm/src/message.ts#Message`](#persistence-type-packagesllmllmsrcmessagetsmessage). @@ -2508,7 +2528,7 @@ Sources: [`packages/util/values/src/index.ts:4`](../packages/util/values/src/ind ### `event:browser-use/stagehand-llm-result` -SHA-256: `05d3b279f065b23cfb5d192a9a95016fe23642d47dbd90378c81be939e8e1b09` +SHA-256: `0e0e81784bc85e1e6554b7ff50ab341169a1a3d32149ae49abe8dddca5ce6efe` | Property | Presence | Type | |---|---|---| @@ -2654,7 +2674,7 @@ Sources: [`packages/compaction/compaction/src/types.ts:82`](../packages/compacti | Property | Presence | Type | |---|---|---| | `shadowedRange` | required | [`event:compaction/prune.data.shadowedRange`](#persistence-type-eventcompactionprunedatashadowedrange) | -| `shadowedSeqs` | required | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `shadowedSeqs` | required | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `shadowedTokenCount` | required | `number` | @@ -2702,7 +2722,7 @@ Sources: [`packages/compaction/compaction/src/types.ts:24`](../packages/compacti ### `event:compaction/summary` -SHA-256: `eb1d32dcd3d76833392f351e039dcbd40686a1ab24b252a7a5003426f9cf4c89` +SHA-256: `f6f3f30109e9008fccf7da2a3268a63da8646b59dd7cf7b75bcd9cf08ac08a59` | Property | Presence | Type | |---|---|---| @@ -2716,7 +2736,7 @@ SHA-256: `eb1d32dcd3d76833392f351e039dcbd40686a1ab24b252a7a5003426f9cf4c89` ### `event:compaction/summary.data` -SHA-256: `0a6fae8c3249943e23a024ae6c0d9dd707df9bff0badc69786f0dc8ad9482d8e` +SHA-256: `feb329074f0206ecc06d8a1aa0a0b3e2f47fd27524ba020fbc849ba63e938896` One of: @@ -2727,7 +2747,7 @@ One of: ### `event:compaction/summary.data[0]` -SHA-256: `1e75cbc0af53f62e5685d751108dc9f6320fe835aed222e579a2a0080dc8fc59` +SHA-256: `9ff2ad8dddb836f75e8f6197fff2371323dfc396c3c6fb667e6e0b42fda79a5c` | Property | Presence | Type | |---|---|---| @@ -2738,7 +2758,7 @@ SHA-256: `1e75cbc0af53f62e5685d751108dc9f6320fe835aed222e579a2a0080dc8fc59` | `provider` | required | `string` | | `rawOutput` | required | [`event:agent/inbox/spliced.data.inserted[0].content`](#persistence-type-eventagentinboxspliceddatainserted0content) | | `shadowedRange` | required | [`event:compaction/prune.data.shadowedRange`](#persistence-type-eventcompactionprunedatashadowedrange) | -| `shadowedSeqs` | required | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `shadowedSeqs` | required | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `shadowedTokenCount` | required | `number` | | `sourceCommandId` | optional | `string` | | `summary` | required | [`event:agent/inbox/spliced.data.inserted[0].content`](#persistence-type-eventagentinboxspliceddatainserted0content) | @@ -2748,7 +2768,7 @@ SHA-256: `1e75cbc0af53f62e5685d751108dc9f6320fe835aed222e579a2a0080dc8fc59` ### `event:compaction/summary.data[1]` -SHA-256: `ac5a8baa3dda8fa667b4975d6a8b40a9bde47d8d754ca8ed6b8ce923a1651828` +SHA-256: `3aa261c8516b5c955ed12080e6257a56cbe513f6c9f0ac9274c12e96c196ebca` | Property | Presence | Type | |---|---|---| @@ -2758,7 +2778,7 @@ SHA-256: `ac5a8baa3dda8fa667b4975d6a8b40a9bde47d8d754ca8ed6b8ce923a1651828` | `provider` | required | `string` | | `rawOutput` | optional | [`event:agent/inbox/spliced.data.inserted[0].content`](#persistence-type-eventagentinboxspliceddatainserted0content) | | `shadowedRange` | required | [`event:compaction/prune.data.shadowedRange`](#persistence-type-eventcompactionprunedatashadowedrange) | -| `shadowedSeqs` | required | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `shadowedSeqs` | required | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `shadowedTokenCount` | required | `number` | | `sourceCommandId` | optional | `string` | | `summary` | required | [`event:agent/inbox/spliced.data.inserted[0].content`](#persistence-type-eventagentinboxspliceddatainserted0content) | @@ -2949,11 +2969,45 @@ Sources: [`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook | `stderrSummary` | optional | `string` | | `turn` | required | `number` | + + +### `event:image/offload` + +SHA-256: `b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e` + +| Property | Presence | Type | +|---|---|---| +| `data` | required | [`event:image/offload.data`](#persistence-type-eventimageoffloaddata) | +| `ignorable` | optional | `true` | +| `seq` | required | `number` | +| `time` | required | `number` | +| `type` | required | `"image/offload"` | + + + +### `event:image/offload.data` + +SHA-256: `477614bca595a9e7adb5a1870be446b9a8684f3ba8950259d6289f2d3b74f028` + +Sources: [`packages/compaction/compaction-image-offload/src/projection.ts:25`](../packages/compaction/compaction-image-offload/src/projection.ts) + +| Property | Presence | Type | +|---|---|---| +| `targets` | required | [`event:image/offload.data.targets`](#persistence-type-eventimageoffloaddatatargets) | + + + +### `event:image/offload.data.targets` + +SHA-256: `9e2c06d8653585b33d72d5c1a80da79c8a24a88c284409bca15cc0f0ead5ebfb` + +Array of [`packages/compaction/compaction-image-offload/src/projection.ts#ImageOffloadTarget`](#persistence-type-packagescompactioncompaction-image-offloadsrcprojectiontsimageoffloadtarget). + ### `event:llm/retry` -SHA-256: `91c397f8f870e812e1dc5ac69c2745f9f105be9dc97dadc980d19ef415a65145` +SHA-256: `525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336` | Property | Presence | Type | |---|---|---| @@ -2981,7 +3035,7 @@ SHA-256: `48e5c9861f16ac07e78cb7b5ae9dabdf7bb85c58baed5a51b4ad275050ea58e3` ### `event:llm/retry.data[0]` -SHA-256: `9096d1dd86a3da77a593e12cd95e596b2ef965fa94fcbee40e4bdcf0433924a1` +SHA-256: `cdee10e87dbed8440d8b6935888624884878efc7af95402e94171b29f173dced` Sources: [`packages/llm/llm-retry/src/types.ts:17`](../packages/llm/llm-retry/src/types.ts) @@ -3002,7 +3056,7 @@ Sources: [`packages/llm/llm-retry/src/types.ts:17`](../packages/llm/llm-retry/sr ### `event:llm/retry.data[1]` -SHA-256: `c0f91c064454e9ae25a4b5caacdc487712aee3acf414b07b7d5f7fd3530a0452` +SHA-256: `374fdd68aee5ff844e378c0cbdd961d247a0f22e4ade0bb03d800cd2a46a2b8b` Sources: [`packages/llm/llm-retry/src/types.ts:30`](../packages/llm/llm-retry/src/types.ts) @@ -3239,7 +3293,7 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` ### `event:session/title-llm-request` -SHA-256: `6091f64d426ada2adc646094a6fe662a1b81d9ee5770929fe76800524e2c89a8` +SHA-256: `e0b5bf44c27bbfc6ab144239e3e4169c4d21146645e0e3121bbb359c3bc8591d` | Property | Presence | Type | |---|---|---| @@ -3410,14 +3464,14 @@ Array of [`packages/session/session-title/src/types.ts#SessionTitleModelIdentity ### `event:system/message` -SHA-256: `0ae7ccdc1cc5d9d43772ebd5c7357ff509011c3a6ba188d9c274d33df2542c42` +SHA-256: `69becfb6b2d3fd5da91518089454cae8ef33f1835637ec44dde35dd077fd4bae` | Property | Presence | Type | |---|---|---| | `data` | required | [`event:system/message.data`](#persistence-type-eventsystemmessagedata) | | `ignorable` | optional | `true` | | `seq` | required | `number` | -| `sourceEventSeqs` | optional | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `sourceEventSeqs` | optional | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `surfaceOp` | required | [`packages/core/session/src/types.ts#SurfaceOp`](#persistence-type-packagescoresessionsrctypestssurfaceop) | | `time` | required | `number` | | `type` | required | `"system/message"` | @@ -3426,7 +3480,7 @@ SHA-256: `0ae7ccdc1cc5d9d43772ebd5c7357ff509011c3a6ba188d9c274d33df2542c42` ### `event:system/message.data` -SHA-256: `d489162978ec04f1f76f40a68c7715326f46ec4f696edf1f321efdf88545fc18` +SHA-256: `2074f40221b76c75fd314246db558965dc3477dfb440bcc952b1eedc9778e366` Sources: [`packages/core/session/src/types.ts:310`](../packages/core/session/src/types.ts) @@ -3524,7 +3578,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:227`](../packages/exper ### `event:team/message/queued` -SHA-256: `2ffcad547cb840f462eb1849fa5f6cb0e17eb93f5d07cc88b61d749b1b78d632` +SHA-256: `443371ec07a03a82a0e93d93abca3e70b03bca55ba5b01d507030fcb66e8fbb4` | Property | Presence | Type | |---|---|---| @@ -3538,7 +3592,7 @@ SHA-256: `2ffcad547cb840f462eb1849fa5f6cb0e17eb93f5d07cc88b61d749b1b78d632` ### `event:team/message/queued.data` -SHA-256: `6132f7814b28287473605ebc6dacd63c7c699435452487da6152bc735b8ba8c4` +SHA-256: `0c3f4d2248d9bc9700e2d7253fa71aa9935f5c39af6f11de5727f08cccaf8383` Sources: [`packages/experimental/agent-team/src/types.ts:225`](../packages/experimental/agent-team/src/types.ts) @@ -3712,7 +3766,7 @@ Sources: [`packages/core/session/src/types.ts:341`](../packages/core/session/src ### `event:tool/ptc-dispatch` -SHA-256: `b5d66eaebed4da391b13498623b11142222149fbf5e025975dbc6d94f0d06796` +SHA-256: `b33142af8176323ffbb20762541be81c1d51b60a60887803836e0b146e5bef0c` | Property | Presence | Type | |---|---|---| @@ -3754,14 +3808,14 @@ Sources: [`packages/core/session/src/types.ts:363`](../packages/core/session/src ### `event:tool/result` -SHA-256: `3a803805bdeb805f32b229e399fb7258be8e32a89f89063a7c99957cfea942f7` +SHA-256: `29af48b840d0cd9e48b6f50bf3b354f8f6340607c5b99220a48e74f60beac9e2` | Property | Presence | Type | |---|---|---| | `data` | required | [`event:tool/result.data`](#persistence-type-eventtoolresultdata) | | `ignorable` | optional | `true` | | `seq` | required | `number` | -| `sourceEventSeqs` | optional | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `sourceEventSeqs` | optional | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `surfaceOp` | required | [`packages/core/session/src/types.ts#SurfaceOp`](#persistence-type-packagescoresessionsrctypestssurfaceop) | | `time` | required | `number` | | `type` | required | `"tool/result"` | @@ -3770,7 +3824,7 @@ SHA-256: `3a803805bdeb805f32b229e399fb7258be8e32a89f89063a7c99957cfea942f7` ### `event:tool/result.data` -SHA-256: `28aa673866746f447863d828aa25213135344d61e6c568b6ca2c1e729cfff6b3` +SHA-256: `356aad27134a0e31af67c994ff8b9e4b91acfb6c4917b3bc1f7a62bbf82794d5` Sources: [`packages/core/session/src/types.ts:355`](../packages/core/session/src/types.ts) @@ -3786,7 +3840,7 @@ Sources: [`packages/core/session/src/types.ts:355`](../packages/core/session/src ### `event:tool/result.data.message.content` -SHA-256: `5eedf09e88b5ed9ca2d5efdb5a32cd4f51352564af5ff82927c17d841a2e3d9e` +SHA-256: `8d6a011f3ef7096a4a922ef420458419ff6b20e1eaeece5d331db7585d2f08d6` | Position | Presence | Type | |---|---|---| @@ -3796,7 +3850,7 @@ SHA-256: `5eedf09e88b5ed9ca2d5efdb5a32cd4f51352564af5ff82927c17d841a2e3d9e` ### `event:turn/end` -SHA-256: `84c24f1209fc3e0153de6ac85d58fd6968b851f955e0e761b92321053956609e` +SHA-256: `bab768260853e13a7cf2e22e65af572a1e76a5b2c01f3cdc2ce09d39b87fb70b` | Property | Presence | Type | |---|---|---| @@ -3810,7 +3864,7 @@ SHA-256: `84c24f1209fc3e0153de6ac85d58fd6968b851f955e0e761b92321053956609e` ### `event:turn/end.data` -SHA-256: `508c0856b8df8574e396880a756f087699b138630428441fefca4a4380f01a28` +SHA-256: `d3e447062a2847dad847dad5168a9508d92c484363dfc76fd82605aa7bd46285` Sources: [`packages/core/session/src/types.ts:285`](../packages/core/session/src/types.ts) @@ -3909,7 +3963,7 @@ Sources: [`packages/core/session/src/types.ts:205`](../packages/core/session/src ### `event:turn/end.data.reason[3]` -SHA-256: `3dad205ad99fbda835c493ddc6bc93fe1441cef2f82774ffa2856364e2cede22` +SHA-256: `79650cfa10aa3c52440328d85e8a25f0ffadc8d7efd784e3c0819d8bb74a1985` Sources: [`packages/core/session/src/types.ts:211`](../packages/core/session/src/types.ts) @@ -3960,14 +4014,14 @@ Sources: [`packages/core/session/src/types.ts:276`](../packages/core/session/src ### `event:user/message` -SHA-256: `2e49ede7fb3a592cce8919ebe982f6ca1c1f67abe4def359ffc028c85e6d1ad8` +SHA-256: `314765bdff29c7862fb6ce820f1773563ba3094a680d163ea21180a2591b8578` | Property | Presence | Type | |---|---|---| | `data` | required | [`packages/llm/llm/src/message.ts#UserMessage`](#persistence-type-packagesllmllmsrcmessagetsusermessage) | | `ignorable` | optional | `true` | | `seq` | required | `number` | -| `sourceEventSeqs` | optional | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `sourceEventSeqs` | optional | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `surfaceOp` | required | [`packages/core/session/src/types.ts#SurfaceOp`](#persistence-type-packagescoresessionsrctypestssurfaceop) | | `time` | required | `number` | | `type` | required | `"user/message"` | @@ -4040,7 +4094,7 @@ SHA-256: `8f6d609794bf5afc01d7ccf3e03811d32f4b8047f58a2c9a98db0154c2ad7cb9` SHA-256: `4e887768586528565381dadbddee6cef555874148089f4579e0b4b1ad096fc9c` -Sources: [`packages/llm/llm/src/types.ts:54`](../packages/llm/llm/src/types.ts) · [`packages/web/web-search-deepseek/src/provider.ts:66`](../packages/web/web-search-deepseek/src/provider.ts) +Sources: [`packages/llm/llm/src/types.ts:61`](../packages/llm/llm/src/types.ts) · [`packages/web/web-search-deepseek/src/provider.ts:66`](../packages/web/web-search-deepseek/src/provider.ts) | Property | Presence | Type | |---|---|---| @@ -4223,6 +4277,14 @@ SHA-256: `3f74ba74acd10644c1ed6947a411d8088bd2cfac74f61bb8540f96d68e31f004` `"image/jpeg"` + + +### `"image/offload"` + +SHA-256: `9acbe713f3f641921dd3333b240aa72e176d795327c24e8d9db9b68b16771b49` + +`"image/offload"` + ### `"image/png"` @@ -4245,7 +4307,7 @@ SHA-256: `ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8` SHA-256: `f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5` -Sources: [`packages/llm/llm/src/types.ts:347`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:381`](../packages/llm/llm/src/types.ts) `"in-history"` @@ -4488,6 +4550,19 @@ One of: - `"image/png"` - `"image/webp"` + + +### `packages/compaction/compaction-image-offload/src/projection.ts#ImageOffloadTarget` + +SHA-256: `1cde1bedde79c5e8a56aee9c05c8e0a579cf5bc611deb85b93fa331a16ca978f` + +Sources: [`packages/compaction/compaction-image-offload/src/projection.ts:9`](../packages/compaction/compaction-image-offload/src/projection.ts) + +| Property | Presence | Type | +|---|---|---| +| `imageIndexes` | required | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | +| `seq` | required | `number` | + ### `packages/context/agent-instructions/src/render.ts#AgentInstructionChange` @@ -4644,7 +4719,7 @@ One of: ### `packages/core/session/src/types.ts#TurnEndReason` -SHA-256: `e3528b95cff70d83a080a96666877da3f9dc205e71955d6bc5b9b315b9e0da22` +SHA-256: `a5a8c15f08af1b0f26f40906e0797b6b0920fd01069e660becce35ab7fc48d77` Sources: [`packages/core/session/src/types.ts:224`](../packages/core/session/src/types.ts) @@ -4674,7 +4749,7 @@ Sources: [`packages/core/tools/src/index.ts:682`](../packages/core/tools/src/ind ### `packages/core/tools/src/types.ts#PtcDispatchEventData` -SHA-256: `4c12ebe118c00be826b47b050eb5998f019df4bc3d8ebd7a93f5921c0e73617b` +SHA-256: `8d2e5a3c4230e6ea6c0b5151f02241d0db02cd2551f2df1b6b9700cddcc0dca3` Sources: [`packages/core/tools/src/types.ts:20`](../packages/core/tools/src/types.ts) @@ -4741,7 +4816,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:47`](../packages/experi ### `packages/experimental/agent-team/src/types.ts#TeamMessageSnapshot` -SHA-256: `10ce3a084c136b5fc9969e5981b64711c35a2d9de7eac1b8443c03f73dc4f3db` +SHA-256: `5b694f394d5e11609887a3ccfa82844935d49806c37e336b30be2186d649ab94` Sources: [`packages/experimental/agent-team/src/types.ts:106`](../packages/experimental/agent-team/src/types.ts) @@ -4807,7 +4882,7 @@ One of: ### `packages/experimental/browser-use-stagehand-native/src/model.ts#RecordedRequest` -SHA-256: `07d0e11cb5f42f2841b28814ceb5e91fb12c43249343f7ed716ec3646a081229` +SHA-256: `4a797416c9d5d0ef7b52baef65c9122332c517364fda90388d994fc4fc62baa4` Sources: [`packages/experimental/browser-use-stagehand-native/src/model.ts:14`](../packages/experimental/browser-use-stagehand-native/src/model.ts) @@ -4829,7 +4904,7 @@ Sources: [`packages/experimental/browser-use-stagehand-native/src/model.ts:14`]( ### `packages/experimental/browser-use-stagehand-native/src/model.ts#StagehandLlmRequest` -SHA-256: `4ef9f82945d583962637197c7d4082b43c54c7c0bb131f19c40a906369a5abfc` +SHA-256: `07796b041bf5e5516b3c9cf80292a19bd1e518ecf11e461a6edb19780d0e7da9` Sources: [`packages/experimental/browser-use-stagehand-native/src/model.ts:17`](../packages/experimental/browser-use-stagehand-native/src/model.ts) @@ -4842,7 +4917,7 @@ Sources: [`packages/experimental/browser-use-stagehand-native/src/model.ts:17`]( ### `packages/experimental/browser-use-stagehand-native/src/model.ts#StagehandLlmResult` -SHA-256: `0b0f2af6d7b1273fcb7a2b5d6321b35b49f9186d065ffb99f2e392949b787c6e` +SHA-256: `2ca560fd44e47e89681d323fb02d7194733a9bf524d3a88a189bfac3c60632b1` Sources: [`packages/experimental/browser-use-stagehand-native/src/model.ts:25`](../packages/experimental/browser-use-stagehand-native/src/model.ts) @@ -5101,7 +5176,7 @@ One of: ### `packages/llm/llm-retry/src/types.ts#LlmRetryEventData` -SHA-256: `35b995af35bfa087f5381a5454a2305928da037e819bfc4759d3f66d9bd55aa9` +SHA-256: `c170da8ab7c37635b10867b2571811d9e317ac3ec394e8ff8efe53f902fc41c1` Sources: [`packages/llm/llm-retry/src/types.ts:16`](../packages/llm/llm-retry/src/types.ts) @@ -5129,7 +5204,7 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr ### `packages/llm/llm/src/assistant-stream.ts#AssistantStreamRecord` -SHA-256: `ecaf9469000a15d46d269ea72994ceaab3652ed31507822906fc12397422da21` +SHA-256: `86cf1e44038cb8b11659863dd0efafe682e41b9c782265c1d50e0a687debfc60` Sources: [`packages/llm/llm/src/assistant-stream.ts:20`](../packages/llm/llm/src/assistant-stream.ts) @@ -5174,7 +5249,7 @@ Sources: [`packages/llm/llm/src/call-config.ts:36`](../packages/llm/llm/src/call ### `packages/llm/llm/src/message.ts#AssistantMessage` -SHA-256: `08a1293eddb9b633bfefaf0893e3b4c807dfbb49c45a447cb32bb68bfd1a8232` +SHA-256: `e50b0a3ed8e6c80b672e4f855c91984f03b5c3fdd5d475bf171171f391a86c83` Sources: [`packages/llm/llm/src/message.ts:148`](../packages/llm/llm/src/message.ts) @@ -5202,7 +5277,7 @@ Sources: [`packages/llm/llm/src/message.ts:65`](../packages/llm/llm/src/message. ### `packages/llm/llm/src/message.ts#Message` -SHA-256: `f051068babb7ebab80e9fd4fff3a70cfe54430c2ef3ca9bb77bb54db8bc34476` +SHA-256: `23c7cce57d20589897f4e2bd5898affc6fc611b87cd0cc8b95be1cfdd8b3a7eb` Sources: [`packages/llm/llm/src/message.ts:131`](../packages/llm/llm/src/message.ts) @@ -5263,7 +5338,7 @@ Sources: [`packages/llm/llm/src/message.ts:24`](../packages/llm/llm/src/message. ### `packages/llm/llm/src/message.ts#SystemMessage` -SHA-256: `7973d82bd129b261f16a9a20d44df834ec60efc52a63242b4943a2515ae0894f` +SHA-256: `302b01ddddcbb3b16cfa3fba2a5d99f5082a55fbd9dc5c83a179a228b8a6a0ca` Sources: [`packages/llm/llm/src/message.ts:158`](../packages/llm/llm/src/message.ts) @@ -5291,7 +5366,7 @@ Sources: [`packages/llm/llm/src/message.ts:29`](../packages/llm/llm/src/message. ### `packages/llm/llm/src/message.ts#ToolResultMessage` -SHA-256: `966fad765834f62051ad6f6a100f7ea38abe5f40f7ef3ae0b6dca0e31c7549a5` +SHA-256: `1b573e2d5c094fa87e2a8bf409ddd4ac5ba92a123085e168de44b9ccdbd5244c` Sources: [`packages/llm/llm/src/message.ts:164`](../packages/llm/llm/src/message.ts) @@ -5306,7 +5381,7 @@ Sources: [`packages/llm/llm/src/message.ts:164`](../packages/llm/llm/src/message ### `packages/llm/llm/src/message.ts#UserMessage` -SHA-256: `40390271dffbed81bffa514370ad1c3c19a13ad10c3647c9c0eae31126c95a5f` +SHA-256: `4de5e7644130ed715ef6b5a4d86a87b30dca4096d1a6683c66acf6f7809379c7` Sources: [`packages/llm/llm/src/message.ts:143`](../packages/llm/llm/src/message.ts) @@ -5321,9 +5396,9 @@ Sources: [`packages/llm/llm/src/message.ts:143`](../packages/llm/llm/src/message ### `packages/llm/llm/src/types.ts#ContentBlock` -SHA-256: `e1161343902a8da654d4d1bb66d729065cc93a2710ebfe2717a242a5c2ec709a` +SHA-256: `c7b24be059d34d08929593049a1a81eaa3855166aacfe9d7603c0bc8bb728d56` -Sources: [`packages/llm/llm/src/types.ts:124`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:137`](../packages/llm/llm/src/types.ts) One of: @@ -5340,7 +5415,7 @@ One of: SHA-256: `757013fff5879d73d5753d8d1120faab56110e88ac3932485ab7e034d9e9ce61` -Sources: [`packages/llm/llm/src/types.ts:122`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:135`](../packages/llm/llm/src/types.ts) One of: @@ -5357,7 +5432,7 @@ One of: SHA-256: `f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef` -Sources: [`packages/llm/llm/src/types.ts:84`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:97`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5368,9 +5443,9 @@ Sources: [`packages/llm/llm/src/types.ts:84`](../packages/llm/llm/src/types.ts) ### `packages/llm/llm/src/types.ts#FinishReason` -SHA-256: `5ed158429dfa6a8633e6ba94b8c81190616c24b65c1777bf1c60cde9dd499a2f` +SHA-256: `5808e36c95355f0e745201b5db1b6bcec4ebf5adb75d3e82d7d59cc89635ab80` -Sources: [`packages/llm/llm/src/types.ts:139`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:152`](../packages/llm/llm/src/types.ts) One of: @@ -5384,20 +5459,21 @@ One of: ### `packages/llm/llm/src/types.ts#ImageBlock` -SHA-256: `8ef06dd236128246969d5f04a6c4a639d031fcda99a0cd13051f52f41b0ca1af` +SHA-256: `12d5808fac1996b9b3c2afa806e03022252ba2d7b88f835862ae1896591f60c7` -Sources: [`packages/llm/llm/src/types.ts:71`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:78`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| | `attachment` | required | [`packages/attachment/attachment/src/types.ts#ImageAttachmentRef`](#persistence-type-packagesattachmentattachmentsrctypestsimageattachmentref) | +| `offloaded` | optional | `true` | | `type` | required | `"image"` | ### `packages/llm/llm/src/types.ts#LlmFailure` -SHA-256: `27cd736e794a0f8703a155fb697820cede894d10776586dd3df24d24c9465c0e` +SHA-256: `5f2117d1856a5ab43c28371faa978ebfaa48f8c7d734ea3fc94842210df83dca` Sources: [`packages/llm/llm/src/types.ts:40`](../packages/llm/llm/src/types.ts) @@ -5405,6 +5481,7 @@ Sources: [`packages/llm/llm/src/types.ts:40`](../packages/llm/llm/src/types.ts) |---|---|---| | `code` | required | `string` | | `message` | required | `string` | +| `offloadImages` | optional | `number` | | `providerRetryAfterMs` | optional | `number` | | `requestId` | optional | `string` | | `status` | optional | `number` | @@ -5415,7 +5492,7 @@ Sources: [`packages/llm/llm/src/types.ts:40`](../packages/llm/llm/src/types.ts) SHA-256: `814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e` -Sources: [`packages/llm/llm/src/types.ts:60`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:67`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5428,7 +5505,7 @@ Sources: [`packages/llm/llm/src/types.ts:60`](../packages/llm/llm/src/types.ts) SHA-256: `0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555` -Sources: [`packages/llm/llm/src/types.ts:368`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:402`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5439,9 +5516,9 @@ Sources: [`packages/llm/llm/src/types.ts:368`](../packages/llm/llm/src/types.ts) ### `packages/llm/llm/src/types.ts#StreamChunk` -SHA-256: `989145d2b165a1e4391d0691196ede9dec5093b09f1cf3d33b1911c24ee6c5ee` +SHA-256: `071d92b9dac661fcc826c678cf64d188f26fb93aab79c627b11a03acdd34dfca` -Sources: [`packages/llm/llm/src/types.ts:390`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:424`](../packages/llm/llm/src/types.ts) One of: @@ -5459,7 +5536,7 @@ One of: SHA-256: `fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394` -Sources: [`packages/llm/llm/src/types.ts:149`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:162`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5476,7 +5553,7 @@ Sources: [`packages/llm/llm/src/types.ts:149`](../packages/llm/llm/src/types.ts) SHA-256: `c1f0f17b6493b85db6453bb30e01b208d703731b25bc1e68e5d4fc72eea9cdea` -Sources: [`packages/llm/llm/src/types.ts:91`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:104`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5489,9 +5566,9 @@ Sources: [`packages/llm/llm/src/types.ts:91`](../packages/llm/llm/src/types.ts) ### `packages/llm/llm/src/types.ts#ToolResultBlock` -SHA-256: `9bdd77f2b39d9ae47be71e494eca8c9e950b0e99b89a4539f86c4a0e7d1398b6` +SHA-256: `d61574d68558c5a184429e70b04da393dd5460bf5cc3ddef814546e659bd5127` -Sources: [`packages/llm/llm/src/types.ts:101`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:114`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5506,7 +5583,7 @@ Sources: [`packages/llm/llm/src/types.ts:101`](../packages/llm/llm/src/types.ts) SHA-256: `7f5f2a8618c2ece530a18b1358b890fb57fb8c35705e47b66bff2296f8c6e889` -Sources: [`packages/llm/llm/src/types.ts:411`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:445`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5686,14 +5763,14 @@ Sources: [`packages/session/session-persistence-jsonl/src/format.ts:82`](../pack ### `packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData` -SHA-256: `08936f7648f573f7539800e1ec2852fbbcba05e1a20487a1e5acf2a0fce1e2de` +SHA-256: `9fe0e77a7816b7fdb4b953ec0e2f0cd7bbd22c5de5c3567e612d1b20eb0992e6` Sources: [`packages/session/session-title-llm/src/index.ts:27`](../packages/session/session-title-llm/src/index.ts) | Property | Presence | Type | |---|---|---| | `maxTokens` | required | `number` | -| `messageSeqs` | required | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `messageSeqs` | required | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `messages` | required | [`event:browser-use/stagehand-llm-request.data.request.messages`](#persistence-type-eventbrowser-usestagehand-llm-requestdatarequestmessages) | | `route` | required | [`packages/session/session-title/src/types.ts#SessionTitleModelIdentity`](#persistence-type-packagessessionsession-titlesrctypestssessiontitlemodelidentity) | | `system` | required | `string` | @@ -5709,7 +5786,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ | Property | Presence | Type | |---|---|---| -| `messageSeqs` | required | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `messageSeqs` | required | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `source` | required | [`packages/session/session-title/src/types.ts#SessionTitleSource`](#persistence-type-packagessessionsession-titlesrctypestssessiontitlesource) | | `title` | required | `string` | @@ -6250,7 +6327,7 @@ SHA-256: `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` One of: -- [`SessionEventEnvelope[40]`](#persistence-type-sessioneventenvelope40) +- [`SessionEventEnvelope[41]`](#persistence-type-sessioneventenvelope41) - [`SessionEventEnvelope[6]`](#persistence-type-sessioneventenvelope6) - [`SessionEventEnvelope[0]`](#persistence-type-sessioneventenvelope0) @@ -6267,9 +6344,9 @@ SHA-256: `e3e77b26f0148755a505f5b8ea843a3be117827d4703bb5754d9dc8ddbef521f` | `time` | required | `number` | | `type` | required | `string` | - + -### `SessionEventEnvelope[40]` +### `SessionEventEnvelope[41]` SHA-256: `998f22585a5d73ec005365e203f07fc3b3a029e82323fdda3fd7f13b0a9639d0` @@ -6277,14 +6354,14 @@ SHA-256: `998f22585a5d73ec005365e203f07fc3b3a029e82323fdda3fd7f13b0a9639d0` |---|---|---| | `ignorable` | optional | `true` | | `seq` | required | `number` | -| `sourceEventSeqs` | optional | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `sourceEventSeqs` | optional | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `surfaceOp` | required | [`packages/core/session/src/types.ts#SurfaceOp`](#persistence-type-packagescoresessionsrctypestssurfaceop) | | `time` | required | `number` | | `type` | required | `string` | - + -### `SessionEventEnvelope[40].sourceEventSeqs` +### `SessionEventEnvelope[41].sourceEventSeqs` SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index c4da4dd7c0..b8be658b11 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -23,20 +23,20 @@ | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`packages/session/session-persistence-jsonl/src/format.ts#HeaderLine`](#persistence-type-packagessessionsession-persistence-jsonlsrcformattsheaderline) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`SessionEventEnvelope`](#persistence-type-sessioneventenvelope) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`event:agent-preset/selected`](#persistence-type-eventagent-presetselected) | -| `event:agent/inbox/spliced` | event | `f757322e25914d63799c6235af4ed4b4f36d7d083fef40d6873a45e94fe6ad65` | [`event:agent/inbox/spliced`](#persistence-type-eventagentinboxspliced) | +| `event:agent/inbox/spliced` | event | `646c2f1d243d3a78c5bc9305786fb340f5209c48a97e65f2a186cc5195491f3f` | [`event:agent/inbox/spliced`](#persistence-type-eventagentinboxspliced) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`event:approval/asked`](#persistence-type-eventapprovalasked) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`event:approval/decided`](#persistence-type-eventapprovaldecided) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`event:approval/policy`](#persistence-type-eventapprovalpolicy) | -| `event:assistant/attempt` | event | `3516f54b427775f483febe8e938e4e10f9afd3583610861547bbe2999ed4d8d9` | [`event:assistant/attempt`](#persistence-type-eventassistantattempt) | -| `event:assistant/message` | event | `a58ca1bed502d425a8b65cac02f59717498cd296de9513c6f81b83b791c20e9b` | [`event:assistant/message`](#persistence-type-eventassistantmessage) | -| `event:browser-use/stagehand-llm-request` | event | `7cf11fcc829f21289bb7311562c6601499368464cce8ee678f80500e6a448b01` | [`event:browser-use/stagehand-llm-request`](#persistence-type-eventbrowser-usestagehand-llm-request) | -| `event:browser-use/stagehand-llm-result` | event | `05d3b279f065b23cfb5d192a9a95016fe23642d47dbd90378c81be939e8e1b09` | [`event:browser-use/stagehand-llm-result`](#persistence-type-eventbrowser-usestagehand-llm-result) | +| `event:assistant/attempt` | event | `c80c89da83c46db7a454f034c10f969e03cfb574859c7316e5bff683f5a14b0e` | [`event:assistant/attempt`](#persistence-type-eventassistantattempt) | +| `event:assistant/message` | event | `1169b301aaabcd992657b93ec93750c43175dda81ada5cac086f14f2eaaeed6d` | [`event:assistant/message`](#persistence-type-eventassistantmessage) | +| `event:browser-use/stagehand-llm-request` | event | `92cda1e2c0e9f57fdcd167a861b1296ec4cb5ac3ae127733c375e108335b32ac` | [`event:browser-use/stagehand-llm-request`](#persistence-type-eventbrowser-usestagehand-llm-request) | +| `event:browser-use/stagehand-llm-result` | event | `0e0e81784bc85e1e6554b7ff50ab341169a1a3d32149ae49abe8dddca5ce6efe` | [`event:browser-use/stagehand-llm-result`](#persistence-type-eventbrowser-usestagehand-llm-result) | | `event:command/done` | event | `15196447222782e773eb943c92b18316ce96b9af0f0cfddb6e57ba8274ecc5ff` | [`event:command/done`](#persistence-type-eventcommanddone) | | `event:command/run` | event | `37184378c6439257d105c4e2022d80fc9c3a3f7c7f6ac661b00bc9f18d871006` | [`event:command/run`](#persistence-type-eventcommandrun) | | `event:compaction/end` | event | `b0127044ab31a702bddfd785d345f5abd7a70876746e895ce443afa3e60ddf2d` | [`event:compaction/end`](#persistence-type-eventcompactionend) | | `event:compaction/prune` | event | `7f7fd5a6b0064f597534b29ff62ef26e786dffccf5e14f654a7d4fcea2c35f04` | [`event:compaction/prune`](#persistence-type-eventcompactionprune) | | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`event:compaction/start`](#persistence-type-eventcompactionstart) | -| `event:compaction/summary` | event | `eb1d32dcd3d76833392f351e039dcbd40686a1ab24b252a7a5003426f9cf4c89` | [`event:compaction/summary`](#persistence-type-eventcompactionsummary) | +| `event:compaction/summary` | event | `f6f3f30109e9008fccf7da2a3268a63da8646b59dd7cf7b75bcd9cf08ac08a59` | [`event:compaction/summary`](#persistence-type-eventcompactionsummary) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`event:deliverables/presented`](#persistence-type-eventdeliverablespresented) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`event:feedback/message-delete`](#persistence-type-eventfeedbackmessage-delete) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`event:feedback/message-put`](#persistence-type-eventfeedbackmessage-put) | @@ -44,7 +44,8 @@ | `event:goal/change` | event | `763c8a20af487263a0080548274f7437ef4a86e0ba8769127d1ced10a72c664d` | [`event:goal/change`](#persistence-type-eventgoalchange) | | `event:hook/invoked` | event | `8a6e1ec9e8db346b0e02f027db73c07a94f067a26d40c1aef1abd09c47ce7ba0` | [`event:hook/invoked`](#persistence-type-eventhookinvoked) | | `event:hook/result` | event | `e75916628f3f10c2d50658bd143052a46285fbf1a9a700ba54947614603d26b4` | [`event:hook/result`](#persistence-type-eventhookresult) | -| `event:llm/retry` | event | `91c397f8f870e812e1dc5ac69c2745f9f105be9dc97dadc980d19ef415a65145` | [`event:llm/retry`](#persistence-type-eventllmretry) | +| `event:image/offload` | event | `b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e` | [`event:image/offload`](#persistence-type-eventimageoffload) | +| `event:llm/retry` | event | `525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336` | [`event:llm/retry`](#persistence-type-eventllmretry) | | `event:llm/retry-started` | event | `48e5c9861f16ac07e78cb7b5ae9dabdf7bb85c58baed5a51b4ad275050ea58e3` | [`event:llm/retry-started`](#persistence-type-eventllmretry-started) | | `event:model/selection` | event | `35203ba7ad5ef6f97d556b85df20ae98f04f09c65748cecdf8eefdb8b6405ffc` | [`event:model/selection`](#persistence-type-eventmodelselection) | | `event:permission/preset` | event | `5c45bf4c544a7211dcd8ba6ba7e5f1bc39b49e7a9df9d5cbdc8e87c22771b37b` | [`event:permission/preset`](#persistence-type-eventpermissionpreset) | @@ -56,16 +57,16 @@ | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`event:session-log-deepseek/delivery-accepted`](#persistence-type-eventsession-log-deepseekdelivery-accepted) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`event:session/end-seed`](#persistence-type-eventsessionend-seed) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`event:session/title`](#persistence-type-eventsessiontitle) | -| `event:session/title-llm-request` | event | `6091f64d426ada2adc646094a6fe662a1b81d9ee5770929fe76800524e2c89a8` | [`event:session/title-llm-request`](#persistence-type-eventsessiontitle-llm-request) | +| `event:session/title-llm-request` | event | `e0b5bf44c27bbfc6ab144239e3e4169c4d21146645e0e3121bbb359c3bc8591d` | [`event:session/title-llm-request`](#persistence-type-eventsessiontitle-llm-request) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`event:step/end`](#persistence-type-eventstepend) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`event:step/start`](#persistence-type-eventstepstart) | | `event:subagent/catalog` | event | `ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871` | [`event:subagent/catalog`](#persistence-type-eventsubagentcatalog) | | `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`event:subagent/descriptor`](#persistence-type-eventsubagentdescriptor) | | `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`event:subagent/model-selection-policy`](#persistence-type-eventsubagentmodel-selection-policy) | -| `event:system/message` | event | `0ae7ccdc1cc5d9d43772ebd5c7357ff509011c3a6ba188d9c274d33df2542c42` | [`event:system/message`](#persistence-type-eventsystemmessage) | +| `event:system/message` | event | `69becfb6b2d3fd5da91518089454cae8ef33f1835637ec44dde35dd077fd4bae` | [`event:system/message`](#persistence-type-eventsystemmessage) | | `event:team/member` | event | `4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc` | [`event:team/member`](#persistence-type-eventteammember) | | `event:team/message/delivered` | event | `48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb` | [`event:team/message/delivered`](#persistence-type-eventteammessagedelivered) | -| `event:team/message/queued` | event | `2ffcad547cb840f462eb1849fa5f6cb0e17eb93f5d07cc88b61d749b1b78d632` | [`event:team/message/queued`](#persistence-type-eventteammessagequeued) | +| `event:team/message/queued` | event | `443371ec07a03a82a0e93d93abca3e70b03bca55ba5b01d507030fcb66e8fbb4` | [`event:team/message/queued`](#persistence-type-eventteammessagequeued) | | `event:team/task` | event | `d595ec73b32b016a6055333c67a5d646032b22e672da1d4a09c5b1ae398a093a` | [`event:team/task`](#persistence-type-eventteamtask) | | `event:todo/write` | event | `b978cff734e62143eb56c9125423ec275405eda969802d42aaf73ecb987d3b26` | [`event:todo/write`](#persistence-type-eventtodowrite) | | `event:tool-workflow/agent-end` | event | `babf9ee4d1af62bf6c3a8103737f7a5e4e78ce179be835ce05a38803e15884b7` | [`event:tool-workflow/agent-end`](#persistence-type-eventtool-workflowagent-end) | @@ -73,12 +74,12 @@ | `event:tool-workflow/run-end` | event | `42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f` | [`event:tool-workflow/run-end`](#persistence-type-eventtool-workflowrun-end) | | `event:tool-workflow/run-start` | event | `c1f9e0405de6d18cabb9ee70782a027f9bbdc57e5abec9dcccdd56119e2e9058` | [`event:tool-workflow/run-start`](#persistence-type-eventtool-workflowrun-start) | | `event:tool/call` | event | `3b1be838223869fe0a08210db85bf773796ed3f2373ac16555dff227cade0c48` | [`event:tool/call`](#persistence-type-eventtoolcall) | -| `event:tool/ptc-dispatch` | event | `b5d66eaebed4da391b13498623b11142222149fbf5e025975dbc6d94f0d06796` | [`event:tool/ptc-dispatch`](#persistence-type-eventtoolptc-dispatch) | +| `event:tool/ptc-dispatch` | event | `b33142af8176323ffbb20762541be81c1d51b60a60887803836e0b146e5bef0c` | [`event:tool/ptc-dispatch`](#persistence-type-eventtoolptc-dispatch) | | `event:tool/ptc-dispatch-start` | event | `ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93` | [`event:tool/ptc-dispatch-start`](#persistence-type-eventtoolptc-dispatch-start) | -| `event:tool/result` | event | `3a803805bdeb805f32b229e399fb7258be8e32a89f89063a7c99957cfea942f7` | [`event:tool/result`](#persistence-type-eventtoolresult) | -| `event:turn/end` | event | `84c24f1209fc3e0153de6ac85d58fd6968b851f955e0e761b92321053956609e` | [`event:turn/end`](#persistence-type-eventturnend) | +| `event:tool/result` | event | `29af48b840d0cd9e48b6f50bf3b354f8f6340607c5b99220a48e74f60beac9e2` | [`event:tool/result`](#persistence-type-eventtoolresult) | +| `event:turn/end` | event | `bab768260853e13a7cf2e22e65af572a1e76a5b2c01f3cdc2ce09d39b87fb70b` | [`event:turn/end`](#persistence-type-eventturnend) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`event:turn/start`](#persistence-type-eventturnstart) | -| `event:user/message` | event | `2e49ede7fb3a592cce8919ebe982f6ca1c1f67abe4def359ffc028c85e6d1ad8` | [`event:user/message`](#persistence-type-eventusermessage) | +| `event:user/message` | event | `314765bdff29c7862fb6ce820f1773563ba3094a680d163ea21180a2591b8578` | [`event:user/message`](#persistence-type-eventusermessage) | | `event:web/deepseek-search-llm-request` | event | `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331` | [`event:web/deepseek-search-llm-request`](#persistence-type-eventwebdeepseek-search-llm-request) | ## 事件信封 @@ -610,6 +611,25 @@ export type SessionEvent = { 来源:[`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook-protocol/src/types.ts) +### `image/*` + + + +#### `image/offload` — log-only + +```ts persistence-catalog +/** + * Permanently omit selected input-image occurrences from subsequent model requests. + * Targets name unique current user/message or tool/result nodes. Nonempty, strictly + * increasing indexes count all images in depth-first order, including nested tool + * results and already omitted images. Message nodes and identities remain unchanged. + * @messageProjection + */ +'image/offload': { targets: ImageOffloadTarget[] } +``` + +来源:[`packages/compaction/compaction-image-offload/src/projection.ts:25`](../packages/compaction/compaction-image-offload/src/projection.ts) + ### `llm/*` @@ -1738,7 +1758,7 @@ SHA-256: `6e52dd55a1d48954ed5464ec46fe34d9eb65f709baabc9561e7a279910b0c406` ### `event:agent/inbox/spliced` -SHA-256: `f757322e25914d63799c6235af4ed4b4f36d7d083fef40d6873a45e94fe6ad65` +SHA-256: `646c2f1d243d3a78c5bc9305786fb340f5209c48a97e65f2a186cc5195491f3f` | 属性 | 存在性 | 类型 | |---|---|---| @@ -1752,7 +1772,7 @@ SHA-256: `f757322e25914d63799c6235af4ed4b4f36d7d083fef40d6873a45e94fe6ad65` ### `event:agent/inbox/spliced.data` -SHA-256: `7115142af1bff1ad9b52c3c785109bf5c989e77e3ddfe899014e213731352042` +SHA-256: `a6a195c1b4a906d953d24ab5864c9f1881d545460e080a51144adfc9cb058cfe` 来源:[`packages/core/agent/src/types.ts:87`](../packages/core/agent/src/types.ts) @@ -1768,7 +1788,7 @@ SHA-256: `7115142af1bff1ad9b52c3c785109bf5c989e77e3ddfe899014e213731352042` ### `event:agent/inbox/spliced.data.inserted` -SHA-256: `86560bf0779bb2d93975405fe895d9d42c1020ab49fbbd2a06e1b3c74fbbc257` +SHA-256: `300341f890f7b9506a1749fff4fa5239d16ea1c63ff6b15851acb03c20f3b48d` [`packages/llm/llm/src/message.ts#UserMessage`](#persistence-type-packagesllmllmsrcmessagetsusermessage) 的数组。 @@ -1776,7 +1796,7 @@ SHA-256: `86560bf0779bb2d93975405fe895d9d42c1020ab49fbbd2a06e1b3c74fbbc257` ### `event:agent/inbox/spliced.data.inserted[0].content` -SHA-256: `0e560aad8fa9f338a60e76cbce6593ce93486ab30d6dd7833b8b5b07c6e6963d` +SHA-256: `bb72d86bc9ef0b3908bfa57ef98ccccae24d67d945c9f6869ca3cb8577889e75` [`packages/llm/llm/src/types.ts#ContentBlock`](#persistence-type-packagesllmllmsrctypestscontentblock) 的数组。 @@ -2088,7 +2108,7 @@ SHA-256: `11f70b595aa76897fc4e25a352bfa718c659f44b799e5aa14e84e5a38fe72912` ### `event:assistant/attempt` -SHA-256: `3516f54b427775f483febe8e938e4e10f9afd3583610861547bbe2999ed4d8d9` +SHA-256: `c80c89da83c46db7a454f034c10f969e03cfb574859c7316e5bff683f5a14b0e` | 属性 | 存在性 | 类型 | |---|---|---| @@ -2102,7 +2122,7 @@ SHA-256: `3516f54b427775f483febe8e938e4e10f9afd3583610861547bbe2999ed4d8d9` ### `event:assistant/attempt.data` -SHA-256: `8725743fcd3db5ddaf3e0aed550cd5d95785d8e30c9d4cbec11b7c90198d3603` +SHA-256: `0c6412d3ba5e41adfaa6929c7d4093f2d52d8598c03f35844fc0d55e3070a3b1` 来源:[`packages/core/session/src/types.ts:335`](../packages/core/session/src/types.ts) @@ -2116,7 +2136,7 @@ SHA-256: `8725743fcd3db5ddaf3e0aed550cd5d95785d8e30c9d4cbec11b7c90198d3603` ### `event:assistant/attempt.data.stream` -SHA-256: `86755151e6c4694ce5ae9511313e1e50f9fb6ead49c67d0a93c056e8650396c4` +SHA-256: `26f3bb7529cfce1e3a0f1ededa365d8b426758ca0feeb0beabeee805f8bb8346` [`packages/llm/llm/src/assistant-stream.ts#AssistantStreamRecord`](#persistence-type-packagesllmllmsrcassistant-streamtsassistantstreamrecord) 的数组。 @@ -2130,7 +2150,7 @@ SHA-256: `e72269f931559e901e5e8fe1cfcbd0669fb795f43cc038ff39fba62f60403c33` | 属性 | 存在性 | 类型 | |---|---|---| -| `dt` | 必需 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `dt` | 必需 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `index` | 必需 | `number` | | `texts` | 必需 | [`event:assistant/attempt.data.stream[0][0].texts`](#persistence-type-eventassistantattemptdatastream00texts) | | `time0` | 必需 | `number` | @@ -2154,7 +2174,7 @@ SHA-256: `71bb3254ec8f1e491ce63b762a7b5f18709de23aa4e8abcc8b886f4b6ce051dd` | 属性 | 存在性 | 类型 | |---|---|---| -| `dt` | 必需 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `dt` | 必需 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `index` | 必需 | `number` | | `texts` | 必需 | [`event:assistant/attempt.data.stream[0][0].texts`](#persistence-type-eventassistantattemptdatastream00texts) | | `time0` | 必需 | `number` | @@ -2171,7 +2191,7 @@ SHA-256: `3860b4757d512a2869be833acb0faf834335b1b056bf3f83df5474dce95f8fa6` | 属性 | 存在性 | 类型 | |---|---|---| | `args` | 必需 | [`event:assistant/attempt.data.stream[0][0].texts`](#persistence-type-eventassistantattemptdatastream00texts) | -| `dt` | 必需 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `dt` | 必需 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `id` | 必需 | `string` | | `index` | 必需 | `number` | | `name` | 可选 | `string` | @@ -2182,7 +2202,7 @@ SHA-256: `3860b4757d512a2869be833acb0faf834335b1b056bf3f83df5474dce95f8fa6` ### `event:assistant/attempt.data.stream[0][3]` -SHA-256: `3a896379a74576c5a527e4720be2fe6807d3225c27134e978a2b89a94068368e` +SHA-256: `c93b6f51b399607349b4bcc66b58e848a44250ffc1e2537207bd092dfff94192` 来源:[`packages/llm/llm/src/assistant-stream.ts:44`](../packages/llm/llm/src/assistant-stream.ts) @@ -2198,7 +2218,7 @@ SHA-256: `3a896379a74576c5a527e4720be2fe6807d3225c27134e978a2b89a94068368e` SHA-256: `647cb2e3aeb58e27d1d4d60011f208bb66dce257e8654193c533b47c4c22c253` -来源:[`packages/llm/llm/src/types.ts:391`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:425`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2212,7 +2232,7 @@ SHA-256: `647cb2e3aeb58e27d1d4d60011f208bb66dce257e8654193c533b47c4c22c253` SHA-256: `95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65` -来源:[`packages/llm/llm/src/types.ts:392`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:426`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2226,7 +2246,7 @@ SHA-256: `95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65` SHA-256: `2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095` -来源:[`packages/llm/llm/src/types.ts:393`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:427`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2240,7 +2260,7 @@ SHA-256: `2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095` SHA-256: `88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a` -来源:[`packages/llm/llm/src/types.ts:394`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:428`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2254,9 +2274,9 @@ SHA-256: `88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a` ### `event:assistant/attempt.data.stream[0][3].chunk[4]` -SHA-256: `6326a7ecfed005f3d54b737ab9d702da4daddb75cfc635d9096e9abb4788024e` +SHA-256: `90c811b8c2668898515e69a24e82caffeb08517f9c7aa193867ff0380f3b535d` -来源:[`packages/llm/llm/src/types.ts:395`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:429`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2270,7 +2290,7 @@ SHA-256: `6326a7ecfed005f3d54b737ab9d702da4daddb75cfc635d9096e9abb4788024e` SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515` -来源:[`packages/llm/llm/src/types.ts:396`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:430`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2281,9 +2301,9 @@ SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515` ### `event:assistant/attempt.data.stream[0][3].chunk[6]` -SHA-256: `a4ed3446d32625902170fce527fb64a0283d6f4817930f54a16fdce3a482b36a` +SHA-256: `a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1` -来源:[`packages/llm/llm/src/types.ts:397`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:431`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2295,9 +2315,9 @@ SHA-256: `a4ed3446d32625902170fce527fb64a0283d6f4817930f54a16fdce3a482b36a` ### `event:assistant/attempt.data.stream[0][3].chunk[6].reason[0]` -SHA-256: `64cfc4dbd3739f0ee040689482cb27058890f1d6113abb1de756836318aeebbc` +SHA-256: `8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0` -来源:[`packages/llm/llm/src/types.ts:134`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:147`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2308,9 +2328,9 @@ SHA-256: `64cfc4dbd3739f0ee040689482cb27058890f1d6113abb1de756836318aeebbc` ### `event:assistant/attempt.data.stream[0][3].chunk[6].reason[1]` -SHA-256: `ca13866102dfc53a503fccddeebc9caf667b8a06ef27217dd65de29f0495e65d` +SHA-256: `693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057` -来源:[`packages/llm/llm/src/types.ts:135`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:148`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2323,7 +2343,7 @@ SHA-256: `ca13866102dfc53a503fccddeebc9caf667b8a06ef27217dd65de29f0495e65d` SHA-256: `ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7` -来源:[`packages/core/session/src/types.ts:213`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:133`](../packages/llm/llm/src/types.ts) +来源:[`packages/core/session/src/types.ts:213`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:146`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2335,7 +2355,7 @@ SHA-256: `ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7` SHA-256: `1d76406cbb2ddadb0dbbb51e9d8bc6cf678fed455cf8eb512f36f7c86d37a62f` -来源:[`packages/llm/llm/src/types.ts:131`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:144`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2347,7 +2367,7 @@ SHA-256: `1d76406cbb2ddadb0dbbb51e9d8bc6cf678fed455cf8eb512f36f7c86d37a62f` SHA-256: `287211846beea1eccb5f9acee00d20388a38fcb2e95858fa2332d6f58529379a` -来源:[`packages/llm/llm/src/types.ts:132`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:145`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2365,7 +2385,7 @@ SHA-256: `b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7` ### `event:assistant/message` -SHA-256: `a58ca1bed502d425a8b65cac02f59717498cd296de9513c6f81b83b791c20e9b` +SHA-256: `1169b301aaabcd992657b93ec93750c43175dda81ada5cac086f14f2eaaeed6d` | 属性 | 存在性 | 类型 | |---|---|---| @@ -2380,7 +2400,7 @@ SHA-256: `a58ca1bed502d425a8b65cac02f59717498cd296de9513c6f81b83b791c20e9b` ### `event:assistant/message.data` -SHA-256: `a2418dd9dd938f04a3eaf1f27694b33facfb931b6e495dcfb4d5fcfa2ef82b28` +SHA-256: `77b8491f946edfc8cc77ebae3ba49183759da31ba2dfa83d4c9e9e618c4b91d2` 来源:[`packages/core/session/src/types.ts:321`](../packages/core/session/src/types.ts) @@ -2397,7 +2417,7 @@ SHA-256: `a2418dd9dd938f04a3eaf1f27694b33facfb931b6e495dcfb4d5fcfa2ef82b28` ### `event:browser-use/stagehand-llm-request` -SHA-256: `7cf11fcc829f21289bb7311562c6601499368464cce8ee678f80500e6a448b01` +SHA-256: `92cda1e2c0e9f57fdcd167a861b1296ec4cb5ac3ae127733c375e108335b32ac` | 属性 | 存在性 | 类型 | |---|---|---| @@ -2411,7 +2431,7 @@ SHA-256: `7cf11fcc829f21289bb7311562c6601499368464cce8ee678f80500e6a448b01` ### `event:browser-use/stagehand-llm-request.data.request.messages` -SHA-256: `f3cfc98798d8b0e726ee22b05b13ada5cb19e9cdbe105ef3529699de99ce357a` +SHA-256: `478738df7e7d2b103b09fb937817a9302c81076809f2b341169e00a9419c5665` [`packages/llm/llm/src/message.ts#Message`](#persistence-type-packagesllmllmsrcmessagetsmessage) 的数组。 @@ -2510,7 +2530,7 @@ SHA-256: `529e0ccd0e34079ab4ff21ed87f810cee0388246263bf22e8aba5986b56fae05` ### `event:browser-use/stagehand-llm-result` -SHA-256: `05d3b279f065b23cfb5d192a9a95016fe23642d47dbd90378c81be939e8e1b09` +SHA-256: `0e0e81784bc85e1e6554b7ff50ab341169a1a3d32149ae49abe8dddca5ce6efe` | 属性 | 存在性 | 类型 | |---|---|---| @@ -2656,7 +2676,7 @@ SHA-256: `5dd766ccd1702a1142ad8b4bd44f2242f8e8a6752669ad3de0a5d224329b1438` | 属性 | 存在性 | 类型 | |---|---|---| | `shadowedRange` | 必需 | [`event:compaction/prune.data.shadowedRange`](#persistence-type-eventcompactionprunedatashadowedrange) | -| `shadowedSeqs` | 必需 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `shadowedSeqs` | 必需 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `shadowedTokenCount` | 必需 | `number` | @@ -2704,7 +2724,7 @@ SHA-256: `3e4e7683b0192c9f0e6a75c59076108f017892cdb302a98032c2632fd79c356f` ### `event:compaction/summary` -SHA-256: `eb1d32dcd3d76833392f351e039dcbd40686a1ab24b252a7a5003426f9cf4c89` +SHA-256: `f6f3f30109e9008fccf7da2a3268a63da8646b59dd7cf7b75bcd9cf08ac08a59` | 属性 | 存在性 | 类型 | |---|---|---| @@ -2718,7 +2738,7 @@ SHA-256: `eb1d32dcd3d76833392f351e039dcbd40686a1ab24b252a7a5003426f9cf4c89` ### `event:compaction/summary.data` -SHA-256: `0a6fae8c3249943e23a024ae6c0d9dd707df9bff0badc69786f0dc8ad9482d8e` +SHA-256: `feb329074f0206ecc06d8a1aa0a0b3e2f47fd27524ba020fbc849ba63e938896` 以下类型之一: @@ -2729,7 +2749,7 @@ SHA-256: `0a6fae8c3249943e23a024ae6c0d9dd707df9bff0badc69786f0dc8ad9482d8e` ### `event:compaction/summary.data[0]` -SHA-256: `1e75cbc0af53f62e5685d751108dc9f6320fe835aed222e579a2a0080dc8fc59` +SHA-256: `9ff2ad8dddb836f75e8f6197fff2371323dfc396c3c6fb667e6e0b42fda79a5c` | 属性 | 存在性 | 类型 | |---|---|---| @@ -2740,7 +2760,7 @@ SHA-256: `1e75cbc0af53f62e5685d751108dc9f6320fe835aed222e579a2a0080dc8fc59` | `provider` | 必需 | `string` | | `rawOutput` | 必需 | [`event:agent/inbox/spliced.data.inserted[0].content`](#persistence-type-eventagentinboxspliceddatainserted0content) | | `shadowedRange` | 必需 | [`event:compaction/prune.data.shadowedRange`](#persistence-type-eventcompactionprunedatashadowedrange) | -| `shadowedSeqs` | 必需 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `shadowedSeqs` | 必需 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `shadowedTokenCount` | 必需 | `number` | | `sourceCommandId` | 可选 | `string` | | `summary` | 必需 | [`event:agent/inbox/spliced.data.inserted[0].content`](#persistence-type-eventagentinboxspliceddatainserted0content) | @@ -2750,7 +2770,7 @@ SHA-256: `1e75cbc0af53f62e5685d751108dc9f6320fe835aed222e579a2a0080dc8fc59` ### `event:compaction/summary.data[1]` -SHA-256: `ac5a8baa3dda8fa667b4975d6a8b40a9bde47d8d754ca8ed6b8ce923a1651828` +SHA-256: `3aa261c8516b5c955ed12080e6257a56cbe513f6c9f0ac9274c12e96c196ebca` | 属性 | 存在性 | 类型 | |---|---|---| @@ -2760,7 +2780,7 @@ SHA-256: `ac5a8baa3dda8fa667b4975d6a8b40a9bde47d8d754ca8ed6b8ce923a1651828` | `provider` | 必需 | `string` | | `rawOutput` | 可选 | [`event:agent/inbox/spliced.data.inserted[0].content`](#persistence-type-eventagentinboxspliceddatainserted0content) | | `shadowedRange` | 必需 | [`event:compaction/prune.data.shadowedRange`](#persistence-type-eventcompactionprunedatashadowedrange) | -| `shadowedSeqs` | 必需 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `shadowedSeqs` | 必需 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `shadowedTokenCount` | 必需 | `number` | | `sourceCommandId` | 可选 | `string` | | `summary` | 必需 | [`event:agent/inbox/spliced.data.inserted[0].content`](#persistence-type-eventagentinboxspliceddatainserted0content) | @@ -2951,11 +2971,45 @@ SHA-256: `8ad45ad053b4d89f8865c17cc9ff3b8748e86045f3f0fb816269a34af2619141` | `stderrSummary` | 可选 | `string` | | `turn` | 必需 | `number` | + + +### `event:image/offload` + +SHA-256: `b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `data` | 必需 | [`event:image/offload.data`](#persistence-type-eventimageoffloaddata) | +| `ignorable` | 可选 | `true` | +| `seq` | 必需 | `number` | +| `time` | 必需 | `number` | +| `type` | 必需 | `"image/offload"` | + + + +### `event:image/offload.data` + +SHA-256: `477614bca595a9e7adb5a1870be446b9a8684f3ba8950259d6289f2d3b74f028` + +来源:[`packages/compaction/compaction-image-offload/src/projection.ts:25`](../packages/compaction/compaction-image-offload/src/projection.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `targets` | 必需 | [`event:image/offload.data.targets`](#persistence-type-eventimageoffloaddatatargets) | + + + +### `event:image/offload.data.targets` + +SHA-256: `9e2c06d8653585b33d72d5c1a80da79c8a24a88c284409bca15cc0f0ead5ebfb` + +[`packages/compaction/compaction-image-offload/src/projection.ts#ImageOffloadTarget`](#persistence-type-packagescompactioncompaction-image-offloadsrcprojectiontsimageoffloadtarget) 的数组。 + ### `event:llm/retry` -SHA-256: `91c397f8f870e812e1dc5ac69c2745f9f105be9dc97dadc980d19ef415a65145` +SHA-256: `525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336` | 属性 | 存在性 | 类型 | |---|---|---| @@ -2983,7 +3037,7 @@ SHA-256: `48e5c9861f16ac07e78cb7b5ae9dabdf7bb85c58baed5a51b4ad275050ea58e3` ### `event:llm/retry.data[0]` -SHA-256: `9096d1dd86a3da77a593e12cd95e596b2ef965fa94fcbee40e4bdcf0433924a1` +SHA-256: `cdee10e87dbed8440d8b6935888624884878efc7af95402e94171b29f173dced` 来源:[`packages/llm/llm-retry/src/types.ts:17`](../packages/llm/llm-retry/src/types.ts) @@ -3004,7 +3058,7 @@ SHA-256: `9096d1dd86a3da77a593e12cd95e596b2ef965fa94fcbee40e4bdcf0433924a1` ### `event:llm/retry.data[1]` -SHA-256: `c0f91c064454e9ae25a4b5caacdc487712aee3acf414b07b7d5f7fd3530a0452` +SHA-256: `374fdd68aee5ff844e378c0cbdd961d247a0f22e4ade0bb03d800cd2a46a2b8b` 来源:[`packages/llm/llm-retry/src/types.ts:30`](../packages/llm/llm-retry/src/types.ts) @@ -3241,7 +3295,7 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` ### `event:session/title-llm-request` -SHA-256: `6091f64d426ada2adc646094a6fe662a1b81d9ee5770929fe76800524e2c89a8` +SHA-256: `e0b5bf44c27bbfc6ab144239e3e4169c4d21146645e0e3121bbb359c3bc8591d` | 属性 | 存在性 | 类型 | |---|---|---| @@ -3412,14 +3466,14 @@ SHA-256: `ba77b58995e36f14bb13886b0493d0a1a63d5db10b1a6cd9f6ac65a72cf832ec` ### `event:system/message` -SHA-256: `0ae7ccdc1cc5d9d43772ebd5c7357ff509011c3a6ba188d9c274d33df2542c42` +SHA-256: `69becfb6b2d3fd5da91518089454cae8ef33f1835637ec44dde35dd077fd4bae` | 属性 | 存在性 | 类型 | |---|---|---| | `data` | 必需 | [`event:system/message.data`](#persistence-type-eventsystemmessagedata) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | -| `sourceEventSeqs` | 可选 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `sourceEventSeqs` | 可选 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `surfaceOp` | 必需 | [`packages/core/session/src/types.ts#SurfaceOp`](#persistence-type-packagescoresessionsrctypestssurfaceop) | | `time` | 必需 | `number` | | `type` | 必需 | `"system/message"` | @@ -3428,7 +3482,7 @@ SHA-256: `0ae7ccdc1cc5d9d43772ebd5c7357ff509011c3a6ba188d9c274d33df2542c42` ### `event:system/message.data` -SHA-256: `d489162978ec04f1f76f40a68c7715326f46ec4f696edf1f321efdf88545fc18` +SHA-256: `2074f40221b76c75fd314246db558965dc3477dfb440bcc952b1eedc9778e366` 来源:[`packages/core/session/src/types.ts:310`](../packages/core/session/src/types.ts) @@ -3526,7 +3580,7 @@ SHA-256: `0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1` ### `event:team/message/queued` -SHA-256: `2ffcad547cb840f462eb1849fa5f6cb0e17eb93f5d07cc88b61d749b1b78d632` +SHA-256: `443371ec07a03a82a0e93d93abca3e70b03bca55ba5b01d507030fcb66e8fbb4` | 属性 | 存在性 | 类型 | |---|---|---| @@ -3540,7 +3594,7 @@ SHA-256: `2ffcad547cb840f462eb1849fa5f6cb0e17eb93f5d07cc88b61d749b1b78d632` ### `event:team/message/queued.data` -SHA-256: `6132f7814b28287473605ebc6dacd63c7c699435452487da6152bc735b8ba8c4` +SHA-256: `0c3f4d2248d9bc9700e2d7253fa71aa9935f5c39af6f11de5727f08cccaf8383` 来源:[`packages/experimental/agent-team/src/types.ts:225`](../packages/experimental/agent-team/src/types.ts) @@ -3714,7 +3768,7 @@ SHA-256: `e4c18e294232c3ba6c9f1999f168263cc55956147cfe4121720899be8e52edd1` ### `event:tool/ptc-dispatch` -SHA-256: `b5d66eaebed4da391b13498623b11142222149fbf5e025975dbc6d94f0d06796` +SHA-256: `b33142af8176323ffbb20762541be81c1d51b60a60887803836e0b146e5bef0c` | 属性 | 存在性 | 类型 | |---|---|---| @@ -3756,14 +3810,14 @@ SHA-256: `a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735` ### `event:tool/result` -SHA-256: `3a803805bdeb805f32b229e399fb7258be8e32a89f89063a7c99957cfea942f7` +SHA-256: `29af48b840d0cd9e48b6f50bf3b354f8f6340607c5b99220a48e74f60beac9e2` | 属性 | 存在性 | 类型 | |---|---|---| | `data` | 必需 | [`event:tool/result.data`](#persistence-type-eventtoolresultdata) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | -| `sourceEventSeqs` | 可选 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `sourceEventSeqs` | 可选 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `surfaceOp` | 必需 | [`packages/core/session/src/types.ts#SurfaceOp`](#persistence-type-packagescoresessionsrctypestssurfaceop) | | `time` | 必需 | `number` | | `type` | 必需 | `"tool/result"` | @@ -3772,7 +3826,7 @@ SHA-256: `3a803805bdeb805f32b229e399fb7258be8e32a89f89063a7c99957cfea942f7` ### `event:tool/result.data` -SHA-256: `28aa673866746f447863d828aa25213135344d61e6c568b6ca2c1e729cfff6b3` +SHA-256: `356aad27134a0e31af67c994ff8b9e4b91acfb6c4917b3bc1f7a62bbf82794d5` 来源:[`packages/core/session/src/types.ts:355`](../packages/core/session/src/types.ts) @@ -3788,7 +3842,7 @@ SHA-256: `28aa673866746f447863d828aa25213135344d61e6c568b6ca2c1e729cfff6b3` ### `event:tool/result.data.message.content` -SHA-256: `5eedf09e88b5ed9ca2d5efdb5a32cd4f51352564af5ff82927c17d841a2e3d9e` +SHA-256: `8d6a011f3ef7096a4a922ef420458419ff6b20e1eaeece5d331db7585d2f08d6` | 位置 | 存在性 | 类型 | |---|---|---| @@ -3798,7 +3852,7 @@ SHA-256: `5eedf09e88b5ed9ca2d5efdb5a32cd4f51352564af5ff82927c17d841a2e3d9e` ### `event:turn/end` -SHA-256: `84c24f1209fc3e0153de6ac85d58fd6968b851f955e0e761b92321053956609e` +SHA-256: `bab768260853e13a7cf2e22e65af572a1e76a5b2c01f3cdc2ce09d39b87fb70b` | 属性 | 存在性 | 类型 | |---|---|---| @@ -3812,7 +3866,7 @@ SHA-256: `84c24f1209fc3e0153de6ac85d58fd6968b851f955e0e761b92321053956609e` ### `event:turn/end.data` -SHA-256: `508c0856b8df8574e396880a756f087699b138630428441fefca4a4380f01a28` +SHA-256: `d3e447062a2847dad847dad5168a9508d92c484363dfc76fd82605aa7bd46285` 来源:[`packages/core/session/src/types.ts:285`](../packages/core/session/src/types.ts) @@ -3911,7 +3965,7 @@ SHA-256: `023a355b2be0d688fb6e4df8388dadb942761d46cfe96d0f499aae9631a159d4` ### `event:turn/end.data.reason[3]` -SHA-256: `3dad205ad99fbda835c493ddc6bc93fe1441cef2f82774ffa2856364e2cede22` +SHA-256: `79650cfa10aa3c52440328d85e8a25f0ffadc8d7efd784e3c0819d8bb74a1985` 来源:[`packages/core/session/src/types.ts:211`](../packages/core/session/src/types.ts) @@ -3962,14 +4016,14 @@ SHA-256: `f90eb4ab2d3897bc20c521a038e3119d581f791ec974b1a0004f63a8ab9e232d` ### `event:user/message` -SHA-256: `2e49ede7fb3a592cce8919ebe982f6ca1c1f67abe4def359ffc028c85e6d1ad8` +SHA-256: `314765bdff29c7862fb6ce820f1773563ba3094a680d163ea21180a2591b8578` | 属性 | 存在性 | 类型 | |---|---|---| | `data` | 必需 | [`packages/llm/llm/src/message.ts#UserMessage`](#persistence-type-packagesllmllmsrcmessagetsusermessage) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | -| `sourceEventSeqs` | 可选 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `sourceEventSeqs` | 可选 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `surfaceOp` | 必需 | [`packages/core/session/src/types.ts#SurfaceOp`](#persistence-type-packagescoresessionsrctypestssurfaceop) | | `time` | 必需 | `number` | | `type` | 必需 | `"user/message"` | @@ -4042,7 +4096,7 @@ SHA-256: `8f6d609794bf5afc01d7ccf3e03811d32f4b8047f58a2c9a98db0154c2ad7cb9` SHA-256: `4e887768586528565381dadbddee6cef555874148089f4579e0b4b1ad096fc9c` -来源:[`packages/llm/llm/src/types.ts:54`](../packages/llm/llm/src/types.ts) · [`packages/web/web-search-deepseek/src/provider.ts:66`](../packages/web/web-search-deepseek/src/provider.ts) +来源:[`packages/llm/llm/src/types.ts:61`](../packages/llm/llm/src/types.ts) · [`packages/web/web-search-deepseek/src/provider.ts:66`](../packages/web/web-search-deepseek/src/provider.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4225,6 +4279,14 @@ SHA-256: `3f74ba74acd10644c1ed6947a411d8088bd2cfac74f61bb8540f96d68e31f004` `"image/jpeg"` + + +### `"image/offload"` + +SHA-256: `9acbe713f3f641921dd3333b240aa72e176d795327c24e8d9db9b68b16771b49` + +`"image/offload"` + ### `"image/png"` @@ -4247,7 +4309,7 @@ SHA-256: `ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8` SHA-256: `f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5` -来源:[`packages/llm/llm/src/types.ts:347`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:381`](../packages/llm/llm/src/types.ts) `"in-history"` @@ -4490,6 +4552,19 @@ SHA-256: `2f1e6a7ebace43d727ae138ddcd352eb3813960009e5805b8c5ac84300c97716` - `"image/png"` - `"image/webp"` + + +### `packages/compaction/compaction-image-offload/src/projection.ts#ImageOffloadTarget` + +SHA-256: `1cde1bedde79c5e8a56aee9c05c8e0a579cf5bc611deb85b93fa331a16ca978f` + +来源:[`packages/compaction/compaction-image-offload/src/projection.ts:9`](../packages/compaction/compaction-image-offload/src/projection.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `imageIndexes` | 必需 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | +| `seq` | 必需 | `number` | + ### `packages/context/agent-instructions/src/render.ts#AgentInstructionChange` @@ -4646,7 +4721,7 @@ SHA-256: `1f8c126cfab3df812cd115cfc23e3256f0fa2383400f4ce91ecad313577e59b0` ### `packages/core/session/src/types.ts#TurnEndReason` -SHA-256: `e3528b95cff70d83a080a96666877da3f9dc205e71955d6bc5b9b315b9e0da22` +SHA-256: `a5a8c15f08af1b0f26f40906e0797b6b0920fd01069e660becce35ab7fc48d77` 来源:[`packages/core/session/src/types.ts:224`](../packages/core/session/src/types.ts) @@ -4676,7 +4751,7 @@ SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f` ### `packages/core/tools/src/types.ts#PtcDispatchEventData` -SHA-256: `4c12ebe118c00be826b47b050eb5998f019df4bc3d8ebd7a93f5921c0e73617b` +SHA-256: `8d2e5a3c4230e6ea6c0b5151f02241d0db02cd2551f2df1b6b9700cddcc0dca3` 来源:[`packages/core/tools/src/types.ts:20`](../packages/core/tools/src/types.ts) @@ -4743,7 +4818,7 @@ SHA-256: `6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25` ### `packages/experimental/agent-team/src/types.ts#TeamMessageSnapshot` -SHA-256: `10ce3a084c136b5fc9969e5981b64711c35a2d9de7eac1b8443c03f73dc4f3db` +SHA-256: `5b694f394d5e11609887a3ccfa82844935d49806c37e336b30be2186d649ab94` 来源:[`packages/experimental/agent-team/src/types.ts:106`](../packages/experimental/agent-team/src/types.ts) @@ -4809,7 +4884,7 @@ SHA-256: `b2d2a01e51341a3bfec809198952c8c830a7899f4cb6ecd3c1dc0b5d9a2af943` ### `packages/experimental/browser-use-stagehand-native/src/model.ts#RecordedRequest` -SHA-256: `07d0e11cb5f42f2841b28814ceb5e91fb12c43249343f7ed716ec3646a081229` +SHA-256: `4a797416c9d5d0ef7b52baef65c9122332c517364fda90388d994fc4fc62baa4` 来源:[`packages/experimental/browser-use-stagehand-native/src/model.ts:14`](../packages/experimental/browser-use-stagehand-native/src/model.ts) @@ -4831,7 +4906,7 @@ SHA-256: `07d0e11cb5f42f2841b28814ceb5e91fb12c43249343f7ed716ec3646a081229` ### `packages/experimental/browser-use-stagehand-native/src/model.ts#StagehandLlmRequest` -SHA-256: `4ef9f82945d583962637197c7d4082b43c54c7c0bb131f19c40a906369a5abfc` +SHA-256: `07796b041bf5e5516b3c9cf80292a19bd1e518ecf11e461a6edb19780d0e7da9` 来源:[`packages/experimental/browser-use-stagehand-native/src/model.ts:17`](../packages/experimental/browser-use-stagehand-native/src/model.ts) @@ -4844,7 +4919,7 @@ SHA-256: `4ef9f82945d583962637197c7d4082b43c54c7c0bb131f19c40a906369a5abfc` ### `packages/experimental/browser-use-stagehand-native/src/model.ts#StagehandLlmResult` -SHA-256: `0b0f2af6d7b1273fcb7a2b5d6321b35b49f9186d065ffb99f2e392949b787c6e` +SHA-256: `2ca560fd44e47e89681d323fb02d7194733a9bf524d3a88a189bfac3c60632b1` 来源:[`packages/experimental/browser-use-stagehand-native/src/model.ts:25`](../packages/experimental/browser-use-stagehand-native/src/model.ts) @@ -5103,7 +5178,7 @@ SHA-256: `930c1cd289def119d443e19dd90008916bf035f0a4d23e07241e7f050f2b89b7` ### `packages/llm/llm-retry/src/types.ts#LlmRetryEventData` -SHA-256: `35b995af35bfa087f5381a5454a2305928da037e819bfc4759d3f66d9bd55aa9` +SHA-256: `c170da8ab7c37635b10867b2571811d9e317ac3ec394e8ff8efe53f902fc41c1` 来源:[`packages/llm/llm-retry/src/types.ts:16`](../packages/llm/llm-retry/src/types.ts) @@ -5131,7 +5206,7 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` ### `packages/llm/llm/src/assistant-stream.ts#AssistantStreamRecord` -SHA-256: `ecaf9469000a15d46d269ea72994ceaab3652ed31507822906fc12397422da21` +SHA-256: `86cf1e44038cb8b11659863dd0efafe682e41b9c782265c1d50e0a687debfc60` 来源:[`packages/llm/llm/src/assistant-stream.ts:20`](../packages/llm/llm/src/assistant-stream.ts) @@ -5176,7 +5251,7 @@ SHA-256: `326a9bfda27dda5882fb3cbe6e715df41dae2298fdae3d66437457268a665693` ### `packages/llm/llm/src/message.ts#AssistantMessage` -SHA-256: `08a1293eddb9b633bfefaf0893e3b4c807dfbb49c45a447cb32bb68bfd1a8232` +SHA-256: `e50b0a3ed8e6c80b672e4f855c91984f03b5c3fdd5d475bf171171f391a86c83` 来源:[`packages/llm/llm/src/message.ts:148`](../packages/llm/llm/src/message.ts) @@ -5204,7 +5279,7 @@ SHA-256: `86e1e384495ee09ac2d60c9db3610f1f1fddac2d248f6f1d6000e78054680219` ### `packages/llm/llm/src/message.ts#Message` -SHA-256: `f051068babb7ebab80e9fd4fff3a70cfe54430c2ef3ca9bb77bb54db8bc34476` +SHA-256: `23c7cce57d20589897f4e2bd5898affc6fc611b87cd0cc8b95be1cfdd8b3a7eb` 来源:[`packages/llm/llm/src/message.ts:131`](../packages/llm/llm/src/message.ts) @@ -5265,7 +5340,7 @@ SHA-256: `719c51f67074f97034a6c94297fe1f4b5d3c8d392c57586dab732aa5398c7a91` ### `packages/llm/llm/src/message.ts#SystemMessage` -SHA-256: `7973d82bd129b261f16a9a20d44df834ec60efc52a63242b4943a2515ae0894f` +SHA-256: `302b01ddddcbb3b16cfa3fba2a5d99f5082a55fbd9dc5c83a179a228b8a6a0ca` 来源:[`packages/llm/llm/src/message.ts:158`](../packages/llm/llm/src/message.ts) @@ -5293,7 +5368,7 @@ SHA-256: `d9c462b9f62fdd3efc76a44b9524dc8781f6b91cd9c7e584b17d971defafebab` ### `packages/llm/llm/src/message.ts#ToolResultMessage` -SHA-256: `966fad765834f62051ad6f6a100f7ea38abe5f40f7ef3ae0b6dca0e31c7549a5` +SHA-256: `1b573e2d5c094fa87e2a8bf409ddd4ac5ba92a123085e168de44b9ccdbd5244c` 来源:[`packages/llm/llm/src/message.ts:164`](../packages/llm/llm/src/message.ts) @@ -5308,7 +5383,7 @@ SHA-256: `966fad765834f62051ad6f6a100f7ea38abe5f40f7ef3ae0b6dca0e31c7549a5` ### `packages/llm/llm/src/message.ts#UserMessage` -SHA-256: `40390271dffbed81bffa514370ad1c3c19a13ad10c3647c9c0eae31126c95a5f` +SHA-256: `4de5e7644130ed715ef6b5a4d86a87b30dca4096d1a6683c66acf6f7809379c7` 来源:[`packages/llm/llm/src/message.ts:143`](../packages/llm/llm/src/message.ts) @@ -5323,9 +5398,9 @@ SHA-256: `40390271dffbed81bffa514370ad1c3c19a13ad10c3647c9c0eae31126c95a5f` ### `packages/llm/llm/src/types.ts#ContentBlock` -SHA-256: `e1161343902a8da654d4d1bb66d729065cc93a2710ebfe2717a242a5c2ec709a` +SHA-256: `c7b24be059d34d08929593049a1a81eaa3855166aacfe9d7603c0bc8bb728d56` -来源:[`packages/llm/llm/src/types.ts:124`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:137`](../packages/llm/llm/src/types.ts) 以下类型之一: @@ -5342,7 +5417,7 @@ SHA-256: `e1161343902a8da654d4d1bb66d729065cc93a2710ebfe2717a242a5c2ec709a` SHA-256: `757013fff5879d73d5753d8d1120faab56110e88ac3932485ab7e034d9e9ce61` -来源:[`packages/llm/llm/src/types.ts:122`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:135`](../packages/llm/llm/src/types.ts) 以下类型之一: @@ -5359,7 +5434,7 @@ SHA-256: `757013fff5879d73d5753d8d1120faab56110e88ac3932485ab7e034d9e9ce61` SHA-256: `f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef` -来源:[`packages/llm/llm/src/types.ts:84`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:97`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5370,9 +5445,9 @@ SHA-256: `f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef` ### `packages/llm/llm/src/types.ts#FinishReason` -SHA-256: `5ed158429dfa6a8633e6ba94b8c81190616c24b65c1777bf1c60cde9dd499a2f` +SHA-256: `5808e36c95355f0e745201b5db1b6bcec4ebf5adb75d3e82d7d59cc89635ab80` -来源:[`packages/llm/llm/src/types.ts:139`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:152`](../packages/llm/llm/src/types.ts) 以下类型之一: @@ -5386,20 +5461,21 @@ SHA-256: `5ed158429dfa6a8633e6ba94b8c81190616c24b65c1777bf1c60cde9dd499a2f` ### `packages/llm/llm/src/types.ts#ImageBlock` -SHA-256: `8ef06dd236128246969d5f04a6c4a639d031fcda99a0cd13051f52f41b0ca1af` +SHA-256: `12d5808fac1996b9b3c2afa806e03022252ba2d7b88f835862ae1896591f60c7` -来源:[`packages/llm/llm/src/types.ts:71`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:78`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `attachment` | 必需 | [`packages/attachment/attachment/src/types.ts#ImageAttachmentRef`](#persistence-type-packagesattachmentattachmentsrctypestsimageattachmentref) | +| `offloaded` | 可选 | `true` | | `type` | 必需 | `"image"` | ### `packages/llm/llm/src/types.ts#LlmFailure` -SHA-256: `27cd736e794a0f8703a155fb697820cede894d10776586dd3df24d24c9465c0e` +SHA-256: `5f2117d1856a5ab43c28371faa978ebfaa48f8c7d734ea3fc94842210df83dca` 来源:[`packages/llm/llm/src/types.ts:40`](../packages/llm/llm/src/types.ts) @@ -5407,6 +5483,7 @@ SHA-256: `27cd736e794a0f8703a155fb697820cede894d10776586dd3df24d24c9465c0e` |---|---|---| | `code` | 必需 | `string` | | `message` | 必需 | `string` | +| `offloadImages` | 可选 | `number` | | `providerRetryAfterMs` | 可选 | `number` | | `requestId` | 可选 | `string` | | `status` | 可选 | `number` | @@ -5417,7 +5494,7 @@ SHA-256: `27cd736e794a0f8703a155fb697820cede894d10776586dd3df24d24c9465c0e` SHA-256: `814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e` -来源:[`packages/llm/llm/src/types.ts:60`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:67`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5430,7 +5507,7 @@ SHA-256: `814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e` SHA-256: `0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555` -来源:[`packages/llm/llm/src/types.ts:368`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:402`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5441,9 +5518,9 @@ SHA-256: `0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555` ### `packages/llm/llm/src/types.ts#StreamChunk` -SHA-256: `989145d2b165a1e4391d0691196ede9dec5093b09f1cf3d33b1911c24ee6c5ee` +SHA-256: `071d92b9dac661fcc826c678cf64d188f26fb93aab79c627b11a03acdd34dfca` -来源:[`packages/llm/llm/src/types.ts:390`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:424`](../packages/llm/llm/src/types.ts) 以下类型之一: @@ -5461,7 +5538,7 @@ SHA-256: `989145d2b165a1e4391d0691196ede9dec5093b09f1cf3d33b1911c24ee6c5ee` SHA-256: `fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394` -来源:[`packages/llm/llm/src/types.ts:149`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:162`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5478,7 +5555,7 @@ SHA-256: `fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394` SHA-256: `c1f0f17b6493b85db6453bb30e01b208d703731b25bc1e68e5d4fc72eea9cdea` -来源:[`packages/llm/llm/src/types.ts:91`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:104`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5491,9 +5568,9 @@ SHA-256: `c1f0f17b6493b85db6453bb30e01b208d703731b25bc1e68e5d4fc72eea9cdea` ### `packages/llm/llm/src/types.ts#ToolResultBlock` -SHA-256: `9bdd77f2b39d9ae47be71e494eca8c9e950b0e99b89a4539f86c4a0e7d1398b6` +SHA-256: `d61574d68558c5a184429e70b04da393dd5460bf5cc3ddef814546e659bd5127` -来源:[`packages/llm/llm/src/types.ts:101`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:114`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5508,7 +5585,7 @@ SHA-256: `9bdd77f2b39d9ae47be71e494eca8c9e950b0e99b89a4539f86c4a0e7d1398b6` SHA-256: `7f5f2a8618c2ece530a18b1358b890fb57fb8c35705e47b66bff2296f8c6e889` -来源:[`packages/llm/llm/src/types.ts:411`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:445`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5688,14 +5765,14 @@ SHA-256: `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` ### `packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData` -SHA-256: `08936f7648f573f7539800e1ec2852fbbcba05e1a20487a1e5acf2a0fce1e2de` +SHA-256: `9fe0e77a7816b7fdb4b953ec0e2f0cd7bbd22c5de5c3567e612d1b20eb0992e6` 来源:[`packages/session/session-title-llm/src/index.ts:27`](../packages/session/session-title-llm/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `maxTokens` | 必需 | `number` | -| `messageSeqs` | 必需 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `messageSeqs` | 必需 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `messages` | 必需 | [`event:browser-use/stagehand-llm-request.data.request.messages`](#persistence-type-eventbrowser-usestagehand-llm-requestdatarequestmessages) | | `route` | 必需 | [`packages/session/session-title/src/types.ts#SessionTitleModelIdentity`](#persistence-type-packagessessionsession-titlesrctypestssessiontitlemodelidentity) | | `system` | 必需 | `string` | @@ -5711,7 +5788,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` | 属性 | 存在性 | 类型 | |---|---|---| -| `messageSeqs` | 必需 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `messageSeqs` | 必需 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `source` | 必需 | [`packages/session/session-title/src/types.ts#SessionTitleSource`](#persistence-type-packagessessionsession-titlesrctypestssessiontitlesource) | | `title` | 必需 | `string` | @@ -6252,7 +6329,7 @@ SHA-256: `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` 以下类型之一: -- [`SessionEventEnvelope[40]`](#persistence-type-sessioneventenvelope40) +- [`SessionEventEnvelope[41]`](#persistence-type-sessioneventenvelope41) - [`SessionEventEnvelope[6]`](#persistence-type-sessioneventenvelope6) - [`SessionEventEnvelope[0]`](#persistence-type-sessioneventenvelope0) @@ -6269,9 +6346,9 @@ SHA-256: `e3e77b26f0148755a505f5b8ea843a3be117827d4703bb5754d9dc8ddbef521f` | `time` | 必需 | `number` | | `type` | 必需 | `string` | - + -### `SessionEventEnvelope[40]` +### `SessionEventEnvelope[41]` SHA-256: `998f22585a5d73ec005365e203f07fc3b3a029e82323fdda3fd7f13b0a9639d0` @@ -6279,14 +6356,14 @@ SHA-256: `998f22585a5d73ec005365e203f07fc3b3a029e82323fdda3fd7f13b0a9639d0` |---|---|---| | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | -| `sourceEventSeqs` | 可选 | [`SessionEventEnvelope[40].sourceEventSeqs`](#persistence-type-sessioneventenvelope40sourceeventseqs) | +| `sourceEventSeqs` | 可选 | [`SessionEventEnvelope[41].sourceEventSeqs`](#persistence-type-sessioneventenvelope41sourceeventseqs) | | `surfaceOp` | 必需 | [`packages/core/session/src/types.ts#SurfaceOp`](#persistence-type-packagescoresessionsrctypestssurfaceop) | | `time` | 必需 | `number` | | `type` | 必需 | `string` | - + -### `SessionEventEnvelope[40].sourceEventSeqs` +### `SessionEventEnvelope[41].sourceEventSeqs` SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` diff --git a/docs/persistence-changes/2026-09-14-image-offload.i18n.yaml b/docs/persistence-changes/2026-09-14-image-offload.i18n.yaml new file mode 100644 index 0000000000..11fc8092c4 --- /dev/null +++ b/docs/persistence-changes/2026-09-14-image-offload.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-09-14-image-offload.md +2026-09-14-image-offload.md: 1d4f47f5a25cc9dd39ad16f111af7f30d906a297 +2026-09-14-image-offload.zh.md: b184b32e96d04a425eda6faf9f5d50f815a9c32a diff --git a/docs/persistence-changes/2026-09-14-image-offload.md b/docs/persistence-changes/2026-09-14-image-offload.md new file mode 100644 index 0000000000..1d4f47f5a2 --- /dev/null +++ b/docs/persistence-changes/2026-09-14-image-offload.md @@ -0,0 +1,96 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-09-14-image-offload + +English | [中文](2026-09-14-image-offload.zh.md) + +## Summary + +Record selected image occurrences with the image/offload event and derive their offloaded marks through the owning plugin's message projection. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-09-14-image-offload +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-09-11-initial" + after: "646c2f1d243d3a78c5bc9305786fb340f5209c48a97e65f2a186cc5195491f3f" + decision: same-version + - root: "event:assistant/attempt" + previous: "2026-09-11-initial" + after: "c80c89da83c46db7a454f034c10f969e03cfb574859c7316e5bff683f5a14b0e" + decision: same-version + - root: "event:assistant/message" + previous: "2026-09-11-initial" + after: "1169b301aaabcd992657b93ec93750c43175dda81ada5cac086f14f2eaaeed6d" + decision: same-version + - root: "event:compaction/summary" + previous: "2026-09-11-initial" + after: "f6f3f30109e9008fccf7da2a3268a63da8646b59dd7cf7b75bcd9cf08ac08a59" + decision: same-version + - root: "event:image/offload" + previous: null + after: "b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e" + decision: same-version + - root: "event:llm/retry" + previous: "2026-09-11-initial" + after: "525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-09-11-initial" + after: "e0b5bf44c27bbfc6ab144239e3e4169c4d21146645e0e3121bbb359c3bc8591d" + decision: same-version + - root: "event:system/message" + previous: "2026-09-11-initial" + after: "69becfb6b2d3fd5da91518089454cae8ef33f1835637ec44dde35dd077fd4bae" + decision: same-version + - root: "event:team/message/queued" + previous: "2026-09-11-initial" + after: "443371ec07a03a82a0e93d93abca3e70b03bca55ba5b01d507030fcb66e8fbb4" + decision: same-version + - root: "event:tool/ptc-dispatch" + previous: "2026-09-12-auto-review-error-metadata" + after: "b33142af8176323ffbb20762541be81c1d51b60a60887803836e0b146e5bef0c" + decision: same-version + - root: "event:tool/result" + previous: "2026-09-12-auto-review-error-metadata" + after: "29af48b840d0cd9e48b6f50bf3b354f8f6340607c5b99220a48e74f60beac9e2" + decision: same-version + - root: "event:turn/end" + previous: "2026-09-11-initial" + after: "bab768260853e13a7cf2e22e65af572a1e76a5b2c01f3cdc2ce09d39b87fb70b" + decision: same-version + - root: "event:user/message" + previous: "2026-09-11-initial" + after: "314765bdff29c7862fb6ce820f1773563ba3094a680d163ea21180a2591b8578" + decision: same-version +``` + + +## Compatibility + +Existing logs remain readable. The optional offloaded image field leaves unmarked occurrences retained. The new event is required on read: older builds that do not recognize image/offload refuse those logs, and current readers require its message projection. Event envelopes and structural Session format versions are unchanged. + + +## Verification + +Session and image-offload tests cover event validation, immutable message projection, missing interpreters, restore, and retry. The focused 1,144-test run and two TypeScript image snapshots pass. The Python advanced SDK recording was refreshed through the built dsh profile and includes the standalone image/offload event. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-09-14-image-offload.schema.json b/docs/persistence-changes/2026-09-14-image-offload.schema.json new file mode 100644 index 0000000000..66fb67e45e --- /dev/null +++ b/docs/persistence-changes/2026-09-14-image-offload.schema.json @@ -0,0 +1,8478 @@ +{ + "formatVersion": 1, + "roots": [ + { + "key": "event:agent/inbox/spliced", + "kind": "event", + "event": "agent/inbox/spliced", + "surface": false, + "digest": "646c2f1d243d3a78c5bc9305786fb340f5209c48a97e65f2a186cc5195491f3f", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 18, + "optional": true + }, + { + "name": "seq", + "type": 11, + "optional": false + }, + { + "name": "time", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 90, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "inserted", + "type": 2, + "optional": false + }, + { + "name": "outcome", + "type": 86, + "optional": true + }, + { + "name": "removedCount", + "type": 11, + "optional": true + }, + { + "name": "start", + "type": 11, + "optional": false + }, + { + "name": "target", + "type": 87, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "role", + "type": 30, + "optional": false + }, + { + "name": "source", + "type": 31, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 9, + 20, + 23, + 26, + 28 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 10, + "optional": false + }, + { + "name": "offloaded", + "type": 18, + "optional": true + }, + { + "name": "type", + "type": 19, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 11, + "optional": false + }, + { + "name": "height", + "type": 11, + "optional": false + }, + { + "name": "mediaType", + "type": 12, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 17, + "optional": true + }, + { + "name": "width", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 13, + 14, + 15, + 16 + ] + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 11, + "optional": false + }, + { + "name": "width", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 21, + "optional": false + }, + { + "name": "type", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "isError", + "type": 24, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 32, + 41, + 43, + 44, + 47, + 52, + 54, + 59, + 60, + 61, + 63, + 65, + 66, + 73, + 75, + 77, + 79, + 81, + 84, + 85 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 18, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 7, + "optional": true + }, + { + "name": "changes", + "type": 33, + "optional": false + }, + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 34 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 35, + "optional": false + }, + { + "name": "digest", + "type": 7, + "optional": true + }, + { + "name": "path", + "type": 7, + "optional": false + }, + { + "name": "scope", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 36, + 37, + 38 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 7, + "optional": true + }, + { + "name": "kind", + "type": 30, + "optional": false + }, + { + "name": "rpcId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 7, + "optional": false + }, + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "ruleId", + "type": 7, + "optional": false + }, + { + "name": "source", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 48, + "optional": false + }, + { + "name": "form", + "type": 50, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + }, + { + "name": "update", + "type": 18, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 49 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 55, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + }, + { + "name": "sections", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 58 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 64, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 50, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + }, + { + "name": "references", + "type": 68, + "optional": false + }, + { + "name": "version", + "type": 72, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 69 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 11, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 70, + "optional": false + }, + { + "name": "compacted", + "type": 24, + "optional": false + }, + { + "name": "inputIndex", + "type": 11, + "optional": false + }, + { + "name": "label", + "type": 7, + "optional": false + }, + { + "name": "omittedBytes", + "type": 11, + "optional": false + }, + { + "name": "omittedMessages", + "type": 11, + "optional": false + }, + { + "name": "originalMessages", + "type": 11, + "optional": false + }, + { + "name": "retainedMessages", + "type": 11, + "optional": false + }, + { + "name": "sessionId", + "type": 7, + "optional": false + }, + { + "name": "truncated", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 11, + 71 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 74, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 64, + "optional": false + }, + { + "name": "kind", + "type": 76, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "revision", + "type": 11, + "optional": false + }, + { + "name": "round", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "messageId", + "type": 7, + "optional": false + }, + { + "name": "senderId", + "type": 7, + "optional": false + }, + { + "name": "senderName", + "type": 7, + "optional": false + }, + { + "name": "teamId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "model", + "type": 7, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "replayState", + "type": 83, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "canceled" + }, + { + "kind": "union", + "types": [ + 88, + 89 + ] + }, + { + "kind": "literal", + "value": "next-step" + }, + { + "kind": "literal", + "value": "next-turn" + }, + { + "kind": "literal", + "value": "agent/inbox/spliced" + } + ] + } + }, + { + "key": "event:assistant/attempt", + "kind": "event", + "event": "assistant/attempt", + "surface": false, + "digest": "c80c89da83c46db7a454f034c10f969e03cfb574859c7316e5bff683f5a14b0e", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 26, + "optional": true + }, + { + "name": "seq", + "type": 2, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 72, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "step", + "type": 2, + "optional": false + }, + { + "name": "stream", + "type": 3, + "optional": false + }, + { + "name": "turn", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 5, + 10, + 68, + 70 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "args", + "type": 6, + "optional": false + }, + { + "name": "dt", + "type": 8, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "time0", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "literal", + "value": "tool-call-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "chunk", + "type": 11, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 12, + 14, + 40, + 43, + 45, + 47, + 64 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "argumentsDelta", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "block", + "type": 15, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 39, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 16, + 18, + 28, + 31, + 35, + 37 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 19, + "optional": false + }, + { + "name": "offloaded", + "type": 26, + "optional": true + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "mediaType", + "type": 20, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 25, + "optional": true + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 21, + 22, + 23, + 24 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 29, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 32, + "optional": false + }, + { + "name": "isError", + "type": 33, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 15 + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "block-end" + }, + { + "kind": "object", + "properties": [ + { + "name": "blockType", + "type": 41, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 17, + 27, + 30, + 34, + 36, + 38 + ] + }, + { + "kind": "literal", + "value": "block-start" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 48, + "optional": false + }, + { + "name": "replayState", + "type": 60, + "optional": true + }, + { + "name": "type", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 49, + 52, + 54, + 56, + 58 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 50, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 7, + "optional": false + }, + { + "name": "message", + "type": 7, + "optional": false + }, + { + "name": "offloadImages", + "type": 2, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 2, + "optional": true + }, + { + "name": "requestId", + "type": 7, + "optional": true + }, + { + "name": "status", + "type": 2, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 50, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 61, + "optional": true + }, + { + "name": "response", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 62 + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "literal", + "value": "finish" + }, + { + "kind": "object", + "properties": [ + { + "name": "type", + "type": 65, + "optional": false + }, + { + "name": "usage", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "usage" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 2, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 2, + "optional": true + }, + { + "name": "inputTokens", + "type": 2, + "optional": false + }, + { + "name": "outputTokens", + "type": 2, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 2, + "optional": true + }, + { + "name": "totalTokens", + "type": 2, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "chunk" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 8, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "texts", + "type": 6, + "optional": false + }, + { + "name": "time0", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 69, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 8, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "texts", + "type": 6, + "optional": false + }, + { + "name": "time0", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 71, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-chunks" + }, + { + "kind": "literal", + "value": "assistant/attempt" + } + ] + } + }, + { + "key": "event:assistant/message", + "kind": "event", + "event": "assistant/message", + "surface": true, + "digest": "1169b301aaabcd992657b93ec93750c43175dda81ada5cac086f14f2eaaeed6d", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 2, + "optional": true + }, + { + "name": "seq", + "type": 11, + "optional": false + }, + { + "name": "surfaceOp", + "type": 76, + "optional": false + }, + { + "name": "time", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "interrupted", + "type": 2, + "optional": true + }, + { + "name": "message", + "type": 3, + "optional": false + }, + { + "name": "step", + "type": 11, + "optional": false + }, + { + "name": "stream", + "type": 33, + "optional": false + }, + { + "name": "turn", + "type": 11, + "optional": false + }, + { + "name": "usage", + "type": 70, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "role", + "type": 29, + "optional": false + }, + { + "name": "source", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 9, + 19, + 22, + 25, + 27 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 10, + "optional": false + }, + { + "name": "offloaded", + "type": 2, + "optional": true + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 11, + "optional": false + }, + { + "name": "height", + "type": 11, + "optional": false + }, + { + "name": "mediaType", + "type": 12, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 17, + "optional": true + }, + { + "name": "width", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 13, + 14, + 15, + 16 + ] + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 11, + "optional": false + }, + { + "name": "width", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 20, + "optional": false + }, + { + "name": "type", + "type": 21, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "isError", + "type": 23, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 31, + "optional": false + }, + { + "name": "model", + "type": 7, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "replayState", + "type": 32, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "array", + "element": 34 + }, + { + "kind": "union", + "types": [ + 35, + 39, + 72, + 74 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "args", + "type": 36, + "optional": false + }, + { + "name": "dt", + "type": 37, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "time0", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "array", + "element": 11 + }, + { + "kind": "literal", + "value": "tool-call-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "chunk", + "type": 40, + "optional": false + }, + { + "name": "time", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 71, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 41, + 43, + 45, + 48, + 50, + 52, + 68 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "argumentsDelta", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "type", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "block", + "type": 5, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "block-end" + }, + { + "kind": "object", + "properties": [ + { + "name": "blockType", + "type": 46, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 47, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 8, + 18, + 21, + 24, + 26, + 28 + ] + }, + { + "kind": "literal", + "value": "block-start" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 53, + "optional": false + }, + { + "name": "replayState", + "type": 65, + "optional": true + }, + { + "name": "type", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 54, + 57, + 59, + 61, + 63 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 55, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 7, + "optional": false + }, + { + "name": "message", + "type": 7, + "optional": false + }, + { + "name": "offloadImages", + "type": 11, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 11, + "optional": true + }, + { + "name": "requestId", + "type": 7, + "optional": true + }, + { + "name": "status", + "type": 11, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 55, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 66, + "optional": true + }, + { + "name": "response", + "type": 32, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 32 + }, + { + "kind": "literal", + "value": "finish" + }, + { + "kind": "object", + "properties": [ + { + "name": "type", + "type": 69, + "optional": false + }, + { + "name": "usage", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "usage" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 11, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 11, + "optional": true + }, + { + "name": "inputTokens", + "type": 11, + "optional": false + }, + { + "name": "outputTokens", + "type": 11, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 11, + "optional": true + }, + { + "name": "totalTokens", + "type": 11, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "chunk" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 37, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "texts", + "type": 36, + "optional": false + }, + { + "name": "time0", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 73, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 37, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "texts", + "type": 36, + "optional": false + }, + { + "name": "time0", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 75, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-chunks" + }, + { + "kind": "union", + "types": [ + 77, + 79 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 11, + "optional": false + }, + { + "name": "op", + "type": 78, + "optional": false + }, + { + "name": "startSeq", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "literal", + "value": "assistant/message" + } + ] + } + }, + { + "key": "event:compaction/summary", + "kind": "event", + "event": "compaction/summary", + "surface": false, + "digest": "f6f3f30109e9008fccf7da2a3268a63da8646b59dd7cf7b75bcd9cf08ac08a59", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 4, + "optional": true + }, + { + "name": "seq", + "type": 5, + "optional": false + }, + { + "name": "time", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 32 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 3, + "optional": false + }, + { + "name": "llmStreamCall", + "type": 4, + "optional": false + }, + { + "name": "maxTokens", + "type": 5, + "optional": true + }, + { + "name": "model", + "type": 3, + "optional": false + }, + { + "name": "provider", + "type": 3, + "optional": false + }, + { + "name": "rawOutput", + "type": 6, + "optional": false + }, + { + "name": "shadowedRange", + "type": 29, + "optional": false + }, + { + "name": "shadowedSeqs", + "type": 30, + "optional": false + }, + { + "name": "shadowedTokenCount", + "type": 5, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 3, + "optional": true + }, + { + "name": "summary", + "type": 6, + "optional": false + }, + { + "name": "usage", + "type": 31, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "union", + "types": [ + 8, + 11, + 13, + 15, + 17, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 6, + "optional": false + }, + { + "name": "isError", + "type": 9, + "optional": true + }, + { + "name": "toolCallId", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 18, + "optional": false + }, + { + "name": "offloaded", + "type": 4, + "optional": true + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 3, + "optional": false + }, + { + "name": "bytes", + "type": 5, + "optional": false + }, + { + "name": "height", + "type": 5, + "optional": false + }, + { + "name": "mediaType", + "type": 19, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": true + }, + { + "name": "originalDimensions", + "type": 24, + "optional": true + }, + { + "name": "width", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 20, + 21, + 22, + 23 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 5, + "optional": false + }, + { + "name": "width", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 27, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 3, + "optional": false + }, + { + "name": "bytes", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "end", + "type": 5, + "optional": false + }, + { + "name": "start", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 5, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 5, + "optional": true + }, + { + "name": "inputTokens", + "type": 5, + "optional": false + }, + { + "name": "outputTokens", + "type": 5, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 5, + "optional": true + }, + { + "name": "totalTokens", + "type": 5, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 3, + "optional": false + }, + { + "name": "maxTokens", + "type": 5, + "optional": true + }, + { + "name": "model", + "type": 3, + "optional": false + }, + { + "name": "provider", + "type": 3, + "optional": false + }, + { + "name": "rawOutput", + "type": 6, + "optional": true + }, + { + "name": "shadowedRange", + "type": 29, + "optional": false + }, + { + "name": "shadowedSeqs", + "type": 30, + "optional": false + }, + { + "name": "shadowedTokenCount", + "type": 5, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 3, + "optional": true + }, + { + "name": "summary", + "type": 6, + "optional": false + }, + { + "name": "usage", + "type": 31, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "compaction/summary" + } + ] + } + }, + { + "key": "event:image/offload", + "kind": "event", + "event": "image/offload", + "surface": false, + "digest": "b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 6, + "optional": true + }, + { + "name": "seq", + "type": 5, + "optional": false + }, + { + "name": "time", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "targets", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "object", + "properties": [ + { + "name": "imageIndexes", + "type": 4, + "optional": false + }, + { + "name": "seq", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image/offload" + } + ] + } + }, + { + "key": "event:llm/retry", + "kind": "event", + "event": "llm/retry", + "surface": false, + "digest": "525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 9, + "optional": true + }, + { + "name": "seq", + "type": 3, + "optional": false + }, + { + "name": "time", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 7 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "delayMs", + "type": 3, + "optional": false + }, + { + "name": "failure", + "type": 4, + "optional": false + }, + { + "name": "maxRetries", + "type": 3, + "optional": false + }, + { + "name": "mode", + "type": 6, + "optional": false + }, + { + "name": "policyKey", + "type": 5, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "retry", + "type": 3, + "optional": false + }, + { + "name": "retryId", + "type": 5, + "optional": false + }, + { + "name": "step", + "type": 3, + "optional": false + }, + { + "name": "turn", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 5, + "optional": false + }, + { + "name": "message", + "type": 5, + "optional": false + }, + { + "name": "offloadImages", + "type": 3, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 3, + "optional": true + }, + { + "name": "requestId", + "type": 5, + "optional": true + }, + { + "name": "status", + "type": 3, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "normal" + }, + { + "kind": "object", + "properties": [ + { + "name": "delayMs", + "type": 3, + "optional": false + }, + { + "name": "failure", + "type": 4, + "optional": false + }, + { + "name": "mode", + "type": 8, + "optional": false + }, + { + "name": "policyKey", + "type": 5, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "retry", + "type": 3, + "optional": false + }, + { + "name": "retryId", + "type": 5, + "optional": false + }, + { + "name": "step", + "type": 3, + "optional": false + }, + { + "name": "turn", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "always" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "llm/retry" + } + ] + } + }, + { + "key": "event:session/title-llm-request", + "kind": "event", + "event": "session/title-llm-request", + "surface": false, + "digest": "e0b5bf44c27bbfc6ab144239e3e4169c4d21146645e0e3121bbb359c3bc8591d", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 19, + "optional": true + }, + { + "name": "seq", + "type": 2, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 91, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "maxTokens", + "type": 2, + "optional": false + }, + { + "name": "messageSeqs", + "type": 3, + "optional": false + }, + { + "name": "messages", + "type": 4, + "optional": false + }, + { + "name": "route", + "type": 90, + "optional": false + }, + { + "name": "system", + "type": 9, + "optional": false + }, + { + "name": "titleProvider", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 9, + "optional": false + }, + { + "name": "role", + "type": 31, + "optional": false + }, + { + "name": "source", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "union", + "types": [ + 8, + 11, + 21, + 24, + 27, + 29 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 9, + "optional": false + }, + { + "name": "id", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 12, + "optional": false + }, + { + "name": "offloaded", + "type": 19, + "optional": true + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 9, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "mediaType", + "type": 13, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": true + }, + { + "name": "originalDimensions", + "type": 18, + "optional": true + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 14, + 15, + 16, + 17 + ] + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 22, + "optional": false + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 9, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 6, + "optional": false + }, + { + "name": "isError", + "type": 25, + "optional": true + }, + { + "name": "toolCallId", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "union", + "types": [ + 32, + 33, + 34 + ] + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 36, + 45, + 47, + 48, + 51, + 56, + 58, + 63, + 64, + 65, + 67, + 69, + 70, + 77, + 79, + 81, + 83, + 85, + 88, + 89 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 19, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 9, + "optional": true + }, + { + "name": "changes", + "type": 37, + "optional": false + }, + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 38 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 39, + "optional": false + }, + { + "name": "digest", + "type": 9, + "optional": true + }, + { + "name": "path", + "type": 9, + "optional": false + }, + { + "name": "scope", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 40, + 41, + 42 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 9, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 9, + "optional": true + }, + { + "name": "kind", + "type": 34, + "optional": false + }, + { + "name": "rpcId", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 9, + "optional": false + }, + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + }, + { + "name": "provider", + "type": 9, + "optional": false + }, + { + "name": "ruleId", + "type": 9, + "optional": false + }, + { + "name": "source", + "type": 9, + "optional": false + }, + { + "name": "summary", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 52, + "optional": false + }, + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "update", + "type": 19, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 53 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 59, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + }, + { + "name": "sections", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 62 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 9, + "optional": false + }, + { + "name": "text", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + }, + { + "name": "summary", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 66, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 68, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 66, + "optional": false + }, + { + "name": "kind", + "type": 71, + "optional": false + }, + { + "name": "references", + "type": 72, + "optional": false + }, + { + "name": "version", + "type": 76, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 73 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 2, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 74, + "optional": false + }, + { + "name": "compacted", + "type": 25, + "optional": false + }, + { + "name": "inputIndex", + "type": 2, + "optional": false + }, + { + "name": "label", + "type": 9, + "optional": false + }, + { + "name": "omittedBytes", + "type": 2, + "optional": false + }, + { + "name": "omittedMessages", + "type": 2, + "optional": false + }, + { + "name": "originalMessages", + "type": 2, + "optional": false + }, + { + "name": "retainedMessages", + "type": 2, + "optional": false + }, + { + "name": "sessionId", + "type": 9, + "optional": false + }, + { + "name": "truncated", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 75 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "senderSessionId", + "type": 9, + "optional": false + }, + { + "name": "summary", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 68, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "senderSessionId", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 9, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "revision", + "type": 2, + "optional": false + }, + { + "name": "round", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 84, + "optional": false + }, + { + "name": "messageId", + "type": 9, + "optional": false + }, + { + "name": "senderId", + "type": 9, + "optional": false + }, + { + "name": "senderName", + "type": 9, + "optional": false + }, + { + "name": "teamId", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 86, + "optional": false + }, + { + "name": "model", + "type": 9, + "optional": false + }, + { + "name": "provider", + "type": 9, + "optional": false + }, + { + "name": "replayState", + "type": 87, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "model", + "type": 9, + "optional": false + }, + { + "name": "provider", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session/title-llm-request" + } + ] + } + }, + { + "key": "event:system/message", + "kind": "event", + "event": "system/message", + "surface": true, + "digest": "69becfb6b2d3fd5da91518089454cae8ef33f1835637ec44dde35dd077fd4bae", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 17, + "optional": true + }, + { + "name": "seq", + "type": 10, + "optional": false + }, + { + "name": "sourceEventSeqs", + "type": 47, + "optional": true + }, + { + "name": "surfaceOp", + "type": 48, + "optional": false + }, + { + "name": "time", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "message", + "type": 2, + "optional": false + }, + { + "name": "step", + "type": 10, + "optional": false + }, + { + "name": "turn", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "role", + "type": 29, + "optional": false + }, + { + "name": "source", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 5, + 8, + 19, + 22, + 25, + 27 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 9, + "optional": false + }, + { + "name": "offloaded", + "type": 17, + "optional": true + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 10, + "optional": false + }, + { + "name": "height", + "type": 10, + "optional": false + }, + { + "name": "mediaType", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": true + }, + { + "name": "originalDimensions", + "type": 16, + "optional": true + }, + { + "name": "width", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 12, + 13, + 14, + 15 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 10, + "optional": false + }, + { + "name": "width", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 20, + "optional": false + }, + { + "name": "type", + "type": 21, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "isError", + "type": 23, + "optional": true + }, + { + "name": "toolCallId", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "union", + "types": [ + 31, + 36, + 38, + 40, + 42, + 44, + 46 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 32, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + }, + { + "name": "sections", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 35 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "text", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + }, + { + "name": "summary", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 10 + }, + { + "kind": "union", + "types": [ + 49, + 51 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 10, + "optional": false + }, + { + "name": "op", + "type": 50, + "optional": false + }, + { + "name": "startSeq", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "literal", + "value": "system/message" + } + ] + } + }, + { + "key": "event:team/message/queued", + "kind": "event", + "event": "team/message/queued", + "surface": false, + "digest": "443371ec07a03a82a0e93d93abca3e70b03bca55ba5b01d507030fcb66e8fbb4", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 24, + "optional": true + }, + { + "name": "seq", + "type": 17, + "optional": false + }, + { + "name": "time", + "type": 17, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "message", + "type": 2, + "optional": false + }, + { + "name": "teamId", + "type": 7, + "optional": false + }, + { + "name": "version", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "senderId", + "type": 7, + "optional": false + }, + { + "name": "senderName", + "type": 7, + "optional": false + }, + { + "name": "targetId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 5, + 9, + 11, + 13, + 15, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "isError", + "type": 6, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 16, + "optional": false + }, + { + "name": "offloaded", + "type": 24, + "optional": true + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "mediaType", + "type": 18, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 23, + "optional": true + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 19, + 20, + 21, + 22 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 27, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "literal", + "value": 2 + }, + { + "kind": "literal", + "value": "team/message/queued" + } + ] + } + }, + { + "key": "event:tool/ptc-dispatch", + "kind": "event", + "event": "tool/ptc-dispatch", + "surface": false, + "digest": "b33142af8176323ffbb20762541be81c1d51b60a60887803836e0b146e5bef0c", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 24, + "optional": true + }, + { + "name": "seq", + "type": 17, + "optional": false + }, + { + "name": "time", + "type": 17, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 2, + "optional": false + }, + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "error", + "type": 29, + "optional": true + }, + { + "name": "isError", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "parentCallId", + "type": 7, + "optional": false + }, + { + "name": "rootCallId", + "type": 7, + "optional": false + }, + { + "name": "subCallId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 5, + 9, + 11, + 13, + 15, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "isError", + "type": 6, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 16, + "optional": false + }, + { + "name": "offloaded", + "type": 24, + "optional": true + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "mediaType", + "type": 18, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 23, + "optional": true + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 19, + 20, + 21, + 22 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 27, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "reason", + "type": 7, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool/ptc-dispatch" + } + ] + } + }, + { + "key": "event:tool/result", + "kind": "event", + "event": "tool/result", + "surface": true, + "digest": "29af48b840d0cd9e48b6f50bf3b354f8f6340607c5b99220a48e74f60beac9e2", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 24, + "optional": true + }, + { + "name": "seq", + "type": 17, + "optional": false + }, + { + "name": "sourceEventSeqs", + "type": 38, + "optional": true + }, + { + "name": "surfaceOp", + "type": 39, + "optional": false + }, + { + "name": "time", + "type": 17, + "optional": false + }, + { + "name": "type", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "error", + "type": 2, + "optional": true + }, + { + "name": "message", + "type": 4, + "optional": false + }, + { + "name": "meta", + "type": 34, + "optional": true + }, + { + "name": "step", + "type": 17, + "optional": false + }, + { + "name": "turn", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + }, + { + "name": "reason", + "type": 3, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "role", + "type": 31, + "optional": false + }, + { + "name": "source", + "type": 32, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "tuple", + "elements": [ + { + "type": 6, + "optional": false, + "rest": false + } + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "isError", + "type": 29, + "optional": true + }, + { + "name": "toolCallId", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 8 + }, + { + "kind": "union", + "types": [ + 6, + 9, + 11, + 13, + 15, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 16, + "optional": false + }, + { + "name": "offloaded", + "type": 24, + "optional": true + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 3, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "mediaType", + "type": 18, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": true + }, + { + "name": "originalDimensions", + "type": 23, + "optional": true + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 19, + 20, + 21, + 22 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 27, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 3, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 3, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "union", + "types": [ + 3, + 17, + 29, + 35, + 36, + 37 + ] + }, + { + "kind": "array", + "element": 34 + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 3, + "value": 34 + } + ] + }, + { + "kind": "array", + "element": 17 + }, + { + "kind": "union", + "types": [ + 40, + 41 + ] + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 17, + "optional": false + }, + { + "name": "op", + "type": 42, + "optional": false + }, + { + "name": "startSeq", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "tool/result" + } + ] + } + }, + { + "key": "event:turn/end", + "kind": "event", + "event": "turn/end", + "surface": false, + "digest": "bab768260853e13a7cf2e22e65af572a1e76a5b2c01f3cdc2ce09d39b87fb70b", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 29, + "optional": true + }, + { + "name": "seq", + "type": 14, + "optional": false + }, + { + "name": "time", + "type": 14, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 2, + "optional": false + }, + { + "name": "turn", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 3, + 5, + 7, + 9, + 11, + 16 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "blocked" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "completed" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "interrupted" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "error", + "type": 12, + "optional": false + }, + { + "name": "kind", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 13, + "optional": false + }, + { + "name": "message", + "type": 13, + "optional": false + }, + { + "name": "offloadImages", + "type": 14, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 14, + "optional": true + }, + { + "name": "requestId", + "type": 13, + "optional": true + }, + { + "name": "status", + "type": 14, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 17, + "optional": false + }, + { + "name": "reason", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "union", + "types": [ + 19, + 21, + 23, + 25, + 27 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "disposed" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "legacy" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "parent" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 28, + "optional": false + }, + { + "name": "reason", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hook" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "turn/end" + } + ] + } + }, + { + "key": "event:user/message", + "kind": "event", + "event": "user/message", + "surface": true, + "digest": "314765bdff29c7862fb6ce820f1773563ba3094a680d163ea21180a2591b8578", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 16, + "optional": true + }, + { + "name": "seq", + "type": 9, + "optional": false + }, + { + "name": "sourceEventSeqs", + "type": 84, + "optional": true + }, + { + "name": "surfaceOp", + "type": 85, + "optional": false + }, + { + "name": "time", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 88, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "role", + "type": 28, + "optional": false + }, + { + "name": "source", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "union", + "types": [ + 4, + 7, + 18, + 21, + 24, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 8, + "optional": false + }, + { + "name": "offloaded", + "type": 16, + "optional": true + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "mediaType", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 15, + "optional": true + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 11, + 12, + 13, + 14 + ] + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 19, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "isError", + "type": 22, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 30, + 39, + 41, + 42, + 45, + 50, + 52, + 57, + 58, + 59, + 61, + 63, + 64, + 71, + 73, + 75, + 77, + 79, + 82, + 83 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 16, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 5, + "optional": true + }, + { + "name": "changes", + "type": 31, + "optional": false + }, + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 32 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 33, + "optional": false + }, + { + "name": "digest", + "type": 5, + "optional": true + }, + { + "name": "path", + "type": 5, + "optional": false + }, + { + "name": "scope", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 34, + 35, + 36 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 5, + "optional": true + }, + { + "name": "kind", + "type": 28, + "optional": false + }, + { + "name": "rpcId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 5, + "optional": false + }, + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "ruleId", + "type": 5, + "optional": false + }, + { + "name": "source", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 46, + "optional": false + }, + { + "name": "form", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + }, + { + "name": "update", + "type": 16, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 47 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 53, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + }, + { + "name": "sections", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 56 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "text", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 60, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 60, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + }, + { + "name": "references", + "type": 66, + "optional": false + }, + { + "name": "version", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 67 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 9, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 68, + "optional": false + }, + { + "name": "compacted", + "type": 22, + "optional": false + }, + { + "name": "inputIndex", + "type": 9, + "optional": false + }, + { + "name": "label", + "type": 5, + "optional": false + }, + { + "name": "omittedBytes", + "type": 9, + "optional": false + }, + { + "name": "omittedMessages", + "type": 9, + "optional": false + }, + { + "name": "originalMessages", + "type": 9, + "optional": false + }, + { + "name": "retainedMessages", + "type": 9, + "optional": false + }, + { + "name": "sessionId", + "type": 5, + "optional": false + }, + { + "name": "truncated", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 9, + 69 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 72, + "optional": false + }, + { + "name": "senderSessionId", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 74, + "optional": false + }, + { + "name": "senderSessionId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 76, + "optional": false + }, + { + "name": "revision", + "type": 9, + "optional": false + }, + { + "name": "round", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "messageId", + "type": 5, + "optional": false + }, + { + "name": "senderId", + "type": 5, + "optional": false + }, + { + "name": "senderName", + "type": 5, + "optional": false + }, + { + "name": "teamId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "model", + "type": 5, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "replayState", + "type": 81, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 9 + }, + { + "kind": "union", + "types": [ + 86, + 87 + ] + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 9, + "optional": false + }, + { + "name": "op", + "type": 35, + "optional": false + }, + { + "name": "startSeq", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user/message" + } + ] + } + } + ], + "types": [] +} diff --git a/docs/persistence-changes/2026-09-14-image-offload.zh.md b/docs/persistence-changes/2026-09-14-image-offload.zh.md new file mode 100644 index 0000000000..b184b32e96 --- /dev/null +++ b/docs/persistence-changes/2026-09-14-image-offload.zh.md @@ -0,0 +1,96 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-09-14-image-offload + +[English](2026-09-14-image-offload.md) | 中文 + +## 概述 + +用 image/offload 事件记录选中的图片出现位置,通过所属插件的消息投影派生 offloaded 标记。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-09-14-image-offload +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-09-11-initial" + after: "646c2f1d243d3a78c5bc9305786fb340f5209c48a97e65f2a186cc5195491f3f" + decision: same-version + - root: "event:assistant/attempt" + previous: "2026-09-11-initial" + after: "c80c89da83c46db7a454f034c10f969e03cfb574859c7316e5bff683f5a14b0e" + decision: same-version + - root: "event:assistant/message" + previous: "2026-09-11-initial" + after: "1169b301aaabcd992657b93ec93750c43175dda81ada5cac086f14f2eaaeed6d" + decision: same-version + - root: "event:compaction/summary" + previous: "2026-09-11-initial" + after: "f6f3f30109e9008fccf7da2a3268a63da8646b59dd7cf7b75bcd9cf08ac08a59" + decision: same-version + - root: "event:image/offload" + previous: null + after: "b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e" + decision: same-version + - root: "event:llm/retry" + previous: "2026-09-11-initial" + after: "525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-09-11-initial" + after: "e0b5bf44c27bbfc6ab144239e3e4169c4d21146645e0e3121bbb359c3bc8591d" + decision: same-version + - root: "event:system/message" + previous: "2026-09-11-initial" + after: "69becfb6b2d3fd5da91518089454cae8ef33f1835637ec44dde35dd077fd4bae" + decision: same-version + - root: "event:team/message/queued" + previous: "2026-09-11-initial" + after: "443371ec07a03a82a0e93d93abca3e70b03bca55ba5b01d507030fcb66e8fbb4" + decision: same-version + - root: "event:tool/ptc-dispatch" + previous: "2026-09-12-auto-review-error-metadata" + after: "b33142af8176323ffbb20762541be81c1d51b60a60887803836e0b146e5bef0c" + decision: same-version + - root: "event:tool/result" + previous: "2026-09-12-auto-review-error-metadata" + after: "29af48b840d0cd9e48b6f50bf3b354f8f6340607c5b99220a48e74f60beac9e2" + decision: same-version + - root: "event:turn/end" + previous: "2026-09-11-initial" + after: "bab768260853e13a7cf2e22e65af572a1e76a5b2c01f3cdc2ce09d39b87fb70b" + decision: same-version + - root: "event:user/message" + previous: "2026-09-11-initial" + after: "314765bdff29c7862fb6ce820f1773563ba3094a680d163ea21180a2591b8578" + decision: same-version +``` + + +## 兼容性 + +现有日志仍可读取。可选的图片字段 offloaded 使未标记的出现位置保持保留状态。新事件在读取时必须被识别:不认识 image/offload 的旧版本拒绝读取这些日志,当前读取器需要对应的消息投影。事件信封和结构性的 Session 格式版本不变。 + + +## 验证 + +Session 和图片省略测试覆盖事件校验、不可变消息投影、缺少处理器、恢复和重试。所选的 1,144 项测试和两项 TypeScript 图片快照通过。Python advanced SDK 录制已通过构建后的 dsh profile 刷新,包含独立的 image/offload 事件。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/historical-formats/README.i18n.yaml b/docs/persistence-changes/historical-formats/README.i18n.yaml index a0daeff098..4a6936fc5a 100644 --- a/docs/persistence-changes/historical-formats/README.i18n.yaml +++ b/docs/persistence-changes/historical-formats/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-changes/historical-formats/README.md -README.md: 65e07738c907ba0a463cd6ffb876cd14b8f66941 -README.zh.md: 8af9aeb332327f5ff6cb2880aed5c6b290e426a7 +README.md: 186bec44b2b910b370d80e3115d5de628d8810e2 +README.zh.md: 680a3e279d724acb632efc1d90b54fcdd1fec31c diff --git a/docs/persistence-changes/historical-formats/README.md b/docs/persistence-changes/historical-formats/README.md index 65e07738c9..186bec44b2 100644 --- a/docs/persistence-changes/historical-formats/README.md +++ b/docs/persistence-changes/historical-formats/README.md @@ -31,7 +31,7 @@ The index is generated from validated snapshots and the current writer constant. | 0 | `dsh-v0.1.2-rc.1` | [V0](v0.md) | [JSON](v0.schema.json) | 54 / 415 | | 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 | -| 3 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 61 / 475 | +| 3 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 62 / 480 | diff --git a/docs/persistence-changes/historical-formats/README.zh.md b/docs/persistence-changes/historical-formats/README.zh.md index 8af9aeb332..680a3e279d 100644 --- a/docs/persistence-changes/historical-formats/README.zh.md +++ b/docs/persistence-changes/historical-formats/README.zh.md @@ -31,7 +31,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式 | 0 | `dsh-v0.1.2-rc.1` | [V0](v0.zh.md) | [JSON](v0.schema.json) | 54 / 415 | | 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 | -| 3 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 61 / 475 | +| 3 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 62 / 480 | diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index fe0b7177a0..a2f960cfba 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -406,7 +406,7 @@ "kind": "event", "event": "agent/inbox/spliced", "surface": false, - "digest": "f757322e25914d63799c6235af4ed4b4f36d7d083fef40d6873a45e94fe6ad65", + "digest": "646c2f1d243d3a78c5bc9305786fb340f5209c48a97e65f2a186cc5195491f3f", "schema": { "root": 0, "nodes": [ @@ -420,7 +420,7 @@ }, { "name": "ignorable", - "type": 32, + "type": 18, "optional": true }, { @@ -491,12 +491,12 @@ }, { "name": "role", - "type": 29, + "type": 30, "optional": false }, { "name": "source", - "type": 30, + "type": 31, "optional": false } ], @@ -511,10 +511,10 @@ "types": [ 6, 9, - 19, - 22, - 25, - 27 + 20, + 23, + 26, + 28 ] }, { @@ -560,8 +560,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 18, + "optional": true + }, + { + "name": "type", + "type": 19, "optional": false } ], @@ -653,6 +658,10 @@ ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -662,12 +671,12 @@ "properties": [ { "name": "attachment", - "type": 20, + "type": 21, "optional": false }, { "name": "type", - "type": 21, + "type": 22, "optional": false } ], @@ -708,7 +717,7 @@ }, { "name": "isError", - "type": 23, + "type": 24, "optional": true }, { @@ -718,7 +727,7 @@ }, { "name": "type", - "type": 24, + "type": 25, "optional": false } ], @@ -742,7 +751,7 @@ }, { "name": "type", - "type": 26, + "type": 27, "optional": false } ], @@ -762,7 +771,7 @@ }, { "name": "type", - "type": 28, + "type": 29, "optional": false } ], @@ -779,7 +788,7 @@ { "kind": "union", "types": [ - 31, + 32, 41, 43, 44, @@ -806,7 +815,7 @@ "properties": [ { "name": "baseline", - "type": 32, + "type": 18, "optional": true }, { @@ -832,10 +841,6 @@ ], "indices": [] }, - { - "kind": "literal", - "value": true - }, { "kind": "array", "element": 34 @@ -924,7 +929,7 @@ }, { "name": "kind", - "type": 29, + "type": 30, "optional": false }, { @@ -1004,7 +1009,7 @@ }, { "name": "update", - "type": 32, + "type": 18, "optional": true } ], @@ -1284,7 +1289,7 @@ }, { "name": "compacted", - "type": 23, + "type": 24, "optional": false }, { @@ -1324,7 +1329,7 @@ }, { "name": "truncated", - "type": 23, + "type": 24, "optional": false } ], @@ -1520,7 +1525,7 @@ "properties": [ { "name": "kind", - "type": 29, + "type": 30, "optional": false } ], @@ -1832,7 +1837,7 @@ "kind": "event", "event": "assistant/attempt", "surface": false, - "digest": "3516f54b427775f483febe8e938e4e10f9afd3583610861547bbe2999ed4d8d9", + "digest": "c80c89da83c46db7a454f034c10f969e03cfb574859c7316e5bff683f5a14b0e", "schema": { "root": 0, "nodes": [ @@ -1846,7 +1851,7 @@ }, { "name": "ignorable", - "type": 71, + "type": 26, "optional": true }, { @@ -1901,8 +1906,8 @@ "types": [ 5, 10, - 67, - 69 + 68, + 70 ] }, { @@ -1977,7 +1982,7 @@ }, { "name": "type", - "type": 66, + "type": 67, "optional": false } ], @@ -1988,11 +1993,11 @@ "types": [ 12, 14, - 39, - 42, - 44, - 46, - 63 + 40, + 43, + 45, + 47, + 64 ] }, { @@ -2045,7 +2050,7 @@ }, { "name": "type", - "type": 38, + "type": 39, "optional": false } ], @@ -2056,10 +2061,10 @@ "types": [ 16, 18, - 27, - 30, - 34, - 36 + 28, + 31, + 35, + 37 ] }, { @@ -2101,8 +2106,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 26, + "optional": true + }, + { + "name": "type", + "type": 27, "optional": false } ], @@ -2190,6 +2200,10 @@ ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -2199,12 +2213,12 @@ "properties": [ { "name": "attachment", - "type": 28, + "type": 29, "optional": false }, { "name": "type", - "type": 29, + "type": 30, "optional": false } ], @@ -2240,12 +2254,12 @@ "properties": [ { "name": "content", - "type": 31, + "type": 32, "optional": false }, { "name": "isError", - "type": 32, + "type": 33, "optional": true }, { @@ -2255,7 +2269,7 @@ }, { "name": "type", - "type": 33, + "type": 34, "optional": false } ], @@ -2283,7 +2297,7 @@ }, { "name": "type", - "type": 35, + "type": 36, "optional": false } ], @@ -2303,7 +2317,7 @@ }, { "name": "type", - "type": 37, + "type": 38, "optional": false } ], @@ -2322,7 +2336,7 @@ "properties": [ { "name": "blockType", - "type": 40, + "type": 41, "optional": false }, { @@ -2332,7 +2346,7 @@ }, { "name": "type", - "type": 41, + "type": 42, "optional": false } ], @@ -2342,11 +2356,11 @@ "kind": "union", "types": [ 17, - 26, - 29, - 33, - 35, - 37 + 27, + 30, + 34, + 36, + 38 ] }, { @@ -2368,7 +2382,7 @@ }, { "name": "type", - "type": 43, + "type": 44, "optional": false } ], @@ -2393,7 +2407,7 @@ }, { "name": "type", - "type": 45, + "type": 46, "optional": false } ], @@ -2408,17 +2422,17 @@ "properties": [ { "name": "reason", - "type": 47, + "type": 48, "optional": false }, { "name": "replayState", - "type": 59, + "type": 60, "optional": true }, { "name": "type", - "type": 62, + "type": 63, "optional": false } ], @@ -2427,11 +2441,11 @@ { "kind": "union", "types": [ - 48, - 51, - 53, - 55, - 57 + 49, + 52, + 54, + 56, + 58 ] }, { @@ -2439,12 +2453,12 @@ "properties": [ { "name": "failure", - "type": 49, + "type": 50, "optional": false }, { "name": "kind", - "type": 50, + "type": 51, "optional": false } ], @@ -2463,6 +2477,11 @@ "type": 7, "optional": false }, + { + "name": "offloadImages", + "type": 2, + "optional": true + }, { "name": "providerRetryAfterMs", "type": 2, @@ -2490,12 +2509,12 @@ "properties": [ { "name": "failure", - "type": 49, + "type": 50, "optional": false }, { "name": "kind", - "type": 52, + "type": 53, "optional": false } ], @@ -2510,7 +2529,7 @@ "properties": [ { "name": "kind", - "type": 54, + "type": 55, "optional": false } ], @@ -2525,7 +2544,7 @@ "properties": [ { "name": "kind", - "type": 56, + "type": 57, "optional": false } ], @@ -2540,7 +2559,7 @@ "properties": [ { "name": "kind", - "type": 58, + "type": 59, "optional": false } ], @@ -2555,12 +2574,12 @@ "properties": [ { "name": "blocks", - "type": 60, + "type": 61, "optional": true }, { "name": "response", - "type": 61, + "type": 62, "optional": false } ], @@ -2568,7 +2587,7 @@ }, { "kind": "array", - "element": 61 + "element": 62 }, { "kind": "opaque", @@ -2583,12 +2602,12 @@ "properties": [ { "name": "type", - "type": 64, + "type": 65, "optional": false }, { "name": "usage", - "type": 65, + "type": 66, "optional": false } ], @@ -2663,7 +2682,7 @@ }, { "name": "type", - "type": 68, + "type": 69, "optional": false } ], @@ -2698,7 +2717,7 @@ }, { "name": "type", - "type": 70, + "type": 71, "optional": false } ], @@ -2708,10 +2727,6 @@ "kind": "literal", "value": "text-chunks" }, - { - "kind": "literal", - "value": true - }, { "kind": "literal", "value": "assistant/attempt" @@ -2724,7 +2739,7 @@ "kind": "event", "event": "assistant/message", "surface": true, - "digest": "a58ca1bed502d425a8b65cac02f59717498cd296de9513c6f81b83b791c20e9b", + "digest": "1169b301aaabcd992657b93ec93750c43175dda81ada5cac086f14f2eaaeed6d", "schema": { "root": 0, "nodes": [ @@ -2887,6 +2902,11 @@ "type": 10, "optional": false }, + { + "name": "offloaded", + "type": 2, + "optional": true + }, { "name": "type", "type": 18, @@ -3443,6 +3463,11 @@ "type": 7, "optional": false }, + { + "name": "offloadImages", + "type": 11, + "optional": true + }, { "name": "providerRetryAfterMs", "type": 11, @@ -3732,7 +3757,7 @@ "kind": "event", "event": "browser-use/stagehand-llm-request", "surface": false, - "digest": "7cf11fcc829f21289bb7311562c6601499368464cce8ee678f80500e6a448b01", + "digest": "92cda1e2c0e9f57fdcd167a861b1296ec4cb5ac3ae127733c375e108335b32ac", "schema": { "root": 0, "nodes": [ @@ -3746,7 +3771,7 @@ }, { "name": "ignorable", - "type": 36, + "type": 19, "optional": true }, { @@ -3867,12 +3892,12 @@ }, { "name": "role", - "type": 30, + "type": 31, "optional": false }, { "name": "source", - "type": 34, + "type": 35, "optional": false } ], @@ -3887,10 +3912,10 @@ "types": [ 8, 11, - 20, - 23, - 26, - 28 + 21, + 24, + 27, + 29 ] }, { @@ -3936,8 +3961,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 19, + "optional": true + }, + { + "name": "type", + "type": 20, "optional": false } ], @@ -4025,6 +4055,10 @@ ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -4034,12 +4068,12 @@ "properties": [ { "name": "attachment", - "type": 21, + "type": 22, "optional": false }, { "name": "type", - "type": 22, + "type": 23, "optional": false } ], @@ -4080,7 +4114,7 @@ }, { "name": "isError", - "type": 24, + "type": 25, "optional": true }, { @@ -4090,7 +4124,7 @@ }, { "name": "type", - "type": 25, + "type": 26, "optional": false } ], @@ -4114,7 +4148,7 @@ }, { "name": "type", - "type": 27, + "type": 28, "optional": false } ], @@ -4134,7 +4168,7 @@ }, { "name": "type", - "type": 29, + "type": 30, "optional": false } ], @@ -4147,9 +4181,9 @@ { "kind": "union", "types": [ - 31, 32, - 33 + 33, + 34 ] }, { @@ -4167,7 +4201,7 @@ { "kind": "union", "types": [ - 35, + 36, 45, 47, 48, @@ -4194,7 +4228,7 @@ "properties": [ { "name": "baseline", - "type": 36, + "type": 19, "optional": true }, { @@ -4220,10 +4254,6 @@ ], "indices": [] }, - { - "kind": "literal", - "value": true - }, { "kind": "array", "element": 38 @@ -4312,7 +4342,7 @@ }, { "name": "kind", - "type": 33, + "type": 34, "optional": false }, { @@ -4392,7 +4422,7 @@ }, { "name": "update", - "type": 36, + "type": 19, "optional": true } ], @@ -4672,7 +4702,7 @@ }, { "name": "compacted", - "type": 24, + "type": 25, "optional": false }, { @@ -4712,7 +4742,7 @@ }, { "name": "truncated", - "type": 24, + "type": 25, "optional": false } ], @@ -4908,7 +4938,7 @@ "properties": [ { "name": "kind", - "type": 33, + "type": 34, "optional": false } ], @@ -4999,7 +5029,7 @@ "types": [ 3, 9, - 24, + 25, 75, 99, 100 @@ -5035,7 +5065,7 @@ "kind": "event", "event": "browser-use/stagehand-llm-result", "surface": false, - "digest": "05d3b279f065b23cfb5d192a9a95016fe23642d47dbd90378c81be939e8e1b09", + "digest": "0e0e81784bc85e1e6554b7ff50ab341169a1a3d32149ae49abe8dddca5ce6efe", "schema": { "root": 0, "nodes": [ @@ -5049,7 +5079,7 @@ }, { "name": "ignorable", - "type": 40, + "type": 16, "optional": true }, { @@ -5080,7 +5110,7 @@ }, { "name": "finish", - "type": 27, + "type": 28, "optional": false }, { @@ -5090,7 +5120,7 @@ }, { "name": "usage", - "type": 39, + "type": 40, "optional": true } ], @@ -5105,10 +5135,10 @@ "types": [ 4, 7, - 17, - 20, - 23, - 25 + 18, + 21, + 24, + 26 ] }, { @@ -5154,8 +5184,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 16, + "optional": true + }, + { + "name": "type", + "type": 17, "optional": false } ], @@ -5247,6 +5282,10 @@ ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -5256,12 +5295,12 @@ "properties": [ { "name": "attachment", - "type": 18, + "type": 19, "optional": false }, { "name": "type", - "type": 19, + "type": 20, "optional": false } ], @@ -5302,7 +5341,7 @@ }, { "name": "isError", - "type": 21, + "type": 22, "optional": true }, { @@ -5312,7 +5351,7 @@ }, { "name": "type", - "type": 22, + "type": 23, "optional": false } ], @@ -5336,7 +5375,7 @@ }, { "name": "type", - "type": 24, + "type": 25, "optional": false } ], @@ -5356,7 +5395,7 @@ }, { "name": "type", - "type": 26, + "type": 27, "optional": false } ], @@ -5369,11 +5408,11 @@ { "kind": "union", "types": [ - 28, - 31, - 33, - 35, - 37 + 29, + 32, + 34, + 36, + 38 ] }, { @@ -5381,12 +5420,12 @@ "properties": [ { "name": "failure", - "type": 29, + "type": 30, "optional": false }, { "name": "kind", - "type": 30, + "type": 31, "optional": false } ], @@ -5405,6 +5444,11 @@ "type": 5, "optional": false }, + { + "name": "offloadImages", + "type": 9, + "optional": true + }, { "name": "providerRetryAfterMs", "type": 9, @@ -5432,12 +5476,12 @@ "properties": [ { "name": "failure", - "type": 29, + "type": 30, "optional": false }, { "name": "kind", - "type": 32, + "type": 33, "optional": false } ], @@ -5452,7 +5496,7 @@ "properties": [ { "name": "kind", - "type": 34, + "type": 35, "optional": false } ], @@ -5467,7 +5511,7 @@ "properties": [ { "name": "kind", - "type": 36, + "type": 37, "optional": false } ], @@ -5482,7 +5526,7 @@ "properties": [ { "name": "kind", - "type": 38, + "type": 39, "optional": false } ], @@ -5528,10 +5572,6 @@ ], "indices": [] }, - { - "kind": "literal", - "value": true - }, { "kind": "literal", "value": "browser-use/stagehand-llm-result" @@ -6027,7 +6067,7 @@ "kind": "event", "event": "compaction/summary", "surface": false, - "digest": "eb1d32dcd3d76833392f351e039dcbd40686a1ab24b252a7a5003426f9cf4c89", + "digest": "f6f3f30109e9008fccf7da2a3268a63da8646b59dd7cf7b75bcd9cf08ac08a59", "schema": { "root": 0, "nodes": [ @@ -6274,6 +6314,11 @@ "type": 18, "optional": false }, + { + "name": "offloaded", + "type": 4, + "optional": true + }, { "name": "type", "type": 25, @@ -7469,12 +7514,102 @@ ] } }, + { + "key": "event:image/offload", + "kind": "event", + "event": "image/offload", + "surface": false, + "digest": "b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 6, + "optional": true + }, + { + "name": "seq", + "type": 5, + "optional": false + }, + { + "name": "time", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "targets", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "object", + "properties": [ + { + "name": "imageIndexes", + "type": 4, + "optional": false + }, + { + "name": "seq", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image/offload" + } + ] + } + }, { "key": "event:llm/retry", "kind": "event", "event": "llm/retry", "surface": false, - "digest": "91c397f8f870e812e1dc5ac69c2745f9f105be9dc97dadc980d19ef415a65145", + "digest": "525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336", "schema": { "root": 0, "nodes": [ @@ -7589,6 +7724,11 @@ "type": 5, "optional": false }, + { + "name": "offloadImages", + "type": 3, + "optional": true + }, { "name": "providerRetryAfterMs", "type": 3, @@ -8986,7 +9126,7 @@ "kind": "event", "event": "session/title-llm-request", "surface": false, - "digest": "6091f64d426ada2adc646094a6fe662a1b81d9ee5770929fe76800524e2c89a8", + "digest": "e0b5bf44c27bbfc6ab144239e3e4169c4d21146645e0e3121bbb359c3bc8591d", "schema": { "root": 0, "nodes": [ @@ -9000,7 +9140,7 @@ }, { "name": "ignorable", - "type": 36, + "type": 19, "optional": true }, { @@ -9084,12 +9224,12 @@ }, { "name": "role", - "type": 30, + "type": 31, "optional": false }, { "name": "source", - "type": 34, + "type": 35, "optional": false } ], @@ -9104,10 +9244,10 @@ "types": [ 8, 11, - 20, - 23, - 26, - 28 + 21, + 24, + 27, + 29 ] }, { @@ -9153,8 +9293,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 19, + "optional": true + }, + { + "name": "type", + "type": 20, "optional": false } ], @@ -9242,6 +9387,10 @@ ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -9251,12 +9400,12 @@ "properties": [ { "name": "attachment", - "type": 21, + "type": 22, "optional": false }, { "name": "type", - "type": 22, + "type": 23, "optional": false } ], @@ -9297,7 +9446,7 @@ }, { "name": "isError", - "type": 24, + "type": 25, "optional": true }, { @@ -9307,7 +9456,7 @@ }, { "name": "type", - "type": 25, + "type": 26, "optional": false } ], @@ -9331,7 +9480,7 @@ }, { "name": "type", - "type": 27, + "type": 28, "optional": false } ], @@ -9351,7 +9500,7 @@ }, { "name": "type", - "type": 29, + "type": 30, "optional": false } ], @@ -9364,9 +9513,9 @@ { "kind": "union", "types": [ - 31, 32, - 33 + 33, + 34 ] }, { @@ -9384,7 +9533,7 @@ { "kind": "union", "types": [ - 35, + 36, 45, 47, 48, @@ -9411,7 +9560,7 @@ "properties": [ { "name": "baseline", - "type": 36, + "type": 19, "optional": true }, { @@ -9437,10 +9586,6 @@ ], "indices": [] }, - { - "kind": "literal", - "value": true - }, { "kind": "array", "element": 38 @@ -9529,7 +9674,7 @@ }, { "name": "kind", - "type": 33, + "type": 34, "optional": false }, { @@ -9609,7 +9754,7 @@ }, { "name": "update", - "type": 36, + "type": 19, "optional": true } ], @@ -9889,7 +10034,7 @@ }, { "name": "compacted", - "type": 24, + "type": 25, "optional": false }, { @@ -9929,7 +10074,7 @@ }, { "name": "truncated", - "type": 24, + "type": 25, "optional": false } ], @@ -10125,7 +10270,7 @@ "properties": [ { "name": "kind", - "type": 33, + "type": 34, "optional": false } ], @@ -10702,7 +10847,7 @@ "kind": "event", "event": "system/message", "surface": true, - "digest": "0ae7ccdc1cc5d9d43772ebd5c7357ff509011c3a6ba188d9c274d33df2542c42", + "digest": "69becfb6b2d3fd5da91518089454cae8ef33f1835637ec44dde35dd077fd4bae", "schema": { "root": 0, "nodes": [ @@ -10716,7 +10861,7 @@ }, { "name": "ignorable", - "type": 46, + "type": 17, "optional": true }, { @@ -10783,12 +10928,12 @@ }, { "name": "role", - "type": 28, + "type": 29, "optional": false }, { "name": "source", - "type": 29, + "type": 30, "optional": false } ], @@ -10803,10 +10948,10 @@ "types": [ 5, 8, - 18, - 21, - 24, - 26 + 19, + 22, + 25, + 27 ] }, { @@ -10852,8 +10997,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 17, + "optional": true + }, + { + "name": "type", + "type": 18, "optional": false } ], @@ -10945,6 +11095,10 @@ ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -10954,12 +11108,12 @@ "properties": [ { "name": "attachment", - "type": 19, + "type": 20, "optional": false }, { "name": "type", - "type": 20, + "type": 21, "optional": false } ], @@ -11000,7 +11154,7 @@ }, { "name": "isError", - "type": 22, + "type": 23, "optional": true }, { @@ -11010,7 +11164,7 @@ }, { "name": "type", - "type": 23, + "type": 24, "optional": false } ], @@ -11034,7 +11188,7 @@ }, { "name": "type", - "type": 25, + "type": 26, "optional": false } ], @@ -11054,7 +11208,7 @@ }, { "name": "type", - "type": 27, + "type": 28, "optional": false } ], @@ -11071,13 +11225,13 @@ { "kind": "union", "types": [ - 30, - 35, - 37, - 39, - 41, - 43, - 45 + 31, + 36, + 38, + 40, + 42, + 44, + 46 ] }, { @@ -11085,12 +11239,12 @@ "properties": [ { "name": "form", - "type": 31, + "type": 32, "optional": false }, { "name": "kind", - "type": 32, + "type": 33, "optional": false }, { @@ -11100,7 +11254,7 @@ }, { "name": "sections", - "type": 33, + "type": 34, "optional": false } ], @@ -11116,7 +11270,7 @@ }, { "kind": "array", - "element": 34 + "element": 35 }, { "kind": "object", @@ -11139,12 +11293,12 @@ "properties": [ { "name": "form", - "type": 36, + "type": 37, "optional": false }, { "name": "kind", - "type": 32, + "type": 33, "optional": false }, { @@ -11169,12 +11323,12 @@ "properties": [ { "name": "form", - "type": 38, + "type": 39, "optional": false }, { "name": "kind", - "type": 32, + "type": 33, "optional": false }, { @@ -11194,12 +11348,12 @@ "properties": [ { "name": "form", - "type": 40, + "type": 41, "optional": false }, { "name": "kind", - "type": 32, + "type": 33, "optional": false }, { @@ -11219,12 +11373,12 @@ "properties": [ { "name": "form", - "type": 42, + "type": 43, "optional": false }, { "name": "kind", - "type": 32, + "type": 33, "optional": false }, { @@ -11244,12 +11398,12 @@ "properties": [ { "name": "form", - "type": 44, + "type": 45, "optional": false }, { "name": "kind", - "type": 32, + "type": 33, "optional": false }, { @@ -11269,7 +11423,7 @@ "properties": [ { "name": "kind", - "type": 32, + "type": 33, "optional": false }, { @@ -11280,10 +11434,6 @@ ], "indices": [] }, - { - "kind": "literal", - "value": true - }, { "kind": "array", "element": 10 @@ -11585,7 +11735,7 @@ "kind": "event", "event": "team/message/queued", "surface": false, - "digest": "2ffcad547cb840f462eb1849fa5f6cb0e17eb93f5d07cc88b61d749b1b78d632", + "digest": "443371ec07a03a82a0e93d93abca3e70b03bca55ba5b01d507030fcb66e8fbb4", "schema": { "root": 0, "nodes": [ @@ -11599,7 +11749,7 @@ }, { "name": "ignorable", - "type": 29, + "type": 24, "optional": true }, { @@ -11635,7 +11785,7 @@ }, { "name": "version", - "type": 28, + "type": 29, "optional": false } ], @@ -11684,7 +11834,7 @@ 11, 13, 15, - 25 + 26 ] }, { @@ -11804,8 +11954,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 24, + "optional": true + }, + { + "name": "type", + "type": 25, "optional": false } ], @@ -11897,6 +12052,10 @@ ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -11906,12 +12065,12 @@ "properties": [ { "name": "attachment", - "type": 26, + "type": 27, "optional": false }, { "name": "type", - "type": 27, + "type": 28, "optional": false } ], @@ -11946,10 +12105,6 @@ "kind": "literal", "value": 2 }, - { - "kind": "literal", - "value": true - }, { "kind": "literal", "value": "team/message/queued" @@ -12681,7 +12836,7 @@ "kind": "event", "event": "tool/ptc-dispatch", "surface": false, - "digest": "b5d66eaebed4da391b13498623b11142222149fbf5e025975dbc6d94f0d06796", + "digest": "b33142af8176323ffbb20762541be81c1d51b60a60887803836e0b146e5bef0c", "schema": { "root": 0, "nodes": [ @@ -12695,7 +12850,7 @@ }, { "name": "ignorable", - "type": 29, + "type": 24, "optional": true }, { @@ -12731,7 +12886,7 @@ }, { "name": "error", - "type": 28, + "type": 29, "optional": true }, { @@ -12778,7 +12933,7 @@ 11, 13, 15, - 25 + 26 ] }, { @@ -12898,8 +13053,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 24, + "optional": true + }, + { + "name": "type", + "type": 25, "optional": false } ], @@ -12991,6 +13151,10 @@ ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -13000,12 +13164,12 @@ "properties": [ { "name": "attachment", - "type": 26, + "type": 27, "optional": false }, { "name": "type", - "type": 27, + "type": 28, "optional": false } ], @@ -13057,10 +13221,6 @@ ], "indices": [] }, - { - "kind": "literal", - "value": true - }, { "kind": "literal", "value": "tool/ptc-dispatch" @@ -13167,7 +13327,7 @@ "kind": "event", "event": "tool/result", "surface": true, - "digest": "3a803805bdeb805f32b229e399fb7258be8e32a89f89063a7c99957cfea942f7", + "digest": "29af48b840d0cd9e48b6f50bf3b354f8f6340607c5b99220a48e74f60beac9e2", "schema": { "root": 0, "nodes": [ @@ -13181,7 +13341,7 @@ }, { "name": "ignorable", - "type": 37, + "type": 24, "optional": true }, { @@ -13227,7 +13387,7 @@ }, { "name": "meta", - "type": 33, + "type": 34, "optional": true }, { @@ -13283,12 +13443,12 @@ }, { "name": "role", - "type": 30, + "type": 31, "optional": false }, { "name": "source", - "type": 31, + "type": 32, "optional": false } ], @@ -13314,7 +13474,7 @@ }, { "name": "isError", - "type": 28, + "type": 29, "optional": true }, { @@ -13324,7 +13484,7 @@ }, { "name": "type", - "type": 29, + "type": 30, "optional": false } ], @@ -13342,7 +13502,7 @@ 11, 13, 15, - 25 + 26 ] }, { @@ -13424,8 +13584,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 24, + "optional": true + }, + { + "name": "type", + "type": 25, "optional": false } ], @@ -13517,6 +13682,10 @@ ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -13526,12 +13695,12 @@ "properties": [ { "name": "attachment", - "type": 26, + "type": 27, "optional": false }, { "name": "type", - "type": 27, + "type": 28, "optional": false } ], @@ -13584,7 +13753,7 @@ }, { "name": "kind", - "type": 32, + "type": 33, "optional": false } ], @@ -13599,15 +13768,15 @@ "types": [ 3, 17, - 28, - 34, + 29, 35, - 36 + 36, + 37 ] }, { "kind": "array", - "element": 33 + "element": 34 }, { "kind": "primitive", @@ -13619,14 +13788,10 @@ "indices": [ { "key": 3, - "value": 33 + "value": 34 } ] }, - { - "kind": "literal", - "value": true - }, { "kind": "array", "element": 17 @@ -13679,7 +13844,7 @@ "kind": "event", "event": "turn/end", "surface": false, - "digest": "84c24f1209fc3e0153de6ac85d58fd6968b851f955e0e761b92321053956609e", + "digest": "bab768260853e13a7cf2e22e65af572a1e76a5b2c01f3cdc2ce09d39b87fb70b", "schema": { "root": 0, "nodes": [ @@ -13830,6 +13995,11 @@ "type": 13, "optional": false }, + { + "name": "offloadImages", + "type": 14, + "optional": true + }, { "name": "providerRetryAfterMs", "type": 14, @@ -14052,7 +14222,7 @@ "kind": "event", "event": "user/message", "surface": true, - "digest": "2e49ede7fb3a592cce8919ebe982f6ca1c1f67abe4def359ffc028c85e6d1ad8", + "digest": "314765bdff29c7862fb6ce820f1773563ba3094a680d163ea21180a2591b8578", "schema": { "root": 0, "nodes": [ @@ -14066,7 +14236,7 @@ }, { "name": "ignorable", - "type": 30, + "type": 16, "optional": true }, { @@ -14112,12 +14282,12 @@ }, { "name": "role", - "type": 27, + "type": 28, "optional": false }, { "name": "source", - "type": 28, + "type": 29, "optional": false } ], @@ -14132,10 +14302,10 @@ "types": [ 4, 7, - 17, - 20, - 23, - 25 + 18, + 21, + 24, + 26 ] }, { @@ -14181,8 +14351,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 16, + "optional": true + }, + { + "name": "type", + "type": 17, "optional": false } ], @@ -14274,6 +14449,10 @@ ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -14283,12 +14462,12 @@ "properties": [ { "name": "attachment", - "type": 18, + "type": 19, "optional": false }, { "name": "type", - "type": 19, + "type": 20, "optional": false } ], @@ -14329,7 +14508,7 @@ }, { "name": "isError", - "type": 21, + "type": 22, "optional": true }, { @@ -14339,7 +14518,7 @@ }, { "name": "type", - "type": 22, + "type": 23, "optional": false } ], @@ -14363,7 +14542,7 @@ }, { "name": "type", - "type": 24, + "type": 25, "optional": false } ], @@ -14383,7 +14562,7 @@ }, { "name": "type", - "type": 26, + "type": 27, "optional": false } ], @@ -14400,7 +14579,7 @@ { "kind": "union", "types": [ - 29, + 30, 39, 41, 42, @@ -14427,7 +14606,7 @@ "properties": [ { "name": "baseline", - "type": 30, + "type": 16, "optional": true }, { @@ -14453,10 +14632,6 @@ ], "indices": [] }, - { - "kind": "literal", - "value": true - }, { "kind": "array", "element": 32 @@ -14545,7 +14720,7 @@ }, { "name": "kind", - "type": 27, + "type": 28, "optional": false }, { @@ -14625,7 +14800,7 @@ }, { "name": "update", - "type": 30, + "type": 16, "optional": true } ], @@ -14905,7 +15080,7 @@ }, { "name": "compacted", - "type": 21, + "type": 22, "optional": false }, { @@ -14945,7 +15120,7 @@ }, { "name": "truncated", - "type": 21, + "type": 22, "optional": false } ], @@ -15141,7 +15316,7 @@ "properties": [ { "name": "kind", - "type": 27, + "type": 28, "optional": false } ], @@ -15483,515 +15658,6 @@ "packages/interaction/user-approval/src/index.ts:60" ] }, - { - "digest": "05d3b279f065b23cfb5d192a9a95016fe23642d47dbd90378c81be939e8e1b09", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 40, - "optional": true - }, - { - "name": "seq", - "type": 9, - "optional": false - }, - { - "name": "time", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "blocks", - "type": 2, - "optional": false - }, - { - "name": "finish", - "type": 27, - "optional": false - }, - { - "name": "requestSeq", - "type": 9, - "optional": false - }, - { - "name": "usage", - "type": 39, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "union", - "types": [ - 4, - 7, - 17, - 20, - 23, - 25 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 5, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 8, - "optional": false - }, - { - "name": "type", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 9, - "optional": false - }, - { - "name": "height", - "type": 9, - "optional": false - }, - { - "name": "mediaType", - "type": 10, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": true - }, - { - "name": "originalDimensions", - "type": 15, - "optional": true - }, - { - "name": "width", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 11, - 12, - 13, - 14 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 9, - "optional": false - }, - { - "name": "width", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 18, - "optional": false - }, - { - "name": "type", - "type": 19, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "isError", - "type": 21, - "optional": true - }, - { - "name": "toolCallId", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "union", - "types": [ - 28, - 31, - 33, - 35, - 37 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 29, - "optional": false - }, - { - "name": "kind", - "type": 30, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 5, - "optional": false - }, - { - "name": "message", - "type": 5, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 9, - "optional": true - }, - { - "name": "requestId", - "type": 5, - "optional": true - }, - { - "name": "status", - "type": 9, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 29, - "optional": false - }, - { - "name": "kind", - "type": 32, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 34, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 36, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "stop" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-calls" - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 9, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 9, - "optional": true - }, - { - "name": "inputTokens", - "type": 9, - "optional": false - }, - { - "name": "outputTokens", - "type": 9, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 9, - "optional": true - }, - { - "name": "totalTokens", - "type": 9, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "literal", - "value": "browser-use/stagehand-llm-result" - } - ] - }, - "names": [ - "event:browser-use/stagehand-llm-result" - ], - "sources": [] - }, { "digest": "0657d3ca5ac4e7bf79b00bcf860b1c7159d01a9da137497221ec2f1776e365f7", "schema": { @@ -16011,31 +15677,48 @@ ] }, { - "digest": "07bd32ad9baaf7ca4d6e778e6e1f707b546bf49f0a8039a0e8e40117759fa5e4", + "digest": "071d92b9dac661fcc826c678cf64d188f26fb93aab79c627b11a03acdd34dfca", "schema": { "root": 0, "nodes": [ + { + "kind": "union", + "types": [ + 1, + 5, + 31, + 34, + 36, + 38, + 55 + ] + }, { "kind": "object", "properties": [ { - "name": "description", - "type": 1, - "optional": true - }, - { - "name": "name", - "type": 1, - "optional": false - }, - { - "name": "schema", + "name": "argumentsDelta", "type": 2, "optional": false }, + { + "name": "id", + "type": 2, + "optional": false + }, + { + "name": "index", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": true + }, { "name": "type", - "type": 8, + "type": 4, "optional": false } ], @@ -16045,167 +15728,44 @@ "kind": "primitive", "type": "string" }, - { - "kind": "union", - "types": [ - 1, - 3, - 4, - 5, - 6, - 7 - ] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "object", - "properties": [], - "indices": [ - { - "key": 1, - "value": 2 - } - ] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "primitive", - "type": "null" - }, { "kind": "primitive", "type": "number" }, { "kind": "literal", - "value": "json_schema" - } - ] - }, - "names": [ - "event:browser-use/stagehand-llm-request.data.responseFormat" - ], - "sources": [] - }, - { - "digest": "07d0e11cb5f42f2841b28814ceb5e91fb12c43249343f7ed716ec3646a081229", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "maxTokens", - "type": 1, - "optional": true - }, - { - "name": "messages", - "type": 2, - "optional": false - }, - { - "name": "model", - "type": 7, - "optional": false - }, - { - "name": "provider", - "type": 7, - "optional": false - }, - { - "name": "purpose", - "type": 88, - "optional": true - }, - { - "name": "reasoningEffort", - "type": 7, - "optional": true - }, - { - "name": "sessionId", - "type": 7, - "optional": true - }, - { - "name": "stop", - "type": 91, - "optional": true - }, - { - "name": "system", - "type": 7, - "optional": true - }, - { - "name": "temperature", - "type": 1, - "optional": true - }, - { - "name": "tools", - "type": 92, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 3 + "value": "tool-call-delta" }, { "kind": "object", "properties": [ { - "name": "content", - "type": 4, + "name": "block", + "type": 6, "optional": false }, { - "name": "id", - "type": 7, + "name": "index", + "type": 3, "optional": false }, { - "name": "role", - "type": 28, - "optional": false - }, - { - "name": "source", - "type": 32, + "name": "type", + "type": 30, "optional": false } ], "indices": [] }, - { - "kind": "array", - "element": 5 - }, { "kind": "union", "types": [ - 6, + 7, 9, - 18, - 21, - 24, - 26 + 19, + 22, + 26, + 28 ] }, { @@ -16213,17 +15773,17 @@ "properties": [ { "name": "arguments", - "type": 7, + "type": 2, "optional": false }, { "name": "id", - "type": 7, + "type": 2, "optional": false }, { "name": "name", - "type": 7, + "type": 2, "optional": false }, { @@ -16234,10 +15794,6 @@ ], "indices": [] }, - { - "kind": "primitive", - "type": "string" - }, { "kind": "literal", "value": "tool-call" @@ -16251,8 +15807,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 17, + "optional": true + }, + { + "name": "type", + "type": 18, "optional": false } ], @@ -16263,17 +15824,17 @@ "properties": [ { "name": "attachmentId", - "type": 7, + "type": 2, "optional": false }, { "name": "bytes", - "type": 1, + "type": 3, "optional": false }, { "name": "height", - "type": 1, + "type": 3, "optional": false }, { @@ -16283,7 +15844,7 @@ }, { "name": "name", - "type": 7, + "type": 2, "optional": true }, { @@ -16293,7 +15854,7 @@ }, { "name": "width", - "type": 1, + "type": 3, "optional": false } ], @@ -16318,28 +15879,32 @@ }, { "kind": "literal", - "value": "image/jpeg" + "value": "image/gif" }, { "kind": "literal", - "value": "image/gif" + "value": "image/jpeg" }, { "kind": "object", "properties": [ { "name": "height", - "type": 1, + "type": 3, "optional": false }, { "name": "width", - "type": 1, + "type": 3, "optional": false } ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -16349,12 +15914,12 @@ "properties": [ { "name": "attachment", - "type": 19, + "type": 20, "optional": false }, { "name": "type", - "type": 20, + "type": 21, "optional": false } ], @@ -16365,17 +15930,17 @@ "properties": [ { "name": "attachmentId", - "type": 7, + "type": 2, "optional": false }, { "name": "bytes", - "type": 1, + "type": 3, "optional": false }, { "name": "name", - "type": 7, + "type": 2, "optional": false } ], @@ -16390,27 +15955,31 @@ "properties": [ { "name": "content", - "type": 4, + "type": 23, "optional": false }, { "name": "isError", - "type": 22, + "type": 24, "optional": true }, { "name": "toolCallId", - "type": 7, + "type": 2, "optional": false }, { "name": "type", - "type": 23, + "type": 25, "optional": false } ], "indices": [] }, + { + "kind": "array", + "element": 6 + }, { "kind": "primitive", "type": "boolean" @@ -16424,12 +15993,12 @@ "properties": [ { "name": "text", - "type": 7, + "type": 2, "optional": false }, { "name": "type", - "type": 25, + "type": 27, "optional": false } ], @@ -16444,12 +16013,12 @@ "properties": [ { "name": "text", - "type": 7, + "type": 2, "optional": false }, { "name": "type", - "type": 27, + "type": 29, "optional": false } ], @@ -16459,71 +16028,132 @@ "kind": "literal", "value": "text" }, + { + "kind": "literal", + "value": "block-end" + }, + { + "kind": "object", + "properties": [ + { + "name": "blockType", + "type": 32, + "optional": false + }, + { + "name": "index", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 33, + "optional": false + } + ], + "indices": [] + }, { "kind": "union", "types": [ - 29, - 30, - 31 + 8, + 18, + 21, + 25, + 27, + 29 ] }, { "kind": "literal", - "value": "system" + "value": "block-start" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 3, + "optional": false + }, + { + "name": "text", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 35, + "optional": false + } + ], + "indices": [] }, { "kind": "literal", - "value": "assistant" + "value": "reasoning-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 3, + "optional": false + }, + { + "name": "text", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 37, + "optional": false + } + ], + "indices": [] }, { "kind": "literal", - "value": "user" + "value": "text-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 39, + "optional": false + }, + { + "name": "replayState", + "type": 51, + "optional": true + }, + { + "name": "type", + "type": 54, + "optional": false + } + ], + "indices": [] }, { "kind": "union", "types": [ - 33, + 40, 43, 45, - 46, - 49, - 54, - 56, - 61, - 62, - 63, - 65, - 67, - 68, - 75, - 77, - 79, - 81, - 83, - 86, - 87 + 47, + 49 ] }, { "kind": "object", "properties": [ { - "name": "baseline", - "type": 34, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 7, - "optional": true - }, - { - "name": "changes", - "type": 35, - "optional": false - }, - { - "name": "form", + "name": "failure", "type": 41, "optional": false }, @@ -16535,74 +16165,52 @@ ], "indices": [] }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 36 - }, { "kind": "object", "properties": [ { - "name": "action", - "type": 37, + "name": "code", + "type": 2, "optional": false }, { - "name": "digest", - "type": 7, + "name": "message", + "type": 2, + "optional": false + }, + { + "name": "offloadImages", + "type": 3, "optional": true }, { - "name": "path", - "type": 7, - "optional": false + "name": "providerRetryAfterMs", + "type": 3, + "optional": true }, { - "name": "scope", - "type": 7, - "optional": false + "name": "requestId", + "type": 2, + "optional": true + }, + { + "name": "status", + "type": 3, + "optional": true } ], "indices": [] }, - { - "kind": "union", - "types": [ - 38, - 39, - 40 - ] - }, { "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" + "value": "aborted" }, { "kind": "object", "properties": [ { - "name": "callId", - "type": 7, + "name": "failure", + "type": 41, "optional": false }, { @@ -16615,685 +16223,148 @@ }, { "kind": "literal", - "value": "tool" + "value": "error" }, { "kind": "object", "properties": [ - { - "name": "clientTimeZone", - "type": 7, - "optional": true - }, { "name": "kind", - "type": 31, - "optional": false - }, - { - "name": "rpcId", - "type": 7, + "type": 46, "optional": false } ], "indices": [] }, + { + "kind": "literal", + "value": "max-tokens" + }, { "kind": "object", "properties": [ - { - "name": "deliveryId", - "type": 7, - "optional": false - }, - { - "name": "form", - "type": 47, - "optional": false - }, { "name": "kind", "type": 48, "optional": false - }, - { - "name": "provider", - "type": 7, - "optional": false - }, - { - "name": "ruleId", - "type": 7, - "optional": false - }, - { - "name": "source", - "type": 7, - "optional": false - }, - { - "name": "summary", - "type": 7, - "optional": false } ], "indices": [] }, { "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" + "value": "stop" }, { "kind": "object", "properties": [ { - "name": "entries", + "name": "kind", "type": 50, "optional": false - }, + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ { - "name": "form", + "name": "blocks", "type": 52, - "optional": false + "optional": true }, { - "name": "kind", + "name": "response", "type": 53, "optional": false - }, - { - "name": "update", - "type": 34, - "optional": true } ], "indices": [] }, { "kind": "array", - "element": 51 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 7, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 57, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - }, - { - "name": "sections", - "type": 59, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 60 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "text", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 47, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - }, - { - "name": "summary", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 64, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 52, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 64, - "optional": false - }, - { - "name": "kind", - "type": 69, - "optional": false - }, - { - "name": "references", - "type": 70, - "optional": false - }, - { - "name": "version", - "type": 74, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 71 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 1, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 72, - "optional": false - }, - { - "name": "compacted", - "type": 22, - "optional": false - }, - { - "name": "inputIndex", - "type": 1, - "optional": false - }, - { - "name": "label", - "type": 7, - "optional": false - }, - { - "name": "omittedBytes", - "type": 1, - "optional": false - }, - { - "name": "omittedMessages", - "type": 1, - "optional": false - }, - { - "name": "originalMessages", - "type": 1, - "optional": false - }, - { - "name": "retainedMessages", - "type": 1, - "optional": false - }, - { - "name": "sessionId", - "type": 7, - "optional": false - }, - { - "name": "truncated", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 1, - 73 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 47, - "optional": false - }, - { - "name": "kind", - "type": 76, - "optional": false - }, - { - "name": "senderSessionId", - "type": 7, - "optional": false - }, - { - "name": "summary", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 78, - "optional": false - }, - { - "name": "senderSessionId", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 7, - "optional": false - }, - { - "name": "kind", - "type": 80, - "optional": false - }, - { - "name": "revision", - "type": 1, - "optional": false - }, - { - "name": "round", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 82, - "optional": false - }, - { - "name": "messageId", - "type": 7, - "optional": false - }, - { - "name": "senderId", - "type": 7, - "optional": false - }, - { - "name": "senderName", - "type": 7, - "optional": false - }, - { - "name": "teamId", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 84, - "optional": false - }, - { - "name": "model", - "type": 7, - "optional": false - }, - { - "name": "provider", - "type": 7, - "optional": false - }, - { - "name": "replayState", - "type": 85, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" + "element": 53 }, { "kind": "opaque", "reason": "unknown" }, + { + "kind": "literal", + "value": "finish" + }, { "kind": "object", "properties": [ { - "name": "kind", - "type": 58, + "name": "type", + "type": 56, "optional": false }, { - "name": "plugin", - "type": 7, + "name": "usage", + "type": 57, "optional": false } ], "indices": [] }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 31, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 89, - 90 - ] - }, { "kind": "literal", - "value": "session-title" - }, - { - "kind": "literal", - "value": "compaction" - }, - { - "kind": "array", - "element": 7 - }, - { - "kind": "array", - "element": 93 + "value": "usage" }, { "kind": "object", "properties": [ { - "name": "description", - "type": 7, + "name": "cacheReadTokens", + "type": 3, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 3, + "optional": true + }, + { + "name": "inputTokens", + "type": 3, "optional": false }, { - "name": "name", - "type": 7, + "name": "outputTokens", + "type": 3, "optional": false }, { - "name": "parameters", - "type": 94, - "optional": false + "name": "reasoningTokens", + "type": 3, + "optional": true + }, + { + "name": "totalTokens", + "type": 3, + "optional": true } ], "indices": [] - }, - { - "kind": "object", - "properties": [], - "indices": [ - { - "key": 7, - "value": 85 - } - ] } ] }, "names": [ - "packages/experimental/browser-use-stagehand-native/src/model.ts#RecordedRequest" + "packages/llm/llm/src/types.ts#StreamChunk" ], "sources": [ - "packages/experimental/browser-use-stagehand-native/src/model.ts:14" + "packages/llm/llm/src/types.ts:424" ] }, { - "digest": "07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235", + "digest": "07796b041bf5e5516b3c9cf80292a19bd1e518ecf11e461a6edb19780d0e7da9", "schema": { "root": 0, "nodes": [ @@ -17301,64 +16372,25 @@ "kind": "object", "properties": [ { - "name": "model", + "name": "request", "type": 1, "optional": false }, { - "name": "provider", - "type": 1, + "name": "responseFormat", + "type": 96, "optional": false } ], "indices": [] }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "packages/session/session-title/src/types.ts#SessionTitleModelIdentity", - "packages/subagent/tool-subagent/src/model-selection.ts#AllowedModelRoute" - ], - "sources": [ - "packages/session/session-title/src/types.ts:20", - "packages/subagent/tool-subagent/src/model-selection.ts:9" - ] - }, - { - "digest": "07eccc9ea2f9b5439d76cc77d5e062936677d43f07a23da749819b0a22ee36b7", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "security-privacy-permission" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "08936f7648f573f7539800e1ec2852fbbcba05e1a20487a1e5acf2a0fce1e2de", - "schema": { - "root": 0, - "nodes": [ { "kind": "object", "properties": [ { "name": "maxTokens", - "type": 1, - "optional": false - }, - { - "name": "messageSeqs", "type": 2, - "optional": false + "optional": true }, { "name": "messages", @@ -17366,19 +16398,49 @@ "optional": false }, { - "name": "route", - "type": 89, + "name": "model", + "type": 8, "optional": false }, + { + "name": "provider", + "type": 8, + "optional": false + }, + { + "name": "purpose", + "type": 89, + "optional": true + }, + { + "name": "reasoningEffort", + "type": 8, + "optional": true + }, + { + "name": "sessionId", + "type": 8, + "optional": true + }, + { + "name": "stop", + "type": 92, + "optional": true + }, { "name": "system", "type": 8, - "optional": false + "optional": true }, { - "name": "titleProvider", - "type": 8, - "optional": false + "name": "temperature", + "type": 2, + "optional": true + }, + { + "name": "tools", + "type": 93, + "optional": true } ], "indices": [] @@ -17387,10 +16449,6 @@ "kind": "primitive", "type": "number" }, - { - "kind": "array", - "element": 1 - }, { "kind": "array", "element": 4 @@ -17410,12 +16468,12 @@ }, { "name": "role", - "type": 29, + "type": 30, "optional": false }, { "name": "source", - "type": 33, + "type": 34, "optional": false } ], @@ -17430,10 +16488,10 @@ "types": [ 7, 10, - 19, - 22, - 25, - 27 + 20, + 23, + 26, + 28 ] }, { @@ -17479,8 +16537,13 @@ "optional": false }, { - "name": "type", + "name": "offloaded", "type": 18, + "optional": true + }, + { + "name": "type", + "type": 19, "optional": false } ], @@ -17496,12 +16559,12 @@ }, { "name": "bytes", - "type": 1, + "type": 2, "optional": false }, { "name": "height", - "type": 1, + "type": 2, "optional": false }, { @@ -17521,7 +16584,7 @@ }, { "name": "width", - "type": 1, + "type": 2, "optional": false } ], @@ -17536,38 +16599,42 @@ 16 ] }, - { - "kind": "literal", - "value": "image/png" - }, { "kind": "literal", "value": "image/gif" }, { "kind": "literal", - "value": "image/jpeg" + "value": "image/png" }, { "kind": "literal", "value": "image/webp" }, + { + "kind": "literal", + "value": "image/jpeg" + }, { "kind": "object", "properties": [ { "name": "height", - "type": 1, + "type": 2, "optional": false }, { "name": "width", - "type": 1, + "type": 2, "optional": false } ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -17577,12 +16644,12 @@ "properties": [ { "name": "attachment", - "type": 20, + "type": 21, "optional": false }, { "name": "type", - "type": 21, + "type": 22, "optional": false } ], @@ -17598,7 +16665,7 @@ }, { "name": "bytes", - "type": 1, + "type": 2, "optional": false }, { @@ -17623,7 +16690,7 @@ }, { "name": "isError", - "type": 23, + "type": 24, "optional": true }, { @@ -17633,7 +16700,7 @@ }, { "name": "type", - "type": 24, + "type": 25, "optional": false } ], @@ -17657,7 +16724,7 @@ }, { "name": "type", - "type": 26, + "type": 27, "optional": false } ], @@ -17677,7 +16744,7 @@ }, { "name": "type", - "type": 28, + "type": 29, "optional": false } ], @@ -17690,18 +16757,18 @@ { "kind": "union", "types": [ - 30, 31, - 32 + 32, + 33 ] }, { "kind": "literal", - "value": "system" + "value": "assistant" }, { "kind": "literal", - "value": "assistant" + "value": "system" }, { "kind": "literal", @@ -17710,7 +16777,7 @@ { "kind": "union", "types": [ - 34, + 35, 44, 46, 47, @@ -17737,7 +16804,7 @@ "properties": [ { "name": "baseline", - "type": 35, + "type": 18, "optional": true }, { @@ -17763,10 +16830,6 @@ ], "indices": [] }, - { - "kind": "literal", - "value": true - }, { "kind": "array", "element": 37 @@ -17855,7 +16918,7 @@ }, { "name": "kind", - "type": 32, + "type": 33, "optional": false }, { @@ -17935,7 +16998,7 @@ }, { "name": "update", - "type": 35, + "type": 18, "optional": true } ], @@ -18205,7 +17268,7 @@ "properties": [ { "name": "capturedFormatVersion", - "type": 1, + "type": 2, "optional": true }, { @@ -18215,12 +17278,12 @@ }, { "name": "compacted", - "type": 23, + "type": 24, "optional": false }, { "name": "inputIndex", - "type": 1, + "type": 2, "optional": false }, { @@ -18230,22 +17293,22 @@ }, { "name": "omittedBytes", - "type": 1, + "type": 2, "optional": false }, { "name": "omittedMessages", - "type": 1, + "type": 2, "optional": false }, { "name": "originalMessages", - "type": 1, + "type": 2, "optional": false }, { "name": "retainedMessages", - "type": 1, + "type": 2, "optional": false }, { @@ -18255,7 +17318,7 @@ }, { "name": "truncated", - "type": 23, + "type": 24, "optional": false } ], @@ -18264,7 +17327,7 @@ { "kind": "union", "types": [ - 1, + 2, 74 ] }, @@ -18346,12 +17409,12 @@ }, { "name": "revision", - "type": 1, + "type": 2, "optional": false }, { "name": "round", - "type": 1, + "type": 2, "optional": false } ], @@ -18451,39 +17514,132 @@ "properties": [ { "name": "kind", - "type": 32, + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 90, + 91 + ] + }, + { + "kind": "literal", + "value": "session-title" + }, + { + "kind": "literal", + "value": "compaction" + }, + { + "kind": "array", + "element": 8 + }, + { + "kind": "array", + "element": 94 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 8, + "optional": false + }, + { + "name": "name", + "type": 8, + "optional": false + }, + { + "name": "parameters", + "type": 95, "optional": false } ], "indices": [] }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 8, + "value": 86 + } + ] + }, { "kind": "object", "properties": [ { - "name": "model", + "name": "description", + "type": 8, + "optional": true + }, + { + "name": "name", "type": 8, "optional": false }, { - "name": "provider", - "type": 8, + "name": "schema", + "type": 97, + "optional": false + }, + { + "name": "type", + "type": 100, "optional": false } ], "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 8, + 24, + 74, + 98, + 99 + ] + }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 8, + "value": 97 + } + ] + }, + { + "kind": "array", + "element": 97 + }, + { + "kind": "literal", + "value": "json_schema" } ] }, "names": [ - "packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData" + "packages/experimental/browser-use-stagehand-native/src/model.ts#StagehandLlmRequest" ], "sources": [ - "packages/session/session-title-llm/src/index.ts:27" + "packages/experimental/browser-use-stagehand-native/src/model.ts:17" ] }, { - "digest": "08a1293eddb9b633bfefaf0893e3b4c807dfbb49c45a447cb32bb68bfd1a8232", + "digest": "07bd32ad9baaf7ca4d6e778e6e1f707b546bf49f0a8039a0e8e40117759fa5e4", "schema": { "root": 0, "nodes": [ @@ -18491,87 +17647,18 @@ "kind": "object", "properties": [ { - "name": "content", + "name": "description", "type": 1, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "role", - "type": 26, - "optional": false - }, - { - "name": "source", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "union", - "types": [ - 3, - 7, - 9, - 11, - 13, - 23 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "isError", - "type": 4, "optional": true }, { - "name": "toolCallId", - "type": 5, + "name": "name", + "type": 1, "optional": false }, { - "name": "type", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, + "name": "schema", + "type": 2, "optional": false }, { @@ -18583,111 +17670,73 @@ "indices": [] }, { - "kind": "literal", - "value": "text" + "kind": "primitive", + "type": "string" + }, + { + "kind": "union", + "types": [ + 1, + 3, + 4, + 5, + 6, + 7 + ] + }, + { + "kind": "array", + "element": 2 }, { "kind": "object", - "properties": [ + "properties": [], + "indices": [ { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false + "key": 1, + "value": 2 } - ], - "indices": [] + ] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "primitive", + "type": "number" }, { "kind": "literal", - "value": "reasoning" - }, + "value": "json_schema" + } + ] + }, + "names": [ + "event:browser-use/stagehand-llm-request.data.responseFormat" + ], + "sources": [] + }, + { + "digest": "07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235", + "schema": { + "root": 0, + "nodes": [ { "kind": "object", "properties": [ { - "name": "arguments", - "type": 5, + "name": "model", + "type": 1, "optional": false }, { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 14, - "optional": false - }, - { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 15, - "optional": false - }, - { - "name": "height", - "type": 15, - "optional": false - }, - { - "name": "mediaType", - "type": 16, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": true - }, - { - "name": "originalDimensions", - "type": 21, - "optional": true - }, - { - "name": "width", - "type": 15, + "name": "provider", + "type": 1, "optional": false } ], @@ -18695,141 +17744,33 @@ }, { "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 17, - 18, - 19, - 20 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 15, - "optional": false - }, - { - "name": "width", - "type": 15, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 24, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 15, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "literal", - "value": "assistant" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 28, - "optional": false - }, - { - "name": "model", - "type": 5, - "optional": false - }, - { - "name": "provider", - "type": 5, - "optional": false - }, - { - "name": "replayState", - "type": 29, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" + "type": "string" } ] }, "names": [ - "packages/llm/llm/src/message.ts#AssistantMessage" + "packages/session/session-title/src/types.ts#SessionTitleModelIdentity", + "packages/subagent/tool-subagent/src/model-selection.ts#AllowedModelRoute" ], "sources": [ - "packages/llm/llm/src/message.ts:148" + "packages/session/session-title/src/types.ts:20", + "packages/subagent/tool-subagent/src/model-selection.ts:9" ] }, + { + "digest": "07eccc9ea2f9b5439d76cc77d5e062936677d43f07a23da749819b0a22ee36b7", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "security-privacy-permission" + } + ] + }, + "names": [], + "sources": [] + }, { "digest": "0a30c3be336cdd4f8f34e66543a3d46d611c43986edd9b3ce6cbb9b721d3b5d3", "schema": { @@ -18958,482 +17899,6 @@ "packages/core/session/src/types.ts:203" ] }, - { - "digest": "0a6fae8c3249943e23a024ae6c0d9dd707df9bff0badc69786f0dc8ad9482d8e", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "union", - "types": [ - 1, - 31 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "compactionId", - "type": 2, - "optional": false - }, - { - "name": "llmStreamCall", - "type": 3, - "optional": false - }, - { - "name": "maxTokens", - "type": 4, - "optional": true - }, - { - "name": "model", - "type": 2, - "optional": false - }, - { - "name": "provider", - "type": 2, - "optional": false - }, - { - "name": "rawOutput", - "type": 5, - "optional": false - }, - { - "name": "shadowedRange", - "type": 28, - "optional": false - }, - { - "name": "shadowedSeqs", - "type": 29, - "optional": false - }, - { - "name": "shadowedTokenCount", - "type": 4, - "optional": false - }, - { - "name": "sourceCommandId", - "type": 2, - "optional": true - }, - { - "name": "summary", - "type": 5, - "optional": false - }, - { - "name": "usage", - "type": 30, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 6 - }, - { - "kind": "union", - "types": [ - 7, - 10, - 12, - 14, - 16, - 25 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 5, - "optional": false - }, - { - "name": "isError", - "type": 8, - "optional": true - }, - { - "name": "toolCallId", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 13, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 2, - "optional": false - }, - { - "name": "id", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 15, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 17, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 2, - "optional": false - }, - { - "name": "bytes", - "type": 4, - "optional": false - }, - { - "name": "height", - "type": 4, - "optional": false - }, - { - "name": "mediaType", - "type": 18, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": true - }, - { - "name": "originalDimensions", - "type": 23, - "optional": true - }, - { - "name": "width", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 19, - 20, - 21, - 22 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 4, - "optional": false - }, - { - "name": "width", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 26, - "optional": false - }, - { - "name": "type", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 2, - "optional": false - }, - { - "name": "bytes", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "end", - "type": 4, - "optional": false - }, - { - "name": "start", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 4 - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 4, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 4, - "optional": true - }, - { - "name": "inputTokens", - "type": 4, - "optional": false - }, - { - "name": "outputTokens", - "type": 4, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 4, - "optional": true - }, - { - "name": "totalTokens", - "type": 4, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "compactionId", - "type": 2, - "optional": false - }, - { - "name": "maxTokens", - "type": 4, - "optional": true - }, - { - "name": "model", - "type": 2, - "optional": false - }, - { - "name": "provider", - "type": 2, - "optional": false - }, - { - "name": "rawOutput", - "type": 5, - "optional": true - }, - { - "name": "shadowedRange", - "type": 28, - "optional": false - }, - { - "name": "shadowedSeqs", - "type": 29, - "optional": false - }, - { - "name": "shadowedTokenCount", - "type": 4, - "optional": false - }, - { - "name": "sourceCommandId", - "type": 2, - "optional": true - }, - { - "name": "summary", - "type": 5, - "optional": false - }, - { - "name": "usage", - "type": 30, - "optional": true - } - ], - "indices": [] - } - ] - }, - "names": [ - "event:compaction/summary.data" - ], - "sources": [] - }, { "digest": "0a8a062bc9c3c0d97fd97742262f2ea53dd32297f834e81114870d38ac3dc6da", "schema": { @@ -19513,643 +17978,9 @@ "packages/llm/llm/src/types.ts#ReplayEnvelope" ], "sources": [ - "packages/llm/llm/src/types.ts:368" + "packages/llm/llm/src/types.ts:402" ] }, - { - "digest": "0ae7ccdc1cc5d9d43772ebd5c7357ff509011c3a6ba188d9c274d33df2542c42", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 46, - "optional": true - }, - { - "name": "seq", - "type": 10, - "optional": false - }, - { - "name": "sourceEventSeqs", - "type": 47, - "optional": true - }, - { - "name": "surfaceOp", - "type": 48, - "optional": false - }, - { - "name": "time", - "type": 10, - "optional": false - }, - { - "name": "type", - "type": 52, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "message", - "type": 2, - "optional": false - }, - { - "name": "step", - "type": 10, - "optional": false - }, - { - "name": "turn", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 3, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "role", - "type": 28, - "optional": false - }, - { - "name": "source", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 4 - }, - { - "kind": "union", - "types": [ - 5, - 8, - 18, - 21, - 24, - 26 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 6, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 17, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 10, - "optional": false - }, - { - "name": "height", - "type": 10, - "optional": false - }, - { - "name": "mediaType", - "type": 11, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": true - }, - { - "name": "originalDimensions", - "type": 16, - "optional": true - }, - { - "name": "width", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 12, - 13, - 14, - 15 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 10, - "optional": false - }, - { - "name": "width", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 19, - "optional": false - }, - { - "name": "type", - "type": 20, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 10, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 3, - "optional": false - }, - { - "name": "isError", - "type": 22, - "optional": true - }, - { - "name": "toolCallId", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "system" - }, - { - "kind": "union", - "types": [ - 30, - 35, - 37, - 39, - 41, - 43, - 45 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 31, - "optional": false - }, - { - "name": "kind", - "type": 32, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - }, - { - "name": "sections", - "type": 33, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 34 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 6, - "optional": false - }, - { - "name": "text", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 36, - "optional": false - }, - { - "name": "kind", - "type": 32, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - }, - { - "name": "summary", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 38, - "optional": false - }, - { - "name": "kind", - "type": 32, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 40, - "optional": false - }, - { - "name": "kind", - "type": 32, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 42, - "optional": false - }, - { - "name": "kind", - "type": 32, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 44, - "optional": false - }, - { - "name": "kind", - "type": 32, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 32, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 10 - }, - { - "kind": "union", - "types": [ - 49, - 51 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "endSeq", - "type": 10, - "optional": false - }, - { - "name": "op", - "type": 50, - "optional": false - }, - { - "name": "startSeq", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "append" - }, - { - "kind": "literal", - "value": "system/message" - } - ] - }, - "names": [ - "event:system/message" - ], - "sources": [] - }, { "digest": "0af3ecfb8c60d416fc40a7cc4fb2c4851a8e46608b2b38ab9e510670c971aa1f", "schema": { @@ -20164,478 +17995,6 @@ "names": [], "sources": [] }, - { - "digest": "0b0f2af6d7b1273fcb7a2b5d6321b35b49f9186d065ffb99f2e392949b787c6e", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "blocks", - "type": 1, - "optional": false - }, - { - "name": "finish", - "type": 26, - "optional": false - }, - { - "name": "requestSeq", - "type": 6, - "optional": false - }, - { - "name": "usage", - "type": 38, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "union", - "types": [ - 3, - 8, - 11, - 13, - 15, - 17 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "isError", - "type": 9, - "optional": true - }, - { - "name": "toolCallId", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 14, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 5, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 18, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 6, - "optional": false - }, - { - "name": "height", - "type": 6, - "optional": false - }, - { - "name": "mediaType", - "type": 19, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": true - }, - { - "name": "originalDimensions", - "type": 24, - "optional": true - }, - { - "name": "width", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 20, - 21, - 22, - 23 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 6, - "optional": false - }, - { - "name": "width", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "union", - "types": [ - 27, - 30, - 32, - 34, - 36 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 28, - "optional": false - }, - { - "name": "kind", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 5, - "optional": false - }, - { - "name": "message", - "type": 5, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 6, - "optional": true - }, - { - "name": "requestId", - "type": 5, - "optional": true - }, - { - "name": "status", - "type": 6, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 28, - "optional": false - }, - { - "name": "kind", - "type": 31, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 33, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "stop" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 35, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-calls" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 37, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 6, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 6, - "optional": true - }, - { - "name": "inputTokens", - "type": 6, - "optional": false - }, - { - "name": "outputTokens", - "type": 6, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 6, - "optional": true - }, - { - "name": "totalTokens", - "type": 6, - "optional": true - } - ], - "indices": [] - } - ] - }, - "names": [ - "packages/experimental/browser-use-stagehand-native/src/model.ts#StagehandLlmResult" - ], - "sources": [ - "packages/experimental/browser-use-stagehand-native/src/model.ts:25" - ] - }, { "digest": "0b6d718c693fdf7f06dcccb0bcbbabe78cc63514eafc62d6b2a3899735279d6f", "schema": { @@ -20736,6 +18095,355 @@ "names": [], "sources": [] }, + { + "digest": "0c3f4d2248d9bc9700e2d7253fa71aa9935f5c39af6f11de5727f08cccaf8383", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "message", + "type": 1, + "optional": false + }, + { + "name": "teamId", + "type": 6, + "optional": false + }, + { + "name": "version", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "senderId", + "type": 6, + "optional": false + }, + { + "name": "senderName", + "type": 6, + "optional": false + }, + { + "name": "targetId", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "union", + "types": [ + 4, + 8, + 10, + 12, + 14, + 25 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "isError", + "type": 5, + "optional": true + }, + { + "name": "toolCallId", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 15, + "optional": false + }, + { + "name": "offloaded", + "type": 23, + "optional": true + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 16, + "optional": false + }, + { + "name": "height", + "type": 16, + "optional": false + }, + { + "name": "mediaType", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": true + }, + { + "name": "originalDimensions", + "type": 22, + "optional": true + }, + { + "name": "width", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 18, + 19, + 20, + 21 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 16, + "optional": false + }, + { + "name": "width", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 26, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 16, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "literal", + "value": 2 + } + ] + }, + "names": [ + "event:team/message/queued.data" + ], + "sources": [ + "packages/experimental/agent-team/src/types.ts:225" + ] + }, { "digest": "0c4205180f75b9ee1e0d878bd9a9851628b5dc18448486389d9c5fdc3f1ccda3", "schema": { @@ -20750,6 +18458,875 @@ "names": [], "sources": [] }, + { + "digest": "0c6412d3ba5e41adfaa6929c7d4093f2d52d8598c03f35844fc0d55e3070a3b1", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "step", + "type": 1, + "optional": false + }, + { + "name": "stream", + "type": 2, + "optional": false + }, + { + "name": "turn", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "union", + "types": [ + 4, + 9, + 67, + 69 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "args", + "type": 5, + "optional": false + }, + { + "name": "dt", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "index", + "type": 1, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": true + }, + { + "name": "time0", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 6 + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "array", + "element": 1 + }, + { + "kind": "literal", + "value": "tool-call-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "chunk", + "type": 10, + "optional": false + }, + { + "name": "time", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 11, + 13, + 39, + 42, + 44, + 46, + 63 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "argumentsDelta", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "index", + "type": 1, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": true + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "block", + "type": 14, + "optional": false + }, + { + "name": "index", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 15, + 17, + 27, + 30, + 34, + 36 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 18, + "optional": false + }, + { + "name": "offloaded", + "type": 25, + "optional": true + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 1, + "optional": false + }, + { + "name": "height", + "type": 1, + "optional": false + }, + { + "name": "mediaType", + "type": 19, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": true + }, + { + "name": "originalDimensions", + "type": 24, + "optional": true + }, + { + "name": "width", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 20, + 21, + 22, + 23 + ] + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 1, + "optional": false + }, + { + "name": "width", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 28, + "optional": false + }, + { + "name": "type", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 1, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 31, + "optional": false + }, + { + "name": "isError", + "type": 32, + "optional": true + }, + { + "name": "toolCallId", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 14 + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "block-end" + }, + { + "kind": "object", + "properties": [ + { + "name": "blockType", + "type": 40, + "optional": false + }, + { + "name": "index", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 41, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 16, + 26, + 29, + 33, + 35, + 37 + ] + }, + { + "kind": "literal", + "value": "block-start" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 1, + "optional": false + }, + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 1, + "optional": false + }, + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 45, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 47, + "optional": false + }, + { + "name": "replayState", + "type": 59, + "optional": true + }, + { + "name": "type", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 48, + 51, + 53, + 55, + 57 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 6, + "optional": false + }, + { + "name": "message", + "type": 6, + "optional": false + }, + { + "name": "offloadImages", + "type": 1, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 1, + "optional": true + }, + { + "name": "requestId", + "type": 6, + "optional": true + }, + { + "name": "status", + "type": 1, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 60, + "optional": true + }, + { + "name": "response", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 61 + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "literal", + "value": "finish" + }, + { + "kind": "object", + "properties": [ + { + "name": "type", + "type": 64, + "optional": false + }, + { + "name": "usage", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "usage" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 1, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 1, + "optional": true + }, + { + "name": "inputTokens", + "type": 1, + "optional": false + }, + { + "name": "outputTokens", + "type": 1, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 1, + "optional": true + }, + { + "name": "totalTokens", + "type": 1, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "chunk" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 1, + "optional": false + }, + { + "name": "texts", + "type": 5, + "optional": false + }, + { + "name": "time0", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 1, + "optional": false + }, + { + "name": "texts", + "type": 5, + "optional": false + }, + { + "name": "time0", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-chunks" + } + ] + }, + "names": [ + "event:assistant/attempt.data" + ], + "sources": [ + "packages/core/session/src/types.ts:335" + ] + }, { "digest": "0d47a8f3d847243c4488e755ad4cab7a04ac5d4621b9c81865524cf3f4d8536f", "schema": { @@ -20836,6 +19413,525 @@ ], "sources": [] }, + { + "digest": "0e0e81784bc85e1e6554b7ff50ab341169a1a3d32149ae49abe8dddca5ce6efe", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 16, + "optional": true + }, + { + "name": "seq", + "type": 9, + "optional": false + }, + { + "name": "time", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 41, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 2, + "optional": false + }, + { + "name": "finish", + "type": 28, + "optional": false + }, + { + "name": "requestSeq", + "type": 9, + "optional": false + }, + { + "name": "usage", + "type": 40, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "union", + "types": [ + 4, + 7, + 18, + 21, + 24, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 8, + "optional": false + }, + { + "name": "offloaded", + "type": 16, + "optional": true + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "mediaType", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 15, + "optional": true + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 11, + 12, + 13, + 14 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 19, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "isError", + "type": 22, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "union", + "types": [ + 29, + 32, + 34, + 36, + 38 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 30, + "optional": false + }, + { + "name": "kind", + "type": 31, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 5, + "optional": false + }, + { + "name": "message", + "type": 5, + "optional": false + }, + { + "name": "offloadImages", + "type": 9, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 9, + "optional": true + }, + { + "name": "requestId", + "type": 5, + "optional": true + }, + { + "name": "status", + "type": 9, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 30, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 39, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 9, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 9, + "optional": true + }, + { + "name": "inputTokens", + "type": 9, + "optional": false + }, + { + "name": "outputTokens", + "type": 9, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 9, + "optional": true + }, + { + "name": "totalTokens", + "type": 9, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "browser-use/stagehand-llm-result" + } + ] + }, + "names": [ + "event:browser-use/stagehand-llm-result" + ], + "sources": [] + }, { "digest": "0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd", "schema": { @@ -20895,299 +19991,6 @@ ], "sources": [] }, - { - "digest": "0e560aad8fa9f338a60e76cbce6593ce93486ab30d6dd7833b8b5b07c6e6963d", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "array", - "element": 1 - }, - { - "kind": "union", - "types": [ - 2, - 5, - 7, - 9, - 19, - 22 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 3, - "optional": false - }, - { - "name": "id", - "type": 3, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 10, - "optional": false - }, - { - "name": "type", - "type": 18, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 3, - "optional": false - }, - { - "name": "bytes", - "type": 11, - "optional": false - }, - { - "name": "height", - "type": 11, - "optional": false - }, - { - "name": "mediaType", - "type": 12, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": true - }, - { - "name": "originalDimensions", - "type": 17, - "optional": true - }, - { - "name": "width", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 13, - 14, - 15, - 16 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 11, - "optional": false - }, - { - "name": "width", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 20, - "optional": false - }, - { - "name": "type", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 3, - "optional": false - }, - { - "name": "bytes", - "type": 11, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 0, - "optional": false - }, - { - "name": "isError", - "type": 23, - "optional": true - }, - { - "name": "toolCallId", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - } - ] - }, - "names": [ - "event:agent/inbox/spliced.data.inserted[0].content", - "event:agent/inbox/spliced.data.inserted[0].content[0][5].content", - "event:assistant/message.data.message.content", - "event:browser-use/stagehand-llm-request.data.request.messages[0].content", - "event:browser-use/stagehand-llm-result.data.blocks", - "event:compaction/summary.data[0].rawOutput", - "event:compaction/summary.data[0].summary", - "event:compaction/summary.data[1].rawOutput", - "event:compaction/summary.data[1].summary", - "event:system/message.data.message.content", - "event:team/message/queued.data.message.content", - "event:tool/ptc-dispatch.data.content" - ], - "sources": [] - }, { "digest": "0e7b2a97f54f1afdb8cdfad1635abe9145327db539bfb69b4045c19d8c17755d", "schema": { @@ -21236,321 +20039,6 @@ "packages/attachment/attachment/src/types.ts:28" ] }, - { - "digest": "10ce3a084c136b5fc9969e5981b64711c35a2d9de7eac1b8443c03f73dc4f3db", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "senderId", - "type": 4, - "optional": false - }, - { - "name": "senderName", - "type": 4, - "optional": false - }, - { - "name": "targetId", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "union", - "types": [ - 3, - 6, - 8, - 10, - 20, - 23 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 4, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 19, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 12, - "optional": false - }, - { - "name": "height", - "type": 12, - "optional": false - }, - { - "name": "mediaType", - "type": 13, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": true - }, - { - "name": "originalDimensions", - "type": 18, - "optional": true - }, - { - "name": "width", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 14, - 15, - 16, - 17 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 12, - "optional": false - }, - { - "name": "width", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 21, - "optional": false - }, - { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 12, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "isError", - "type": 24, - "optional": true - }, - { - "name": "toolCallId", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - } - ] - }, - "names": [ - "packages/experimental/agent-team/src/types.ts#TeamMessageSnapshot" - ], - "sources": [ - "packages/experimental/agent-team/src/types.ts:106" - ] - }, { "digest": "10e386801903f9d424602c31d3ff837a94aef623c8b391376d8d9e253525fabe", "schema": { @@ -21579,6 +20067,1024 @@ "names": [], "sources": [] }, + { + "digest": "1169b301aaabcd992657b93ec93750c43175dda81ada5cac086f14f2eaaeed6d", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 2, + "optional": true + }, + { + "name": "seq", + "type": 11, + "optional": false + }, + { + "name": "surfaceOp", + "type": 76, + "optional": false + }, + { + "name": "time", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "interrupted", + "type": 2, + "optional": true + }, + { + "name": "message", + "type": 3, + "optional": false + }, + { + "name": "step", + "type": 11, + "optional": false + }, + { + "name": "stream", + "type": 33, + "optional": false + }, + { + "name": "turn", + "type": 11, + "optional": false + }, + { + "name": "usage", + "type": 70, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "role", + "type": 29, + "optional": false + }, + { + "name": "source", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 9, + 19, + 22, + 25, + 27 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 10, + "optional": false + }, + { + "name": "offloaded", + "type": 2, + "optional": true + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 11, + "optional": false + }, + { + "name": "height", + "type": 11, + "optional": false + }, + { + "name": "mediaType", + "type": 12, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 17, + "optional": true + }, + { + "name": "width", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 13, + 14, + 15, + 16 + ] + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 11, + "optional": false + }, + { + "name": "width", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 20, + "optional": false + }, + { + "name": "type", + "type": 21, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "isError", + "type": 23, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 31, + "optional": false + }, + { + "name": "model", + "type": 7, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "replayState", + "type": 32, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "array", + "element": 34 + }, + { + "kind": "union", + "types": [ + 35, + 39, + 72, + 74 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "args", + "type": 36, + "optional": false + }, + { + "name": "dt", + "type": 37, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "time0", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "array", + "element": 11 + }, + { + "kind": "literal", + "value": "tool-call-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "chunk", + "type": 40, + "optional": false + }, + { + "name": "time", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 71, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 41, + 43, + 45, + 48, + 50, + 52, + 68 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "argumentsDelta", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "type", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "block", + "type": 5, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "block-end" + }, + { + "kind": "object", + "properties": [ + { + "name": "blockType", + "type": 46, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 47, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 8, + 18, + 21, + 24, + 26, + 28 + ] + }, + { + "kind": "literal", + "value": "block-start" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 53, + "optional": false + }, + { + "name": "replayState", + "type": 65, + "optional": true + }, + { + "name": "type", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 54, + 57, + 59, + 61, + 63 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 55, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 7, + "optional": false + }, + { + "name": "message", + "type": 7, + "optional": false + }, + { + "name": "offloadImages", + "type": 11, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 11, + "optional": true + }, + { + "name": "requestId", + "type": 7, + "optional": true + }, + { + "name": "status", + "type": 11, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 55, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 66, + "optional": true + }, + { + "name": "response", + "type": 32, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 32 + }, + { + "kind": "literal", + "value": "finish" + }, + { + "kind": "object", + "properties": [ + { + "name": "type", + "type": 69, + "optional": false + }, + { + "name": "usage", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "usage" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 11, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 11, + "optional": true + }, + { + "name": "inputTokens", + "type": 11, + "optional": false + }, + { + "name": "outputTokens", + "type": 11, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 11, + "optional": true + }, + { + "name": "totalTokens", + "type": 11, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "chunk" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 37, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "texts", + "type": 36, + "optional": false + }, + { + "name": "time0", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 73, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 37, + "optional": false + }, + { + "name": "index", + "type": 11, + "optional": false + }, + { + "name": "texts", + "type": 36, + "optional": false + }, + { + "name": "time0", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 75, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-chunks" + }, + { + "kind": "union", + "types": [ + 77, + 79 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 11, + "optional": false + }, + { + "name": "op", + "type": 78, + "optional": false + }, + { + "name": "startSeq", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "literal", + "value": "assistant/message" + } + ] + }, + "names": [ + "event:assistant/message" + ], + "sources": [] + }, { "digest": "119abe2b9c0b2f46ecb31d82b9acba4d4b30e049f655e2475e8b4ae12d5a9e25", "schema": { @@ -21710,6 +21216,139 @@ "names": [], "sources": [] }, + { + "digest": "12d5808fac1996b9b3c2afa806e03022252ba2d7b88f835862ae1896591f60c7", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 1, + "optional": false + }, + { + "name": "offloaded", + "type": 10, + "optional": true + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 2, + "optional": false + }, + { + "name": "bytes", + "type": 3, + "optional": false + }, + { + "name": "height", + "type": 3, + "optional": false + }, + { + "name": "mediaType", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": true + }, + { + "name": "originalDimensions", + "type": 9, + "optional": true + }, + { + "name": "width", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 5, + 6, + 7, + 8 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 3, + "optional": false + }, + { + "name": "width", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + } + ] + }, + "names": [ + "packages/llm/llm/src/types.ts#ImageBlock" + ], + "sources": [ + "packages/llm/llm/src/types.ts:78" + ] + }, { "digest": "137ac60e26bcb192210d9a32a704b73b0924ed34019323defa1b787be95bd45e", "schema": { @@ -22363,6 +22002,359 @@ "packages/session/session-title/src/types.ts:29" ] }, + { + "digest": "1b573e2d5c094fa87e2a8bf409ddd4ac5ba92a123085e168de44b9ccdbd5244c", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "role", + "type": 28, + "optional": false + }, + { + "name": "source", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "tuple", + "elements": [ + { + "type": 2, + "optional": false, + "rest": false + } + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "isError", + "type": 26, + "optional": true + }, + { + "name": "toolCallId", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 2, + 5, + 8, + 10, + 12, + 23 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 13, + "optional": false + }, + { + "name": "offloaded", + "type": 21, + "optional": true + }, + { + "name": "type", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 14, + "optional": false + }, + { + "name": "height", + "type": 14, + "optional": false + }, + { + "name": "mediaType", + "type": 15, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": true + }, + { + "name": "originalDimensions", + "type": 20, + "optional": true + }, + { + "name": "width", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 16, + 17, + 18, + 19 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 14, + "optional": false + }, + { + "name": "width", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 24, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 14, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 6, + "optional": false + }, + { + "name": "kind", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + } + ] + }, + "names": [ + "packages/llm/llm/src/message.ts#ToolResultMessage" + ], + "sources": [ + "packages/llm/llm/src/message.ts:164" + ] + }, { "digest": "1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5", "schema": { @@ -22554,6 +22546,44 @@ "names": [], "sources": [] }, + { + "digest": "1cde1bedde79c5e8a56aee9c05c8e0a579cf5bc611deb85b93fa331a16ca978f", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "imageIndexes", + "type": 1, + "optional": false + }, + { + "name": "seq", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "primitive", + "type": "number" + } + ] + }, + "names": [ + "packages/compaction/compaction-image-offload/src/projection.ts#ImageOffloadTarget" + ], + "sources": [ + "packages/compaction/compaction-image-offload/src/projection.ts:9" + ] + }, { "digest": "1d5f04a03ea75fc8eeece544129eba0f49f0c82046e9c404461920390135c7b2", "schema": { @@ -22594,7 +22624,7 @@ "event:assistant/attempt.data.stream[0][3].chunk[6].reason[3]" ], "sources": [ - "packages/llm/llm/src/types.ts:131" + "packages/llm/llm/src/types.ts:144" ] }, { @@ -22854,414 +22884,6 @@ "names": [], "sources": [] }, - { - "digest": "1e75cbc0af53f62e5685d751108dc9f6320fe835aed222e579a2a0080dc8fc59", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "compactionId", - "type": 1, - "optional": false - }, - { - "name": "llmStreamCall", - "type": 2, - "optional": false - }, - { - "name": "maxTokens", - "type": 3, - "optional": true - }, - { - "name": "model", - "type": 1, - "optional": false - }, - { - "name": "provider", - "type": 1, - "optional": false - }, - { - "name": "rawOutput", - "type": 4, - "optional": false - }, - { - "name": "shadowedRange", - "type": 27, - "optional": false - }, - { - "name": "shadowedSeqs", - "type": 28, - "optional": false - }, - { - "name": "shadowedTokenCount", - "type": 3, - "optional": false - }, - { - "name": "sourceCommandId", - "type": 1, - "optional": true - }, - { - "name": "summary", - "type": 4, - "optional": false - }, - { - "name": "usage", - "type": 29, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 5 - }, - { - "kind": "union", - "types": [ - 6, - 9, - 11, - 13, - 15, - 24 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 4, - "optional": false - }, - { - "name": "isError", - "type": 7, - "optional": true - }, - { - "name": "toolCallId", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 1, - "optional": false - }, - { - "name": "id", - "type": 1, - "optional": false - }, - { - "name": "name", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 14, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 16, - "optional": false - }, - { - "name": "type", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 1, - "optional": false - }, - { - "name": "bytes", - "type": 3, - "optional": false - }, - { - "name": "height", - "type": 3, - "optional": false - }, - { - "name": "mediaType", - "type": 17, - "optional": false - }, - { - "name": "name", - "type": 1, - "optional": true - }, - { - "name": "originalDimensions", - "type": 22, - "optional": true - }, - { - "name": "width", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 18, - 19, - 20, - 21 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 3, - "optional": false - }, - { - "name": "width", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 25, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 1, - "optional": false - }, - { - "name": "bytes", - "type": 3, - "optional": false - }, - { - "name": "name", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "end", - "type": 3, - "optional": false - }, - { - "name": "start", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 3, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 3, - "optional": true - }, - { - "name": "inputTokens", - "type": 3, - "optional": false - }, - { - "name": "outputTokens", - "type": 3, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 3, - "optional": true - }, - { - "name": "totalTokens", - "type": 3, - "optional": true - } - ], - "indices": [] - } - ] - }, - "names": [ - "event:compaction/summary.data[0]" - ], - "sources": [] - }, { "digest": "1ebc6f9ff3aa9e3a38f8695f49bacdab1f8b7a3832741853ca46f40af56ccd01", "schema": { @@ -23398,6 +23020,562 @@ "packages/core/session/src/types.ts:195" ] }, + { + "digest": "2074f40221b76c75fd314246db558965dc3477dfb440bcc952b1eedc9778e366", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "message", + "type": 1, + "optional": false + }, + { + "name": "step", + "type": 9, + "optional": false + }, + { + "name": "turn", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "role", + "type": 28, + "optional": false + }, + { + "name": "source", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "union", + "types": [ + 4, + 7, + 18, + 21, + 24, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 8, + "optional": false + }, + { + "name": "offloaded", + "type": 16, + "optional": true + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "mediaType", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 15, + "optional": true + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 11, + 12, + 13, + 14 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 19, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "isError", + "type": 22, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "union", + "types": [ + 30, + 35, + 37, + 39, + 41, + 43, + 45 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 31, + "optional": false + }, + { + "name": "kind", + "type": 32, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + }, + { + "name": "sections", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 34 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "text", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 36, + "optional": false + }, + { + "name": "kind", + "type": 32, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 38, + "optional": false + }, + { + "name": "kind", + "type": 32, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 32, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 32, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 44, + "optional": false + }, + { + "name": "kind", + "type": 32, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 32, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + } + ] + }, + "names": [ + "event:system/message.data" + ], + "sources": [ + "packages/core/session/src/types.ts:310" + ] + }, { "digest": "20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5", "schema": { @@ -23522,6 +23700,1091 @@ "names": [], "sources": [] }, + { + "digest": "23c7cce57d20589897f4e2bd5898affc6fc611b87cd0cc8b95be1cfdd8b3a7eb", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "id", + "type": 4, + "optional": false + }, + { + "name": "role", + "type": 27, + "optional": false + }, + { + "name": "source", + "type": 31, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "union", + "types": [ + 3, + 6, + 17, + 20, + 23, + 25 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 7, + "optional": false + }, + { + "name": "offloaded", + "type": 15, + "optional": true + }, + { + "name": "type", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 4, + "optional": false + }, + { + "name": "bytes", + "type": 8, + "optional": false + }, + { + "name": "height", + "type": 8, + "optional": false + }, + { + "name": "mediaType", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "originalDimensions", + "type": 14, + "optional": true + }, + { + "name": "width", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 10, + 11, + 12, + 13 + ] + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 8, + "optional": false + }, + { + "name": "width", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 18, + "optional": false + }, + { + "name": "type", + "type": 19, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 4, + "optional": false + }, + { + "name": "bytes", + "type": 8, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "isError", + "type": 21, + "optional": true + }, + { + "name": "toolCallId", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "union", + "types": [ + 28, + 29, + 30 + ] + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 32, + 41, + 43, + 44, + 47, + 52, + 54, + 59, + 60, + 61, + 63, + 65, + 66, + 73, + 75, + 77, + 79, + 81, + 84, + 85 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 15, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 4, + "optional": true + }, + { + "name": "changes", + "type": 33, + "optional": false + }, + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 34 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 35, + "optional": false + }, + { + "name": "digest", + "type": 4, + "optional": true + }, + { + "name": "path", + "type": 4, + "optional": false + }, + { + "name": "scope", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 36, + 37, + 38 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 4, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 4, + "optional": true + }, + { + "name": "kind", + "type": 30, + "optional": false + }, + { + "name": "rpcId", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 4, + "optional": false + }, + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "provider", + "type": 4, + "optional": false + }, + { + "name": "ruleId", + "type": 4, + "optional": false + }, + { + "name": "source", + "type": 4, + "optional": false + }, + { + "name": "summary", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 48, + "optional": false + }, + { + "name": "form", + "type": 50, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + }, + { + "name": "update", + "type": 15, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 49 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 55, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + }, + { + "name": "sections", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 58 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 4, + "optional": false + }, + { + "name": "text", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + }, + { + "name": "summary", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 64, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 50, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + }, + { + "name": "references", + "type": 68, + "optional": false + }, + { + "name": "version", + "type": 72, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 69 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 8, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 70, + "optional": false + }, + { + "name": "compacted", + "type": 21, + "optional": false + }, + { + "name": "inputIndex", + "type": 8, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": false + }, + { + "name": "omittedBytes", + "type": 8, + "optional": false + }, + { + "name": "omittedMessages", + "type": 8, + "optional": false + }, + { + "name": "originalMessages", + "type": 8, + "optional": false + }, + { + "name": "retainedMessages", + "type": 8, + "optional": false + }, + { + "name": "sessionId", + "type": 4, + "optional": false + }, + { + "name": "truncated", + "type": 21, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 8, + 71 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 74, + "optional": false + }, + { + "name": "senderSessionId", + "type": 4, + "optional": false + }, + { + "name": "summary", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 64, + "optional": false + }, + { + "name": "kind", + "type": 76, + "optional": false + }, + { + "name": "senderSessionId", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 4, + "optional": false + }, + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "revision", + "type": 8, + "optional": false + }, + { + "name": "round", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "messageId", + "type": 4, + "optional": false + }, + { + "name": "senderId", + "type": 4, + "optional": false + }, + { + "name": "senderName", + "type": 4, + "optional": false + }, + { + "name": "teamId", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "model", + "type": 4, + "optional": false + }, + { + "name": "provider", + "type": 4, + "optional": false + }, + { + "name": "replayState", + "type": 83, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 30, + "optional": false + } + ], + "indices": [] + } + ] + }, + "names": [ + "packages/llm/llm/src/message.ts#Message" + ], + "sources": [ + "packages/llm/llm/src/message.ts:131" + ] + }, { "digest": "23cf7e0cf3fec1283b60fa15713506169bc9c079a08e124d1cb9733baca80b40", "schema": { @@ -24036,6 +25299,853 @@ "names": [], "sources": [] }, + { + "digest": "26f3bb7529cfce1e3a0f1ededa365d8b426758ca0feeb0beabeee805f8bb8346", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "array", + "element": 1 + }, + { + "kind": "union", + "types": [ + 2, + 8, + 66, + 68 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "args", + "type": 3, + "optional": false + }, + { + "name": "dt", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 4, + "optional": false + }, + { + "name": "index", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "time0", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "array", + "element": 6 + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "tool-call-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "chunk", + "type": 9, + "optional": false + }, + { + "name": "time", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 10, + 12, + 38, + 41, + 43, + 45, + 62 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "argumentsDelta", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 4, + "optional": false + }, + { + "name": "index", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "block", + "type": 13, + "optional": false + }, + { + "name": "index", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 14, + 16, + 26, + 29, + 33, + 35 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 17, + "optional": false + }, + { + "name": "offloaded", + "type": 24, + "optional": true + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 4, + "optional": false + }, + { + "name": "bytes", + "type": 6, + "optional": false + }, + { + "name": "height", + "type": 6, + "optional": false + }, + { + "name": "mediaType", + "type": 18, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "originalDimensions", + "type": 23, + "optional": true + }, + { + "name": "width", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 19, + 20, + 21, + 22 + ] + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 6, + "optional": false + }, + { + "name": "width", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 27, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 4, + "optional": false + }, + { + "name": "bytes", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 30, + "optional": false + }, + { + "name": "isError", + "type": 31, + "optional": true + }, + { + "name": "toolCallId", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 32, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 13 + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "block-end" + }, + { + "kind": "object", + "properties": [ + { + "name": "blockType", + "type": 39, + "optional": false + }, + { + "name": "index", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 15, + 25, + 28, + 32, + 34, + 36 + ] + }, + { + "kind": "literal", + "value": "block-start" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 6, + "optional": false + }, + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 6, + "optional": false + }, + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 46, + "optional": false + }, + { + "name": "replayState", + "type": 58, + "optional": true + }, + { + "name": "type", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 47, + 50, + 52, + 54, + 56 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 4, + "optional": false + }, + { + "name": "message", + "type": 4, + "optional": false + }, + { + "name": "offloadImages", + "type": 6, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 6, + "optional": true + }, + { + "name": "requestId", + "type": 4, + "optional": true + }, + { + "name": "status", + "type": 6, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 53, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 59, + "optional": true + }, + { + "name": "response", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 60 + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "literal", + "value": "finish" + }, + { + "kind": "object", + "properties": [ + { + "name": "type", + "type": 63, + "optional": false + }, + { + "name": "usage", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "usage" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 6, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 6, + "optional": true + }, + { + "name": "inputTokens", + "type": 6, + "optional": false + }, + { + "name": "outputTokens", + "type": 6, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 6, + "optional": true + }, + { + "name": "totalTokens", + "type": 6, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "chunk" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 5, + "optional": false + }, + { + "name": "index", + "type": 6, + "optional": false + }, + { + "name": "texts", + "type": 3, + "optional": false + }, + { + "name": "time0", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 5, + "optional": false + }, + { + "name": "index", + "type": 6, + "optional": false + }, + { + "name": "texts", + "type": 3, + "optional": false + }, + { + "name": "time0", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 69, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-chunks" + } + ] + }, + "names": [ + "event:assistant/attempt.data.stream", + "event:assistant/message.data.stream" + ], + "sources": [] + }, { "digest": "2765e9119edf5816ac1ab4c95c5ee32c90403f3742962eff11544f7752d59d2e", "schema": { @@ -24064,59 +26174,6 @@ "names": [], "sources": [] }, - { - "digest": "27cd736e794a0f8703a155fb697820cede894d10776586dd3df24d24c9465c0e", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 1, - "optional": false - }, - { - "name": "message", - "type": 1, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 2, - "optional": true - }, - { - "name": "requestId", - "type": 1, - "optional": true - }, - { - "name": "status", - "type": 2, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - } - ] - }, - "names": [ - "packages/llm/llm/src/types.ts#LlmFailure" - ], - "sources": [ - "packages/llm/llm/src/types.ts:40" - ] - }, { "digest": "27e337d3d6b402b221e50c247a3f518a70d0c474cc60beee15fac3e9c5cbfcbe", "schema": { @@ -24200,11 +26257,11 @@ "event:assistant/attempt.data.stream[0][3].chunk[6].reason[4]" ], "sources": [ - "packages/llm/llm/src/types.ts:132" + "packages/llm/llm/src/types.ts:145" ] }, { - "digest": "28aa673866746f447863d828aa25213135344d61e6c568b6ca2c1e729cfff6b3", + "digest": "29af48b840d0cd9e48b6f50bf3b354f8f6340607c5b99220a48e74f60beac9e2", "schema": { "root": 0, "nodes": [ @@ -24212,28 +26269,69 @@ "kind": "object", "properties": [ { - "name": "error", + "name": "data", "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 24, + "optional": true + }, + { + "name": "seq", + "type": 17, + "optional": false + }, + { + "name": "sourceEventSeqs", + "type": 38, + "optional": true + }, + { + "name": "surfaceOp", + "type": 39, + "optional": false + }, + { + "name": "time", + "type": 17, + "optional": false + }, + { + "name": "type", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "error", + "type": 2, "optional": true }, { "name": "message", - "type": 3, + "type": 4, "optional": false }, { "name": "meta", - "type": 32, + "type": 34, "optional": true }, { "name": "step", - "type": 16, + "type": 17, "optional": false }, { "name": "turn", - "type": 16, + "type": 17, "optional": false } ], @@ -24244,17 +26342,17 @@ "properties": [ { "name": "code", - "type": 2, + "type": 3, "optional": false }, { "name": "name", - "type": 2, + "type": 3, "optional": false }, { "name": "reason", - "type": 2, + "type": 3, "optional": true } ], @@ -24269,22 +26367,22 @@ "properties": [ { "name": "content", - "type": 4, + "type": 5, "optional": false }, { "name": "id", - "type": 2, + "type": 3, "optional": false }, { "name": "role", - "type": 29, + "type": 31, "optional": false }, { "name": "source", - "type": 30, + "type": 32, "optional": false } ], @@ -24294,7 +26392,7 @@ "kind": "tuple", "elements": [ { - "type": 5, + "type": 6, "optional": false, "rest": false } @@ -24305,22 +26403,22 @@ "properties": [ { "name": "content", - "type": 6, + "type": 7, "optional": false }, { "name": "isError", - "type": 27, + "type": 29, "optional": true }, { "name": "toolCallId", - "type": 2, + "type": 3, "optional": false }, { "name": "type", - "type": 28, + "type": 30, "optional": false } ], @@ -24328,17 +26426,17 @@ }, { "kind": "array", - "element": 7 + "element": 8 }, { "kind": "union", "types": [ - 5, - 8, - 10, - 12, - 14, - 24 + 6, + 9, + 11, + 13, + 15, + 26 ] }, { @@ -24346,32 +26444,12 @@ "properties": [ { "name": "text", - "type": 2, + "type": 3, "optional": false }, { "name": "type", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 11, + "type": 10, "optional": false } ], @@ -24385,23 +26463,43 @@ "kind": "object", "properties": [ { - "name": "arguments", - "type": 2, - "optional": false - }, - { - "name": "id", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 2, + "name": "text", + "type": 3, "optional": false }, { "name": "type", - "type": 13, + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 14, "optional": false } ], @@ -24416,12 +26514,17 @@ "properties": [ { "name": "attachment", - "type": 15, + "type": 16, "optional": false }, + { + "name": "offloaded", + "type": 24, + "optional": true + }, { "name": "type", - "type": 23, + "type": 25, "optional": false } ], @@ -24432,37 +26535,37 @@ "properties": [ { "name": "attachmentId", - "type": 2, + "type": 3, "optional": false }, { "name": "bytes", - "type": 16, - "optional": false - }, - { - "name": "height", - "type": 16, - "optional": false - }, - { - "name": "mediaType", "type": 17, "optional": false }, + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "mediaType", + "type": 18, + "optional": false + }, { "name": "name", - "type": 2, + "type": 3, "optional": true }, { "name": "originalDimensions", - "type": 22, + "type": 23, "optional": true }, { "name": "width", - "type": 16, + "type": 17, "optional": false } ], @@ -24475,10 +26578,10 @@ { "kind": "union", "types": [ - 18, 19, 20, - 21 + 21, + 22 ] }, { @@ -24502,17 +26605,21 @@ "properties": [ { "name": "height", - "type": 16, + "type": 17, "optional": false }, { "name": "width", - "type": 16, + "type": 17, "optional": false } ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -24522,12 +26629,12 @@ "properties": [ { "name": "attachment", - "type": 25, + "type": 27, "optional": false }, { "name": "type", - "type": 26, + "type": 28, "optional": false } ], @@ -24538,17 +26645,17 @@ "properties": [ { "name": "attachmentId", - "type": 2, + "type": 3, "optional": false }, { "name": "bytes", - "type": 16, + "type": 17, "optional": false }, { "name": "name", - "type": 2, + "type": 3, "optional": false } ], @@ -24575,12 +26682,12 @@ "properties": [ { "name": "callId", - "type": 2, + "type": 3, "optional": false }, { "name": "kind", - "type": 31, + "type": 33, "optional": false } ], @@ -24593,17 +26700,17 @@ { "kind": "union", "types": [ - 2, - 16, - 27, - 33, - 34, - 35 + 3, + 17, + 29, + 35, + 36, + 37 ] }, { "kind": "array", - "element": 32 + "element": 34 }, { "kind": "primitive", @@ -24614,19 +26721,61 @@ "properties": [], "indices": [ { - "key": 2, - "value": 32 + "key": 3, + "value": 34 } ] + }, + { + "kind": "array", + "element": 17 + }, + { + "kind": "union", + "types": [ + 40, + 41 + ] + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 17, + "optional": false + }, + { + "name": "op", + "type": 42, + "optional": false + }, + { + "name": "startSeq", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "tool/result" } ] }, "names": [ - "event:tool/result.data" + "event:tool/result" ], - "sources": [ - "packages/core/session/src/types.ts:355" - ] + "sources": [] }, { "digest": "2a7f86849ae54b3398ee49661a757c4fcb59a6192b7036ee2ff514617e13fb42", @@ -25104,7 +27253,7 @@ "event:assistant/attempt.data.stream[0][3].chunk[2]" ], "sources": [ - "packages/llm/llm/src/types.ts:393" + "packages/llm/llm/src/types.ts:427" ] }, { @@ -25149,6 +27298,492 @@ "names": [], "sources": [] }, + { + "digest": "2ca560fd44e47e89681d323fb02d7194733a9bf524d3a88a189bfac3c60632b1", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 1, + "optional": false + }, + { + "name": "finish", + "type": 27, + "optional": false + }, + { + "name": "requestSeq", + "type": 6, + "optional": false + }, + { + "name": "usage", + "type": 39, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "union", + "types": [ + 3, + 15, + 18, + 21, + 23, + 25 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 4, + "optional": false + }, + { + "name": "offloaded", + "type": 13, + "optional": true + }, + { + "name": "type", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 6, + "optional": false + }, + { + "name": "height", + "type": 6, + "optional": false + }, + { + "name": "mediaType", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 12, + "optional": true + }, + { + "name": "width", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 8, + 9, + 10, + 11 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 6, + "optional": false + }, + { + "name": "width", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 16, + "optional": false + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "isError", + "type": 19, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "union", + "types": [ + 28, + 31, + 33, + 35, + 37 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 29, + "optional": false + }, + { + "name": "kind", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 5, + "optional": false + }, + { + "name": "message", + "type": 5, + "optional": false + }, + { + "name": "offloadImages", + "type": 6, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 6, + "optional": true + }, + { + "name": "requestId", + "type": 5, + "optional": true + }, + { + "name": "status", + "type": 6, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 29, + "optional": false + }, + { + "name": "kind", + "type": 32, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 6, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 6, + "optional": true + }, + { + "name": "inputTokens", + "type": 6, + "optional": false + }, + { + "name": "outputTokens", + "type": 6, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 6, + "optional": true + }, + { + "name": "totalTokens", + "type": 6, + "optional": true + } + ], + "indices": [] + } + ] + }, + "names": [ + "packages/experimental/browser-use-stagehand-native/src/model.ts#StagehandLlmResult" + ], + "sources": [ + "packages/experimental/browser-use-stagehand-native/src/model.ts:25" + ] + }, { "digest": "2ce7ef3b3daebf95815c7ae454a751d3e67ca7a22e2e0734fe2942df393bb66c", "schema": { @@ -25256,1149 +27891,6 @@ "names": [], "sources": [] }, - { - "digest": "2e49ede7fb3a592cce8919ebe982f6ca1c1f67abe4def359ffc028c85e6d1ad8", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 30, - "optional": true - }, - { - "name": "seq", - "type": 9, - "optional": false - }, - { - "name": "sourceEventSeqs", - "type": 84, - "optional": true - }, - { - "name": "surfaceOp", - "type": 85, - "optional": false - }, - { - "name": "time", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 88, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "role", - "type": 27, - "optional": false - }, - { - "name": "source", - "type": 28, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "union", - "types": [ - 4, - 7, - 17, - 20, - 23, - 25 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 5, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 8, - "optional": false - }, - { - "name": "type", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 9, - "optional": false - }, - { - "name": "height", - "type": 9, - "optional": false - }, - { - "name": "mediaType", - "type": 10, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": true - }, - { - "name": "originalDimensions", - "type": 15, - "optional": true - }, - { - "name": "width", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 11, - 12, - 13, - 14 - ] - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 9, - "optional": false - }, - { - "name": "width", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 18, - "optional": false - }, - { - "name": "type", - "type": 19, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "isError", - "type": 21, - "optional": true - }, - { - "name": "toolCallId", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "union", - "types": [ - 29, - 39, - 41, - 42, - 45, - 50, - 52, - 57, - 58, - 59, - 61, - 63, - 64, - 71, - 73, - 75, - 77, - 79, - 82, - 83 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "baseline", - "type": 30, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 5, - "optional": true - }, - { - "name": "changes", - "type": 31, - "optional": false - }, - { - "name": "form", - "type": 37, - "optional": false - }, - { - "name": "kind", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 32 - }, - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 33, - "optional": false - }, - { - "name": "digest", - "type": 5, - "optional": true - }, - { - "name": "path", - "type": 5, - "optional": false - }, - { - "name": "scope", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 34, - 35, - 36 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 5, - "optional": false - }, - { - "name": "kind", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "object", - "properties": [ - { - "name": "clientTimeZone", - "type": 5, - "optional": true - }, - { - "name": "kind", - "type": 27, - "optional": false - }, - { - "name": "rpcId", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 5, - "optional": false - }, - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 44, - "optional": false - }, - { - "name": "provider", - "type": 5, - "optional": false - }, - { - "name": "ruleId", - "type": 5, - "optional": false - }, - { - "name": "source", - "type": 5, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - }, - { - "kind": "object", - "properties": [ - { - "name": "entries", - "type": 46, - "optional": false - }, - { - "name": "form", - "type": 48, - "optional": false - }, - { - "name": "kind", - "type": 49, - "optional": false - }, - { - "name": "update", - "type": 30, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 47 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 37, - "optional": false - }, - { - "name": "kind", - "type": 51, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 53, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - }, - { - "name": "sections", - "type": 55, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 56 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "text", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 37, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 60, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 62, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 48, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 60, - "optional": false - }, - { - "name": "kind", - "type": 65, - "optional": false - }, - { - "name": "references", - "type": 66, - "optional": false - }, - { - "name": "version", - "type": 70, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 67 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 9, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 68, - "optional": false - }, - { - "name": "compacted", - "type": 21, - "optional": false - }, - { - "name": "inputIndex", - "type": 9, - "optional": false - }, - { - "name": "label", - "type": 5, - "optional": false - }, - { - "name": "omittedBytes", - "type": 9, - "optional": false - }, - { - "name": "omittedMessages", - "type": 9, - "optional": false - }, - { - "name": "originalMessages", - "type": 9, - "optional": false - }, - { - "name": "retainedMessages", - "type": 9, - "optional": false - }, - { - "name": "sessionId", - "type": 5, - "optional": false - }, - { - "name": "truncated", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 9, - 69 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 72, - "optional": false - }, - { - "name": "senderSessionId", - "type": 5, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 62, - "optional": false - }, - { - "name": "kind", - "type": 74, - "optional": false - }, - { - "name": "senderSessionId", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 5, - "optional": false - }, - { - "name": "kind", - "type": 76, - "optional": false - }, - { - "name": "revision", - "type": 9, - "optional": false - }, - { - "name": "round", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 78, - "optional": false - }, - { - "name": "messageId", - "type": 5, - "optional": false - }, - { - "name": "senderId", - "type": 5, - "optional": false - }, - { - "name": "senderName", - "type": 5, - "optional": false - }, - { - "name": "teamId", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 80, - "optional": false - }, - { - "name": "model", - "type": 5, - "optional": false - }, - { - "name": "provider", - "type": 5, - "optional": false - }, - { - "name": "replayState", - "type": 81, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 9 - }, - { - "kind": "union", - "types": [ - 86, - 87 - ] - }, - { - "kind": "literal", - "value": "append" - }, - { - "kind": "object", - "properties": [ - { - "name": "endSeq", - "type": 9, - "optional": false - }, - { - "name": "op", - "type": 35, - "optional": false - }, - { - "name": "startSeq", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "user/message" - } - ] - }, - "names": [ - "event:user/message" - ], - "sources": [] - }, { "digest": "2e4debca12f2c449f96dc6574f3fbf2fb346193443114a241d01a236362ec48c", "schema": { @@ -26740,88 +28232,35 @@ ] }, { - "digest": "2ffcad547cb840f462eb1849fa5f6cb0e17eb93f5d07cc88b61d749b1b78d632", + "digest": "300341f890f7b9506a1749fff4fa5239d16ea1c63ff6b15851acb03c20f3b48d", "schema": { "root": 0, "nodes": [ { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 29, - "optional": true - }, - { - "name": "seq", - "type": 17, - "optional": false - }, - { - "name": "time", - "type": 17, - "optional": false - }, - { - "name": "type", - "type": 30, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "message", - "type": 2, - "optional": false - }, - { - "name": "teamId", - "type": 7, - "optional": false - }, - { - "name": "version", - "type": 28, - "optional": false - } - ], - "indices": [] + "kind": "array", + "element": 1 }, { "kind": "object", "properties": [ { "name": "content", - "type": 3, + "type": 2, "optional": false }, { "name": "id", - "type": 7, + "type": 5, "optional": false }, { - "name": "senderId", - "type": 7, + "name": "role", + "type": 28, "optional": false }, { - "name": "senderName", - "type": 7, - "optional": false - }, - { - "name": "targetId", - "type": 7, + "name": "source", + "type": 29, "optional": false } ], @@ -26829,41 +28268,231 @@ }, { "kind": "array", - "element": 4 + "element": 3 }, { "kind": "union", "types": [ - 5, - 9, - 11, - 13, - 15, - 25 + 4, + 7, + 18, + 21, + 24, + 26 ] }, { "kind": "object", "properties": [ { - "name": "content", - "type": 3, + "name": "arguments", + "type": 5, "optional": false }, { - "name": "isError", - "type": 6, - "optional": true + "name": "id", + "type": 5, + "optional": false }, { - "name": "toolCallId", - "type": 7, + "name": "name", + "type": 5, "optional": false }, { "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", "type": 8, "optional": false + }, + { + "name": "offloaded", + "type": 16, + "optional": true + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "mediaType", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 15, + "optional": true + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 11, + 12, + 13, + 14 + ] + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 19, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "isError", + "type": 22, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 23, + "optional": false } ], "indices": [] @@ -26872,10 +28501,6 @@ "kind": "primitive", "type": "boolean" }, - { - "kind": "primitive", - "type": "string" - }, { "kind": "literal", "value": "tool-result" @@ -26885,12 +28510,12 @@ "properties": [ { "name": "text", - "type": 7, + "type": 5, "optional": false }, { "name": "type", - "type": 10, + "type": 25, "optional": false } ], @@ -26905,12 +28530,12 @@ "properties": [ { "name": "text", - "type": 7, + "type": 5, "optional": false }, { "name": "type", - "type": 12, + "type": 27, "optional": false } ], @@ -26920,32 +28545,839 @@ "kind": "literal", "value": "text" }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 30, + 39, + 41, + 42, + 45, + 50, + 52, + 57, + 58, + 59, + 61, + 63, + 64, + 71, + 73, + 75, + 77, + 79, + 82, + 83 + ] + }, { "kind": "object", "properties": [ { - "name": "arguments", - "type": 7, + "name": "baseline", + "type": 16, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 5, + "optional": true + }, + { + "name": "changes", + "type": 31, "optional": false }, { - "name": "id", - "type": 7, + "name": "form", + "type": 37, "optional": false }, { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 14, + "name": "kind", + "type": 38, "optional": false } ], "indices": [] }, + { + "kind": "array", + "element": 32 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 33, + "optional": false + }, + { + "name": "digest", + "type": 5, + "optional": true + }, + { + "name": "path", + "type": 5, + "optional": false + }, + { + "name": "scope", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 34, + 35, + 36 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 5, + "optional": true + }, + { + "name": "kind", + "type": 28, + "optional": false + }, + { + "name": "rpcId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 5, + "optional": false + }, + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "ruleId", + "type": 5, + "optional": false + }, + { + "name": "source", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 46, + "optional": false + }, + { + "name": "form", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + }, + { + "name": "update", + "type": 16, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 47 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 53, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + }, + { + "name": "sections", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 56 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "text", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 60, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 60, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + }, + { + "name": "references", + "type": 66, + "optional": false + }, + { + "name": "version", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 67 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 9, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 68, + "optional": false + }, + { + "name": "compacted", + "type": 22, + "optional": false + }, + { + "name": "inputIndex", + "type": 9, + "optional": false + }, + { + "name": "label", + "type": 5, + "optional": false + }, + { + "name": "omittedBytes", + "type": 9, + "optional": false + }, + { + "name": "omittedMessages", + "type": 9, + "optional": false + }, + { + "name": "originalMessages", + "type": 9, + "optional": false + }, + { + "name": "retainedMessages", + "type": 9, + "optional": false + }, + { + "name": "sessionId", + "type": 5, + "optional": false + }, + { + "name": "truncated", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 9, + 69 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 72, + "optional": false + }, + { + "name": "senderSessionId", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 74, + "optional": false + }, + { + "name": "senderSessionId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 76, + "optional": false + }, + { + "name": "revision", + "type": 9, + "optional": false + }, + { + "name": "round", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "messageId", + "type": 5, + "optional": false + }, + { + "name": "senderId", + "type": 5, + "optional": false + }, + { + "name": "senderName", + "type": 5, + "optional": false + }, + { + "name": "teamId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "model", + "type": 5, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "replayState", + "type": 81, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 28, + "optional": false + } + ], + "indices": [] + } + ] + }, + "names": [ + "event:agent/inbox/spliced.data.inserted" + ], + "sources": [] + }, + { + "digest": "302b01ddddcbb3b16cfa3fba2a5d99f5082a55fbd9dc5c83a179a228b8a6a0ca", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "id", + "type": 4, + "optional": false + }, + { + "name": "role", + "type": 27, + "optional": false + }, + { + "name": "source", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "union", + "types": [ + 3, + 6, + 17, + 20, + 23, + 25 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, { "kind": "literal", "value": "tool-call" @@ -26955,12 +29387,17 @@ "properties": [ { "name": "attachment", - "type": 16, + "type": 7, "optional": false }, + { + "name": "offloaded", + "type": 15, + "optional": true + }, { "name": "type", - "type": 24, + "type": 16, "optional": false } ], @@ -26971,37 +29408,37 @@ "properties": [ { "name": "attachmentId", - "type": 7, + "type": 4, "optional": false }, { "name": "bytes", - "type": 17, + "type": 8, "optional": false }, { "name": "height", - "type": 17, + "type": 8, "optional": false }, { "name": "mediaType", - "type": 18, + "type": 9, "optional": false }, { "name": "name", - "type": 7, + "type": 4, "optional": true }, { "name": "originalDimensions", - "type": 23, + "type": 14, "optional": true }, { "name": "width", - "type": 17, + "type": 8, "optional": false } ], @@ -27014,10 +29451,10 @@ { "kind": "union", "types": [ - 19, - 20, - 21, - 22 + 10, + 11, + 12, + 13 ] }, { @@ -27041,17 +29478,21 @@ "properties": [ { "name": "height", - "type": 17, + "type": 8, "optional": false }, { "name": "width", - "type": 17, + "type": 8, "optional": false } ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -27061,12 +29502,12 @@ "properties": [ { "name": "attachment", - "type": 26, + "type": 18, "optional": false }, { "name": "type", - "type": 27, + "type": 19, "optional": false } ], @@ -27077,17 +29518,17 @@ "properties": [ { "name": "attachmentId", - "type": 7, + "type": 4, "optional": false }, { "name": "bytes", - "type": 17, + "type": 8, "optional": false }, { "name": "name", - "type": 7, + "type": 4, "optional": false } ], @@ -27098,23 +29539,303 @@ "value": "file" }, { - "kind": "literal", - "value": 2 + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "isError", + "type": 21, + "optional": true + }, + { + "name": "toolCallId", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" }, { "kind": "literal", - "value": true + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] }, { "kind": "literal", - "value": "team/message/queued" + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "union", + "types": [ + 29, + 34, + 36, + 38, + 40, + 42, + 44 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 30, + "optional": false + }, + { + "name": "kind", + "type": 31, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + }, + { + "name": "sections", + "type": 32, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 33 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 4, + "optional": false + }, + { + "name": "text", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 31, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + }, + { + "name": "summary", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 31, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 31, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 31, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 31, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 31, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] } ] }, "names": [ - "event:team/message/queued" + "packages/llm/llm/src/message.ts#SystemMessage" ], - "sources": [] + "sources": [ + "packages/llm/llm/src/message.ts:158" + ] }, { "digest": "30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f", @@ -27213,6 +29934,1154 @@ "packages/session/session-title/src/types.ts:28" ] }, + { + "digest": "314765bdff29c7862fb6ce820f1773563ba3094a680d163ea21180a2591b8578", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 16, + "optional": true + }, + { + "name": "seq", + "type": 9, + "optional": false + }, + { + "name": "sourceEventSeqs", + "type": 84, + "optional": true + }, + { + "name": "surfaceOp", + "type": 85, + "optional": false + }, + { + "name": "time", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 88, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "role", + "type": 28, + "optional": false + }, + { + "name": "source", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "union", + "types": [ + 4, + 7, + 18, + 21, + 24, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 8, + "optional": false + }, + { + "name": "offloaded", + "type": 16, + "optional": true + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "mediaType", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 15, + "optional": true + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 11, + 12, + 13, + 14 + ] + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 19, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "isError", + "type": 22, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 30, + 39, + 41, + 42, + 45, + 50, + 52, + 57, + 58, + 59, + 61, + 63, + 64, + 71, + 73, + 75, + 77, + 79, + 82, + 83 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 16, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 5, + "optional": true + }, + { + "name": "changes", + "type": 31, + "optional": false + }, + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 32 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 33, + "optional": false + }, + { + "name": "digest", + "type": 5, + "optional": true + }, + { + "name": "path", + "type": 5, + "optional": false + }, + { + "name": "scope", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 34, + 35, + 36 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 5, + "optional": true + }, + { + "name": "kind", + "type": 28, + "optional": false + }, + { + "name": "rpcId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 5, + "optional": false + }, + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "ruleId", + "type": 5, + "optional": false + }, + { + "name": "source", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 46, + "optional": false + }, + { + "name": "form", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + }, + { + "name": "update", + "type": 16, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 47 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 53, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + }, + { + "name": "sections", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 56 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "text", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 60, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 60, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + }, + { + "name": "references", + "type": 66, + "optional": false + }, + { + "name": "version", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 67 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 9, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 68, + "optional": false + }, + { + "name": "compacted", + "type": 22, + "optional": false + }, + { + "name": "inputIndex", + "type": 9, + "optional": false + }, + { + "name": "label", + "type": 5, + "optional": false + }, + { + "name": "omittedBytes", + "type": 9, + "optional": false + }, + { + "name": "omittedMessages", + "type": 9, + "optional": false + }, + { + "name": "originalMessages", + "type": 9, + "optional": false + }, + { + "name": "retainedMessages", + "type": 9, + "optional": false + }, + { + "name": "sessionId", + "type": 5, + "optional": false + }, + { + "name": "truncated", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 9, + 69 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 72, + "optional": false + }, + { + "name": "senderSessionId", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 74, + "optional": false + }, + { + "name": "senderSessionId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 76, + "optional": false + }, + { + "name": "revision", + "type": 9, + "optional": false + }, + { + "name": "round", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "messageId", + "type": 5, + "optional": false + }, + { + "name": "senderId", + "type": 5, + "optional": false + }, + { + "name": "senderName", + "type": 5, + "optional": false + }, + { + "name": "teamId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "model", + "type": 5, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "replayState", + "type": 81, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 9 + }, + { + "kind": "union", + "types": [ + 86, + 87 + ] + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 9, + "optional": false + }, + { + "name": "op", + "type": 35, + "optional": false + }, + { + "name": "startSeq", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user/message" + } + ] + }, + "names": [ + "event:user/message" + ], + "sources": [] + }, { "digest": "326a9bfda27dda5882fb3cbe6e715df41dae2298fdae3d66437457268a665693", "schema": { @@ -27368,898 +31237,6 @@ "names": [], "sources": [] }, - { - "digest": "3516f54b427775f483febe8e938e4e10f9afd3583610861547bbe2999ed4d8d9", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 71, - "optional": true - }, - { - "name": "seq", - "type": 2, - "optional": false - }, - { - "name": "time", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 72, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "step", - "type": 2, - "optional": false - }, - { - "name": "stream", - "type": 3, - "optional": false - }, - { - "name": "turn", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 4 - }, - { - "kind": "union", - "types": [ - 5, - 10, - 67, - 69 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "args", - "type": 6, - "optional": false - }, - { - "name": "dt", - "type": 8, - "optional": false - }, - { - "name": "id", - "type": 7, - "optional": false - }, - { - "name": "index", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": true - }, - { - "name": "time0", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 7 - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "literal", - "value": "tool-call-chunks" - }, - { - "kind": "object", - "properties": [ - { - "name": "chunk", - "type": 11, - "optional": false - }, - { - "name": "time", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 66, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 12, - 14, - 39, - 42, - 44, - 46, - 63 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "argumentsDelta", - "type": 7, - "optional": false - }, - { - "name": "id", - "type": 7, - "optional": false - }, - { - "name": "index", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": true - }, - { - "name": "type", - "type": 13, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "block", - "type": 15, - "optional": false - }, - { - "name": "index", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 16, - 18, - 27, - 30, - 34, - 36 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 7, - "optional": false - }, - { - "name": "id", - "type": 7, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 17, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 19, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 7, - "optional": false - }, - { - "name": "bytes", - "type": 2, - "optional": false - }, - { - "name": "height", - "type": 2, - "optional": false - }, - { - "name": "mediaType", - "type": 20, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": true - }, - { - "name": "originalDimensions", - "type": 25, - "optional": true - }, - { - "name": "width", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 21, - 22, - 23, - 24 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 2, - "optional": false - }, - { - "name": "width", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 28, - "optional": false - }, - { - "name": "type", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 7, - "optional": false - }, - { - "name": "bytes", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 31, - "optional": false - }, - { - "name": "isError", - "type": 32, - "optional": true - }, - { - "name": "toolCallId", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 33, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 15 - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 35, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 37, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "block-end" - }, - { - "kind": "object", - "properties": [ - { - "name": "blockType", - "type": 40, - "optional": false - }, - { - "name": "index", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 17, - 26, - 29, - 33, - 35, - 37 - ] - }, - { - "kind": "literal", - "value": "block-start" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 2, - "optional": false - }, - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 2, - "optional": false - }, - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 45, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "reason", - "type": 47, - "optional": false - }, - { - "name": "replayState", - "type": 59, - "optional": true - }, - { - "name": "type", - "type": 62, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 48, - 51, - 53, - 55, - 57 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 49, - "optional": false - }, - { - "name": "kind", - "type": 50, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 7, - "optional": false - }, - { - "name": "message", - "type": 7, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 2, - "optional": true - }, - { - "name": "requestId", - "type": 7, - "optional": true - }, - { - "name": "status", - "type": 2, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 49, - "optional": false - }, - { - "name": "kind", - "type": 52, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 54, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 56, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "stop" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 58, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-calls" - }, - { - "kind": "object", - "properties": [ - { - "name": "blocks", - "type": 60, - "optional": true - }, - { - "name": "response", - "type": 61, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 61 - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "literal", - "value": "finish" - }, - { - "kind": "object", - "properties": [ - { - "name": "type", - "type": 64, - "optional": false - }, - { - "name": "usage", - "type": 65, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "usage" - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 2, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 2, - "optional": true - }, - { - "name": "inputTokens", - "type": 2, - "optional": false - }, - { - "name": "outputTokens", - "type": 2, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 2, - "optional": true - }, - { - "name": "totalTokens", - "type": 2, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "chunk" - }, - { - "kind": "object", - "properties": [ - { - "name": "dt", - "type": 8, - "optional": false - }, - { - "name": "index", - "type": 2, - "optional": false - }, - { - "name": "texts", - "type": 6, - "optional": false - }, - { - "name": "time0", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 68, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-chunks" - }, - { - "kind": "object", - "properties": [ - { - "name": "dt", - "type": 8, - "optional": false - }, - { - "name": "index", - "type": 2, - "optional": false - }, - { - "name": "texts", - "type": 6, - "optional": false - }, - { - "name": "time0", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 70, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-chunks" - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "literal", - "value": "assistant/attempt" - } - ] - }, - "names": [ - "event:assistant/attempt" - ], - "sources": [] - }, { "digest": "35203ba7ad5ef6f97d556b85df20ae98f04f09c65748cecdf8eefdb8b6405ffc", "schema": { @@ -28341,102 +31318,56 @@ "sources": [] }, { - "digest": "35b995af35bfa087f5381a5454a2305928da037e819bfc4759d3f66d9bd55aa9", + "digest": "356aad27134a0e31af67c994ff8b9e4b91acfb6c4917b3bc1f7a62bbf82794d5", "schema": { "root": 0, "nodes": [ - { - "kind": "union", - "types": [ - 1, - 6 - ] - }, { "kind": "object", "properties": [ { - "name": "delayMs", - "type": 2, - "optional": false + "name": "error", + "type": 1, + "optional": true }, { - "name": "failure", + "name": "message", "type": 3, "optional": false }, { - "name": "maxRetries", - "type": 2, - "optional": false - }, - { - "name": "mode", - "type": 5, - "optional": false - }, - { - "name": "policyKey", - "type": 4, - "optional": false - }, - { - "name": "provider", - "type": 4, - "optional": false - }, - { - "name": "retry", - "type": 2, - "optional": false - }, - { - "name": "retryId", - "type": 4, - "optional": false + "name": "meta", + "type": 33, + "optional": true }, { "name": "step", - "type": 2, + "type": 16, "optional": false }, { "name": "turn", - "type": 2, + "type": 16, "optional": false } ], "indices": [] }, - { - "kind": "primitive", - "type": "number" - }, { "kind": "object", "properties": [ { "name": "code", - "type": 4, - "optional": false - }, - { - "name": "message", - "type": 4, - "optional": false - }, - { - "name": "providerRetryAfterMs", "type": 2, - "optional": true + "optional": false }, { - "name": "requestId", - "type": 4, - "optional": true + "name": "name", + "type": 2, + "optional": false }, { - "name": "status", + "name": "reason", "type": 2, "optional": true } @@ -28448,54 +31379,298 @@ "type": "string" }, { - "kind": "literal", - "value": "normal" + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 2, + "optional": false + }, + { + "name": "role", + "type": 30, + "optional": false + }, + { + "name": "source", + "type": 31, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "tuple", + "elements": [ + { + "type": 5, + "optional": false, + "rest": false + } + ] }, { "kind": "object", "properties": [ { - "name": "delayMs", + "name": "content", + "type": 6, + "optional": false + }, + { + "name": "isError", + "type": 28, + "optional": true + }, + { + "name": "toolCallId", "type": 2, "optional": false }, { - "name": "failure", - "type": 3, + "name": "type", + "type": 29, "optional": false - }, + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "union", + "types": [ + 5, + 8, + 10, + 12, + 14, + 25 + ] + }, + { + "kind": "object", + "properties": [ { - "name": "mode", - "type": 7, - "optional": false - }, - { - "name": "policyKey", - "type": 4, - "optional": false - }, - { - "name": "provider", - "type": 4, - "optional": false - }, - { - "name": "retry", + "name": "text", "type": 2, "optional": false }, { - "name": "retryId", - "type": 4, + "name": "type", + "type": 9, "optional": false - }, + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ { - "name": "step", + "name": "text", "type": 2, "optional": false }, { - "name": "turn", + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 2, + "optional": false + }, + { + "name": "id", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 15, + "optional": false + }, + { + "name": "offloaded", + "type": 23, + "optional": true + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 2, + "optional": false + }, + { + "name": "bytes", + "type": 16, + "optional": false + }, + { + "name": "height", + "type": 16, + "optional": false + }, + { + "name": "mediaType", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": true + }, + { + "name": "originalDimensions", + "type": 22, + "optional": true + }, + { + "name": "width", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 18, + 19, + 20, + 21 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 16, + "optional": false + }, + { + "name": "width", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 26, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 2, + "optional": false + }, + { + "name": "bytes", + "type": 16, + "optional": false + }, + { + "name": "name", "type": 2, "optional": false } @@ -28504,15 +31679,76 @@ }, { "kind": "literal", - "value": "always" + "value": "file" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 2, + "optional": false + }, + { + "name": "kind", + "type": 32, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "union", + "types": [ + 2, + 16, + 28, + 34, + 35, + 36 + ] + }, + { + "kind": "array", + "element": 33 + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 2, + "value": 33 + } + ] } ] }, "names": [ - "packages/llm/llm-retry/src/types.ts#LlmRetryEventData" + "event:tool/result.data" ], "sources": [ - "packages/llm/llm-retry/src/types.ts:16" + "packages/core/session/src/types.ts:355" ] }, { @@ -28629,6 +31865,119 @@ ], "sources": [] }, + { + "digest": "374fdd68aee5ff844e378c0cbdd961d247a0f22e4ade0bb03d800cd2a46a2b8b", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "delayMs", + "type": 1, + "optional": false + }, + { + "name": "failure", + "type": 2, + "optional": false + }, + { + "name": "mode", + "type": 4, + "optional": false + }, + { + "name": "policyKey", + "type": 3, + "optional": false + }, + { + "name": "provider", + "type": 3, + "optional": false + }, + { + "name": "retry", + "type": 1, + "optional": false + }, + { + "name": "retryId", + "type": 3, + "optional": false + }, + { + "name": "step", + "type": 1, + "optional": false + }, + { + "name": "turn", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 3, + "optional": false + }, + { + "name": "message", + "type": 3, + "optional": false + }, + { + "name": "offloadImages", + "type": 1, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 1, + "optional": true + }, + { + "name": "requestId", + "type": 3, + "optional": true + }, + { + "name": "status", + "type": 1, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "always" + } + ] + }, + "names": [ + "event:llm/retry.data[1]" + ], + "sources": [ + "packages/llm/llm-retry/src/types.ts:30" + ] + }, { "digest": "378f90f9f6022a0991928a1eaf7991094ea1920fe89a9248358a1da106e1a970", "schema": { @@ -28851,7 +32200,7 @@ ] }, { - "digest": "3a803805bdeb805f32b229e399fb7258be8e32a89f89063a7c99957cfea942f7", + "digest": "3aa261c8516b5c955ed12080e6257a56cbe513f6c9f0ac9274c12e96c196ebca", "schema": { "root": 0, "nodes": [ @@ -28859,90 +32208,58 @@ "kind": "object", "properties": [ { - "name": "data", + "name": "compactionId", "type": 1, "optional": false }, { - "name": "ignorable", - "type": 37, - "optional": true - }, - { - "name": "seq", - "type": 17, - "optional": false - }, - { - "name": "sourceEventSeqs", - "type": 38, - "optional": true - }, - { - "name": "surfaceOp", - "type": 39, - "optional": false - }, - { - "name": "time", - "type": 17, - "optional": false - }, - { - "name": "type", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "error", + "name": "maxTokens", "type": 2, "optional": true }, { - "name": "message", - "type": 4, + "name": "model", + "type": 1, "optional": false }, { - "name": "meta", - "type": 33, + "name": "provider", + "type": 1, + "optional": false + }, + { + "name": "rawOutput", + "type": 3, "optional": true }, { - "name": "step", - "type": 17, + "name": "shadowedRange", + "type": 27, "optional": false }, { - "name": "turn", - "type": 17, + "name": "shadowedSeqs", + "type": 28, "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ + }, { - "name": "code", + "name": "shadowedTokenCount", + "type": 2, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 1, + "optional": true + }, + { + "name": "summary", "type": 3, "optional": false }, { - "name": "name", - "type": 3, - "optional": false - }, - { - "name": "reason", - "type": 3, + "name": "usage", + "type": 29, "optional": true } ], @@ -28952,300 +32269,51 @@ "kind": "primitive", "type": "string" }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 5, - "optional": false - }, - { - "name": "id", - "type": 3, - "optional": false - }, - { - "name": "role", - "type": 30, - "optional": false - }, - { - "name": "source", - "type": 31, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "tuple", - "elements": [ - { - "type": 6, - "optional": false, - "rest": false - } - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 7, - "optional": false - }, - { - "name": "isError", - "type": 28, - "optional": true - }, - { - "name": "toolCallId", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 8 - }, - { - "kind": "union", - "types": [ - 6, - 9, - 11, - 13, - 15, - 25 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 3, - "optional": false - }, - { - "name": "id", - "type": 3, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 14, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 16, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 3, - "optional": false - }, - { - "name": "bytes", - "type": 17, - "optional": false - }, - { - "name": "height", - "type": 17, - "optional": false - }, - { - "name": "mediaType", - "type": 18, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": true - }, - { - "name": "originalDimensions", - "type": 23, - "optional": true - }, - { - "name": "width", - "type": 17, - "optional": false - } - ], - "indices": [] - }, { "kind": "primitive", "type": "number" }, + { + "kind": "array", + "element": 4 + }, { "kind": "union", "types": [ - 19, - 20, - 21, - 22 + 5, + 8, + 10, + 12, + 14, + 24 ] }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, { "kind": "object", "properties": [ { - "name": "height", - "type": 17, + "name": "content", + "type": 3, "optional": false }, { - "name": "width", - "type": 17, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ + "name": "isError", + "type": 6, + "optional": true + }, { - "name": "attachment", - "type": 26, + "name": "toolCallId", + "type": 1, "optional": false }, { "name": "type", - "type": 27, + "type": 7, "optional": false } ], "indices": [] }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 3, - "optional": false - }, - { - "name": "bytes", - "type": 17, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, { "kind": "primitive", "type": "boolean" @@ -29254,245 +32322,14 @@ "kind": "literal", "value": "tool-result" }, - { - "kind": "literal", - "value": "user" - }, { "kind": "object", "properties": [ { - "name": "callId", - "type": 3, - "optional": false - }, - { - "name": "kind", - "type": 32, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "union", - "types": [ - 3, - 17, - 28, - 34, - 35, - 36 - ] - }, - { - "kind": "array", - "element": 33 - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "object", - "properties": [], - "indices": [ - { - "key": 3, - "value": 33 - } - ] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 17 - }, - { - "kind": "union", - "types": [ - 40, - 41 - ] - }, - { - "kind": "literal", - "value": "append" - }, - { - "kind": "object", - "properties": [ - { - "name": "endSeq", - "type": 17, - "optional": false - }, - { - "name": "op", - "type": 42, - "optional": false - }, - { - "name": "startSeq", - "type": 17, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "tool/result" - } - ] - }, - "names": [ - "event:tool/result" - ], - "sources": [] - }, - { - "digest": "3a896379a74576c5a527e4720be2fe6807d3225c27134e978a2b89a94068368e", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "chunk", + "name": "text", "type": 1, "optional": false }, - { - "name": "time", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 58, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 2, - 6, - 31, - 34, - 36, - 38, - 55 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "argumentsDelta", - "type": 3, - "optional": false - }, - { - "name": "id", - "type": 3, - "optional": false - }, - { - "name": "index", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": true - }, - { - "name": "type", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "tool-call-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "block", - "type": 7, - "optional": false - }, - { - "name": "index", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 30, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 8, - 10, - 19, - 22, - 26, - 28 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 3, - "optional": false - }, - { - "name": "id", - "type": 3, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": false - }, { "name": "type", "type": 9, @@ -29501,6 +32338,56 @@ ], "indices": [] }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 1, + "optional": false + }, + { + "name": "id", + "type": 1, + "optional": false + }, + { + "name": "name", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, { "kind": "literal", "value": "tool-call" @@ -29510,12 +32397,17 @@ "properties": [ { "name": "attachment", - "type": 11, + "type": 15, "optional": false }, + { + "name": "offloaded", + "type": 22, + "optional": true + }, { "name": "type", - "type": 18, + "type": 23, "optional": false } ], @@ -29526,37 +32418,37 @@ "properties": [ { "name": "attachmentId", - "type": 3, + "type": 1, "optional": false }, { "name": "bytes", - "type": 4, + "type": 2, "optional": false }, { "name": "height", - "type": 4, + "type": 2, "optional": false }, { "name": "mediaType", - "type": 12, + "type": 16, "optional": false }, { "name": "name", - "type": 3, + "type": 1, "optional": true }, { "name": "originalDimensions", - "type": 17, + "type": 21, "optional": true }, { "name": "width", - "type": 4, + "type": 2, "optional": false } ], @@ -29565,12 +32457,16 @@ { "kind": "union", "types": [ - 13, - 14, - 15, - 16 + 17, + 18, + 19, + 20 ] }, + { + "kind": "literal", + "value": "image/gif" + }, { "kind": "literal", "value": "image/jpeg" @@ -29583,26 +32479,26 @@ "kind": "literal", "value": "image/webp" }, - { - "kind": "literal", - "value": "image/gif" - }, { "kind": "object", "properties": [ { "name": "height", - "type": 4, + "type": 2, "optional": false }, { "name": "width", - "type": 4, + "type": 2, "optional": false } ], "indices": [] }, + { + "kind": "literal", + "value": true + }, { "kind": "literal", "value": "image" @@ -29612,12 +32508,12 @@ "properties": [ { "name": "attachment", - "type": 20, + "type": 25, "optional": false }, { "name": "type", - "type": 21, + "type": 26, "optional": false } ], @@ -29628,17 +32524,17 @@ "properties": [ { "name": "attachmentId", - "type": 3, + "type": 1, "optional": false }, { "name": "bytes", - "type": 4, + "type": 2, "optional": false }, { "name": "name", - "type": 3, + "type": 1, "optional": false } ], @@ -29652,23 +32548,13 @@ "kind": "object", "properties": [ { - "name": "content", - "type": 23, + "name": "end", + "type": 2, "optional": false }, { - "name": "isError", - "type": 24, - "optional": true - }, - { - "name": "toolCallId", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 25, + "name": "start", + "type": 2, "optional": false } ], @@ -29676,389 +32562,50 @@ }, { "kind": "array", - "element": 7 - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "block-end" - }, - { - "kind": "object", - "properties": [ - { - "name": "blockType", - "type": 32, - "optional": false - }, - { - "name": "index", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 33, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 9, - 18, - 21, - 25, - 27, - 29 - ] - }, - { - "kind": "literal", - "value": "block-start" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 4, - "optional": false - }, - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 35, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 4, - "optional": false - }, - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 37, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "reason", - "type": 39, - "optional": false - }, - { - "name": "replayState", - "type": 51, - "optional": true - }, - { - "name": "type", - "type": 54, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 40, - 43, - 45, - 47, - 49 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 3, - "optional": false - }, - { - "name": "message", - "type": 3, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 4, - "optional": true - }, - { - "name": "requestId", - "type": 3, - "optional": true - }, - { - "name": "status", - "type": 4, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 44, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 46, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 48, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "stop" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 50, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-calls" - }, - { - "kind": "object", - "properties": [ - { - "name": "blocks", - "type": 52, - "optional": true - }, - { - "name": "response", - "type": 53, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 53 - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "literal", - "value": "finish" - }, - { - "kind": "object", - "properties": [ - { - "name": "type", - "type": 56, - "optional": false - }, - { - "name": "usage", - "type": 57, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "usage" + "element": 2 }, { "kind": "object", "properties": [ { "name": "cacheReadTokens", - "type": 4, + "type": 2, "optional": true }, { "name": "cacheWriteTokens", - "type": 4, + "type": 2, "optional": true }, { "name": "inputTokens", - "type": 4, + "type": 2, "optional": false }, { "name": "outputTokens", - "type": 4, + "type": 2, "optional": false }, { "name": "reasoningTokens", - "type": 4, + "type": 2, "optional": true }, { "name": "totalTokens", - "type": 4, + "type": 2, "optional": true } ], "indices": [] - }, - { - "kind": "literal", - "value": "chunk" } ] }, "names": [ - "event:assistant/attempt.data.stream[0][3]" + "event:compaction/summary.data[1]" ], - "sources": [ - "packages/llm/llm/src/assistant-stream.ts:44" - ] + "sources": [] }, { "digest": "3ac1196f59d5e9e0e9345122c8c403b102478b526404a2e591a38f613483321e", @@ -30553,79 +33100,6 @@ "packages/feedback/message-feedback/src/types.ts:20" ] }, - { - "digest": "3dad205ad99fbda835c493ddc6bc93fe1441cef2f82774ffa2856364e2cede22", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "error", - "type": 1, - "optional": false - }, - { - "name": "kind", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 2, - "optional": false - }, - { - "name": "message", - "type": 2, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 3, - "optional": true - }, - { - "name": "requestId", - "type": 2, - "optional": true - }, - { - "name": "status", - "type": 3, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "error" - } - ] - }, - "names": [ - "event:turn/end.data.reason[3]" - ], - "sources": [ - "packages/core/session/src/types.ts:211" - ] - }, { "digest": "3e0eff5946255ff05d88a10839c60305135801f97794ea48d409f8503303317e", "schema": { @@ -30797,1070 +33271,6 @@ "names": [], "sources": [] }, - { - "digest": "40390271dffbed81bffa514370ad1c3c19a13ad10c3647c9c0eae31126c95a5f", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "role", - "type": 26, - "optional": false - }, - { - "name": "source", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "union", - "types": [ - 3, - 6, - 16, - 19, - 22, - 24 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 4, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 15, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 8, - "optional": false - }, - { - "name": "height", - "type": 8, - "optional": false - }, - { - "name": "mediaType", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": true - }, - { - "name": "originalDimensions", - "type": 14, - "optional": true - }, - { - "name": "width", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 10, - 11, - 12, - 13 - ] - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 8, - "optional": false - }, - { - "name": "width", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 17, - "optional": false - }, - { - "name": "type", - "type": 18, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 8, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "isError", - "type": 20, - "optional": true - }, - { - "name": "toolCallId", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "union", - "types": [ - 28, - 38, - 40, - 41, - 44, - 49, - 51, - 56, - 57, - 58, - 60, - 62, - 63, - 70, - 72, - 74, - 76, - 78, - 81, - 82 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "baseline", - "type": 29, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 4, - "optional": true - }, - { - "name": "changes", - "type": 30, - "optional": false - }, - { - "name": "form", - "type": 36, - "optional": false - }, - { - "name": "kind", - "type": 37, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 31 - }, - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 32, - "optional": false - }, - { - "name": "digest", - "type": 4, - "optional": true - }, - { - "name": "path", - "type": 4, - "optional": false - }, - { - "name": "scope", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 33, - 34, - 35 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 4, - "optional": false - }, - { - "name": "kind", - "type": 39, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "object", - "properties": [ - { - "name": "clientTimeZone", - "type": 4, - "optional": true - }, - { - "name": "kind", - "type": 26, - "optional": false - }, - { - "name": "rpcId", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 4, - "optional": false - }, - { - "name": "form", - "type": 42, - "optional": false - }, - { - "name": "kind", - "type": 43, - "optional": false - }, - { - "name": "provider", - "type": 4, - "optional": false - }, - { - "name": "ruleId", - "type": 4, - "optional": false - }, - { - "name": "source", - "type": 4, - "optional": false - }, - { - "name": "summary", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - }, - { - "kind": "object", - "properties": [ - { - "name": "entries", - "type": 45, - "optional": false - }, - { - "name": "form", - "type": 47, - "optional": false - }, - { - "name": "kind", - "type": 48, - "optional": false - }, - { - "name": "update", - "type": 29, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 46 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 36, - "optional": false - }, - { - "name": "kind", - "type": 50, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 52, - "optional": false - }, - { - "name": "kind", - "type": 53, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - }, - { - "name": "sections", - "type": 54, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 55 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 4, - "optional": false - }, - { - "name": "text", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 42, - "optional": false - }, - { - "name": "kind", - "type": 53, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - }, - { - "name": "summary", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 36, - "optional": false - }, - { - "name": "kind", - "type": 53, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 59, - "optional": false - }, - { - "name": "kind", - "type": 53, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 61, - "optional": false - }, - { - "name": "kind", - "type": 53, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 47, - "optional": false - }, - { - "name": "kind", - "type": 53, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 59, - "optional": false - }, - { - "name": "kind", - "type": 64, - "optional": false - }, - { - "name": "references", - "type": 65, - "optional": false - }, - { - "name": "version", - "type": 69, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 66 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 8, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 67, - "optional": false - }, - { - "name": "compacted", - "type": 20, - "optional": false - }, - { - "name": "inputIndex", - "type": 8, - "optional": false - }, - { - "name": "label", - "type": 4, - "optional": false - }, - { - "name": "omittedBytes", - "type": 8, - "optional": false - }, - { - "name": "omittedMessages", - "type": 8, - "optional": false - }, - { - "name": "originalMessages", - "type": 8, - "optional": false - }, - { - "name": "retainedMessages", - "type": 8, - "optional": false - }, - { - "name": "sessionId", - "type": 4, - "optional": false - }, - { - "name": "truncated", - "type": 20, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 8, - 68 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 42, - "optional": false - }, - { - "name": "kind", - "type": 71, - "optional": false - }, - { - "name": "senderSessionId", - "type": 4, - "optional": false - }, - { - "name": "summary", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 61, - "optional": false - }, - { - "name": "kind", - "type": 73, - "optional": false - }, - { - "name": "senderSessionId", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 4, - "optional": false - }, - { - "name": "kind", - "type": 75, - "optional": false - }, - { - "name": "revision", - "type": 8, - "optional": false - }, - { - "name": "round", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 77, - "optional": false - }, - { - "name": "messageId", - "type": 4, - "optional": false - }, - { - "name": "senderId", - "type": 4, - "optional": false - }, - { - "name": "senderName", - "type": 4, - "optional": false - }, - { - "name": "teamId", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 79, - "optional": false - }, - { - "name": "model", - "type": 4, - "optional": false - }, - { - "name": "provider", - "type": 4, - "optional": false - }, - { - "name": "replayState", - "type": 80, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 53, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 26, - "optional": false - } - ], - "indices": [] - } - ] - }, - "names": [ - "packages/llm/llm/src/message.ts#UserMessage" - ], - "sources": [ - "packages/llm/llm/src/message.ts:143" - ] - }, { "digest": "42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f", "schema": { @@ -31970,6 +33380,388 @@ "names": [], "sources": [] }, + { + "digest": "443371ec07a03a82a0e93d93abca3e70b03bca55ba5b01d507030fcb66e8fbb4", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 24, + "optional": true + }, + { + "name": "seq", + "type": 17, + "optional": false + }, + { + "name": "time", + "type": 17, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "message", + "type": 2, + "optional": false + }, + { + "name": "teamId", + "type": 7, + "optional": false + }, + { + "name": "version", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "senderId", + "type": 7, + "optional": false + }, + { + "name": "senderName", + "type": 7, + "optional": false + }, + { + "name": "targetId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 5, + 9, + 11, + 13, + 15, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "isError", + "type": 6, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 16, + "optional": false + }, + { + "name": "offloaded", + "type": 24, + "optional": true + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "mediaType", + "type": 18, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 23, + "optional": true + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 19, + 20, + 21, + 22 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 27, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "literal", + "value": 2 + }, + { + "kind": "literal", + "value": "team/message/queued" + } + ] + }, + "names": [ + "event:team/message/queued" + ], + "sources": [] + }, { "digest": "44818000e56acb97afdb5045ce235eb55e3fb1ccabc9fc7d0750bb38f36a7f8e", "schema": { @@ -32113,6 +33905,1147 @@ "packages/interaction/user-approval/src/types.ts:44" ] }, + { + "digest": "477614bca595a9e7adb5a1870be446b9a8684f3ba8950259d6289f2d3b74f028", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "targets", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "object", + "properties": [ + { + "name": "imageIndexes", + "type": 3, + "optional": false + }, + { + "name": "seq", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "primitive", + "type": "number" + } + ] + }, + "names": [ + "event:image/offload.data" + ], + "sources": [ + "packages/compaction/compaction-image-offload/src/projection.ts:25" + ] + }, + { + "digest": "478738df7e7d2b103b09fb937817a9302c81076809f2b341169e00a9419c5665", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "array", + "element": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "role", + "type": 28, + "optional": false + }, + { + "name": "source", + "type": 32, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "union", + "types": [ + 4, + 7, + 18, + 21, + 24, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 8, + "optional": false + }, + { + "name": "offloaded", + "type": 16, + "optional": true + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "mediaType", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 15, + "optional": true + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 11, + 12, + 13, + 14 + ] + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 9, + "optional": false + }, + { + "name": "width", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 19, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "isError", + "type": 22, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "union", + "types": [ + 29, + 30, + 31 + ] + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 33, + 42, + 44, + 45, + 48, + 53, + 55, + 60, + 61, + 62, + 64, + 66, + 67, + 74, + 76, + 78, + 80, + 82, + 85, + 86 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 16, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 5, + "optional": true + }, + { + "name": "changes", + "type": 34, + "optional": false + }, + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 41, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 35 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 36, + "optional": false + }, + { + "name": "digest", + "type": 5, + "optional": true + }, + { + "name": "path", + "type": 5, + "optional": false + }, + { + "name": "scope", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 37, + 38, + 39 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 5, + "optional": true + }, + { + "name": "kind", + "type": 31, + "optional": false + }, + { + "name": "rpcId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 5, + "optional": false + }, + { + "name": "form", + "type": 46, + "optional": false + }, + { + "name": "kind", + "type": 47, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "ruleId", + "type": 5, + "optional": false + }, + { + "name": "source", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 49, + "optional": false + }, + { + "name": "form", + "type": 51, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "update", + "type": 16, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 50 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 56, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + }, + { + "name": "sections", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 59 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "text", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 46, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 63, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 65, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 51, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 63, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + }, + { + "name": "references", + "type": 69, + "optional": false + }, + { + "name": "version", + "type": 73, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 70 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 9, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 71, + "optional": false + }, + { + "name": "compacted", + "type": 22, + "optional": false + }, + { + "name": "inputIndex", + "type": 9, + "optional": false + }, + { + "name": "label", + "type": 5, + "optional": false + }, + { + "name": "omittedBytes", + "type": 9, + "optional": false + }, + { + "name": "omittedMessages", + "type": 9, + "optional": false + }, + { + "name": "originalMessages", + "type": 9, + "optional": false + }, + { + "name": "retainedMessages", + "type": 9, + "optional": false + }, + { + "name": "sessionId", + "type": 5, + "optional": false + }, + { + "name": "truncated", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 9, + 72 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 46, + "optional": false + }, + { + "name": "kind", + "type": 75, + "optional": false + }, + { + "name": "senderSessionId", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 65, + "optional": false + }, + { + "name": "kind", + "type": 77, + "optional": false + }, + { + "name": "senderSessionId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 79, + "optional": false + }, + { + "name": "revision", + "type": 9, + "optional": false + }, + { + "name": "round", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 81, + "optional": false + }, + { + "name": "messageId", + "type": 5, + "optional": false + }, + { + "name": "senderId", + "type": 5, + "optional": false + }, + { + "name": "senderName", + "type": 5, + "optional": false + }, + { + "name": "teamId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 83, + "optional": false + }, + { + "name": "model", + "type": 5, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "replayState", + "type": 84, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "plugin", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 31, + "optional": false + } + ], + "indices": [] + } + ] + }, + "names": [ + "event:browser-use/stagehand-llm-request.data.request.messages", + "event:session/title-llm-request.data.messages" + ], + "sources": [] + }, { "digest": "47e5d84f1a93450a78c7f3954b43990d0f629af7c034a56313e5deb63e772c1b", "schema": { @@ -32301,6 +35234,1210 @@ ], "sources": [] }, + { + "digest": "4a797416c9d5d0ef7b52baef65c9122332c517364fda90388d994fc4fc62baa4", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "maxTokens", + "type": 1, + "optional": true + }, + { + "name": "messages", + "type": 2, + "optional": false + }, + { + "name": "model", + "type": 7, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "purpose", + "type": 88, + "optional": true + }, + { + "name": "reasoningEffort", + "type": 7, + "optional": true + }, + { + "name": "sessionId", + "type": 7, + "optional": true + }, + { + "name": "stop", + "type": 91, + "optional": true + }, + { + "name": "system", + "type": 7, + "optional": true + }, + { + "name": "temperature", + "type": 1, + "optional": true + }, + { + "name": "tools", + "type": 92, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "role", + "type": 29, + "optional": false + }, + { + "name": "source", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 9, + 19, + 22, + 25, + 27 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 10, + "optional": false + }, + { + "name": "offloaded", + "type": 17, + "optional": true + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 1, + "optional": false + }, + { + "name": "height", + "type": 1, + "optional": false + }, + { + "name": "mediaType", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 16, + "optional": true + }, + { + "name": "width", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 12, + 13, + 14, + 15 + ] + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 1, + "optional": false + }, + { + "name": "width", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 20, + "optional": false + }, + { + "name": "type", + "type": 21, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 1, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "isError", + "type": 23, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "union", + "types": [ + 30, + 31, + 32 + ] + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 34, + 43, + 45, + 46, + 49, + 54, + 56, + 61, + 62, + 63, + 65, + 67, + 68, + 75, + 77, + 79, + 81, + 83, + 86, + 87 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 17, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 7, + "optional": true + }, + { + "name": "changes", + "type": 35, + "optional": false + }, + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 36 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 37, + "optional": false + }, + { + "name": "digest", + "type": 7, + "optional": true + }, + { + "name": "path", + "type": 7, + "optional": false + }, + { + "name": "scope", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 38, + 39, + 40 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 7, + "optional": true + }, + { + "name": "kind", + "type": 32, + "optional": false + }, + { + "name": "rpcId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 7, + "optional": false + }, + { + "name": "form", + "type": 47, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "ruleId", + "type": 7, + "optional": false + }, + { + "name": "source", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 50, + "optional": false + }, + { + "name": "form", + "type": 52, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "update", + "type": 17, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 51 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 57, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + }, + { + "name": "sections", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 60 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 47, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 64, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 66, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 52, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 64, + "optional": false + }, + { + "name": "kind", + "type": 69, + "optional": false + }, + { + "name": "references", + "type": 70, + "optional": false + }, + { + "name": "version", + "type": 74, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 71 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 1, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 72, + "optional": false + }, + { + "name": "compacted", + "type": 23, + "optional": false + }, + { + "name": "inputIndex", + "type": 1, + "optional": false + }, + { + "name": "label", + "type": 7, + "optional": false + }, + { + "name": "omittedBytes", + "type": 1, + "optional": false + }, + { + "name": "omittedMessages", + "type": 1, + "optional": false + }, + { + "name": "originalMessages", + "type": 1, + "optional": false + }, + { + "name": "retainedMessages", + "type": 1, + "optional": false + }, + { + "name": "sessionId", + "type": 7, + "optional": false + }, + { + "name": "truncated", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 1, + 73 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 47, + "optional": false + }, + { + "name": "kind", + "type": 76, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 66, + "optional": false + }, + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "revision", + "type": 1, + "optional": false + }, + { + "name": "round", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "messageId", + "type": 7, + "optional": false + }, + { + "name": "senderId", + "type": 7, + "optional": false + }, + { + "name": "senderName", + "type": 7, + "optional": false + }, + { + "name": "teamId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 84, + "optional": false + }, + { + "name": "model", + "type": 7, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "replayState", + "type": 85, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 58, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 32, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 89, + 90 + ] + }, + { + "kind": "literal", + "value": "session-title" + }, + { + "kind": "literal", + "value": "compaction" + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "array", + "element": 93 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "parameters", + "type": 94, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 7, + "value": 85 + } + ] + } + ] + }, + "names": [ + "packages/experimental/browser-use-stagehand-native/src/model.ts#RecordedRequest" + ], + "sources": [ + "packages/experimental/browser-use-stagehand-native/src/model.ts:14" + ] + }, { "digest": "4a9bef9f27a942520cc2279ca7dd3082556e4bff543f66b5c0333e1bbed1cd86", "schema": { @@ -32502,361 +36639,6 @@ "packages/core/session/src/types.ts:192" ] }, - { - "digest": "4c12ebe118c00be826b47b050eb5998f019df4bc3d8ebd7a93f5921c0e73617b", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 1, - "optional": false - }, - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "error", - "type": 27, - "optional": true - }, - { - "name": "isError", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - }, - { - "name": "parentCallId", - "type": 6, - "optional": false - }, - { - "name": "rootCallId", - "type": 6, - "optional": false - }, - { - "name": "subCallId", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "union", - "types": [ - 4, - 8, - 10, - 12, - 14, - 24 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "isError", - "type": 5, - "optional": true - }, - { - "name": "toolCallId", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 6, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 13, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 15, - "optional": false - }, - { - "name": "type", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 16, - "optional": false - }, - { - "name": "height", - "type": 16, - "optional": false - }, - { - "name": "mediaType", - "type": 17, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": true - }, - { - "name": "originalDimensions", - "type": 22, - "optional": true - }, - { - "name": "width", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 18, - 19, - 20, - 21 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 16, - "optional": false - }, - { - "name": "width", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 25, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 16, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - }, - { - "name": "reason", - "type": 6, - "optional": true - } - ], - "indices": [] - } - ] - }, - "names": [ - "packages/core/tools/src/types.ts#PtcDispatchEventData" - ], - "sources": [ - "packages/core/tools/src/types.ts:20" - ] - }, { "digest": "4c965666d5b42116945bc356087cd5e1fd549e6dece560785ed60511059a8283", "schema": { @@ -33701,6 +37483,1075 @@ "packages/workflow/workflow/src/types.ts:13" ] }, + { + "digest": "4de5e7644130ed715ef6b5a4d86a87b30dca4096d1a6683c66acf6f7809379c7", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "id", + "type": 4, + "optional": false + }, + { + "name": "role", + "type": 27, + "optional": false + }, + { + "name": "source", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "union", + "types": [ + 3, + 6, + 17, + 20, + 23, + 25 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 7, + "optional": false + }, + { + "name": "offloaded", + "type": 15, + "optional": true + }, + { + "name": "type", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 4, + "optional": false + }, + { + "name": "bytes", + "type": 8, + "optional": false + }, + { + "name": "height", + "type": 8, + "optional": false + }, + { + "name": "mediaType", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "originalDimensions", + "type": 14, + "optional": true + }, + { + "name": "width", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 10, + 11, + 12, + 13 + ] + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 8, + "optional": false + }, + { + "name": "width", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 18, + "optional": false + }, + { + "name": "type", + "type": 19, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 4, + "optional": false + }, + { + "name": "bytes", + "type": 8, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "isError", + "type": 21, + "optional": true + }, + { + "name": "toolCallId", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 29, + 38, + 40, + 41, + 44, + 49, + 51, + 56, + 57, + 58, + 60, + 62, + 63, + 70, + 72, + 74, + 76, + 78, + 81, + 82 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 15, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 4, + "optional": true + }, + { + "name": "changes", + "type": 30, + "optional": false + }, + { + "name": "form", + "type": 36, + "optional": false + }, + { + "name": "kind", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 31 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 32, + "optional": false + }, + { + "name": "digest", + "type": 4, + "optional": true + }, + { + "name": "path", + "type": 4, + "optional": false + }, + { + "name": "scope", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 33, + 34, + 35 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 4, + "optional": false + }, + { + "name": "kind", + "type": 39, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 4, + "optional": true + }, + { + "name": "kind", + "type": 27, + "optional": false + }, + { + "name": "rpcId", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 4, + "optional": false + }, + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 43, + "optional": false + }, + { + "name": "provider", + "type": 4, + "optional": false + }, + { + "name": "ruleId", + "type": 4, + "optional": false + }, + { + "name": "source", + "type": 4, + "optional": false + }, + { + "name": "summary", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 45, + "optional": false + }, + { + "name": "form", + "type": 47, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "update", + "type": 15, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 46 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 36, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 52, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + }, + { + "name": "sections", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 55 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 4, + "optional": false + }, + { + "name": "text", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + }, + { + "name": "summary", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 36, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 59, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 61, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 47, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 59, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + }, + { + "name": "references", + "type": 65, + "optional": false + }, + { + "name": "version", + "type": 69, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 66 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 8, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 67, + "optional": false + }, + { + "name": "compacted", + "type": 21, + "optional": false + }, + { + "name": "inputIndex", + "type": 8, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": false + }, + { + "name": "omittedBytes", + "type": 8, + "optional": false + }, + { + "name": "omittedMessages", + "type": 8, + "optional": false + }, + { + "name": "originalMessages", + "type": 8, + "optional": false + }, + { + "name": "retainedMessages", + "type": 8, + "optional": false + }, + { + "name": "sessionId", + "type": 4, + "optional": false + }, + { + "name": "truncated", + "type": 21, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 8, + 68 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 71, + "optional": false + }, + { + "name": "senderSessionId", + "type": 4, + "optional": false + }, + { + "name": "summary", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 61, + "optional": false + }, + { + "name": "kind", + "type": 73, + "optional": false + }, + { + "name": "senderSessionId", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 4, + "optional": false + }, + { + "name": "kind", + "type": 75, + "optional": false + }, + { + "name": "revision", + "type": 8, + "optional": false + }, + { + "name": "round", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 77, + "optional": false + }, + { + "name": "messageId", + "type": 4, + "optional": false + }, + { + "name": "senderId", + "type": 4, + "optional": false + }, + { + "name": "senderName", + "type": 4, + "optional": false + }, + { + "name": "teamId", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 79, + "optional": false + }, + { + "name": "model", + "type": 4, + "optional": false + }, + { + "name": "provider", + "type": 4, + "optional": false + }, + { + "name": "replayState", + "type": 80, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "plugin", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 27, + "optional": false + } + ], + "indices": [] + } + ] + }, + "names": [ + "packages/llm/llm/src/message.ts#UserMessage" + ], + "sources": [ + "packages/llm/llm/src/message.ts:143" + ] + }, { "digest": "4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159", "schema": { @@ -33765,7 +38616,7 @@ "packages/llm/llm/src/types.ts#TextBlock" ], "sources": [ - "packages/llm/llm/src/types.ts:54", + "packages/llm/llm/src/types.ts:61", "packages/web/web-search-deepseek/src/provider.ts:66" ] }, @@ -33821,1276 +38672,6 @@ "packages/subagent/subagent/src/continuation-messages.ts:30" ] }, - { - "digest": "4ef9f82945d583962637197c7d4082b43c54c7c0bb131f19c40a906369a5abfc", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "request", - "type": 1, - "optional": false - }, - { - "name": "responseFormat", - "type": 96, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "maxTokens", - "type": 2, - "optional": true - }, - { - "name": "messages", - "type": 3, - "optional": false - }, - { - "name": "model", - "type": 8, - "optional": false - }, - { - "name": "provider", - "type": 8, - "optional": false - }, - { - "name": "purpose", - "type": 89, - "optional": true - }, - { - "name": "reasoningEffort", - "type": 8, - "optional": true - }, - { - "name": "sessionId", - "type": 8, - "optional": true - }, - { - "name": "stop", - "type": 92, - "optional": true - }, - { - "name": "system", - "type": 8, - "optional": true - }, - { - "name": "temperature", - "type": 2, - "optional": true - }, - { - "name": "tools", - "type": 93, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 4 - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 5, - "optional": false - }, - { - "name": "id", - "type": 8, - "optional": false - }, - { - "name": "role", - "type": 29, - "optional": false - }, - { - "name": "source", - "type": 33, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 6 - }, - { - "kind": "union", - "types": [ - 7, - 10, - 19, - 22, - 25, - 27 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 8, - "optional": false - }, - { - "name": "id", - "type": 8, - "optional": false - }, - { - "name": "name", - "type": 8, - "optional": false - }, - { - "name": "type", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 18, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 8, - "optional": false - }, - { - "name": "bytes", - "type": 2, - "optional": false - }, - { - "name": "height", - "type": 2, - "optional": false - }, - { - "name": "mediaType", - "type": 12, - "optional": false - }, - { - "name": "name", - "type": 8, - "optional": true - }, - { - "name": "originalDimensions", - "type": 17, - "optional": true - }, - { - "name": "width", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 13, - 14, - 15, - 16 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 2, - "optional": false - }, - { - "name": "width", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 20, - "optional": false - }, - { - "name": "type", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 8, - "optional": false - }, - { - "name": "bytes", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 5, - "optional": false - }, - { - "name": "isError", - "type": 23, - "optional": true - }, - { - "name": "toolCallId", - "type": 8, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 8, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 8, - "optional": false - }, - { - "name": "type", - "type": 28, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "union", - "types": [ - 30, - 31, - 32 - ] - }, - { - "kind": "literal", - "value": "assistant" - }, - { - "kind": "literal", - "value": "system" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "union", - "types": [ - 34, - 44, - 46, - 47, - 50, - 55, - 57, - 62, - 63, - 64, - 66, - 68, - 69, - 76, - 78, - 80, - 82, - 84, - 87, - 88 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "baseline", - "type": 35, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 8, - "optional": true - }, - { - "name": "changes", - "type": 36, - "optional": false - }, - { - "name": "form", - "type": 42, - "optional": false - }, - { - "name": "kind", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 37 - }, - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 38, - "optional": false - }, - { - "name": "digest", - "type": 8, - "optional": true - }, - { - "name": "path", - "type": 8, - "optional": false - }, - { - "name": "scope", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 39, - 40, - 41 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 8, - "optional": false - }, - { - "name": "kind", - "type": 45, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "object", - "properties": [ - { - "name": "clientTimeZone", - "type": 8, - "optional": true - }, - { - "name": "kind", - "type": 32, - "optional": false - }, - { - "name": "rpcId", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 8, - "optional": false - }, - { - "name": "form", - "type": 48, - "optional": false - }, - { - "name": "kind", - "type": 49, - "optional": false - }, - { - "name": "provider", - "type": 8, - "optional": false - }, - { - "name": "ruleId", - "type": 8, - "optional": false - }, - { - "name": "source", - "type": 8, - "optional": false - }, - { - "name": "summary", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - }, - { - "kind": "object", - "properties": [ - { - "name": "entries", - "type": 51, - "optional": false - }, - { - "name": "form", - "type": 53, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "update", - "type": 35, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 52 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 8, - "optional": false - }, - { - "name": "name", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 42, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "name", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 58, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - }, - { - "name": "plugin", - "type": 8, - "optional": false - }, - { - "name": "sections", - "type": 60, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 61 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 8, - "optional": false - }, - { - "name": "text", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 48, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - }, - { - "name": "plugin", - "type": 8, - "optional": false - }, - { - "name": "summary", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 42, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - }, - { - "name": "plugin", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 65, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - }, - { - "name": "plugin", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 67, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - }, - { - "name": "plugin", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 53, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - }, - { - "name": "plugin", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 65, - "optional": false - }, - { - "name": "kind", - "type": 70, - "optional": false - }, - { - "name": "references", - "type": 71, - "optional": false - }, - { - "name": "version", - "type": 75, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 72 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 2, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 73, - "optional": false - }, - { - "name": "compacted", - "type": 23, - "optional": false - }, - { - "name": "inputIndex", - "type": 2, - "optional": false - }, - { - "name": "label", - "type": 8, - "optional": false - }, - { - "name": "omittedBytes", - "type": 2, - "optional": false - }, - { - "name": "omittedMessages", - "type": 2, - "optional": false - }, - { - "name": "originalMessages", - "type": 2, - "optional": false - }, - { - "name": "retainedMessages", - "type": 2, - "optional": false - }, - { - "name": "sessionId", - "type": 8, - "optional": false - }, - { - "name": "truncated", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 2, - 74 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 48, - "optional": false - }, - { - "name": "kind", - "type": 77, - "optional": false - }, - { - "name": "senderSessionId", - "type": 8, - "optional": false - }, - { - "name": "summary", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 67, - "optional": false - }, - { - "name": "kind", - "type": 79, - "optional": false - }, - { - "name": "senderSessionId", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 8, - "optional": false - }, - { - "name": "kind", - "type": 81, - "optional": false - }, - { - "name": "revision", - "type": 2, - "optional": false - }, - { - "name": "round", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 83, - "optional": false - }, - { - "name": "messageId", - "type": 8, - "optional": false - }, - { - "name": "senderId", - "type": 8, - "optional": false - }, - { - "name": "senderName", - "type": 8, - "optional": false - }, - { - "name": "teamId", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 85, - "optional": false - }, - { - "name": "model", - "type": 8, - "optional": false - }, - { - "name": "provider", - "type": 8, - "optional": false - }, - { - "name": "replayState", - "type": 86, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 59, - "optional": false - }, - { - "name": "plugin", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 32, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 90, - 91 - ] - }, - { - "kind": "literal", - "value": "session-title" - }, - { - "kind": "literal", - "value": "compaction" - }, - { - "kind": "array", - "element": 8 - }, - { - "kind": "array", - "element": 94 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 8, - "optional": false - }, - { - "name": "name", - "type": 8, - "optional": false - }, - { - "name": "parameters", - "type": 95, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [], - "indices": [ - { - "key": 8, - "value": 86 - } - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 8, - "optional": true - }, - { - "name": "name", - "type": 8, - "optional": false - }, - { - "name": "schema", - "type": 97, - "optional": false - }, - { - "name": "type", - "type": 100, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 2, - 8, - 23, - 74, - 98, - 99 - ] - }, - { - "kind": "object", - "properties": [], - "indices": [ - { - "key": 8, - "value": 97 - } - ] - }, - { - "kind": "array", - "element": 97 - }, - { - "kind": "literal", - "value": "json_schema" - } - ] - }, - "names": [ - "packages/experimental/browser-use-stagehand-native/src/model.ts#StagehandLlmRequest" - ], - "sources": [ - "packages/experimental/browser-use-stagehand-native/src/model.ts:17" - ] - }, { "digest": "4f507598cf476214aa7646ce7841a7064a9dcb05d0a1eaaefaf79c2b738f160f", "schema": { @@ -35363,276 +38944,6 @@ ], "sources": [] }, - { - "digest": "508c0856b8df8574e396880a756f087699b138630428441fefca4a4380f01a28", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "reason", - "type": 1, - "optional": false - }, - { - "name": "turn", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 2, - 4, - 6, - 11, - 24, - 26 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "interrupted" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "error", - "type": 7, - "optional": false - }, - { - "name": "kind", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 8, - "optional": false - }, - { - "name": "message", - "type": 8, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 9, - "optional": true - }, - { - "name": "requestId", - "type": 8, - "optional": true - }, - { - "name": "status", - "type": 9, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 12, - "optional": false - }, - { - "name": "reason", - "type": 13, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "union", - "types": [ - 14, - 16, - 18, - 20, - 22 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 15, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "disposed" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 17, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "legacy" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 19, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "parent" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 21, - "optional": false - }, - { - "name": "reason", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "hook" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "blocked" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "completed" - } - ] - }, - "names": [ - "event:turn/end.data" - ], - "sources": [ - "packages/core/session/src/types.ts:285" - ] - }, { "digest": "50cf02d875f8a1f1411f4a6fbcffab9120ccef5f74a47331fbf29480e5f68f48", "schema": { @@ -35866,6 +39177,223 @@ "packages/experimental/agent-team/src/types.ts:74" ] }, + { + "digest": "525254db03b1d1e6b74cf55aced52817568331ac1f96c0818728910b6692e336", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 9, + "optional": true + }, + { + "name": "seq", + "type": 3, + "optional": false + }, + { + "name": "time", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 7 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "delayMs", + "type": 3, + "optional": false + }, + { + "name": "failure", + "type": 4, + "optional": false + }, + { + "name": "maxRetries", + "type": 3, + "optional": false + }, + { + "name": "mode", + "type": 6, + "optional": false + }, + { + "name": "policyKey", + "type": 5, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "retry", + "type": 3, + "optional": false + }, + { + "name": "retryId", + "type": 5, + "optional": false + }, + { + "name": "step", + "type": 3, + "optional": false + }, + { + "name": "turn", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 5, + "optional": false + }, + { + "name": "message", + "type": 5, + "optional": false + }, + { + "name": "offloadImages", + "type": 3, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 3, + "optional": true + }, + { + "name": "requestId", + "type": 5, + "optional": true + }, + { + "name": "status", + "type": 3, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "normal" + }, + { + "kind": "object", + "properties": [ + { + "name": "delayMs", + "type": 3, + "optional": false + }, + { + "name": "failure", + "type": 4, + "optional": false + }, + { + "name": "mode", + "type": 8, + "optional": false + }, + { + "name": "policyKey", + "type": 5, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "retry", + "type": 3, + "optional": false + }, + { + "name": "retryId", + "type": 5, + "optional": false + }, + { + "name": "step", + "type": 3, + "optional": false + }, + { + "name": "turn", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "always" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "llm/retry" + } + ] + }, + "names": [ + "event:llm/retry" + ], + "sources": [] + }, { "digest": "529e0ccd0e34079ab4ff21ed87f810cee0388246263bf22e8aba5986b56fae05", "schema": { @@ -36198,6 +39726,159 @@ "names": [], "sources": [] }, + { + "digest": "5808e36c95355f0e745201b5db1b6bcec4ebf5adb75d3e82d7d59cc89635ab80", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "union", + "types": [ + 1, + 3, + 5, + 10, + 12 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 6, + "optional": false + }, + { + "name": "kind", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 7, + "optional": false + }, + { + "name": "message", + "type": 7, + "optional": false + }, + { + "name": "offloadImages", + "type": 8, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 8, + "optional": true + }, + { + "name": "requestId", + "type": 7, + "optional": true + }, + { + "name": "status", + "type": 8, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 6, + "optional": false + }, + { + "name": "kind", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + } + ] + }, + "names": [ + "packages/llm/llm/src/types.ts#FinishReason" + ], + "sources": [ + "packages/llm/llm/src/types.ts:152" + ] + }, { "digest": "587bb11da730e06aa52b62c087d217d6f8f22459b25f31cb9e1e1baf17944e5e", "schema": { @@ -36254,6 +39935,330 @@ "names": [], "sources": [] }, + { + "digest": "5b694f394d5e11609887a3ccfa82844935d49806c37e336b30be2186d649ab94", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "senderId", + "type": 5, + "optional": false + }, + { + "name": "senderName", + "type": 5, + "optional": false + }, + { + "name": "targetId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "union", + "types": [ + 3, + 7, + 9, + 11, + 13, + 24 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "isError", + "type": 4, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 14, + "optional": false + }, + { + "name": "offloaded", + "type": 22, + "optional": true + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 15, + "optional": false + }, + { + "name": "height", + "type": 15, + "optional": false + }, + { + "name": "mediaType", + "type": 16, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 21, + "optional": true + }, + { + "name": "width", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 17, + 18, + 19, + 20 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 15, + "optional": false + }, + { + "name": "width", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 25, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 15, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + } + ] + }, + "names": [ + "packages/experimental/agent-team/src/types.ts#TeamMessageSnapshot" + ], + "sources": [ + "packages/experimental/agent-team/src/types.ts:106" + ] + }, { "digest": "5b6fb1f226ff56402db08a6aae82af76100b14056271875f69ec526a1ae05d51", "schema": { @@ -36417,15 +40422,16 @@ ] }, "names": [ - "SessionEventEnvelope[40].sourceEventSeqs", - "SessionEventEnvelope[53].sourceEventSeqs", - "SessionEventEnvelope[56].sourceEventSeqs", + "SessionEventEnvelope[41].sourceEventSeqs", + "SessionEventEnvelope[54].sourceEventSeqs", + "SessionEventEnvelope[57].sourceEventSeqs", "event:assistant/attempt.data.stream[0][0].dt", "event:assistant/attempt.data.stream[0][1].dt", "event:assistant/attempt.data.stream[0][2].dt", "event:compaction/prune.data.shadowedSeqs", "event:compaction/summary.data[0].shadowedSeqs", "event:compaction/summary.data[1].shadowedSeqs", + "event:image/offload.data.targets[0].imageIndexes", "event:session/title-llm-request.data.messageSeqs", "event:session/title.data.messageSeqs", "event:system/message.sourceEventSeqs", @@ -36647,92 +40653,41 @@ "sources": [] }, { - "digest": "5ed158429dfa6a8633e6ba94b8c81190616c24b65c1777bf1c60cde9dd499a2f", + "digest": "5f2117d1856a5ab43c28371faa978ebfaa48f8c7d734ea3fc94842210df83dca", "schema": { "root": 0, "nodes": [ - { - "kind": "union", - "types": [ - 1, - 3, - 5, - 10, - 12 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "stop" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-calls" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 6, - "optional": false - }, - { - "name": "kind", - "type": 9, - "optional": false - } - ], - "indices": [] - }, { "kind": "object", "properties": [ { "name": "code", - "type": 7, + "type": 1, "optional": false }, { "name": "message", - "type": 7, + "type": 1, "optional": false }, + { + "name": "offloadImages", + "type": 2, + "optional": true + }, { "name": "providerRetryAfterMs", - "type": 8, + "type": 2, "optional": true }, { "name": "requestId", - "type": 7, + "type": 1, "optional": true }, { "name": "status", - "type": 8, + "type": 2, "optional": true } ], @@ -36745,347 +40700,16 @@ { "kind": "primitive", "type": "number" - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 6, - "optional": false - }, - { - "name": "kind", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 13, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" } ] }, "names": [ - "packages/llm/llm/src/types.ts#FinishReason" + "packages/llm/llm/src/types.ts#LlmFailure" ], "sources": [ - "packages/llm/llm/src/types.ts:139" + "packages/llm/llm/src/types.ts:40" ] }, - { - "digest": "5eedf09e88b5ed9ca2d5efdb5a32cd4f51352564af5ff82927c17d841a2e3d9e", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "tuple", - "elements": [ - { - "type": 1, - "optional": false, - "rest": false - } - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "isError", - "type": 24, - "optional": true - }, - { - "name": "toolCallId", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "union", - "types": [ - 1, - 4, - 7, - 9, - 11, - 21 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 5, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 12, - "optional": false - }, - { - "name": "type", - "type": 20, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 13, - "optional": false - }, - { - "name": "height", - "type": 13, - "optional": false - }, - { - "name": "mediaType", - "type": 14, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": true - }, - { - "name": "originalDimensions", - "type": 19, - "optional": true - }, - { - "name": "width", - "type": 13, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 15, - 16, - 17, - 18 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 13, - "optional": false - }, - { - "name": "width", - "type": 13, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 22, - "optional": false - }, - { - "name": "type", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 13, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - } - ] - }, - "names": [ - "event:tool/result.data.message.content" - ], - "sources": [] - }, { "digest": "5f26a6c20b37632f8f57729d171c671def4683d994ac6257a8dffcd855101627", "schema": { @@ -37228,1179 +40852,6 @@ "packages/subagent/subagent/src/descriptor.ts:61" ] }, - { - "digest": "6091f64d426ada2adc646094a6fe662a1b81d9ee5770929fe76800524e2c89a8", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 36, - "optional": true - }, - { - "name": "seq", - "type": 2, - "optional": false - }, - { - "name": "time", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 91, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "maxTokens", - "type": 2, - "optional": false - }, - { - "name": "messageSeqs", - "type": 3, - "optional": false - }, - { - "name": "messages", - "type": 4, - "optional": false - }, - { - "name": "route", - "type": 90, - "optional": false - }, - { - "name": "system", - "type": 9, - "optional": false - }, - { - "name": "titleProvider", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "array", - "element": 5 - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 6, - "optional": false - }, - { - "name": "id", - "type": 9, - "optional": false - }, - { - "name": "role", - "type": 30, - "optional": false - }, - { - "name": "source", - "type": 34, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 7 - }, - { - "kind": "union", - "types": [ - 8, - 11, - 20, - 23, - 26, - 28 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 9, - "optional": false - }, - { - "name": "id", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 12, - "optional": false - }, - { - "name": "type", - "type": 19, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 9, - "optional": false - }, - { - "name": "bytes", - "type": 2, - "optional": false - }, - { - "name": "height", - "type": 2, - "optional": false - }, - { - "name": "mediaType", - "type": 13, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": true - }, - { - "name": "originalDimensions", - "type": 18, - "optional": true - }, - { - "name": "width", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 14, - 15, - 16, - 17 - ] - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 2, - "optional": false - }, - { - "name": "width", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 21, - "optional": false - }, - { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 9, - "optional": false - }, - { - "name": "bytes", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 6, - "optional": false - }, - { - "name": "isError", - "type": 24, - "optional": true - }, - { - "name": "toolCallId", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "union", - "types": [ - 31, - 32, - 33 - ] - }, - { - "kind": "literal", - "value": "system" - }, - { - "kind": "literal", - "value": "assistant" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "union", - "types": [ - 35, - 45, - 47, - 48, - 51, - 56, - 58, - 63, - 64, - 65, - 67, - 69, - 70, - 77, - 79, - 81, - 83, - 85, - 88, - 89 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "baseline", - "type": 36, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 9, - "optional": true - }, - { - "name": "changes", - "type": 37, - "optional": false - }, - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 44, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 38 - }, - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 39, - "optional": false - }, - { - "name": "digest", - "type": 9, - "optional": true - }, - { - "name": "path", - "type": 9, - "optional": false - }, - { - "name": "scope", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 40, - 41, - 42 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 9, - "optional": false - }, - { - "name": "kind", - "type": 46, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "object", - "properties": [ - { - "name": "clientTimeZone", - "type": 9, - "optional": true - }, - { - "name": "kind", - "type": 33, - "optional": false - }, - { - "name": "rpcId", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 9, - "optional": false - }, - { - "name": "form", - "type": 49, - "optional": false - }, - { - "name": "kind", - "type": 50, - "optional": false - }, - { - "name": "provider", - "type": 9, - "optional": false - }, - { - "name": "ruleId", - "type": 9, - "optional": false - }, - { - "name": "source", - "type": 9, - "optional": false - }, - { - "name": "summary", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - }, - { - "kind": "object", - "properties": [ - { - "name": "entries", - "type": 52, - "optional": false - }, - { - "name": "form", - "type": 54, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "update", - "type": 36, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 53 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 59, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - }, - { - "name": "sections", - "type": 61, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 62 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 9, - "optional": false - }, - { - "name": "text", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 49, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - }, - { - "name": "summary", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 68, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 54, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 71, - "optional": false - }, - { - "name": "references", - "type": 72, - "optional": false - }, - { - "name": "version", - "type": 76, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 73 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 2, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 74, - "optional": false - }, - { - "name": "compacted", - "type": 24, - "optional": false - }, - { - "name": "inputIndex", - "type": 2, - "optional": false - }, - { - "name": "label", - "type": 9, - "optional": false - }, - { - "name": "omittedBytes", - "type": 2, - "optional": false - }, - { - "name": "omittedMessages", - "type": 2, - "optional": false - }, - { - "name": "originalMessages", - "type": 2, - "optional": false - }, - { - "name": "retainedMessages", - "type": 2, - "optional": false - }, - { - "name": "sessionId", - "type": 9, - "optional": false - }, - { - "name": "truncated", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 2, - 75 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 49, - "optional": false - }, - { - "name": "kind", - "type": 78, - "optional": false - }, - { - "name": "senderSessionId", - "type": 9, - "optional": false - }, - { - "name": "summary", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 68, - "optional": false - }, - { - "name": "kind", - "type": 80, - "optional": false - }, - { - "name": "senderSessionId", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 9, - "optional": false - }, - { - "name": "kind", - "type": 82, - "optional": false - }, - { - "name": "revision", - "type": 2, - "optional": false - }, - { - "name": "round", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 84, - "optional": false - }, - { - "name": "messageId", - "type": 9, - "optional": false - }, - { - "name": "senderId", - "type": 9, - "optional": false - }, - { - "name": "senderName", - "type": 9, - "optional": false - }, - { - "name": "teamId", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 86, - "optional": false - }, - { - "name": "model", - "type": 9, - "optional": false - }, - { - "name": "provider", - "type": 9, - "optional": false - }, - { - "name": "replayState", - "type": 87, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 33, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "model", - "type": 9, - "optional": false - }, - { - "name": "provider", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session/title-llm-request" - } - ] - }, - "names": [ - "event:session/title-llm-request" - ], - "sources": [] - }, { "digest": "611cae9a3539733aa0c42f66035a7d90f19dcbbdfb5d9e3b628b13705253942d", "schema": { @@ -38415,346 +40866,6 @@ "names": [], "sources": [] }, - { - "digest": "6132f7814b28287473605ebc6dacd63c7c699435452487da6152bc735b8ba8c4", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "message", - "type": 1, - "optional": false - }, - { - "name": "teamId", - "type": 6, - "optional": false - }, - { - "name": "version", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "senderId", - "type": 6, - "optional": false - }, - { - "name": "senderName", - "type": 6, - "optional": false - }, - { - "name": "targetId", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "union", - "types": [ - 4, - 8, - 10, - 12, - 14, - 24 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "isError", - "type": 5, - "optional": true - }, - { - "name": "toolCallId", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 6, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 13, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 15, - "optional": false - }, - { - "name": "type", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 16, - "optional": false - }, - { - "name": "height", - "type": 16, - "optional": false - }, - { - "name": "mediaType", - "type": 17, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": true - }, - { - "name": "originalDimensions", - "type": 22, - "optional": true - }, - { - "name": "width", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 18, - 19, - 20, - 21 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 16, - "optional": false - }, - { - "name": "width", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 25, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 16, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "literal", - "value": 2 - } - ] - }, - "names": [ - "event:team/message/queued.data" - ], - "sources": [ - "packages/experimental/agent-team/src/types.ts:225" - ] - }, { "digest": "616f93a5047de668e1c30d57b1786abd76beeb4123599637ec1dc5290320ede5", "schema": { @@ -38831,7 +40942,7 @@ "sources": [] }, { - "digest": "6326a7ecfed005f3d54b737ab9d702da4daddb75cfc635d9096e9abb4788024e", + "digest": "646c2f1d243d3a78c5bc9305786fb340f5209c48a97e65f2a186cc5195491f3f", "schema": { "root": 0, "nodes": [ @@ -38839,55 +40950,59 @@ "kind": "object", "properties": [ { - "name": "block", + "name": "data", "type": 1, "optional": false }, { - "name": "index", - "type": 15, + "name": "ignorable", + "type": 18, + "optional": true + }, + { + "name": "seq", + "type": 11, + "optional": false + }, + { + "name": "time", + "type": 11, "optional": false }, { "name": "type", - "type": 26, + "type": 90, "optional": false } ], "indices": [] }, - { - "kind": "union", - "types": [ - 2, - 7, - 9, - 11, - 13, - 23 - ] - }, { "kind": "object", "properties": [ { - "name": "content", - "type": 3, + "name": "inserted", + "type": 2, "optional": false }, { - "name": "isError", - "type": 4, + "name": "outcome", + "type": 86, "optional": true }, { - "name": "toolCallId", - "type": 5, + "name": "removedCount", + "type": 11, + "optional": true + }, + { + "name": "start", + "type": 11, "optional": false }, { - "name": "type", - "type": 6, + "name": "target", + "type": 87, "optional": false } ], @@ -38895,26 +41010,65 @@ }, { "kind": "array", - "element": 1 - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-result" + "element": 3 }, { "kind": "object", "properties": [ { - "name": "text", - "type": 5, + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "role", + "type": 30, + "optional": false + }, + { + "name": "source", + "type": 31, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 9, + 20, + 23, + 26, + 28 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, "optional": false }, { @@ -38926,54 +41080,8 @@ "indices": [] }, { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 5, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 12, - "optional": false - } - ], - "indices": [] + "kind": "primitive", + "type": "string" }, { "kind": "literal", @@ -38984,7 +41092,122 @@ "properties": [ { "name": "attachment", - "type": 14, + "type": 10, + "optional": false + }, + { + "name": "offloaded", + "type": 18, + "optional": true + }, + { + "name": "type", + "type": 19, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 11, + "optional": false + }, + { + "name": "height", + "type": 11, + "optional": false + }, + { + "name": "mediaType", + "type": 12, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 17, + "optional": true + }, + { + "name": "width", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 13, + 14, + 15, + 16 + ] + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 11, + "optional": false + }, + { + "name": "width", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 21, "optional": false }, { @@ -39000,82 +41223,17 @@ "properties": [ { "name": "attachmentId", - "type": 5, + "type": 7, "optional": false }, { "name": "bytes", - "type": 15, - "optional": false - }, - { - "name": "height", - "type": 15, - "optional": false - }, - { - "name": "mediaType", - "type": 16, + "type": 11, "optional": false }, { "name": "name", - "type": 5, - "optional": true - }, - { - "name": "originalDimensions", - "type": 21, - "optional": true - }, - { - "name": "width", - "type": 15, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 17, - 18, - 19, - 20 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 15, - "optional": false - }, - { - "name": "width", - "type": 15, + "type": 7, "optional": false } ], @@ -39083,14 +41241,24 @@ }, { "kind": "literal", - "value": "image" + "value": "file" }, { "kind": "object", "properties": [ { - "name": "attachment", + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "isError", "type": 24, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, "optional": false }, { @@ -39101,22 +41269,25 @@ ], "indices": [] }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, { "kind": "object", "properties": [ { - "name": "attachmentId", - "type": 5, + "name": "text", + "type": 7, "optional": false }, { - "name": "bytes", - "type": 15, - "optional": false - }, - { - "name": "name", - "type": 5, + "name": "type", + "type": 27, "optional": false } ], @@ -39124,20 +41295,807 @@ }, { "kind": "literal", - "value": "file" + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 29, + "optional": false + } + ], + "indices": [] }, { "kind": "literal", - "value": "block-end" + "value": "text" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 32, + 41, + 43, + 44, + 47, + 52, + 54, + 59, + 60, + 61, + 63, + 65, + 66, + 73, + 75, + 77, + 79, + 81, + 84, + 85 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 18, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 7, + "optional": true + }, + { + "name": "changes", + "type": 33, + "optional": false + }, + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 34 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 35, + "optional": false + }, + { + "name": "digest", + "type": 7, + "optional": true + }, + { + "name": "path", + "type": 7, + "optional": false + }, + { + "name": "scope", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 36, + 37, + 38 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 7, + "optional": true + }, + { + "name": "kind", + "type": 30, + "optional": false + }, + { + "name": "rpcId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 7, + "optional": false + }, + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "ruleId", + "type": 7, + "optional": false + }, + { + "name": "source", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 48, + "optional": false + }, + { + "name": "form", + "type": 50, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + }, + { + "name": "update", + "type": 18, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 49 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 55, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + }, + { + "name": "sections", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 58 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 64, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 50, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 62, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + }, + { + "name": "references", + "type": 68, + "optional": false + }, + { + "name": "version", + "type": 72, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 69 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 11, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 70, + "optional": false + }, + { + "name": "compacted", + "type": 24, + "optional": false + }, + { + "name": "inputIndex", + "type": 11, + "optional": false + }, + { + "name": "label", + "type": 7, + "optional": false + }, + { + "name": "omittedBytes", + "type": 11, + "optional": false + }, + { + "name": "omittedMessages", + "type": 11, + "optional": false + }, + { + "name": "originalMessages", + "type": 11, + "optional": false + }, + { + "name": "retainedMessages", + "type": 11, + "optional": false + }, + { + "name": "sessionId", + "type": 7, + "optional": false + }, + { + "name": "truncated", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 11, + 71 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 74, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 64, + "optional": false + }, + { + "name": "kind", + "type": 76, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "revision", + "type": 11, + "optional": false + }, + { + "name": "round", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "messageId", + "type": 7, + "optional": false + }, + { + "name": "senderId", + "type": 7, + "optional": false + }, + { + "name": "senderName", + "type": 7, + "optional": false + }, + { + "name": "teamId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "model", + "type": 7, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "replayState", + "type": 83, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "plugin", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "canceled" + }, + { + "kind": "union", + "types": [ + 88, + 89 + ] + }, + { + "kind": "literal", + "value": "next-step" + }, + { + "kind": "literal", + "value": "next-turn" + }, + { + "kind": "literal", + "value": "agent/inbox/spliced" } ] }, "names": [ - "event:assistant/attempt.data.stream[0][3].chunk[4]" + "event:agent/inbox/spliced" ], - "sources": [ - "packages/llm/llm/src/types.ts:395" - ] + "sources": [] }, { "digest": "647cb2e3aeb58e27d1d4d60011f208bb66dce257e8654193c533b47c4c22c253", @@ -39214,7 +42172,7 @@ "event:assistant/attempt.data.stream[0][3].chunk[0]" ], "sources": [ - "packages/llm/llm/src/types.ts:391" + "packages/llm/llm/src/types.ts:425" ] }, { @@ -39314,79 +42272,6 @@ "packages/subagent/subagent/src/descriptor.ts:72" ] }, - { - "digest": "64cfc4dbd3739f0ee040689482cb27058890f1d6113abb1de756836318aeebbc", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 1, - "optional": false - }, - { - "name": "kind", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 2, - "optional": false - }, - { - "name": "message", - "type": 2, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 3, - "optional": true - }, - { - "name": "requestId", - "type": 2, - "optional": true - }, - { - "name": "status", - "type": 3, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "aborted" - } - ] - }, - "names": [ - "event:assistant/attempt.data.stream[0][3].chunk[6].reason[0]" - ], - "sources": [ - "packages/llm/llm/src/types.ts:134" - ] - }, { "digest": "658d895d187b26d83f3b53109a3bf77bbfba9f6f9a6d4bc4ee4315b4eb36239e", "schema": { @@ -39737,6 +42622,723 @@ "names": [], "sources": [] }, + { + "digest": "693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 1, + "optional": false + }, + { + "name": "kind", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 2, + "optional": false + }, + { + "name": "message", + "type": 2, + "optional": false + }, + { + "name": "offloadImages", + "type": 3, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 3, + "optional": true + }, + { + "name": "requestId", + "type": 2, + "optional": true + }, + { + "name": "status", + "type": 3, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "error" + } + ] + }, + "names": [ + "event:assistant/attempt.data.stream[0][3].chunk[6].reason[1]" + ], + "sources": [ + "packages/llm/llm/src/types.ts:148" + ] + }, + { + "digest": "69becfb6b2d3fd5da91518089454cae8ef33f1835637ec44dde35dd077fd4bae", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 17, + "optional": true + }, + { + "name": "seq", + "type": 10, + "optional": false + }, + { + "name": "sourceEventSeqs", + "type": 47, + "optional": true + }, + { + "name": "surfaceOp", + "type": 48, + "optional": false + }, + { + "name": "time", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "message", + "type": 2, + "optional": false + }, + { + "name": "step", + "type": 10, + "optional": false + }, + { + "name": "turn", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "role", + "type": 29, + "optional": false + }, + { + "name": "source", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 5, + 8, + 19, + 22, + 25, + 27 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 9, + "optional": false + }, + { + "name": "offloaded", + "type": 17, + "optional": true + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 10, + "optional": false + }, + { + "name": "height", + "type": 10, + "optional": false + }, + { + "name": "mediaType", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": true + }, + { + "name": "originalDimensions", + "type": 16, + "optional": true + }, + { + "name": "width", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 12, + 13, + 14, + 15 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 10, + "optional": false + }, + { + "name": "width", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 20, + "optional": false + }, + { + "name": "type", + "type": 21, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "isError", + "type": 23, + "optional": true + }, + { + "name": "toolCallId", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "union", + "types": [ + 31, + 36, + 38, + 40, + 42, + 44, + 46 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 32, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + }, + { + "name": "sections", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 35 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "text", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 37, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + }, + { + "name": "summary", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 45, + "optional": false + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 10 + }, + { + "kind": "union", + "types": [ + 49, + 51 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 10, + "optional": false + }, + { + "name": "op", + "type": 50, + "optional": false + }, + { + "name": "startSeq", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "literal", + "value": "system/message" + } + ] + }, + "names": [ + "event:system/message" + ], + "sources": [] + }, { "digest": "69d238a6e9b08d67f601b1825962963d8d3523cb69ebf6208c697dc5d058c199", "schema": { @@ -40316,1124 +43918,6 @@ "names": [], "sources": [] }, - { - "digest": "7115142af1bff1ad9b52c3c785109bf5c989e77e3ddfe899014e213731352042", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "inserted", - "type": 1, - "optional": false - }, - { - "name": "outcome", - "type": 85, - "optional": true - }, - { - "name": "removedCount", - "type": 10, - "optional": true - }, - { - "name": "start", - "type": 10, - "optional": false - }, - { - "name": "target", - "type": 86, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 3, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "role", - "type": 28, - "optional": false - }, - { - "name": "source", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 4 - }, - { - "kind": "union", - "types": [ - 5, - 8, - 18, - 21, - 24, - 26 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 6, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 17, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 10, - "optional": false - }, - { - "name": "height", - "type": 10, - "optional": false - }, - { - "name": "mediaType", - "type": 11, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": true - }, - { - "name": "originalDimensions", - "type": 16, - "optional": true - }, - { - "name": "width", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 12, - 13, - 14, - 15 - ] - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 10, - "optional": false - }, - { - "name": "width", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 19, - "optional": false - }, - { - "name": "type", - "type": 20, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 10, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 3, - "optional": false - }, - { - "name": "isError", - "type": 22, - "optional": true - }, - { - "name": "toolCallId", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "union", - "types": [ - 30, - 40, - 42, - 43, - 46, - 51, - 53, - 58, - 59, - 60, - 62, - 64, - 65, - 72, - 74, - 76, - 78, - 80, - 83, - 84 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "baseline", - "type": 31, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 6, - "optional": true - }, - { - "name": "changes", - "type": 32, - "optional": false - }, - { - "name": "form", - "type": 38, - "optional": false - }, - { - "name": "kind", - "type": 39, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 33 - }, - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 34, - "optional": false - }, - { - "name": "digest", - "type": 6, - "optional": true - }, - { - "name": "path", - "type": 6, - "optional": false - }, - { - "name": "scope", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 35, - 36, - 37 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 6, - "optional": false - }, - { - "name": "kind", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "object", - "properties": [ - { - "name": "clientTimeZone", - "type": 6, - "optional": true - }, - { - "name": "kind", - "type": 28, - "optional": false - }, - { - "name": "rpcId", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 6, - "optional": false - }, - { - "name": "form", - "type": 44, - "optional": false - }, - { - "name": "kind", - "type": 45, - "optional": false - }, - { - "name": "provider", - "type": 6, - "optional": false - }, - { - "name": "ruleId", - "type": 6, - "optional": false - }, - { - "name": "source", - "type": 6, - "optional": false - }, - { - "name": "summary", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - }, - { - "kind": "object", - "properties": [ - { - "name": "entries", - "type": 47, - "optional": false - }, - { - "name": "form", - "type": 49, - "optional": false - }, - { - "name": "kind", - "type": 50, - "optional": false - }, - { - "name": "update", - "type": 31, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 48 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 38, - "optional": false - }, - { - "name": "kind", - "type": 52, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 54, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - }, - { - "name": "sections", - "type": 56, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 57 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 6, - "optional": false - }, - { - "name": "text", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 44, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - }, - { - "name": "summary", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 38, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 61, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 63, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 49, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 61, - "optional": false - }, - { - "name": "kind", - "type": 66, - "optional": false - }, - { - "name": "references", - "type": 67, - "optional": false - }, - { - "name": "version", - "type": 71, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 68 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 10, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 69, - "optional": false - }, - { - "name": "compacted", - "type": 22, - "optional": false - }, - { - "name": "inputIndex", - "type": 10, - "optional": false - }, - { - "name": "label", - "type": 6, - "optional": false - }, - { - "name": "omittedBytes", - "type": 10, - "optional": false - }, - { - "name": "omittedMessages", - "type": 10, - "optional": false - }, - { - "name": "originalMessages", - "type": 10, - "optional": false - }, - { - "name": "retainedMessages", - "type": 10, - "optional": false - }, - { - "name": "sessionId", - "type": 6, - "optional": false - }, - { - "name": "truncated", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 10, - 70 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 44, - "optional": false - }, - { - "name": "kind", - "type": 73, - "optional": false - }, - { - "name": "senderSessionId", - "type": 6, - "optional": false - }, - { - "name": "summary", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 63, - "optional": false - }, - { - "name": "kind", - "type": 75, - "optional": false - }, - { - "name": "senderSessionId", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 6, - "optional": false - }, - { - "name": "kind", - "type": 77, - "optional": false - }, - { - "name": "revision", - "type": 10, - "optional": false - }, - { - "name": "round", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 79, - "optional": false - }, - { - "name": "messageId", - "type": 6, - "optional": false - }, - { - "name": "senderId", - "type": 6, - "optional": false - }, - { - "name": "senderName", - "type": 6, - "optional": false - }, - { - "name": "teamId", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 81, - "optional": false - }, - { - "name": "model", - "type": 6, - "optional": false - }, - { - "name": "provider", - "type": 6, - "optional": false - }, - { - "name": "replayState", - "type": 82, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "plugin", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 28, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "canceled" - }, - { - "kind": "union", - "types": [ - 87, - 88 - ] - }, - { - "kind": "literal", - "value": "next-step" - }, - { - "kind": "literal", - "value": "next-turn" - } - ] - }, - "names": [ - "event:agent/inbox/spliced.data" - ], - "sources": [ - "packages/core/agent/src/types.ts:87" - ] - }, { "digest": "7180844e1066230c5e891f1b9ccd942423b0fe48aac6ded7eef2a285b288a732", "schema": { @@ -41680,7 +44164,7 @@ "packages/llm/llm/src/types.ts#ContentBlockType" ], "sources": [ - "packages/llm/llm/src/types.ts:122" + "packages/llm/llm/src/types.ts:135" ] }, { @@ -41982,9520 +44466,7 @@ "sources": [] }, { - "digest": "7973d82bd129b261f16a9a20d44df834ec60efc52a63242b4943a2515ae0894f", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "role", - "type": 26, - "optional": false - }, - { - "name": "source", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "union", - "types": [ - 3, - 6, - 16, - 19, - 22, - 24 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 4, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 15, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 8, - "optional": false - }, - { - "name": "height", - "type": 8, - "optional": false - }, - { - "name": "mediaType", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": true - }, - { - "name": "originalDimensions", - "type": 14, - "optional": true - }, - { - "name": "width", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 10, - 11, - 12, - 13 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 8, - "optional": false - }, - { - "name": "width", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 17, - "optional": false - }, - { - "name": "type", - "type": 18, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 8, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "isError", - "type": 20, - "optional": true - }, - { - "name": "toolCallId", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "system" - }, - { - "kind": "union", - "types": [ - 28, - 33, - 35, - 37, - 39, - 41, - 43 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 29, - "optional": false - }, - { - "name": "kind", - "type": 30, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - }, - { - "name": "sections", - "type": 31, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 32 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 4, - "optional": false - }, - { - "name": "text", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 34, - "optional": false - }, - { - "name": "kind", - "type": 30, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - }, - { - "name": "summary", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 36, - "optional": false - }, - { - "name": "kind", - "type": 30, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 38, - "optional": false - }, - { - "name": "kind", - "type": 30, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 40, - "optional": false - }, - { - "name": "kind", - "type": 30, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 42, - "optional": false - }, - { - "name": "kind", - "type": 30, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 30, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - } - ] - }, - "names": [ - "packages/llm/llm/src/message.ts#SystemMessage" - ], - "sources": [ - "packages/llm/llm/src/message.ts:158" - ] - }, - { - "digest": "79ddc907312f97cf9553d4841e8b3fc937097bd8d97900a790efad3c05255e99", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "todos", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 3, - "optional": false - }, - { - "name": "status", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "union", - "types": [ - 5, - 6, - 7 - ] - }, - { - "kind": "literal", - "value": "completed" - }, - { - "kind": "literal", - "value": "in_progress" - }, - { - "kind": "literal", - "value": "pending" - } - ] - }, - "names": [ - "event:todo/write.data" - ], - "sources": [ - "packages/todo/tool-todo/src/types.ts:31" - ] - }, - { - "digest": "7a651f3b672c52fb615f22c0ce9cb3cc5b22aad92e6bbeefb0808fd7b9233a50", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "session/title" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "7af85bf70d4eafce63e739adae38ea85501e68df3a0e22f97f3a0be90fd9e4cb", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "ignorable", - "type": 1, - "optional": true - }, - { - "name": "seq", - "type": 2, - "optional": false - }, - { - "name": "surfaceOp", - "type": 3, - "optional": false - }, - { - "name": "time", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 4, - 5 - ] - }, - { - "kind": "literal", - "value": "append" - }, - { - "kind": "object", - "properties": [ - { - "name": "endSeq", - "type": 2, - "optional": false - }, - { - "name": "op", - "type": 6, - "optional": false - }, - { - "name": "startSeq", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "SessionEventEnvelope[6]" - ], - "sources": [] - }, - { - "digest": "7c6129b6961cf4b2a030f36c78d2b0a5be38617651a9b81322e054ee512bee06", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "messageId", - "type": 1, - "optional": false - }, - { - "name": "sessionId", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "packages/feedback/message-feedback/src/types.ts#MessageFeedbackDelete" - ], - "sources": [ - "packages/feedback/message-feedback/src/types.ts:46" - ] - }, - { - "digest": "7cf11fcc829f21289bb7311562c6601499368464cce8ee678f80500e6a448b01", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 36, - "optional": true - }, - { - "name": "seq", - "type": 3, - "optional": false - }, - { - "name": "time", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 102, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "request", - "type": 2, - "optional": false - }, - { - "name": "responseFormat", - "type": 97, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "maxTokens", - "type": 3, - "optional": true - }, - { - "name": "messages", - "type": 4, - "optional": false - }, - { - "name": "model", - "type": 9, - "optional": false - }, - { - "name": "provider", - "type": 9, - "optional": false - }, - { - "name": "purpose", - "type": 90, - "optional": true - }, - { - "name": "reasoningEffort", - "type": 9, - "optional": true - }, - { - "name": "sessionId", - "type": 9, - "optional": true - }, - { - "name": "stop", - "type": 93, - "optional": true - }, - { - "name": "system", - "type": 9, - "optional": true - }, - { - "name": "temperature", - "type": 3, - "optional": true - }, - { - "name": "tools", - "type": 94, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 5 - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 6, - "optional": false - }, - { - "name": "id", - "type": 9, - "optional": false - }, - { - "name": "role", - "type": 30, - "optional": false - }, - { - "name": "source", - "type": 34, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 7 - }, - { - "kind": "union", - "types": [ - 8, - 11, - 20, - 23, - 26, - 28 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 9, - "optional": false - }, - { - "name": "id", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 12, - "optional": false - }, - { - "name": "type", - "type": 19, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 9, - "optional": false - }, - { - "name": "bytes", - "type": 3, - "optional": false - }, - { - "name": "height", - "type": 3, - "optional": false - }, - { - "name": "mediaType", - "type": 13, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": true - }, - { - "name": "originalDimensions", - "type": 18, - "optional": true - }, - { - "name": "width", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 14, - 15, - 16, - 17 - ] - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 3, - "optional": false - }, - { - "name": "width", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 21, - "optional": false - }, - { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 9, - "optional": false - }, - { - "name": "bytes", - "type": 3, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 6, - "optional": false - }, - { - "name": "isError", - "type": 24, - "optional": true - }, - { - "name": "toolCallId", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 9, - "optional": false - }, - { - "name": "type", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "union", - "types": [ - 31, - 32, - 33 - ] - }, - { - "kind": "literal", - "value": "assistant" - }, - { - "kind": "literal", - "value": "system" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "union", - "types": [ - 35, - 45, - 47, - 48, - 51, - 56, - 58, - 63, - 64, - 65, - 67, - 69, - 70, - 77, - 79, - 81, - 83, - 85, - 88, - 89 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "baseline", - "type": 36, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 9, - "optional": true - }, - { - "name": "changes", - "type": 37, - "optional": false - }, - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 44, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 38 - }, - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 39, - "optional": false - }, - { - "name": "digest", - "type": 9, - "optional": true - }, - { - "name": "path", - "type": 9, - "optional": false - }, - { - "name": "scope", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 40, - 41, - 42 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 9, - "optional": false - }, - { - "name": "kind", - "type": 46, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "object", - "properties": [ - { - "name": "clientTimeZone", - "type": 9, - "optional": true - }, - { - "name": "kind", - "type": 33, - "optional": false - }, - { - "name": "rpcId", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 9, - "optional": false - }, - { - "name": "form", - "type": 49, - "optional": false - }, - { - "name": "kind", - "type": 50, - "optional": false - }, - { - "name": "provider", - "type": 9, - "optional": false - }, - { - "name": "ruleId", - "type": 9, - "optional": false - }, - { - "name": "source", - "type": 9, - "optional": false - }, - { - "name": "summary", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - }, - { - "kind": "object", - "properties": [ - { - "name": "entries", - "type": 52, - "optional": false - }, - { - "name": "form", - "type": 54, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "update", - "type": 36, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 53 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 59, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - }, - { - "name": "sections", - "type": 61, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 62 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 9, - "optional": false - }, - { - "name": "text", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 49, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - }, - { - "name": "summary", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 68, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 54, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 71, - "optional": false - }, - { - "name": "references", - "type": 72, - "optional": false - }, - { - "name": "version", - "type": 76, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 73 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 3, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 74, - "optional": false - }, - { - "name": "compacted", - "type": 24, - "optional": false - }, - { - "name": "inputIndex", - "type": 3, - "optional": false - }, - { - "name": "label", - "type": 9, - "optional": false - }, - { - "name": "omittedBytes", - "type": 3, - "optional": false - }, - { - "name": "omittedMessages", - "type": 3, - "optional": false - }, - { - "name": "originalMessages", - "type": 3, - "optional": false - }, - { - "name": "retainedMessages", - "type": 3, - "optional": false - }, - { - "name": "sessionId", - "type": 9, - "optional": false - }, - { - "name": "truncated", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 3, - 75 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 49, - "optional": false - }, - { - "name": "kind", - "type": 78, - "optional": false - }, - { - "name": "senderSessionId", - "type": 9, - "optional": false - }, - { - "name": "summary", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 68, - "optional": false - }, - { - "name": "kind", - "type": 80, - "optional": false - }, - { - "name": "senderSessionId", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 9, - "optional": false - }, - { - "name": "kind", - "type": 82, - "optional": false - }, - { - "name": "revision", - "type": 3, - "optional": false - }, - { - "name": "round", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 84, - "optional": false - }, - { - "name": "messageId", - "type": 9, - "optional": false - }, - { - "name": "senderId", - "type": 9, - "optional": false - }, - { - "name": "senderName", - "type": 9, - "optional": false - }, - { - "name": "teamId", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 86, - "optional": false - }, - { - "name": "model", - "type": 9, - "optional": false - }, - { - "name": "provider", - "type": 9, - "optional": false - }, - { - "name": "replayState", - "type": 87, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "plugin", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 33, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 91, - 92 - ] - }, - { - "kind": "literal", - "value": "session-title" - }, - { - "kind": "literal", - "value": "compaction" - }, - { - "kind": "array", - "element": 9 - }, - { - "kind": "array", - "element": 95 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 9, - "optional": false - }, - { - "name": "parameters", - "type": 96, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [], - "indices": [ - { - "key": 9, - "value": 87 - } - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 9, - "optional": true - }, - { - "name": "name", - "type": 9, - "optional": false - }, - { - "name": "schema", - "type": 98, - "optional": false - }, - { - "name": "type", - "type": 101, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 3, - 9, - 24, - 75, - 99, - 100 - ] - }, - { - "kind": "object", - "properties": [], - "indices": [ - { - "key": 9, - "value": 98 - } - ] - }, - { - "kind": "array", - "element": 98 - }, - { - "kind": "literal", - "value": "json_schema" - }, - { - "kind": "literal", - "value": "browser-use/stagehand-llm-request" - } - ] - }, - "names": [ - "event:browser-use/stagehand-llm-request" - ], - "sources": [] - }, - { - "digest": "7d226b9fd95b4ba213d4237c22e930a91c3c769c95ca7f92977b9d625ba13438", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "union", - "types": [ - 1, - 8 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "cleared", - "type": 2, - "optional": false - }, - { - "name": "clearedAt", - "type": 4, - "optional": false - }, - { - "name": "kind", - "type": 5, - "optional": false - }, - { - "name": "operation", - "type": 6, - "optional": false - }, - { - "name": "version", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "id", - "type": 3, - "optional": false - }, - { - "name": "revision", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "goal/change" - }, - { - "kind": "literal", - "value": "clear" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "createdAt", - "type": 4, - "optional": false - }, - { - "name": "goal", - "type": 9, - "optional": false - }, - { - "name": "kind", - "type": 5, - "optional": false - }, - { - "name": "operation", - "type": 16, - "optional": false - }, - { - "name": "roundsStarted", - "type": 4, - "optional": false - }, - { - "name": "updatedAt", - "type": 4, - "optional": false - }, - { - "name": "version", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "blockedReason", - "type": 10, - "optional": true - }, - { - "name": "id", - "type": 3, - "optional": false - }, - { - "name": "maxGoalRounds", - "type": 4, - "optional": false - }, - { - "name": "objective", - "type": 3, - "optional": false - }, - { - "name": "phase", - "type": 11, - "optional": false - }, - { - "name": "revision", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 3, - "optional": false - }, - { - "name": "message", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 12, - 13, - 14, - 15 - ] - }, - { - "kind": "literal", - "value": "active" - }, - { - "kind": "literal", - "value": "blocked" - }, - { - "kind": "literal", - "value": "complete" - }, - { - "kind": "literal", - "value": "paused" - }, - { - "kind": "union", - "types": [ - 14, - 17, - 18, - 19, - 20, - 21 - ] - }, - { - "kind": "literal", - "value": "block" - }, - { - "kind": "literal", - "value": "create" - }, - { - "kind": "literal", - "value": "edit" - }, - { - "kind": "literal", - "value": "resume" - }, - { - "kind": "literal", - "value": "pause" - } - ] - }, - "names": [ - "packages/goal/goal/src/domain.ts#GoalChangeMeta" - ], - "sources": [ - "packages/goal/goal/src/domain.ts:44" - ] - }, - { - "digest": "7d54a5a5e629e2e7783bfac72683bcc5e969e9f43426b9289826aaf294ff6344", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "category", - "type": 1, - "optional": true - }, - { - "name": "text", - "type": 9, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ] - }, - { - "kind": "literal", - "value": "instruction-following" - }, - { - "kind": "literal", - "value": "other" - }, - { - "kind": "literal", - "value": "product-interaction" - }, - { - "kind": "literal", - "value": "resource-cost" - }, - { - "kind": "literal", - "value": "security-privacy-permission" - }, - { - "kind": "literal", - "value": "service-stability" - }, - { - "kind": "literal", - "value": "task-result" - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "packages/feedback/command-feedback/src/types.ts#FeedbackRecord" - ], - "sources": [ - "packages/feedback/command-feedback/src/types.ts:27" - ] - }, - { - "digest": "7f13860ee89a2d35ce09f958e85a04566cd59cb1de1d73a9619a9b631961dc71", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "session-reference" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "7f5f2a8618c2ece530a18b1358b890fb57fb8c35705e47b66bff2296f8c6e889", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 1, - "optional": false - }, - { - "name": "name", - "type": 1, - "optional": false - }, - { - "name": "parameters", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "object", - "properties": [], - "indices": [ - { - "key": 1, - "value": 3 - } - ] - }, - { - "kind": "opaque", - "reason": "unknown" - } - ] - }, - "names": [ - "packages/llm/llm/src/types.ts#ToolSchema" - ], - "sources": [ - "packages/llm/llm/src/types.ts:411" - ] - }, - { - "digest": "7f7fd5a6b0064f597534b29ff62ef26e786dffccf5e14f654a7d4fcea2c35f04", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 5, - "optional": true - }, - { - "name": "seq", - "type": 3, - "optional": false - }, - { - "name": "time", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "shadowedRange", - "type": 2, - "optional": false - }, - { - "name": "shadowedSeqs", - "type": 4, - "optional": false - }, - { - "name": "shadowedTokenCount", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "end", - "type": 3, - "optional": false - }, - { - "name": "start", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "literal", - "value": "compaction/prune" - } - ] - }, - "names": [ - "event:compaction/prune" - ], - "sources": [] - }, - { - "digest": "7f9365bae9fcb73e1e34061a1acfe0422c1a5fd351848cace9bebbe806f28462", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "runId", - "type": 1, - "optional": false - }, - { - "name": "stopReason", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "union", - "types": [ - 3, - 4, - 5 - ] - }, - { - "kind": "literal", - "value": "cancelled" - }, - { - "kind": "literal", - "value": "completed" - }, - { - "kind": "literal", - "value": "error" - } - ] - }, - "names": [ - "packages/workflow/tool-workflow/src/types.ts#ToolWorkflowRunEndData" - ], - "sources": [ - "packages/workflow/tool-workflow/src/types.ts:36" - ] - }, - { - "digest": "7f959241c24128b4983b559984811861dc59774b85905c8fa57c6cb0148a3dbc", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 1, - "optional": false - }, - { - "name": "kind", - "type": 2, - "optional": false - }, - { - "name": "revision", - "type": 3, - "optional": false - }, - { - "name": "round", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "primitive", - "type": "number" - } - ] - }, - "names": [ - "packages/goal/goal/src/domain.ts#GoalMessageSource" - ], - "sources": [ - "packages/goal/goal/src/domain.ts:47" - ] - }, - { - "digest": "811229eedcec57979dbadca7dbea832a59f8580cff30cb217fca5e2da16314bd", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "allowed-once" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "reasoning" - } - ] - }, - "names": [ - "packages/llm/llm/src/types.ts#ReasoningBlock" - ], - "sources": [ - "packages/llm/llm/src/types.ts:60" - ] - }, - { - "digest": "82470020b5bfcb1a843e2aa05602af7d37df99e82d01f015c7c589ba054fd89d", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "agent-message" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "829a2fb3be6efd4ba36aef8ed2d381a4c6fb2a0aaf50c823a6f3f8290786b809", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "agent/inbox/spliced" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "83079c8a3f733ac3fa603eefa0fbb4737ca4d8b69e7e2e7298b60f9ac4098693", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 1, - "optional": false - }, - { - "name": "name", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "event:agent/inbox/spliced.data.inserted[0].source[17].entries[0]" - ], - "sources": [ - "packages/skill/tool-skill/src/index.ts:40" - ] - }, - { - "digest": "83da72c6857601c75da11212ef847f57d457b9c99f0a82246dcdff3e26700239", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 1, - "optional": false - }, - { - "name": "digest", - "type": 5, - "optional": true - }, - { - "name": "path", - "type": 5, - "optional": false - }, - { - "name": "scope", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 2, - 3, - 4 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "packages/context/agent-instructions/src/render.ts#AgentInstructionChange" - ], - "sources": [ - "packages/context/agent-instructions/src/render.ts:47" - ] - }, - { - "digest": "8465ece5e9d9007c038b357015d849c15262cb0bc25abc59e47692d87ee01a15", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "union", - "types": [ - 1, - 2, - 3 - ] - }, - { - "kind": "literal", - "value": "cancelled" - }, - { - "kind": "literal", - "value": "completed" - }, - { - "kind": "literal", - "value": "error" - } - ] - }, - "names": [ - "packages/workflow/workflow/src/types.ts#WorkflowStopReason" - ], - "sources": [ - "packages/workflow/workflow/src/types.ts:63" - ] - }, - { - "digest": "84ac8c1a66ffc697262e181f42e851259eae14038c113ca329fb4700ccdb1f7c", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 1, - "optional": false - }, - { - "name": "message", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "packages/goal/goal/src/types.ts#GoalBlockReason" - ], - "sources": [ - "packages/goal/goal/src/types.ts:52" - ] - }, - { - "digest": "84c24f1209fc3e0153de6ac85d58fd6968b851f955e0e761b92321053956609e", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 29, - "optional": true - }, - { - "name": "seq", - "type": 14, - "optional": false - }, - { - "name": "time", - "type": 14, - "optional": false - }, - { - "name": "type", - "type": 30, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "reason", - "type": 2, - "optional": false - }, - { - "name": "turn", - "type": 14, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 3, - 5, - 7, - 9, - 11, - 16 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "blocked" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "completed" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "interrupted" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "error", - "type": 12, - "optional": false - }, - { - "name": "kind", - "type": 15, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 13, - "optional": false - }, - { - "name": "message", - "type": 13, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 14, - "optional": true - }, - { - "name": "requestId", - "type": 13, - "optional": true - }, - { - "name": "status", - "type": 14, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 17, - "optional": false - }, - { - "name": "reason", - "type": 18, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "union", - "types": [ - 19, - 21, - 23, - 25, - 27 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 20, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "disposed" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "legacy" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "parent" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 28, - "optional": false - }, - { - "name": "reason", - "type": 13, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "hook" - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "literal", - "value": "turn/end" - } - ] - }, - "names": [ - "event:turn/end" - ], - "sources": [] - }, - { - "digest": "85e1bd563d6c80c1f377f99f91103fec4e315148a7330f3b3dae7c2d04e33b01", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 1, - "optional": false - }, - { - "name": "kind", - "type": 2, - "optional": false - }, - { - "name": "plugin", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "event:agent/inbox/spliced.data.inserted[0].source[7]", - "event:system/message.data.message.source[6]" - ], - "sources": [] - }, - { - "digest": "86560bf0779bb2d93975405fe895d9d42c1020ab49fbbd2a06e1b3c74fbbc257", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "array", - "element": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "role", - "type": 27, - "optional": false - }, - { - "name": "source", - "type": 28, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "union", - "types": [ - 4, - 7, - 17, - 20, - 23, - 25 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 5, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 8, - "optional": false - }, - { - "name": "type", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 9, - "optional": false - }, - { - "name": "height", - "type": 9, - "optional": false - }, - { - "name": "mediaType", - "type": 10, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": true - }, - { - "name": "originalDimensions", - "type": 15, - "optional": true - }, - { - "name": "width", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 11, - 12, - 13, - 14 - ] - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 9, - "optional": false - }, - { - "name": "width", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 18, - "optional": false - }, - { - "name": "type", - "type": 19, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "isError", - "type": 21, - "optional": true - }, - { - "name": "toolCallId", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "union", - "types": [ - 29, - 39, - 41, - 42, - 45, - 50, - 52, - 57, - 58, - 59, - 61, - 63, - 64, - 71, - 73, - 75, - 77, - 79, - 82, - 83 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "baseline", - "type": 30, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 5, - "optional": true - }, - { - "name": "changes", - "type": 31, - "optional": false - }, - { - "name": "form", - "type": 37, - "optional": false - }, - { - "name": "kind", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 32 - }, - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 33, - "optional": false - }, - { - "name": "digest", - "type": 5, - "optional": true - }, - { - "name": "path", - "type": 5, - "optional": false - }, - { - "name": "scope", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 34, - 35, - 36 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 5, - "optional": false - }, - { - "name": "kind", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "object", - "properties": [ - { - "name": "clientTimeZone", - "type": 5, - "optional": true - }, - { - "name": "kind", - "type": 27, - "optional": false - }, - { - "name": "rpcId", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 5, - "optional": false - }, - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 44, - "optional": false - }, - { - "name": "provider", - "type": 5, - "optional": false - }, - { - "name": "ruleId", - "type": 5, - "optional": false - }, - { - "name": "source", - "type": 5, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - }, - { - "kind": "object", - "properties": [ - { - "name": "entries", - "type": 46, - "optional": false - }, - { - "name": "form", - "type": 48, - "optional": false - }, - { - "name": "kind", - "type": 49, - "optional": false - }, - { - "name": "update", - "type": 30, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 47 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 37, - "optional": false - }, - { - "name": "kind", - "type": 51, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 53, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - }, - { - "name": "sections", - "type": 55, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 56 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "text", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 37, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 60, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 62, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 48, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 60, - "optional": false - }, - { - "name": "kind", - "type": 65, - "optional": false - }, - { - "name": "references", - "type": 66, - "optional": false - }, - { - "name": "version", - "type": 70, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 67 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 9, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 68, - "optional": false - }, - { - "name": "compacted", - "type": 21, - "optional": false - }, - { - "name": "inputIndex", - "type": 9, - "optional": false - }, - { - "name": "label", - "type": 5, - "optional": false - }, - { - "name": "omittedBytes", - "type": 9, - "optional": false - }, - { - "name": "omittedMessages", - "type": 9, - "optional": false - }, - { - "name": "originalMessages", - "type": 9, - "optional": false - }, - { - "name": "retainedMessages", - "type": 9, - "optional": false - }, - { - "name": "sessionId", - "type": 5, - "optional": false - }, - { - "name": "truncated", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 9, - 69 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 72, - "optional": false - }, - { - "name": "senderSessionId", - "type": 5, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 62, - "optional": false - }, - { - "name": "kind", - "type": 74, - "optional": false - }, - { - "name": "senderSessionId", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 5, - "optional": false - }, - { - "name": "kind", - "type": 76, - "optional": false - }, - { - "name": "revision", - "type": 9, - "optional": false - }, - { - "name": "round", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 78, - "optional": false - }, - { - "name": "messageId", - "type": 5, - "optional": false - }, - { - "name": "senderId", - "type": 5, - "optional": false - }, - { - "name": "senderName", - "type": 5, - "optional": false - }, - { - "name": "teamId", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 80, - "optional": false - }, - { - "name": "model", - "type": 5, - "optional": false - }, - { - "name": "provider", - "type": 5, - "optional": false - }, - { - "name": "replayState", - "type": 81, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 27, - "optional": false - } - ], - "indices": [] - } - ] - }, - "names": [ - "event:agent/inbox/spliced.data.inserted" - ], - "sources": [] - }, - { - "digest": "86755151e6c4694ce5ae9511313e1e50f9fb6ead49c67d0a93c056e8650396c4", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "array", - "element": 1 - }, - { - "kind": "union", - "types": [ - 2, - 8, - 65, - 67 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "args", - "type": 3, - "optional": false - }, - { - "name": "dt", - "type": 5, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "index", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": true - }, - { - "name": "time0", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 4 - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "array", - "element": 6 - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "tool-call-chunks" - }, - { - "kind": "object", - "properties": [ - { - "name": "chunk", - "type": 9, - "optional": false - }, - { - "name": "time", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 64, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 10, - 12, - 37, - 40, - 42, - 44, - 61 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "argumentsDelta", - "type": 4, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "index", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": true - }, - { - "name": "type", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "block", - "type": 13, - "optional": false - }, - { - "name": "index", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 36, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 14, - 16, - 25, - 28, - 32, - 34 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 4, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 15, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 17, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 6, - "optional": false - }, - { - "name": "height", - "type": 6, - "optional": false - }, - { - "name": "mediaType", - "type": 18, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": true - }, - { - "name": "originalDimensions", - "type": 23, - "optional": true - }, - { - "name": "width", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 19, - 20, - 21, - 22 - ] - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 6, - "optional": false - }, - { - "name": "width", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 26, - "optional": false - }, - { - "name": "type", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 29, - "optional": false - }, - { - "name": "isError", - "type": 30, - "optional": true - }, - { - "name": "toolCallId", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 31, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 13 - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 33, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 35, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "block-end" - }, - { - "kind": "object", - "properties": [ - { - "name": "blockType", - "type": 38, - "optional": false - }, - { - "name": "index", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 39, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 15, - 24, - 27, - 31, - 33, - 35 - ] - }, - { - "kind": "literal", - "value": "block-start" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 6, - "optional": false - }, - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 6, - "optional": false - }, - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "reason", - "type": 45, - "optional": false - }, - { - "name": "replayState", - "type": 57, - "optional": true - }, - { - "name": "type", - "type": 60, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 46, - 49, - 51, - 53, - 55 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 47, - "optional": false - }, - { - "name": "kind", - "type": 48, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 4, - "optional": false - }, - { - "name": "message", - "type": 4, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 6, - "optional": true - }, - { - "name": "requestId", - "type": 4, - "optional": true - }, - { - "name": "status", - "type": 6, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 47, - "optional": false - }, - { - "name": "kind", - "type": 50, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 52, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 54, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "stop" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 56, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-calls" - }, - { - "kind": "object", - "properties": [ - { - "name": "blocks", - "type": 58, - "optional": true - }, - { - "name": "response", - "type": 59, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 59 - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "literal", - "value": "finish" - }, - { - "kind": "object", - "properties": [ - { - "name": "type", - "type": 62, - "optional": false - }, - { - "name": "usage", - "type": 63, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "usage" - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 6, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 6, - "optional": true - }, - { - "name": "inputTokens", - "type": 6, - "optional": false - }, - { - "name": "outputTokens", - "type": 6, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 6, - "optional": true - }, - { - "name": "totalTokens", - "type": 6, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "chunk" - }, - { - "kind": "object", - "properties": [ - { - "name": "dt", - "type": 5, - "optional": false - }, - { - "name": "index", - "type": 6, - "optional": false - }, - { - "name": "texts", - "type": 3, - "optional": false - }, - { - "name": "time0", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 66, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-chunks" - }, - { - "kind": "object", - "properties": [ - { - "name": "dt", - "type": 5, - "optional": false - }, - { - "name": "index", - "type": 6, - "optional": false - }, - { - "name": "texts", - "type": 3, - "optional": false - }, - { - "name": "time0", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 68, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-chunks" - } - ] - }, - "names": [ - "event:assistant/attempt.data.stream", - "event:assistant/message.data.stream" - ], - "sources": [] - }, - { - "digest": "86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "union", - "types": [ - 1, - 7 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "agentModel", - "type": 2, - "optional": true - }, - { - "name": "agentProvider", - "type": 2, - "optional": true - }, - { - "name": "agentReasoningEffort", - "type": 2, - "optional": true - }, - { - "name": "label", - "type": 2, - "optional": false - }, - { - "name": "mode", - "type": 3, - "optional": false - }, - { - "name": "persona", - "type": 2, - "optional": true - }, - { - "name": "provider", - "type": 2, - "optional": false - }, - { - "name": "toolFilter", - "type": 4, - "optional": true - }, - { - "name": "version", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "continuable" - }, - { - "kind": "object", - "properties": [ - { - "name": "allow", - "type": 5, - "optional": true - }, - { - "name": "deny", - "type": 5, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "object", - "properties": [ - { - "name": "label", - "type": 2, - "optional": true - }, - { - "name": "mode", - "type": 8, - "optional": false - }, - { - "name": "provider", - "type": 2, - "optional": false - }, - { - "name": "version", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "one-shot" - } - ] - }, - "names": [ - "packages/subagent/subagent/src/descriptor.ts#SubagentDescriptorData" - ], - "sources": [ - "packages/subagent/subagent/src/descriptor.ts:89" - ] - }, - { - "digest": "86e1e384495ee09ac2d60c9db3610f1f1fddac2d248f6f1d6000e78054680219", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 1, - "optional": false - }, - { - "name": "text", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "packages/llm/llm/src/message.ts#ContextSnapshotSection" - ], - "sources": [ - "packages/llm/llm/src/message.ts:65" - ] - }, - { - "digest": "871bfd6f7bb91061246768d3377f3fd58d6b8ea7c31f44c714ae7ab215650d62", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "provisioning" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "8725743fcd3db5ddaf3e0aed550cd5d95785d8e30c9d4cbec11b7c90198d3603", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "step", - "type": 1, - "optional": false - }, - { - "name": "stream", - "type": 2, - "optional": false - }, - { - "name": "turn", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "union", - "types": [ - 4, - 9, - 66, - 68 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "args", - "type": 5, - "optional": false - }, - { - "name": "dt", - "type": 7, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "index", - "type": 1, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": true - }, - { - "name": "time0", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 6 - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "array", - "element": 1 - }, - { - "kind": "literal", - "value": "tool-call-chunks" - }, - { - "kind": "object", - "properties": [ - { - "name": "chunk", - "type": 10, - "optional": false - }, - { - "name": "time", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 65, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 11, - 13, - 38, - 41, - 43, - 45, - 62 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "argumentsDelta", - "type": 6, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "index", - "type": 1, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": true - }, - { - "name": "type", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "block", - "type": 14, - "optional": false - }, - { - "name": "index", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 37, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 15, - 17, - 26, - 29, - 33, - 35 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 6, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 18, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 1, - "optional": false - }, - { - "name": "height", - "type": 1, - "optional": false - }, - { - "name": "mediaType", - "type": 19, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": true - }, - { - "name": "originalDimensions", - "type": 24, - "optional": true - }, - { - "name": "width", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 20, - 21, - 22, - 23 - ] - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 1, - "optional": false - }, - { - "name": "width", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 27, - "optional": false - }, - { - "name": "type", - "type": 28, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 1, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 30, - "optional": false - }, - { - "name": "isError", - "type": 31, - "optional": true - }, - { - "name": "toolCallId", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 32, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 14 - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 34, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 36, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "block-end" - }, - { - "kind": "object", - "properties": [ - { - "name": "blockType", - "type": 39, - "optional": false - }, - { - "name": "index", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 16, - 25, - 28, - 32, - 34, - 36 - ] - }, - { - "kind": "literal", - "value": "block-start" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 1, - "optional": false - }, - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 1, - "optional": false - }, - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 44, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "reason", - "type": 46, - "optional": false - }, - { - "name": "replayState", - "type": 58, - "optional": true - }, - { - "name": "type", - "type": 61, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 47, - 50, - 52, - 54, - 56 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 48, - "optional": false - }, - { - "name": "kind", - "type": 49, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 6, - "optional": false - }, - { - "name": "message", - "type": 6, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 1, - "optional": true - }, - { - "name": "requestId", - "type": 6, - "optional": true - }, - { - "name": "status", - "type": 1, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 48, - "optional": false - }, - { - "name": "kind", - "type": 51, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 53, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 55, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "stop" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 57, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-calls" - }, - { - "kind": "object", - "properties": [ - { - "name": "blocks", - "type": 59, - "optional": true - }, - { - "name": "response", - "type": 60, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 60 - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "literal", - "value": "finish" - }, - { - "kind": "object", - "properties": [ - { - "name": "type", - "type": 63, - "optional": false - }, - { - "name": "usage", - "type": 64, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "usage" - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 1, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 1, - "optional": true - }, - { - "name": "inputTokens", - "type": 1, - "optional": false - }, - { - "name": "outputTokens", - "type": 1, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 1, - "optional": true - }, - { - "name": "totalTokens", - "type": 1, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "chunk" - }, - { - "kind": "object", - "properties": [ - { - "name": "dt", - "type": 7, - "optional": false - }, - { - "name": "index", - "type": 1, - "optional": false - }, - { - "name": "texts", - "type": 5, - "optional": false - }, - { - "name": "time0", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 67, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-chunks" - }, - { - "kind": "object", - "properties": [ - { - "name": "dt", - "type": 7, - "optional": false - }, - { - "name": "index", - "type": 1, - "optional": false - }, - { - "name": "texts", - "type": 5, - "optional": false - }, - { - "name": "time0", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 69, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-chunks" - } - ] - }, - "names": [ - "event:assistant/attempt.data" - ], - "sources": [ - "packages/core/session/src/types.ts:335" - ] - }, - { - "digest": "88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "argumentsDelta", - "type": 1, - "optional": false - }, - { - "name": "id", - "type": 1, - "optional": false - }, - { - "name": "index", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 1, - "optional": true - }, - { - "name": "type", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "tool-call-delta" - } - ] - }, - "names": [ - "event:assistant/attempt.data.stream[0][3].chunk[3]" - ], - "sources": [ - "packages/llm/llm/src/types.ts:394" - ] - }, - { - "digest": "8a6e1ec9e8db346b0e02f027db73c07a94f067a26d40c1aef1abd09c47ce7ba0", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 7, - "optional": true - }, - { - "name": "seq", - "type": 6, - "optional": false - }, - { - "name": "time", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "dialect", - "type": 2, - "optional": false - }, - { - "name": "handlerId", - "type": 5, - "optional": false - }, - { - "name": "matcher", - "type": 5, - "optional": true - }, - { - "name": "point", - "type": 5, - "optional": false - }, - { - "name": "turn", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 3, - 4 - ] - }, - { - "kind": "literal", - "value": "claude-code" - }, - { - "kind": "literal", - "value": "codex" - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "literal", - "value": "hook/invoked" - } - ] - }, - "names": [ - "event:hook/invoked" - ], - "sources": [] - }, - { - "digest": "8a9c990773f798eac10ca8878e6bcc531ee080bc589cac956730e462cc6a09ad", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "preset", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "event:permission/preset.data" - ], - "sources": [ - "packages/interaction/permission-presets/src/index.ts:57" - ] - }, - { - "digest": "8ad45ad053b4d89f8865c17cc9ff3b8748e86045f3f0fb816269a34af2619141", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "decision", - "type": 1, - "optional": false - }, - { - "name": "durationMs", - "type": 2, - "optional": false - }, - { - "name": "exitCode", - "type": 2, - "optional": true - }, - { - "name": "handlerId", - "type": 1, - "optional": false - }, - { - "name": "point", - "type": 1, - "optional": false - }, - { - "name": "stderrSummary", - "type": 1, - "optional": true - }, - { - "name": "turn", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - } - ] - }, - "names": [ - "event:hook/result.data" - ], - "sources": [ - "packages/hooks/hook-protocol/src/types.ts:31" - ] - }, - { - "digest": "8af841fc9b265ade3441caf9fe40dd24db24226befba48150e6d0eeccc1389c3", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "system" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "sessionFormatVersion", - "type": 1, - "optional": true - }, - { - "name": "sessionId", - "type": 2, - "optional": false - }, - { - "name": "throughSeq", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "event:session-log-deepseek/delivery-accepted.data" - ], - "sources": [ - "packages/session/session-log-deepseek/src/types.ts:81" - ] - }, - { - "digest": "8cdd3fdd295cee53035de16a1b23411716f75e9f994fe9510cac5d7fef605c76", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "command/done" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "id", - "type": 1, - "optional": false - }, - { - "name": "operation", - "type": 2, - "optional": false - }, - { - "name": "version", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "dispatch" - }, - { - "kind": "literal", - "value": 1 - } - ] - }, - "names": [ - "packages/schedule/schedule/src/types.ts#OneShotScheduleDispatchChange" - ], - "sources": [ - "packages/schedule/schedule/src/types.ts:86" - ] - }, - { - "digest": "8ea13bcd983eab596dbfd41c5c2b62a0b318b71cd2a987a6fd752c6ced7e4489", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 4, - "optional": true - }, - { - "name": "seq", - "type": 6, - "optional": false - }, - { - "name": "time", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 18, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "header", - "type": 2, - "optional": false - }, - { - "name": "reason", - "type": 13, - "optional": false - }, - { - "name": "startsSeries", - "type": 4, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "adapterDefaults", - "type": 3, - "optional": true - }, - { - "name": "config", - "type": 5, - "optional": false - }, - { - "name": "tools", - "type": 9, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "maxTokens", - "type": 4, - "optional": true - }, - { - "name": "reasoningEffort", - "type": 4, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "object", - "properties": [ - { - "name": "maxTokens", - "type": 6, - "optional": true - }, - { - "name": "model", - "type": 7, - "optional": false - }, - { - "name": "provider", - "type": 7, - "optional": false - }, - { - "name": "reasoningEffort", - "type": 7, - "optional": true - }, - { - "name": "stop", - "type": 8, - "optional": true - }, - { - "name": "temperature", - "type": 6, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "array", - "element": 7 - }, - { - "kind": "array", - "element": 10 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 7, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "parameters", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [], - "indices": [ - { - "key": 7, - "value": 12 - } - ] - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "union", - "types": [ - 14, - 15, - 16, - 17 - ] - }, - { - "kind": "literal", - "value": "initial" - }, - { - "kind": "literal", - "value": "resume" - }, - { - "kind": "literal", - "value": "series" - }, - { - "kind": "literal", - "value": "change" - }, - { - "kind": "literal", - "value": "request/header" - } - ] - }, - "names": [ - "event:request/header" - ], - "sources": [] - }, - { - "digest": "8ef06dd236128246969d5f04a6c4a639d031fcda99a0cd13051f52f41b0ca1af", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 2, - "optional": false - }, - { - "name": "bytes", - "type": 3, - "optional": false - }, - { - "name": "height", - "type": 3, - "optional": false - }, - { - "name": "mediaType", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": true - }, - { - "name": "originalDimensions", - "type": 9, - "optional": true - }, - { - "name": "width", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 5, - 6, - 7, - 8 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 3, - "optional": false - }, - { - "name": "width", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - } - ] - }, - "names": [ - "packages/llm/llm/src/types.ts#ImageBlock" - ], - "sources": [ - "packages/llm/llm/src/types.ts:71" - ] - }, - { - "digest": "8f6d609794bf5afc01d7ccf3e03811d32f4b8047f58a2c9a98db0154c2ad7cb9", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "tuple", - "elements": [ - { - "type": 1, - "optional": false, - "rest": false - } - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "text" - } - ] - }, - "names": [ - "event:web/deepseek-search-llm-request.data.body.messages[0].content" - ], - "sources": [] - }, - { - "digest": "904bf499d98be4a3a5f836fcac533b6c4899569b018071e6604e1d32657d00fd", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "union", - "types": [ - 1, - 2, - 3 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - } - ] - }, - "names": [ - "event:agent/inbox/spliced.data.inserted[0].source[16].changes[0].action" - ], - "sources": [] - }, - { - "digest": "9096d1dd86a3da77a593e12cd95e596b2ef965fa94fcbee40e4bdcf0433924a1", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "delayMs", - "type": 1, - "optional": false - }, - { - "name": "failure", - "type": 2, - "optional": false - }, - { - "name": "maxRetries", - "type": 1, - "optional": false - }, - { - "name": "mode", - "type": 4, - "optional": false - }, - { - "name": "policyKey", - "type": 3, - "optional": false - }, - { - "name": "provider", - "type": 3, - "optional": false - }, - { - "name": "retry", - "type": 1, - "optional": false - }, - { - "name": "retryId", - "type": 3, - "optional": false - }, - { - "name": "step", - "type": 1, - "optional": false - }, - { - "name": "turn", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 3, - "optional": false - }, - { - "name": "message", - "type": 3, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 1, - "optional": true - }, - { - "name": "requestId", - "type": 3, - "optional": true - }, - { - "name": "status", - "type": 1, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "normal" - } - ] - }, - "names": [ - "event:llm/retry.data[0]" - ], - "sources": [ - "packages/llm/llm-retry/src/types.ts:17" - ] - }, - { - "digest": "91c397f8f870e812e1dc5ac69c2745f9f105be9dc97dadc980d19ef415a65145", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 9, - "optional": true - }, - { - "name": "seq", - "type": 3, - "optional": false - }, - { - "name": "time", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 2, - 7 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "delayMs", - "type": 3, - "optional": false - }, - { - "name": "failure", - "type": 4, - "optional": false - }, - { - "name": "maxRetries", - "type": 3, - "optional": false - }, - { - "name": "mode", - "type": 6, - "optional": false - }, - { - "name": "policyKey", - "type": 5, - "optional": false - }, - { - "name": "provider", - "type": 5, - "optional": false - }, - { - "name": "retry", - "type": 3, - "optional": false - }, - { - "name": "retryId", - "type": 5, - "optional": false - }, - { - "name": "step", - "type": 3, - "optional": false - }, - { - "name": "turn", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 5, - "optional": false - }, - { - "name": "message", - "type": 5, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 3, - "optional": true - }, - { - "name": "requestId", - "type": 5, - "optional": true - }, - { - "name": "status", - "type": 3, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "normal" - }, - { - "kind": "object", - "properties": [ - { - "name": "delayMs", - "type": 3, - "optional": false - }, - { - "name": "failure", - "type": 4, - "optional": false - }, - { - "name": "mode", - "type": 8, - "optional": false - }, - { - "name": "policyKey", - "type": 5, - "optional": false - }, - { - "name": "provider", - "type": 5, - "optional": false - }, - { - "name": "retry", - "type": 3, - "optional": false - }, - { - "name": "retryId", - "type": 5, - "optional": false - }, - { - "name": "step", - "type": 3, - "optional": false - }, - { - "name": "turn", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "always" - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "literal", - "value": "llm/retry" - } - ] - }, - "names": [ - "event:llm/retry" - ], - "sources": [] - }, - { - "digest": "930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "max_tokens", - "type": 1, - "optional": false - }, - { - "name": "messages", - "type": 2, - "optional": false - }, - { - "name": "model", - "type": 6, - "optional": false - }, - { - "name": "tools", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "tuple", - "elements": [ - { - "type": 3, - "optional": false, - "rest": false - } - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 4, - "optional": false - }, - { - "name": "role", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "tuple", - "elements": [ - { - "type": 5, - "optional": false, - "rest": false - } - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "tuple", - "elements": [ - { - "type": 10, - "optional": false, - "rest": false - } - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "max_uses", - "type": 1, - "optional": false - }, - { - "name": "name", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "web_search" - }, - { - "kind": "literal", - "value": "web_search_20250305" - } - ] - }, - "names": [ - "event:web/deepseek-search-llm-request.data.body" - ], - "sources": [ - "packages/web/web-search-deepseek/src/provider.ts:61" - ] - }, - { - "digest": "930c1cd289def119d443e19dd90008916bf035f0a4d23e07241e7f050f2b89b7", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "union", - "types": [ - 1, - 2, - 3, - 4 - ] - }, - { - "kind": "literal", - "value": "allowed-once" - }, - { - "kind": "literal", - "value": "cancelled" - }, - { - "kind": "literal", - "value": "rejected" - }, - { - "kind": "literal", - "value": "unavailable" - } - ] - }, - "names": [ - "packages/interaction/user-approval/src/types.ts#ApprovalOutcome" - ], - "sources": [ - "packages/interaction/user-approval/src/types.ts:32" - ] - }, - { - "digest": "935c8d486292b13c87b57ca280eb8c58b5b2f5239be86fcd4334c33fdeb87682", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "web_search" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "938f7c7357bc403ea3b281a9dd016939df7a0880fc8db12612585be574d0d615", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "primitive", - "type": "null" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "93c33d9687613293f8c95d46c4d922fe9ceae83b84c384beff5c3315abe005f2", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "array", - "element": 1 - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "event:assistant/attempt.data.stream[0][0].texts", - "event:assistant/attempt.data.stream[0][1].texts", - "event:assistant/attempt.data.stream[0][2].args", - "event:browser-use/stagehand-llm-request.data.request.stop", - "event:request/header.data.header.config.stop", - "event:subagent/descriptor.data[1].toolFilter.allow", - "event:subagent/descriptor.data[1].toolFilter.deny", - "event:team/task.data.task.blockedBy", - "event:team/task.data.task.writeScopes" - ], - "sources": [] - }, - { - "digest": "93cbbc5a7fd0b1aa1ff1b9a6bb5ec52b073f48e5a5200365c45c0cd54059e1b9", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "id", - "type": 1, - "optional": false - }, - { - "name": "outcome", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "union", - "types": [ - 3, - 4, - 5, - 6 - ] - }, - { - "kind": "literal", - "value": "allowed-once" - }, - { - "kind": "literal", - "value": "cancelled" - }, - { - "kind": "literal", - "value": "rejected" - }, - { - "kind": "literal", - "value": "unavailable" - } - ] - }, - "names": [ - "event:approval/decided.data" - ], - "sources": [ - "packages/interaction/user-approval/src/types.ts:55" - ] - }, - { - "digest": "93d38436347f67c9022aacbc4472e283d722bcab8eb73fc376aaf7b6a6b9fb35", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": 0 - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "94c56aeb1564ad735eb773d88636b40761d063efa1610f1308004d32923f5378", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "in_progress" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 1, - "optional": false - }, - { - "name": "text", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "text-delta" - } - ] - }, - "names": [ - "event:assistant/attempt.data.stream[0][3].chunk[1]" - ], - "sources": [ - "packages/llm/llm/src/types.ts:392" - ] - }, - { - "digest": "95c0e88fcf0bafb2087f912ad9e2369de6ae6530cc0c4c2772c96205b57f7f60", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "assistant" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "966fad765834f62051ad6f6a100f7ea38abe5f40f7ef3ae0b6dca0e31c7549a5", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "role", - "type": 27, - "optional": false - }, - { - "name": "source", - "type": 28, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "tuple", - "elements": [ - { - "type": 2, - "optional": false, - "rest": false - } - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 3, - "optional": false - }, - { - "name": "isError", - "type": 25, - "optional": true - }, - { - "name": "toolCallId", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 4 - }, - { - "kind": "union", - "types": [ - 2, - 5, - 8, - 10, - 12, - 22 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 6, - "optional": false - }, - { - "name": "id", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - }, - { - "name": "type", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 13, - "optional": false - }, - { - "name": "type", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 14, - "optional": false - }, - { - "name": "height", - "type": 14, - "optional": false - }, - { - "name": "mediaType", - "type": 15, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": true - }, - { - "name": "originalDimensions", - "type": 20, - "optional": true - }, - { - "name": "width", - "type": 14, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 16, - 17, - 18, - 19 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 14, - "optional": false - }, - { - "name": "width", - "type": 14, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 23, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 14, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 6, - "optional": false - }, - { - "name": "kind", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - } - ] - }, - "names": [ - "packages/llm/llm/src/message.ts#ToolResultMessage" - ], - "sources": [ - "packages/llm/llm/src/message.ts:164" - ] - }, - { - "digest": "96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "union", - "types": [ - 1, - 2 - ] - }, - { - "kind": "literal", - "value": "next-step" - }, - { - "kind": "literal", - "value": "next-turn" - } - ] - }, - "names": [ - "packages/core/agent/src/types.ts#InboxTarget" - ], - "sources": [ - "packages/core/agent/src/types.ts:30" - ] - }, - { - "digest": "9890113e97dd3541787bfe6704f2b223d3a890b5325ca0acae2b0398909b413e", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "mode", - "type": 1, - "optional": false - }, - { - "name": "source", - "type": 5, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 2, - 3, - 4 - ] - }, - { - "kind": "literal", - "value": "danger-full-access" - }, - { - "kind": "literal", - "value": "read-only" - }, - { - "kind": "literal", - "value": "workspace-write" - }, - { - "kind": "literal", - "value": "delegation" - } - ] - }, - "names": [ - "event:sandbox/mode.data" - ], - "sources": [ - "packages/sandbox/sandbox-policy/src/session-mode.ts:33" - ] - }, - { - "digest": "989145d2b165a1e4391d0691196ede9dec5093b09f1cf3d33b1911c24ee6c5ee", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "union", - "types": [ - 1, - 5, - 30, - 33, - 35, - 37, - 54 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "argumentsDelta", - "type": 2, - "optional": false - }, - { - "name": "id", - "type": 2, - "optional": false - }, - { - "name": "index", - "type": 3, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": true - }, - { - "name": "type", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "tool-call-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "block", - "type": 6, - "optional": false - }, - { - "name": "index", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 7, - 9, - 18, - 21, - 25, - 27 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 2, - "optional": false - }, - { - "name": "id", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 10, - "optional": false - }, - { - "name": "type", - "type": 17, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 2, - "optional": false - }, - { - "name": "bytes", - "type": 3, - "optional": false - }, - { - "name": "height", - "type": 3, - "optional": false - }, - { - "name": "mediaType", - "type": 11, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": true - }, - { - "name": "originalDimensions", - "type": 16, - "optional": true - }, - { - "name": "width", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 12, - 13, - 14, - 15 - ] - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 3, - "optional": false - }, - { - "name": "width", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 19, - "optional": false - }, - { - "name": "type", - "type": 20, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 2, - "optional": false - }, - { - "name": "bytes", - "type": 3, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 22, - "optional": false - }, - { - "name": "isError", - "type": 23, - "optional": true - }, - { - "name": "toolCallId", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 6 - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 28, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "block-end" - }, - { - "kind": "object", - "properties": [ - { - "name": "blockType", - "type": 31, - "optional": false - }, - { - "name": "index", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 32, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 8, - 17, - 20, - 24, - 26, - 28 - ] - }, - { - "kind": "literal", - "value": "block-start" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 3, - "optional": false - }, - { - "name": "text", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 34, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 3, - "optional": false - }, - { - "name": "text", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 36, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "reason", - "type": 38, - "optional": false - }, - { - "name": "replayState", - "type": 50, - "optional": true - }, - { - "name": "type", - "type": 53, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 39, - 42, - 44, - 46, - 48 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 40, - "optional": false - }, - { - "name": "kind", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 2, - "optional": false - }, - { - "name": "message", - "type": 2, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 3, - "optional": true - }, - { - "name": "requestId", - "type": 2, - "optional": true - }, - { - "name": "status", - "type": 3, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 40, - "optional": false - }, - { - "name": "kind", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 45, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 47, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "stop" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 49, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-calls" - }, - { - "kind": "object", - "properties": [ - { - "name": "blocks", - "type": 51, - "optional": true - }, - { - "name": "response", - "type": 52, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 52 - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "literal", - "value": "finish" - }, - { - "kind": "object", - "properties": [ - { - "name": "type", - "type": 55, - "optional": false - }, - { - "name": "usage", - "type": 56, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "usage" - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 3, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 3, - "optional": true - }, - { - "name": "inputTokens", - "type": 3, - "optional": false - }, - { - "name": "outputTokens", - "type": 3, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 3, - "optional": true - }, - { - "name": "totalTokens", - "type": 3, - "optional": true - } - ], - "indices": [] - } - ] - }, - "names": [ - "packages/llm/llm/src/types.ts#StreamChunk" - ], - "sources": [ - "packages/llm/llm/src/types.ts:390" - ] - }, - { - "digest": "998f22585a5d73ec005365e203f07fc3b3a029e82323fdda3fd7f13b0a9639d0", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "ignorable", - "type": 1, - "optional": true - }, - { - "name": "seq", - "type": 2, - "optional": false - }, - { - "name": "sourceEventSeqs", - "type": 3, - "optional": true - }, - { - "name": "surfaceOp", - "type": 4, - "optional": false - }, - { - "name": "time", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "union", - "types": [ - 5, - 6 - ] - }, - { - "kind": "literal", - "value": "append" - }, - { - "kind": "object", - "properties": [ - { - "name": "endSeq", - "type": 2, - "optional": false - }, - { - "name": "op", - "type": 7, - "optional": false - }, - { - "name": "startSeq", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "SessionEventEnvelope[40]", - "SessionEventEnvelope[53]", - "SessionEventEnvelope[56]" - ], - "sources": [] - }, - { - "digest": "9a2a9029f8d7ede05336980d8342737557f487b913bfd28853d0ab5214600ab5", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "role", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "tuple", - "elements": [ - { - "type": 2, - "optional": false, - "rest": false - } - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "user" - } - ] - }, - "names": [ - "event:web/deepseek-search-llm-request.data.body.messages[0]" - ], - "sources": [ - "packages/web/web-search-deepseek/src/provider.ts:64" - ] - }, - { - "digest": "9a410ddf56792b2b7cd4a224a2e54dc4dee30a8e8e6b1c2b9a1d19992b217adc", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "childId", - "type": 1, - "optional": false - }, - { - "name": "label", - "type": 1, - "optional": false - }, - { - "name": "phase", - "type": 1, - "optional": true - }, - { - "name": "runId", - "type": 1, - "optional": false - }, - { - "name": "seq", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - } - ] - }, - "names": [ - "packages/workflow/tool-workflow/src/types.ts#ToolWorkflowAgentStartData" - ], - "sources": [ - "packages/workflow/tool-workflow/src/types.ts:20" - ] - }, - { - "digest": "9b4fd974c124c83743efe030ba4e1250e087505f3f74588716a521fb8d453267", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "tool-call-delta" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "9bdd77f2b39d9ae47be71e494eca8c9e950b0e99b89a4539f86c4a0e7d1398b6", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "isError", - "type": 23, - "optional": true - }, - { - "name": "toolCallId", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "union", - "types": [ - 0, - 3, - 6, - 8, - 10, - 20 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 4, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 19, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 12, - "optional": false - }, - { - "name": "height", - "type": 12, - "optional": false - }, - { - "name": "mediaType", - "type": 13, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": true - }, - { - "name": "originalDimensions", - "type": 18, - "optional": true - }, - { - "name": "width", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 14, - 15, - 16, - 17 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 12, - "optional": false - }, - { - "name": "width", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 21, - "optional": false - }, - { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 12, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - } - ] - }, - "names": [ - "packages/llm/llm/src/types.ts#ToolResultBlock" - ], - "sources": [ - "packages/llm/llm/src/types.ts:101" - ] - }, - { - "digest": "9d80129e13edfb6feb03ed327e6085c6ef60b97ad074244b2f5fd4667424a6a4", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "other" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "9e07693fd7cd9babe0688c22c986e4d519ae1c0e40eeea13b2dc583bed2abc86", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "ask" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "9e41386b3a0c9572b0d63078492ebb3997da7d3a830d44e0259418bd02f4bcb2", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "end", - "type": 1, - "optional": false - }, - { - "name": "start", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - } - ] - }, - "names": [ - "event:compaction/prune.data.shadowedRange", - "event:compaction/summary.data[0].shadowedRange", - "event:compaction/summary.data[1].shadowedRange" - ], - "sources": [ - "packages/compaction/compaction/src/types.ts:38", - "packages/compaction/compaction/src/types.ts:84" - ] - }, - { - "digest": "9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 1, - "optional": false - }, - { - "name": "form", - "type": 2, - "optional": false - }, - { - "name": "kind", - "type": 3, - "optional": false - }, - { - "name": "provider", - "type": 1, - "optional": false - }, - { - "name": "ruleId", - "type": 1, - "optional": false - }, - { - "name": "source", - "type": 1, - "optional": false - }, - { - "name": "summary", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - } - ] - }, - "names": [ - "event:agent/inbox/spliced.data.inserted[0].source[19]" - ], - "sources": [ - "packages/webhook/webhook/src/types.ts:74" - ] - }, - { - "digest": "a0f906856fc06f1c6f2083eb970e5a657a82b1a84c06c095445e5d6d161cda80", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": 2 - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 3, - "optional": true - }, - { - "name": "seq", - "type": 4, - "optional": false - }, - { - "name": "time", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "agentPreset", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "agent-preset/selected" - } - ] - }, - "names": [ - "event:agent-preset/selected" - ], - "sources": [] - }, - { - "digest": "a13d8f379222ece8a02a86efe4c8334af4f108ecb3034b38c0a42bb3046e6847", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "tool" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "a2418dd9dd938f04a3eaf1f27694b33facfb931b6e495dcfb4d5fcfa2ef82b28", + "digest": "77b8491f946edfc8cc77ebae3ba49183759da31ba2dfa83d4c9e9e618c4b91d2", "schema": { "root": 0, "nodes": [ @@ -51622,6 +44593,11 @@ "type": 9, "optional": false }, + { + "name": "offloaded", + "type": 1, + "optional": true + }, { "name": "type", "type": 17, @@ -52178,6 +45154,11 @@ "type": 6, "optional": false }, + { + "name": "offloadImages", + "type": 10, + "optional": true + }, { "name": "providerRetryAfterMs", "type": 10, @@ -52428,6 +45409,7984 @@ "packages/core/session/src/types.ts:321" ] }, + { + "digest": "79650cfa10aa3c52440328d85e8a25f0ffadc8d7efd784e3c0819d8bb74a1985", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "error", + "type": 1, + "optional": false + }, + { + "name": "kind", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 2, + "optional": false + }, + { + "name": "message", + "type": 2, + "optional": false + }, + { + "name": "offloadImages", + "type": 3, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 3, + "optional": true + }, + { + "name": "requestId", + "type": 2, + "optional": true + }, + { + "name": "status", + "type": 3, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "error" + } + ] + }, + "names": [ + "event:turn/end.data.reason[3]" + ], + "sources": [ + "packages/core/session/src/types.ts:211" + ] + }, + { + "digest": "79ddc907312f97cf9553d4841e8b3fc937097bd8d97900a790efad3c05255e99", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "todos", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "status", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "union", + "types": [ + 5, + 6, + 7 + ] + }, + { + "kind": "literal", + "value": "completed" + }, + { + "kind": "literal", + "value": "in_progress" + }, + { + "kind": "literal", + "value": "pending" + } + ] + }, + "names": [ + "event:todo/write.data" + ], + "sources": [ + "packages/todo/tool-todo/src/types.ts:31" + ] + }, + { + "digest": "7a651f3b672c52fb615f22c0ce9cb3cc5b22aad92e6bbeefb0808fd7b9233a50", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "session/title" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "7af85bf70d4eafce63e739adae38ea85501e68df3a0e22f97f3a0be90fd9e4cb", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "ignorable", + "type": 1, + "optional": true + }, + { + "name": "seq", + "type": 2, + "optional": false + }, + { + "name": "surfaceOp", + "type": 3, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 4, + 5 + ] + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 2, + "optional": false + }, + { + "name": "op", + "type": 6, + "optional": false + }, + { + "name": "startSeq", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "SessionEventEnvelope[6]" + ], + "sources": [] + }, + { + "digest": "7c6129b6961cf4b2a030f36c78d2b0a5be38617651a9b81322e054ee512bee06", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "messageId", + "type": 1, + "optional": false + }, + { + "name": "sessionId", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "packages/feedback/message-feedback/src/types.ts#MessageFeedbackDelete" + ], + "sources": [ + "packages/feedback/message-feedback/src/types.ts:46" + ] + }, + { + "digest": "7d226b9fd95b4ba213d4237c22e930a91c3c769c95ca7f92977b9d625ba13438", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "union", + "types": [ + 1, + 8 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "cleared", + "type": 2, + "optional": false + }, + { + "name": "clearedAt", + "type": 4, + "optional": false + }, + { + "name": "kind", + "type": 5, + "optional": false + }, + { + "name": "operation", + "type": 6, + "optional": false + }, + { + "name": "version", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "revision", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "goal/change" + }, + { + "kind": "literal", + "value": "clear" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "createdAt", + "type": 4, + "optional": false + }, + { + "name": "goal", + "type": 9, + "optional": false + }, + { + "name": "kind", + "type": 5, + "optional": false + }, + { + "name": "operation", + "type": 16, + "optional": false + }, + { + "name": "roundsStarted", + "type": 4, + "optional": false + }, + { + "name": "updatedAt", + "type": 4, + "optional": false + }, + { + "name": "version", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "blockedReason", + "type": 10, + "optional": true + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "maxGoalRounds", + "type": 4, + "optional": false + }, + { + "name": "objective", + "type": 3, + "optional": false + }, + { + "name": "phase", + "type": 11, + "optional": false + }, + { + "name": "revision", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 3, + "optional": false + }, + { + "name": "message", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 12, + 13, + 14, + 15 + ] + }, + { + "kind": "literal", + "value": "active" + }, + { + "kind": "literal", + "value": "blocked" + }, + { + "kind": "literal", + "value": "complete" + }, + { + "kind": "literal", + "value": "paused" + }, + { + "kind": "union", + "types": [ + 14, + 17, + 18, + 19, + 20, + 21 + ] + }, + { + "kind": "literal", + "value": "block" + }, + { + "kind": "literal", + "value": "create" + }, + { + "kind": "literal", + "value": "edit" + }, + { + "kind": "literal", + "value": "resume" + }, + { + "kind": "literal", + "value": "pause" + } + ] + }, + "names": [ + "packages/goal/goal/src/domain.ts#GoalChangeMeta" + ], + "sources": [ + "packages/goal/goal/src/domain.ts:44" + ] + }, + { + "digest": "7d54a5a5e629e2e7783bfac72683bcc5e969e9f43426b9289826aaf294ff6344", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "category", + "type": 1, + "optional": true + }, + { + "name": "text", + "type": 9, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + { + "kind": "literal", + "value": "instruction-following" + }, + { + "kind": "literal", + "value": "other" + }, + { + "kind": "literal", + "value": "product-interaction" + }, + { + "kind": "literal", + "value": "resource-cost" + }, + { + "kind": "literal", + "value": "security-privacy-permission" + }, + { + "kind": "literal", + "value": "service-stability" + }, + { + "kind": "literal", + "value": "task-result" + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "packages/feedback/command-feedback/src/types.ts#FeedbackRecord" + ], + "sources": [ + "packages/feedback/command-feedback/src/types.ts:27" + ] + }, + { + "digest": "7f13860ee89a2d35ce09f958e85a04566cd59cb1de1d73a9619a9b631961dc71", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "session-reference" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "7f5f2a8618c2ece530a18b1358b890fb57fb8c35705e47b66bff2296f8c6e889", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 1, + "optional": false + }, + { + "name": "name", + "type": 1, + "optional": false + }, + { + "name": "parameters", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 1, + "value": 3 + } + ] + }, + { + "kind": "opaque", + "reason": "unknown" + } + ] + }, + "names": [ + "packages/llm/llm/src/types.ts#ToolSchema" + ], + "sources": [ + "packages/llm/llm/src/types.ts:445" + ] + }, + { + "digest": "7f7fd5a6b0064f597534b29ff62ef26e786dffccf5e14f654a7d4fcea2c35f04", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 5, + "optional": true + }, + { + "name": "seq", + "type": 3, + "optional": false + }, + { + "name": "time", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "shadowedRange", + "type": 2, + "optional": false + }, + { + "name": "shadowedSeqs", + "type": 4, + "optional": false + }, + { + "name": "shadowedTokenCount", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "end", + "type": 3, + "optional": false + }, + { + "name": "start", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "compaction/prune" + } + ] + }, + "names": [ + "event:compaction/prune" + ], + "sources": [] + }, + { + "digest": "7f9365bae9fcb73e1e34061a1acfe0422c1a5fd351848cace9bebbe806f28462", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "runId", + "type": 1, + "optional": false + }, + { + "name": "stopReason", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "union", + "types": [ + 3, + 4, + 5 + ] + }, + { + "kind": "literal", + "value": "cancelled" + }, + { + "kind": "literal", + "value": "completed" + }, + { + "kind": "literal", + "value": "error" + } + ] + }, + "names": [ + "packages/workflow/tool-workflow/src/types.ts#ToolWorkflowRunEndData" + ], + "sources": [ + "packages/workflow/tool-workflow/src/types.ts:36" + ] + }, + { + "digest": "7f959241c24128b4983b559984811861dc59774b85905c8fa57c6cb0148a3dbc", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 1, + "optional": false + }, + { + "name": "kind", + "type": 2, + "optional": false + }, + { + "name": "revision", + "type": 3, + "optional": false + }, + { + "name": "round", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "primitive", + "type": "number" + } + ] + }, + "names": [ + "packages/goal/goal/src/domain.ts#GoalMessageSource" + ], + "sources": [ + "packages/goal/goal/src/domain.ts:47" + ] + }, + { + "digest": "811229eedcec57979dbadca7dbea832a59f8580cff30cb217fca5e2da16314bd", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "allowed-once" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + } + ] + }, + "names": [ + "packages/llm/llm/src/types.ts#ReasoningBlock" + ], + "sources": [ + "packages/llm/llm/src/types.ts:67" + ] + }, + { + "digest": "82470020b5bfcb1a843e2aa05602af7d37df99e82d01f015c7c589ba054fd89d", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "agent-message" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "829a2fb3be6efd4ba36aef8ed2d381a4c6fb2a0aaf50c823a6f3f8290786b809", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "agent/inbox/spliced" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "83079c8a3f733ac3fa603eefa0fbb4737ca4d8b69e7e2e7298b60f9ac4098693", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 1, + "optional": false + }, + { + "name": "name", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "event:agent/inbox/spliced.data.inserted[0].source[17].entries[0]" + ], + "sources": [ + "packages/skill/tool-skill/src/index.ts:40" + ] + }, + { + "digest": "83da72c6857601c75da11212ef847f57d457b9c99f0a82246dcdff3e26700239", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 1, + "optional": false + }, + { + "name": "digest", + "type": 5, + "optional": true + }, + { + "name": "path", + "type": 5, + "optional": false + }, + { + "name": "scope", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 3, + 4 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "packages/context/agent-instructions/src/render.ts#AgentInstructionChange" + ], + "sources": [ + "packages/context/agent-instructions/src/render.ts:47" + ] + }, + { + "digest": "8465ece5e9d9007c038b357015d849c15262cb0bc25abc59e47692d87ee01a15", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "union", + "types": [ + 1, + 2, + 3 + ] + }, + { + "kind": "literal", + "value": "cancelled" + }, + { + "kind": "literal", + "value": "completed" + }, + { + "kind": "literal", + "value": "error" + } + ] + }, + "names": [ + "packages/workflow/workflow/src/types.ts#WorkflowStopReason" + ], + "sources": [ + "packages/workflow/workflow/src/types.ts:63" + ] + }, + { + "digest": "84ac8c1a66ffc697262e181f42e851259eae14038c113ca329fb4700ccdb1f7c", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 1, + "optional": false + }, + { + "name": "message", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "packages/goal/goal/src/types.ts#GoalBlockReason" + ], + "sources": [ + "packages/goal/goal/src/types.ts:52" + ] + }, + { + "digest": "85e1bd563d6c80c1f377f99f91103fec4e315148a7330f3b3dae7c2d04e33b01", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 1, + "optional": false + }, + { + "name": "kind", + "type": 2, + "optional": false + }, + { + "name": "plugin", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "event:agent/inbox/spliced.data.inserted[0].source[7]", + "event:system/message.data.message.source[6]" + ], + "sources": [] + }, + { + "digest": "86cf1e44038cb8b11659863dd0efafe682e41b9c782265c1d50e0a687debfc60", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "union", + "types": [ + 1, + 7, + 65, + 67 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "args", + "type": 2, + "optional": false + }, + { + "name": "dt", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "index", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": true + }, + { + "name": "time0", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "tool-call-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "chunk", + "type": 8, + "optional": false + }, + { + "name": "time", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 9, + 11, + 37, + 40, + 42, + 44, + 61 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "argumentsDelta", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "index", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": true + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "block", + "type": 12, + "optional": false + }, + { + "name": "index", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 13, + 15, + 25, + 28, + 32, + 34 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 16, + "optional": false + }, + { + "name": "offloaded", + "type": 23, + "optional": true + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 3, + "optional": false + }, + { + "name": "bytes", + "type": 5, + "optional": false + }, + { + "name": "height", + "type": 5, + "optional": false + }, + { + "name": "mediaType", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": true + }, + { + "name": "originalDimensions", + "type": 22, + "optional": true + }, + { + "name": "width", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 18, + 19, + 20, + 21 + ] + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 5, + "optional": false + }, + { + "name": "width", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 26, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 3, + "optional": false + }, + { + "name": "bytes", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 29, + "optional": false + }, + { + "name": "isError", + "type": 30, + "optional": true + }, + { + "name": "toolCallId", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 31, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 12 + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "block-end" + }, + { + "kind": "object", + "properties": [ + { + "name": "blockType", + "type": 38, + "optional": false + }, + { + "name": "index", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 39, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 14, + 24, + 27, + 31, + 33, + 35 + ] + }, + { + "kind": "literal", + "value": "block-start" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 5, + "optional": false + }, + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 41, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 5, + "optional": false + }, + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 45, + "optional": false + }, + { + "name": "replayState", + "type": 57, + "optional": true + }, + { + "name": "type", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 46, + 49, + 51, + 53, + 55 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 47, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 3, + "optional": false + }, + { + "name": "message", + "type": 3, + "optional": false + }, + { + "name": "offloadImages", + "type": 5, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 5, + "optional": true + }, + { + "name": "requestId", + "type": 3, + "optional": true + }, + { + "name": "status", + "type": 5, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 47, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 58, + "optional": true + }, + { + "name": "response", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 59 + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "literal", + "value": "finish" + }, + { + "kind": "object", + "properties": [ + { + "name": "type", + "type": 62, + "optional": false + }, + { + "name": "usage", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "usage" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 5, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 5, + "optional": true + }, + { + "name": "inputTokens", + "type": 5, + "optional": false + }, + { + "name": "outputTokens", + "type": 5, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 5, + "optional": true + }, + { + "name": "totalTokens", + "type": 5, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "chunk" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 4, + "optional": false + }, + { + "name": "index", + "type": 5, + "optional": false + }, + { + "name": "texts", + "type": 2, + "optional": false + }, + { + "name": "time0", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 4, + "optional": false + }, + { + "name": "index", + "type": 5, + "optional": false + }, + { + "name": "texts", + "type": 2, + "optional": false + }, + { + "name": "time0", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-chunks" + } + ] + }, + "names": [ + "packages/llm/llm/src/assistant-stream.ts#AssistantStreamRecord" + ], + "sources": [ + "packages/llm/llm/src/assistant-stream.ts:20" + ] + }, + { + "digest": "86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "union", + "types": [ + 1, + 7 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "agentModel", + "type": 2, + "optional": true + }, + { + "name": "agentProvider", + "type": 2, + "optional": true + }, + { + "name": "agentReasoningEffort", + "type": 2, + "optional": true + }, + { + "name": "label", + "type": 2, + "optional": false + }, + { + "name": "mode", + "type": 3, + "optional": false + }, + { + "name": "persona", + "type": 2, + "optional": true + }, + { + "name": "provider", + "type": 2, + "optional": false + }, + { + "name": "toolFilter", + "type": 4, + "optional": true + }, + { + "name": "version", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "continuable" + }, + { + "kind": "object", + "properties": [ + { + "name": "allow", + "type": 5, + "optional": true + }, + { + "name": "deny", + "type": 5, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "object", + "properties": [ + { + "name": "label", + "type": 2, + "optional": true + }, + { + "name": "mode", + "type": 8, + "optional": false + }, + { + "name": "provider", + "type": 2, + "optional": false + }, + { + "name": "version", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "one-shot" + } + ] + }, + "names": [ + "packages/subagent/subagent/src/descriptor.ts#SubagentDescriptorData" + ], + "sources": [ + "packages/subagent/subagent/src/descriptor.ts:89" + ] + }, + { + "digest": "86e1e384495ee09ac2d60c9db3610f1f1fddac2d248f6f1d6000e78054680219", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 1, + "optional": false + }, + { + "name": "text", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "packages/llm/llm/src/message.ts#ContextSnapshotSection" + ], + "sources": [ + "packages/llm/llm/src/message.ts:65" + ] + }, + { + "digest": "871bfd6f7bb91061246768d3377f3fd58d6b8ea7c31f44c714ae7ab215650d62", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "provisioning" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "argumentsDelta", + "type": 1, + "optional": false + }, + { + "name": "id", + "type": 1, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 1, + "optional": true + }, + { + "name": "type", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "tool-call-delta" + } + ] + }, + "names": [ + "event:assistant/attempt.data.stream[0][3].chunk[3]" + ], + "sources": [ + "packages/llm/llm/src/types.ts:428" + ] + }, + { + "digest": "8a6e1ec9e8db346b0e02f027db73c07a94f067a26d40c1aef1abd09c47ce7ba0", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 7, + "optional": true + }, + { + "name": "seq", + "type": 6, + "optional": false + }, + { + "name": "time", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "dialect", + "type": 2, + "optional": false + }, + { + "name": "handlerId", + "type": 5, + "optional": false + }, + { + "name": "matcher", + "type": 5, + "optional": true + }, + { + "name": "point", + "type": 5, + "optional": false + }, + { + "name": "turn", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 3, + 4 + ] + }, + { + "kind": "literal", + "value": "claude-code" + }, + { + "kind": "literal", + "value": "codex" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "hook/invoked" + } + ] + }, + "names": [ + "event:hook/invoked" + ], + "sources": [] + }, + { + "digest": "8a9c990773f798eac10ca8878e6bcc531ee080bc589cac956730e462cc6a09ad", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "preset", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "event:permission/preset.data" + ], + "sources": [ + "packages/interaction/permission-presets/src/index.ts:57" + ] + }, + { + "digest": "8ad45ad053b4d89f8865c17cc9ff3b8748e86045f3f0fb816269a34af2619141", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "decision", + "type": 1, + "optional": false + }, + { + "name": "durationMs", + "type": 2, + "optional": false + }, + { + "name": "exitCode", + "type": 2, + "optional": true + }, + { + "name": "handlerId", + "type": 1, + "optional": false + }, + { + "name": "point", + "type": 1, + "optional": false + }, + { + "name": "stderrSummary", + "type": 1, + "optional": true + }, + { + "name": "turn", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + } + ] + }, + "names": [ + "event:hook/result.data" + ], + "sources": [ + "packages/hooks/hook-protocol/src/types.ts:31" + ] + }, + { + "digest": "8af841fc9b265ade3441caf9fe40dd24db24226befba48150e6d0eeccc1389c3", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "system" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "sessionFormatVersion", + "type": 1, + "optional": true + }, + { + "name": "sessionId", + "type": 2, + "optional": false + }, + { + "name": "throughSeq", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "event:session-log-deepseek/delivery-accepted.data" + ], + "sources": [ + "packages/session/session-log-deepseek/src/types.ts:81" + ] + }, + { + "digest": "8cdd3fdd295cee53035de16a1b23411716f75e9f994fe9510cac5d7fef605c76", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "command/done" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "8d2e5a3c4230e6ea6c0b5151f02241d0db02cd2551f2df1b6b9700cddcc0dca3", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 1, + "optional": false + }, + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "error", + "type": 28, + "optional": true + }, + { + "name": "isError", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "parentCallId", + "type": 6, + "optional": false + }, + { + "name": "rootCallId", + "type": 6, + "optional": false + }, + { + "name": "subCallId", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "union", + "types": [ + 4, + 8, + 10, + 12, + 14, + 25 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "isError", + "type": 5, + "optional": true + }, + { + "name": "toolCallId", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 15, + "optional": false + }, + { + "name": "offloaded", + "type": 23, + "optional": true + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 16, + "optional": false + }, + { + "name": "height", + "type": 16, + "optional": false + }, + { + "name": "mediaType", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": true + }, + { + "name": "originalDimensions", + "type": 22, + "optional": true + }, + { + "name": "width", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 18, + 19, + 20, + 21 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 16, + "optional": false + }, + { + "name": "width", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 26, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 16, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "reason", + "type": 6, + "optional": true + } + ], + "indices": [] + } + ] + }, + "names": [ + "packages/core/tools/src/types.ts#PtcDispatchEventData" + ], + "sources": [ + "packages/core/tools/src/types.ts:20" + ] + }, + { + "digest": "8d6a011f3ef7096a4a922ef420458419ff6b20e1eaeece5d331db7585d2f08d6", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "tuple", + "elements": [ + { + "type": 1, + "optional": false, + "rest": false + } + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "isError", + "type": 25, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "union", + "types": [ + 1, + 4, + 7, + 9, + 11, + 22 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 12, + "optional": false + }, + { + "name": "offloaded", + "type": 20, + "optional": true + }, + { + "name": "type", + "type": 21, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 13, + "optional": false + }, + { + "name": "height", + "type": 13, + "optional": false + }, + { + "name": "mediaType", + "type": 14, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 19, + "optional": true + }, + { + "name": "width", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 15, + 16, + 17, + 18 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 13, + "optional": false + }, + { + "name": "width", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 23, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 13, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + } + ] + }, + "names": [ + "event:tool/result.data.message.content" + ], + "sources": [] + }, + { + "digest": "8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 1, + "optional": false + }, + { + "name": "kind", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 2, + "optional": false + }, + { + "name": "message", + "type": 2, + "optional": false + }, + { + "name": "offloadImages", + "type": 3, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 3, + "optional": true + }, + { + "name": "requestId", + "type": 2, + "optional": true + }, + { + "name": "status", + "type": 3, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "aborted" + } + ] + }, + "names": [ + "event:assistant/attempt.data.stream[0][3].chunk[6].reason[0]" + ], + "sources": [ + "packages/llm/llm/src/types.ts:147" + ] + }, + { + "digest": "8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "id", + "type": 1, + "optional": false + }, + { + "name": "operation", + "type": 2, + "optional": false + }, + { + "name": "version", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "dispatch" + }, + { + "kind": "literal", + "value": 1 + } + ] + }, + "names": [ + "packages/schedule/schedule/src/types.ts#OneShotScheduleDispatchChange" + ], + "sources": [ + "packages/schedule/schedule/src/types.ts:86" + ] + }, + { + "digest": "8ea13bcd983eab596dbfd41c5c2b62a0b318b71cd2a987a6fd752c6ced7e4489", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 4, + "optional": true + }, + { + "name": "seq", + "type": 6, + "optional": false + }, + { + "name": "time", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "header", + "type": 2, + "optional": false + }, + { + "name": "reason", + "type": 13, + "optional": false + }, + { + "name": "startsSeries", + "type": 4, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "adapterDefaults", + "type": 3, + "optional": true + }, + { + "name": "config", + "type": 5, + "optional": false + }, + { + "name": "tools", + "type": 9, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "maxTokens", + "type": 4, + "optional": true + }, + { + "name": "reasoningEffort", + "type": 4, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "object", + "properties": [ + { + "name": "maxTokens", + "type": 6, + "optional": true + }, + { + "name": "model", + "type": 7, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "reasoningEffort", + "type": 7, + "optional": true + }, + { + "name": "stop", + "type": 8, + "optional": true + }, + { + "name": "temperature", + "type": 6, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "array", + "element": 10 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "parameters", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 7, + "value": 12 + } + ] + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "union", + "types": [ + 14, + 15, + 16, + 17 + ] + }, + { + "kind": "literal", + "value": "initial" + }, + { + "kind": "literal", + "value": "resume" + }, + { + "kind": "literal", + "value": "series" + }, + { + "kind": "literal", + "value": "change" + }, + { + "kind": "literal", + "value": "request/header" + } + ] + }, + "names": [ + "event:request/header" + ], + "sources": [] + }, + { + "digest": "8f6d609794bf5afc01d7ccf3e03811d32f4b8047f58a2c9a98db0154c2ad7cb9", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "tuple", + "elements": [ + { + "type": 1, + "optional": false, + "rest": false + } + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "text" + } + ] + }, + "names": [ + "event:web/deepseek-search-llm-request.data.body.messages[0].content" + ], + "sources": [] + }, + { + "digest": "904bf499d98be4a3a5f836fcac533b6c4899569b018071e6604e1d32657d00fd", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "union", + "types": [ + 1, + 2, + 3 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + } + ] + }, + "names": [ + "event:agent/inbox/spliced.data.inserted[0].source[16].changes[0].action" + ], + "sources": [] + }, + { + "digest": "90c811b8c2668898515e69a24e82caffeb08517f9c7aa193867ff0380f3b535d", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "block", + "type": 1, + "optional": false + }, + { + "name": "index", + "type": 15, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 7, + 9, + 11, + 13, + 24 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "isError", + "type": 4, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 1 + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 14, + "optional": false + }, + { + "name": "offloaded", + "type": 22, + "optional": true + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 15, + "optional": false + }, + { + "name": "height", + "type": 15, + "optional": false + }, + { + "name": "mediaType", + "type": 16, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 21, + "optional": true + }, + { + "name": "width", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 17, + 18, + 19, + 20 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 15, + "optional": false + }, + { + "name": "width", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 25, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 15, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "literal", + "value": "block-end" + } + ] + }, + "names": [ + "event:assistant/attempt.data.stream[0][3].chunk[4]" + ], + "sources": [ + "packages/llm/llm/src/types.ts:429" + ] + }, + { + "digest": "92cda1e2c0e9f57fdcd167a861b1296ec4cb5ac3ae127733c375e108335b32ac", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 19, + "optional": true + }, + { + "name": "seq", + "type": 3, + "optional": false + }, + { + "name": "time", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 102, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "request", + "type": 2, + "optional": false + }, + { + "name": "responseFormat", + "type": 97, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "maxTokens", + "type": 3, + "optional": true + }, + { + "name": "messages", + "type": 4, + "optional": false + }, + { + "name": "model", + "type": 9, + "optional": false + }, + { + "name": "provider", + "type": 9, + "optional": false + }, + { + "name": "purpose", + "type": 90, + "optional": true + }, + { + "name": "reasoningEffort", + "type": 9, + "optional": true + }, + { + "name": "sessionId", + "type": 9, + "optional": true + }, + { + "name": "stop", + "type": 93, + "optional": true + }, + { + "name": "system", + "type": 9, + "optional": true + }, + { + "name": "temperature", + "type": 3, + "optional": true + }, + { + "name": "tools", + "type": 94, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 9, + "optional": false + }, + { + "name": "role", + "type": 31, + "optional": false + }, + { + "name": "source", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "union", + "types": [ + 8, + 11, + 21, + 24, + 27, + 29 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 9, + "optional": false + }, + { + "name": "id", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 12, + "optional": false + }, + { + "name": "offloaded", + "type": 19, + "optional": true + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 9, + "optional": false + }, + { + "name": "bytes", + "type": 3, + "optional": false + }, + { + "name": "height", + "type": 3, + "optional": false + }, + { + "name": "mediaType", + "type": 13, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": true + }, + { + "name": "originalDimensions", + "type": 18, + "optional": true + }, + { + "name": "width", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 14, + 15, + 16, + 17 + ] + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 3, + "optional": false + }, + { + "name": "width", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 22, + "optional": false + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 9, + "optional": false + }, + { + "name": "bytes", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 6, + "optional": false + }, + { + "name": "isError", + "type": 25, + "optional": true + }, + { + "name": "toolCallId", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "union", + "types": [ + 32, + 33, + 34 + ] + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 36, + 45, + 47, + 48, + 51, + 56, + 58, + 63, + 64, + 65, + 67, + 69, + 70, + 77, + 79, + 81, + 83, + 85, + 88, + 89 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 19, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 9, + "optional": true + }, + { + "name": "changes", + "type": 37, + "optional": false + }, + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 38 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 39, + "optional": false + }, + { + "name": "digest", + "type": 9, + "optional": true + }, + { + "name": "path", + "type": 9, + "optional": false + }, + { + "name": "scope", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 40, + 41, + 42 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 9, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 9, + "optional": true + }, + { + "name": "kind", + "type": 34, + "optional": false + }, + { + "name": "rpcId", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 9, + "optional": false + }, + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + }, + { + "name": "provider", + "type": 9, + "optional": false + }, + { + "name": "ruleId", + "type": 9, + "optional": false + }, + { + "name": "source", + "type": 9, + "optional": false + }, + { + "name": "summary", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 52, + "optional": false + }, + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "update", + "type": 19, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 53 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 59, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + }, + { + "name": "sections", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 62 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 9, + "optional": false + }, + { + "name": "text", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + }, + { + "name": "summary", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 66, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 68, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 66, + "optional": false + }, + { + "name": "kind", + "type": 71, + "optional": false + }, + { + "name": "references", + "type": 72, + "optional": false + }, + { + "name": "version", + "type": 76, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 73 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 3, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 74, + "optional": false + }, + { + "name": "compacted", + "type": 25, + "optional": false + }, + { + "name": "inputIndex", + "type": 3, + "optional": false + }, + { + "name": "label", + "type": 9, + "optional": false + }, + { + "name": "omittedBytes", + "type": 3, + "optional": false + }, + { + "name": "omittedMessages", + "type": 3, + "optional": false + }, + { + "name": "originalMessages", + "type": 3, + "optional": false + }, + { + "name": "retainedMessages", + "type": 3, + "optional": false + }, + { + "name": "sessionId", + "type": 9, + "optional": false + }, + { + "name": "truncated", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 3, + 75 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "senderSessionId", + "type": 9, + "optional": false + }, + { + "name": "summary", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 68, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "senderSessionId", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 9, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "revision", + "type": 3, + "optional": false + }, + { + "name": "round", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 84, + "optional": false + }, + { + "name": "messageId", + "type": 9, + "optional": false + }, + { + "name": "senderId", + "type": 9, + "optional": false + }, + { + "name": "senderName", + "type": 9, + "optional": false + }, + { + "name": "teamId", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 86, + "optional": false + }, + { + "name": "model", + "type": 9, + "optional": false + }, + { + "name": "provider", + "type": 9, + "optional": false + }, + { + "name": "replayState", + "type": 87, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 91, + 92 + ] + }, + { + "kind": "literal", + "value": "session-title" + }, + { + "kind": "literal", + "value": "compaction" + }, + { + "kind": "array", + "element": 9 + }, + { + "kind": "array", + "element": 95 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + }, + { + "name": "parameters", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 9, + "value": 87 + } + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 9, + "optional": true + }, + { + "name": "name", + "type": 9, + "optional": false + }, + { + "name": "schema", + "type": 98, + "optional": false + }, + { + "name": "type", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 3, + 9, + 25, + 75, + 99, + 100 + ] + }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 9, + "value": 98 + } + ] + }, + { + "kind": "array", + "element": 98 + }, + { + "kind": "literal", + "value": "json_schema" + }, + { + "kind": "literal", + "value": "browser-use/stagehand-llm-request" + } + ] + }, + "names": [ + "event:browser-use/stagehand-llm-request" + ], + "sources": [] + }, + { + "digest": "930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "max_tokens", + "type": 1, + "optional": false + }, + { + "name": "messages", + "type": 2, + "optional": false + }, + { + "name": "model", + "type": 6, + "optional": false + }, + { + "name": "tools", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "tuple", + "elements": [ + { + "type": 3, + "optional": false, + "rest": false + } + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "role", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "tuple", + "elements": [ + { + "type": 5, + "optional": false, + "rest": false + } + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "tuple", + "elements": [ + { + "type": 10, + "optional": false, + "rest": false + } + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "max_uses", + "type": 1, + "optional": false + }, + { + "name": "name", + "type": 11, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "web_search" + }, + { + "kind": "literal", + "value": "web_search_20250305" + } + ] + }, + "names": [ + "event:web/deepseek-search-llm-request.data.body" + ], + "sources": [ + "packages/web/web-search-deepseek/src/provider.ts:61" + ] + }, + { + "digest": "930c1cd289def119d443e19dd90008916bf035f0a4d23e07241e7f050f2b89b7", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "union", + "types": [ + 1, + 2, + 3, + 4 + ] + }, + { + "kind": "literal", + "value": "allowed-once" + }, + { + "kind": "literal", + "value": "cancelled" + }, + { + "kind": "literal", + "value": "rejected" + }, + { + "kind": "literal", + "value": "unavailable" + } + ] + }, + "names": [ + "packages/interaction/user-approval/src/types.ts#ApprovalOutcome" + ], + "sources": [ + "packages/interaction/user-approval/src/types.ts:32" + ] + }, + { + "digest": "935c8d486292b13c87b57ca280eb8c58b5b2f5239be86fcd4334c33fdeb87682", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "web_search" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "938f7c7357bc403ea3b281a9dd016939df7a0880fc8db12612585be574d0d615", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "primitive", + "type": "null" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "93c33d9687613293f8c95d46c4d922fe9ceae83b84c384beff5c3315abe005f2", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "array", + "element": 1 + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "event:assistant/attempt.data.stream[0][0].texts", + "event:assistant/attempt.data.stream[0][1].texts", + "event:assistant/attempt.data.stream[0][2].args", + "event:browser-use/stagehand-llm-request.data.request.stop", + "event:request/header.data.header.config.stop", + "event:subagent/descriptor.data[1].toolFilter.allow", + "event:subagent/descriptor.data[1].toolFilter.deny", + "event:team/task.data.task.blockedBy", + "event:team/task.data.task.writeScopes" + ], + "sources": [] + }, + { + "digest": "93cbbc5a7fd0b1aa1ff1b9a6bb5ec52b073f48e5a5200365c45c0cd54059e1b9", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "id", + "type": 1, + "optional": false + }, + { + "name": "outcome", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "union", + "types": [ + 3, + 4, + 5, + 6 + ] + }, + { + "kind": "literal", + "value": "allowed-once" + }, + { + "kind": "literal", + "value": "cancelled" + }, + { + "kind": "literal", + "value": "rejected" + }, + { + "kind": "literal", + "value": "unavailable" + } + ] + }, + "names": [ + "event:approval/decided.data" + ], + "sources": [ + "packages/interaction/user-approval/src/types.ts:55" + ] + }, + { + "digest": "93d38436347f67c9022aacbc4472e283d722bcab8eb73fc376aaf7b6a6b9fb35", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": 0 + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "94c56aeb1564ad735eb773d88636b40761d063efa1610f1308004d32923f5378", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "in_progress" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 1, + "optional": false + }, + { + "name": "text", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "text-delta" + } + ] + }, + "names": [ + "event:assistant/attempt.data.stream[0][3].chunk[1]" + ], + "sources": [ + "packages/llm/llm/src/types.ts:426" + ] + }, + { + "digest": "95c0e88fcf0bafb2087f912ad9e2369de6ae6530cc0c4c2772c96205b57f7f60", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "assistant" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "union", + "types": [ + 1, + 2 + ] + }, + { + "kind": "literal", + "value": "next-step" + }, + { + "kind": "literal", + "value": "next-turn" + } + ] + }, + "names": [ + "packages/core/agent/src/types.ts#InboxTarget" + ], + "sources": [ + "packages/core/agent/src/types.ts:30" + ] + }, + { + "digest": "9890113e97dd3541787bfe6704f2b223d3a890b5325ca0acae2b0398909b413e", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "mode", + "type": 1, + "optional": false + }, + { + "name": "source", + "type": 5, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 3, + 4 + ] + }, + { + "kind": "literal", + "value": "danger-full-access" + }, + { + "kind": "literal", + "value": "read-only" + }, + { + "kind": "literal", + "value": "workspace-write" + }, + { + "kind": "literal", + "value": "delegation" + } + ] + }, + "names": [ + "event:sandbox/mode.data" + ], + "sources": [ + "packages/sandbox/sandbox-policy/src/session-mode.ts:33" + ] + }, + { + "digest": "998f22585a5d73ec005365e203f07fc3b3a029e82323fdda3fd7f13b0a9639d0", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "ignorable", + "type": 1, + "optional": true + }, + { + "name": "seq", + "type": 2, + "optional": false + }, + { + "name": "sourceEventSeqs", + "type": 3, + "optional": true + }, + { + "name": "surfaceOp", + "type": 4, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "union", + "types": [ + 5, + 6 + ] + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 2, + "optional": false + }, + { + "name": "op", + "type": 7, + "optional": false + }, + { + "name": "startSeq", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "SessionEventEnvelope[41]", + "SessionEventEnvelope[54]", + "SessionEventEnvelope[57]" + ], + "sources": [] + }, + { + "digest": "9a2a9029f8d7ede05336980d8342737557f487b913bfd28853d0ab5214600ab5", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "role", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "tuple", + "elements": [ + { + "type": 2, + "optional": false, + "rest": false + } + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "user" + } + ] + }, + "names": [ + "event:web/deepseek-search-llm-request.data.body.messages[0]" + ], + "sources": [ + "packages/web/web-search-deepseek/src/provider.ts:64" + ] + }, + { + "digest": "9a410ddf56792b2b7cd4a224a2e54dc4dee30a8e8e6b1c2b9a1d19992b217adc", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "childId", + "type": 1, + "optional": false + }, + { + "name": "label", + "type": 1, + "optional": false + }, + { + "name": "phase", + "type": 1, + "optional": true + }, + { + "name": "runId", + "type": 1, + "optional": false + }, + { + "name": "seq", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + } + ] + }, + "names": [ + "packages/workflow/tool-workflow/src/types.ts#ToolWorkflowAgentStartData" + ], + "sources": [ + "packages/workflow/tool-workflow/src/types.ts:20" + ] + }, + { + "digest": "9acbe713f3f641921dd3333b240aa72e176d795327c24e8d9db9b68b16771b49", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "image/offload" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "9b4fd974c124c83743efe030ba4e1250e087505f3f74588716a521fb8d453267", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "tool-call-delta" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "9d80129e13edfb6feb03ed327e6085c6ef60b97ad074244b2f5fd4667424a6a4", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "other" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "9e07693fd7cd9babe0688c22c986e4d519ae1c0e40eeea13b2dc583bed2abc86", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "ask" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "9e2c06d8653585b33d72d5c1a80da79c8a24a88c284409bca15cc0f0ead5ebfb", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "array", + "element": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "imageIndexes", + "type": 2, + "optional": false + }, + { + "name": "seq", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "primitive", + "type": "number" + } + ] + }, + "names": [ + "event:image/offload.data.targets" + ], + "sources": [] + }, + { + "digest": "9e41386b3a0c9572b0d63078492ebb3997da7d3a830d44e0259418bd02f4bcb2", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "end", + "type": 1, + "optional": false + }, + { + "name": "start", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + } + ] + }, + "names": [ + "event:compaction/prune.data.shadowedRange", + "event:compaction/summary.data[0].shadowedRange", + "event:compaction/summary.data[1].shadowedRange" + ], + "sources": [ + "packages/compaction/compaction/src/types.ts:38", + "packages/compaction/compaction/src/types.ts:84" + ] + }, + { + "digest": "9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 1, + "optional": false + }, + { + "name": "form", + "type": 2, + "optional": false + }, + { + "name": "kind", + "type": 3, + "optional": false + }, + { + "name": "provider", + "type": 1, + "optional": false + }, + { + "name": "ruleId", + "type": 1, + "optional": false + }, + { + "name": "source", + "type": 1, + "optional": false + }, + { + "name": "summary", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + } + ] + }, + "names": [ + "event:agent/inbox/spliced.data.inserted[0].source[19]" + ], + "sources": [ + "packages/webhook/webhook/src/types.ts:74" + ] + }, + { + "digest": "9fe0e77a7816b7fdb4b953ec0e2f0cd7bbd22c5de5c3567e612d1b20eb0992e6", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "maxTokens", + "type": 1, + "optional": false + }, + { + "name": "messageSeqs", + "type": 2, + "optional": false + }, + { + "name": "messages", + "type": 3, + "optional": false + }, + { + "name": "route", + "type": 89, + "optional": false + }, + { + "name": "system", + "type": 8, + "optional": false + }, + { + "name": "titleProvider", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 1 + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 8, + "optional": false + }, + { + "name": "role", + "type": 30, + "optional": false + }, + { + "name": "source", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 6 + }, + { + "kind": "union", + "types": [ + 7, + 10, + 20, + 23, + 26, + 28 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 8, + "optional": false + }, + { + "name": "id", + "type": 8, + "optional": false + }, + { + "name": "name", + "type": 8, + "optional": false + }, + { + "name": "type", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 11, + "optional": false + }, + { + "name": "offloaded", + "type": 18, + "optional": true + }, + { + "name": "type", + "type": 19, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 8, + "optional": false + }, + { + "name": "bytes", + "type": 1, + "optional": false + }, + { + "name": "height", + "type": 1, + "optional": false + }, + { + "name": "mediaType", + "type": 12, + "optional": false + }, + { + "name": "name", + "type": 8, + "optional": true + }, + { + "name": "originalDimensions", + "type": 17, + "optional": true + }, + { + "name": "width", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 13, + 14, + 15, + 16 + ] + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 1, + "optional": false + }, + { + "name": "width", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 21, + "optional": false + }, + { + "name": "type", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 8, + "optional": false + }, + { + "name": "bytes", + "type": 1, + "optional": false + }, + { + "name": "name", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 5, + "optional": false + }, + { + "name": "isError", + "type": 24, + "optional": true + }, + { + "name": "toolCallId", + "type": 8, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 8, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 8, + "optional": false + }, + { + "name": "type", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "union", + "types": [ + 31, + 32, + 33 + ] + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 35, + 44, + 46, + 47, + 50, + 55, + 57, + 62, + 63, + 64, + 66, + 68, + 69, + 76, + 78, + 80, + 82, + 84, + 87, + 88 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 18, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 8, + "optional": true + }, + { + "name": "changes", + "type": 36, + "optional": false + }, + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 37 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 38, + "optional": false + }, + { + "name": "digest", + "type": 8, + "optional": true + }, + { + "name": "path", + "type": 8, + "optional": false + }, + { + "name": "scope", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 39, + 40, + 41 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 8, + "optional": false + }, + { + "name": "kind", + "type": 45, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 8, + "optional": true + }, + { + "name": "kind", + "type": 33, + "optional": false + }, + { + "name": "rpcId", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 8, + "optional": false + }, + { + "name": "form", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + }, + { + "name": "provider", + "type": 8, + "optional": false + }, + { + "name": "ruleId", + "type": 8, + "optional": false + }, + { + "name": "source", + "type": 8, + "optional": false + }, + { + "name": "summary", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 51, + "optional": false + }, + { + "name": "form", + "type": 53, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "update", + "type": 18, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 52 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 8, + "optional": false + }, + { + "name": "name", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "name", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 58, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "plugin", + "type": 8, + "optional": false + }, + { + "name": "sections", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 61 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 8, + "optional": false + }, + { + "name": "text", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "plugin", + "type": 8, + "optional": false + }, + { + "name": "summary", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "plugin", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 65, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "plugin", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 67, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "plugin", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 53, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "plugin", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 65, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + }, + { + "name": "references", + "type": 71, + "optional": false + }, + { + "name": "version", + "type": 75, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 72 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 1, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 73, + "optional": false + }, + { + "name": "compacted", + "type": 24, + "optional": false + }, + { + "name": "inputIndex", + "type": 1, + "optional": false + }, + { + "name": "label", + "type": 8, + "optional": false + }, + { + "name": "omittedBytes", + "type": 1, + "optional": false + }, + { + "name": "omittedMessages", + "type": 1, + "optional": false + }, + { + "name": "originalMessages", + "type": 1, + "optional": false + }, + { + "name": "retainedMessages", + "type": 1, + "optional": false + }, + { + "name": "sessionId", + "type": 8, + "optional": false + }, + { + "name": "truncated", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 1, + 74 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 48, + "optional": false + }, + { + "name": "kind", + "type": 77, + "optional": false + }, + { + "name": "senderSessionId", + "type": 8, + "optional": false + }, + { + "name": "summary", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 67, + "optional": false + }, + { + "name": "kind", + "type": 79, + "optional": false + }, + { + "name": "senderSessionId", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 8, + "optional": false + }, + { + "name": "kind", + "type": 81, + "optional": false + }, + { + "name": "revision", + "type": 1, + "optional": false + }, + { + "name": "round", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 83, + "optional": false + }, + { + "name": "messageId", + "type": 8, + "optional": false + }, + { + "name": "senderId", + "type": 8, + "optional": false + }, + { + "name": "senderName", + "type": 8, + "optional": false + }, + { + "name": "teamId", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 85, + "optional": false + }, + { + "name": "model", + "type": 8, + "optional": false + }, + { + "name": "provider", + "type": 8, + "optional": false + }, + { + "name": "replayState", + "type": 86, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "plugin", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "model", + "type": 8, + "optional": false + }, + { + "name": "provider", + "type": 8, + "optional": false + } + ], + "indices": [] + } + ] + }, + "names": [ + "packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData" + ], + "sources": [ + "packages/session/session-title-llm/src/index.ts:27" + ] + }, + { + "digest": "9ff2ad8dddb836f75e8f6197fff2371323dfc396c3c6fb667e6e0b42fda79a5c", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 1, + "optional": false + }, + { + "name": "llmStreamCall", + "type": 2, + "optional": false + }, + { + "name": "maxTokens", + "type": 3, + "optional": true + }, + { + "name": "model", + "type": 1, + "optional": false + }, + { + "name": "provider", + "type": 1, + "optional": false + }, + { + "name": "rawOutput", + "type": 4, + "optional": false + }, + { + "name": "shadowedRange", + "type": 27, + "optional": false + }, + { + "name": "shadowedSeqs", + "type": 28, + "optional": false + }, + { + "name": "shadowedTokenCount", + "type": 3, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 1, + "optional": true + }, + { + "name": "summary", + "type": 4, + "optional": false + }, + { + "name": "usage", + "type": 29, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 9, + 11, + 13, + 15, + 24 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "isError", + "type": 7, + "optional": true + }, + { + "name": "toolCallId", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 1, + "optional": false + }, + { + "name": "id", + "type": 1, + "optional": false + }, + { + "name": "name", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 16, + "optional": false + }, + { + "name": "offloaded", + "type": 2, + "optional": true + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 1, + "optional": false + }, + { + "name": "bytes", + "type": 3, + "optional": false + }, + { + "name": "height", + "type": 3, + "optional": false + }, + { + "name": "mediaType", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 1, + "optional": true + }, + { + "name": "originalDimensions", + "type": 22, + "optional": true + }, + { + "name": "width", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 18, + 19, + 20, + 21 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 3, + "optional": false + }, + { + "name": "width", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 25, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 1, + "optional": false + }, + { + "name": "bytes", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "end", + "type": 3, + "optional": false + }, + { + "name": "start", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 3, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 3, + "optional": true + }, + { + "name": "inputTokens", + "type": 3, + "optional": false + }, + { + "name": "outputTokens", + "type": 3, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 3, + "optional": true + }, + { + "name": "totalTokens", + "type": 3, + "optional": true + } + ], + "indices": [] + } + ] + }, + "names": [ + "event:compaction/summary.data[0]" + ], + "sources": [] + }, + { + "digest": "a0f906856fc06f1c6f2083eb970e5a657a82b1a84c06c095445e5d6d161cda80", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": 2 + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 3, + "optional": true + }, + { + "name": "seq", + "type": 4, + "optional": false + }, + { + "name": "time", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "agentPreset", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "agent-preset/selected" + } + ] + }, + "names": [ + "event:agent-preset/selected" + ], + "sources": [] + }, + { + "digest": "a13d8f379222ece8a02a86efe4c8334af4f108ecb3034b38c0a42bb3046e6847", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "tool" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 1, + "optional": false + }, + { + "name": "replayState", + "type": 15, + "optional": true + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 7, + 9, + 11, + 13 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 3, + "optional": false + }, + { + "name": "kind", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 4, + "optional": false + }, + { + "name": "message", + "type": 4, + "optional": false + }, + { + "name": "offloadImages", + "type": 5, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 5, + "optional": true + }, + { + "name": "requestId", + "type": 4, + "optional": true + }, + { + "name": "status", + "type": 5, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 3, + "optional": false + }, + { + "name": "kind", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 16, + "optional": true + }, + { + "name": "response", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 17 + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "literal", + "value": "finish" + } + ] + }, + "names": [ + "event:assistant/attempt.data.stream[0][3].chunk[6]" + ], + "sources": [ + "packages/llm/llm/src/types.ts:431" + ] + }, { "digest": "a2be0f0cb59f0d4a06557d45847d1938aced238baa77a6da416e590b8b958553", "schema": { @@ -52593,52 +53552,62 @@ ] }, { - "digest": "a4ed3446d32625902170fce527fb64a0283d6f4817930f54a16fdce3a482b36a", + "digest": "a5a8c15f08af1b0f26f40906e0797b6b0920fd01069e660becce35ab7fc48d77", "schema": { "root": 0, "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "reason", - "type": 1, - "optional": false - }, - { - "name": "replayState", - "type": 15, - "optional": true - }, - { - "name": "type", - "type": 18, - "optional": false - } - ], - "indices": [] - }, { "kind": "union", "types": [ - 2, - 7, - 9, - 11, - 13 + 1, + 3, + 5, + 10, + 23, + 25 ] }, { "kind": "object", "properties": [ { - "name": "failure", - "type": 3, + "name": "kind", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "interrupted" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "error", + "type": 6, "optional": false }, { "name": "kind", - "type": 6, + "type": 9, "optional": false } ], @@ -52649,27 +53618,32 @@ "properties": [ { "name": "code", - "type": 4, + "type": 7, "optional": false }, { "name": "message", - "type": 4, + "type": 7, "optional": false }, + { + "name": "offloadImages", + "type": 8, + "optional": true + }, { "name": "providerRetryAfterMs", - "type": 5, + "type": 8, "optional": true }, { "name": "requestId", - "type": 4, + "type": 7, "optional": true }, { "name": "status", - "type": 5, + "type": 8, "optional": true } ], @@ -52683,26 +53657,6 @@ "kind": "primitive", "type": "number" }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 3, - "optional": false - }, - { - "name": "kind", - "type": 8, - "optional": false - } - ], - "indices": [] - }, { "kind": "literal", "value": "error" @@ -52712,21 +53666,11 @@ "properties": [ { "name": "kind", - "type": 10, + "type": 11, "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ + }, { - "name": "kind", + "name": "reason", "type": 12, "optional": false } @@ -52735,7 +53679,17 @@ }, { "kind": "literal", - "value": "stop" + "value": "aborted" + }, + { + "kind": "union", + "types": [ + 13, + 15, + 17, + 19, + 21 + ] }, { "kind": "object", @@ -52750,338 +53704,48 @@ }, { "kind": "literal", - "value": "tool-calls" + "value": "disposed" }, { "kind": "object", "properties": [ { - "name": "blocks", + "name": "kind", "type": 16, - "optional": true - }, - { - "name": "response", - "type": 17, "optional": false } ], "indices": [] }, - { - "kind": "array", - "element": 17 - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "literal", - "value": "finish" - } - ] - }, - "names": [ - "event:assistant/attempt.data.stream[0][3].chunk[6]" - ], - "sources": [ - "packages/llm/llm/src/types.ts:397" - ] - }, - { - "digest": "a58ca1bed502d425a8b65cac02f59717498cd296de9513c6f81b83b791c20e9b", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 2, - "optional": true - }, - { - "name": "seq", - "type": 11, - "optional": false - }, - { - "name": "surfaceOp", - "type": 76, - "optional": false - }, - { - "name": "time", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 80, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "interrupted", - "type": 2, - "optional": true - }, - { - "name": "message", - "type": 3, - "optional": false - }, - { - "name": "step", - "type": 11, - "optional": false - }, - { - "name": "stream", - "type": 33, - "optional": false - }, - { - "name": "turn", - "type": 11, - "optional": false - }, - { - "name": "usage", - "type": 70, - "optional": true - } - ], - "indices": [] - }, { "kind": "literal", - "value": true + "value": "legacy" }, { "kind": "object", "properties": [ { - "name": "content", - "type": 4, - "optional": false - }, - { - "name": "id", - "type": 7, - "optional": false - }, - { - "name": "role", - "type": 29, - "optional": false - }, - { - "name": "source", - "type": 30, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 5 - }, - { - "kind": "union", - "types": [ - 6, - 9, - 19, - 22, - 25, - 27 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 7, - "optional": false - }, - { - "name": "id", - "type": 7, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 10, - "optional": false - }, - { - "name": "type", + "name": "kind", "type": 18, "optional": false } ], "indices": [] }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 7, - "optional": false - }, - { - "name": "bytes", - "type": 11, - "optional": false - }, - { - "name": "height", - "type": 11, - "optional": false - }, - { - "name": "mediaType", - "type": 12, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": true - }, - { - "name": "originalDimensions", - "type": 17, - "optional": true - }, - { - "name": "width", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 13, - 14, - 15, - 16 - ] - }, { "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" + "value": "parent" }, { "kind": "object", "properties": [ { - "name": "height", - "type": 11, - "optional": false - }, - { - "name": "width", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", + "name": "kind", "type": 20, "optional": false }, { - "name": "type", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 7, - "optional": false - }, - { - "name": "bytes", - "type": 11, - "optional": false - }, - { - "name": "name", + "name": "reason", "type": 7, "optional": false } @@ -53090,52 +53754,43 @@ }, { "kind": "literal", - "value": "file" + "value": "hook" }, { "kind": "object", "properties": [ { - "name": "content", - "type": 4, + "name": "kind", + "type": 22, "optional": false - }, + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ { - "name": "isError", - "type": 23, - "optional": true - }, - { - "name": "toolCallId", - "type": 7, - "optional": false - }, - { - "name": "type", + "name": "kind", "type": 24, "optional": false } ], "indices": [] }, - { - "kind": "primitive", - "type": "boolean" - }, { "kind": "literal", - "value": "tool-result" + "value": "blocked" }, { "kind": "object", "properties": [ { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", + "name": "kind", "type": 26, "optional": false } @@ -53144,658 +53799,16 @@ }, { "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 28, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "assistant" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 31, - "optional": false - }, - { - "name": "model", - "type": 7, - "optional": false - }, - { - "name": "provider", - "type": 7, - "optional": false - }, - { - "name": "replayState", - "type": 32, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "array", - "element": 34 - }, - { - "kind": "union", - "types": [ - 35, - 39, - 72, - 74 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "args", - "type": 36, - "optional": false - }, - { - "name": "dt", - "type": 37, - "optional": false - }, - { - "name": "id", - "type": 7, - "optional": false - }, - { - "name": "index", - "type": 11, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": true - }, - { - "name": "time0", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 7 - }, - { - "kind": "array", - "element": 11 - }, - { - "kind": "literal", - "value": "tool-call-chunks" - }, - { - "kind": "object", - "properties": [ - { - "name": "chunk", - "type": 40, - "optional": false - }, - { - "name": "time", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 71, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 41, - 43, - 45, - 48, - 50, - 52, - 68 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "argumentsDelta", - "type": 7, - "optional": false - }, - { - "name": "id", - "type": 7, - "optional": false - }, - { - "name": "index", - "type": 11, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": true - }, - { - "name": "type", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "block", - "type": 5, - "optional": false - }, - { - "name": "index", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 44, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "block-end" - }, - { - "kind": "object", - "properties": [ - { - "name": "blockType", - "type": 46, - "optional": false - }, - { - "name": "index", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 47, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 8, - 18, - 21, - 24, - 26, - 28 - ] - }, - { - "kind": "literal", - "value": "block-start" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 11, - "optional": false - }, - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 49, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 11, - "optional": false - }, - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 51, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "reason", - "type": 53, - "optional": false - }, - { - "name": "replayState", - "type": 65, - "optional": true - }, - { - "name": "type", - "type": 67, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 54, - 57, - 59, - 61, - 63 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 7, - "optional": false - }, - { - "name": "message", - "type": 7, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 11, - "optional": true - }, - { - "name": "requestId", - "type": 7, - "optional": true - }, - { - "name": "status", - "type": 11, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 60, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 62, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "stop" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 64, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-calls" - }, - { - "kind": "object", - "properties": [ - { - "name": "blocks", - "type": 66, - "optional": true - }, - { - "name": "response", - "type": 32, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 32 - }, - { - "kind": "literal", - "value": "finish" - }, - { - "kind": "object", - "properties": [ - { - "name": "type", - "type": 69, - "optional": false - }, - { - "name": "usage", - "type": 70, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "usage" - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 11, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 11, - "optional": true - }, - { - "name": "inputTokens", - "type": 11, - "optional": false - }, - { - "name": "outputTokens", - "type": 11, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 11, - "optional": true - }, - { - "name": "totalTokens", - "type": 11, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "chunk" - }, - { - "kind": "object", - "properties": [ - { - "name": "dt", - "type": 37, - "optional": false - }, - { - "name": "index", - "type": 11, - "optional": false - }, - { - "name": "texts", - "type": 36, - "optional": false - }, - { - "name": "time0", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 73, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-chunks" - }, - { - "kind": "object", - "properties": [ - { - "name": "dt", - "type": 37, - "optional": false - }, - { - "name": "index", - "type": 11, - "optional": false - }, - { - "name": "texts", - "type": 36, - "optional": false - }, - { - "name": "time0", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 75, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-chunks" - }, - { - "kind": "union", - "types": [ - 77, - 79 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "endSeq", - "type": 11, - "optional": false - }, - { - "name": "op", - "type": 78, - "optional": false - }, - { - "name": "startSeq", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "append" - }, - { - "kind": "literal", - "value": "assistant/message" + "value": "completed" } ] }, "names": [ - "event:assistant/message" + "packages/core/session/src/types.ts#TurnEndReason" ], - "sources": [] + "sources": [ + "packages/core/session/src/types.ts:224" + ] }, { "digest": "a61aff9b651b49c92a6248da47225c39951c16ce725dc0e19efd5eb7c81a3577", @@ -53901,6 +53914,1129 @@ ], "sources": [] }, + { + "digest": "a6a195c1b4a906d953d24ab5864c9f1881d545460e080a51144adfc9cb058cfe", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "inserted", + "type": 1, + "optional": false + }, + { + "name": "outcome", + "type": 85, + "optional": true + }, + { + "name": "removedCount", + "type": 10, + "optional": true + }, + { + "name": "start", + "type": 10, + "optional": false + }, + { + "name": "target", + "type": 86, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "role", + "type": 29, + "optional": false + }, + { + "name": "source", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 5, + 8, + 19, + 22, + 25, + 27 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 9, + "optional": false + }, + { + "name": "offloaded", + "type": 17, + "optional": true + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 10, + "optional": false + }, + { + "name": "height", + "type": 10, + "optional": false + }, + { + "name": "mediaType", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": true + }, + { + "name": "originalDimensions", + "type": 16, + "optional": true + }, + { + "name": "width", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 12, + 13, + 14, + 15 + ] + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 10, + "optional": false + }, + { + "name": "width", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 20, + "optional": false + }, + { + "name": "type", + "type": 21, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "isError", + "type": 23, + "optional": true + }, + { + "name": "toolCallId", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 31, + 40, + 42, + 43, + 46, + 51, + 53, + 58, + 59, + 60, + 62, + 64, + 65, + 72, + 74, + 76, + 78, + 80, + 83, + 84 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 17, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 6, + "optional": true + }, + { + "name": "changes", + "type": 32, + "optional": false + }, + { + "name": "form", + "type": 38, + "optional": false + }, + { + "name": "kind", + "type": 39, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 33 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 34, + "optional": false + }, + { + "name": "digest", + "type": 6, + "optional": true + }, + { + "name": "path", + "type": 6, + "optional": false + }, + { + "name": "scope", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 35, + 36, + 37 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 6, + "optional": false + }, + { + "name": "kind", + "type": 41, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 6, + "optional": true + }, + { + "name": "kind", + "type": 29, + "optional": false + }, + { + "name": "rpcId", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 6, + "optional": false + }, + { + "name": "form", + "type": 44, + "optional": false + }, + { + "name": "kind", + "type": 45, + "optional": false + }, + { + "name": "provider", + "type": 6, + "optional": false + }, + { + "name": "ruleId", + "type": 6, + "optional": false + }, + { + "name": "source", + "type": 6, + "optional": false + }, + { + "name": "summary", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 47, + "optional": false + }, + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + }, + { + "name": "update", + "type": 17, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 48 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 38, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + }, + { + "name": "sections", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 57 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "text", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 44, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + }, + { + "name": "summary", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 38, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 61, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 63, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 61, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + }, + { + "name": "references", + "type": 67, + "optional": false + }, + { + "name": "version", + "type": 71, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 68 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 10, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 69, + "optional": false + }, + { + "name": "compacted", + "type": 23, + "optional": false + }, + { + "name": "inputIndex", + "type": 10, + "optional": false + }, + { + "name": "label", + "type": 6, + "optional": false + }, + { + "name": "omittedBytes", + "type": 10, + "optional": false + }, + { + "name": "omittedMessages", + "type": 10, + "optional": false + }, + { + "name": "originalMessages", + "type": 10, + "optional": false + }, + { + "name": "retainedMessages", + "type": 10, + "optional": false + }, + { + "name": "sessionId", + "type": 6, + "optional": false + }, + { + "name": "truncated", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 10, + 70 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 44, + "optional": false + }, + { + "name": "kind", + "type": 73, + "optional": false + }, + { + "name": "senderSessionId", + "type": 6, + "optional": false + }, + { + "name": "summary", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 63, + "optional": false + }, + { + "name": "kind", + "type": 75, + "optional": false + }, + { + "name": "senderSessionId", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 6, + "optional": false + }, + { + "name": "kind", + "type": 77, + "optional": false + }, + { + "name": "revision", + "type": 10, + "optional": false + }, + { + "name": "round", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 79, + "optional": false + }, + { + "name": "messageId", + "type": 6, + "optional": false + }, + { + "name": "senderId", + "type": 6, + "optional": false + }, + { + "name": "senderName", + "type": 6, + "optional": false + }, + { + "name": "teamId", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 81, + "optional": false + }, + { + "name": "model", + "type": 6, + "optional": false + }, + { + "name": "provider", + "type": 6, + "optional": false + }, + { + "name": "replayState", + "type": 82, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "plugin", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "canceled" + }, + { + "kind": "union", + "types": [ + 87, + 88 + ] + }, + { + "kind": "literal", + "value": "next-step" + }, + { + "kind": "literal", + "value": "next-turn" + } + ] + }, + "names": [ + "event:agent/inbox/spliced.data" + ], + "sources": [ + "packages/core/agent/src/types.ts:87" + ] + }, { "digest": "a6b41f5bede8094d7bf635be5d69b1bd7a25e006ca9cf9415bceb9322f1a7feb", "schema": { @@ -54458,7 +55594,7 @@ ], "sources": [ "packages/core/session/src/types.ts:213", - "packages/llm/llm/src/types.ts:133" + "packages/llm/llm/src/types.ts:146" ] }, { @@ -54497,405 +55633,6 @@ ], "sources": [] }, - { - "digest": "ac5a8baa3dda8fa667b4975d6a8b40a9bde47d8d754ca8ed6b8ce923a1651828", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "compactionId", - "type": 1, - "optional": false - }, - { - "name": "maxTokens", - "type": 2, - "optional": true - }, - { - "name": "model", - "type": 1, - "optional": false - }, - { - "name": "provider", - "type": 1, - "optional": false - }, - { - "name": "rawOutput", - "type": 3, - "optional": true - }, - { - "name": "shadowedRange", - "type": 26, - "optional": false - }, - { - "name": "shadowedSeqs", - "type": 27, - "optional": false - }, - { - "name": "shadowedTokenCount", - "type": 2, - "optional": false - }, - { - "name": "sourceCommandId", - "type": 1, - "optional": true - }, - { - "name": "summary", - "type": 3, - "optional": false - }, - { - "name": "usage", - "type": 28, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "array", - "element": 4 - }, - { - "kind": "union", - "types": [ - 5, - 8, - 10, - 12, - 14, - 23 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 3, - "optional": false - }, - { - "name": "isError", - "type": 6, - "optional": true - }, - { - "name": "toolCallId", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 1, - "optional": false - }, - { - "name": "id", - "type": 1, - "optional": false - }, - { - "name": "name", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 13, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 15, - "optional": false - }, - { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 1, - "optional": false - }, - { - "name": "bytes", - "type": 2, - "optional": false - }, - { - "name": "height", - "type": 2, - "optional": false - }, - { - "name": "mediaType", - "type": 16, - "optional": false - }, - { - "name": "name", - "type": 1, - "optional": true - }, - { - "name": "originalDimensions", - "type": 21, - "optional": true - }, - { - "name": "width", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 17, - 18, - 19, - 20 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 2, - "optional": false - }, - { - "name": "width", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 24, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 1, - "optional": false - }, - { - "name": "bytes", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "end", - "type": 2, - "optional": false - }, - { - "name": "start", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 2, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 2, - "optional": true - }, - { - "name": "inputTokens", - "type": 2, - "optional": false - }, - { - "name": "outputTokens", - "type": 2, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 2, - "optional": true - }, - { - "name": "totalTokens", - "type": 2, - "optional": true - } - ], - "indices": [] - } - ] - }, - "names": [ - "event:compaction/summary.data[1]" - ], - "sources": [] - }, { "digest": "acd36e1717d2b4ef9e98a8e6fac17ca1203f3f448e5bca51677f129a8f4999e2", "schema": { @@ -55621,6 +56358,96 @@ "names": [], "sources": [] }, + { + "digest": "b222069eea2d768065161c1147b1f2c78c1b54328c84b3586ae5c8f91b8ed35e", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 6, + "optional": true + }, + { + "name": "seq", + "type": 5, + "optional": false + }, + { + "name": "time", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "targets", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "object", + "properties": [ + { + "name": "imageIndexes", + "type": 4, + "optional": false + }, + { + "name": "seq", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image/offload" + } + ] + }, + "names": [ + "event:image/offload" + ], + "sources": [] + }, { "digest": "b2d2a01e51341a3bfec809198952c8c830a7899f4cb6ecd3c1dc0b5d9a2af943", "schema": { @@ -55660,6 +56487,403 @@ "packages/experimental/agent-team/src/types.ts:71" ] }, + { + "digest": "b33142af8176323ffbb20762541be81c1d51b60a60887803836e0b146e5bef0c", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 24, + "optional": true + }, + { + "name": "seq", + "type": 17, + "optional": false + }, + { + "name": "time", + "type": 17, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 2, + "optional": false + }, + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "error", + "type": 29, + "optional": true + }, + { + "name": "isError", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "parentCallId", + "type": 7, + "optional": false + }, + { + "name": "rootCallId", + "type": 7, + "optional": false + }, + { + "name": "subCallId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 5, + 9, + 11, + 13, + 15, + 26 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "isError", + "type": 6, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 16, + "optional": false + }, + { + "name": "offloaded", + "type": 24, + "optional": true + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "mediaType", + "type": 18, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 23, + "optional": true + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 19, + 20, + 21, + 22 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 27, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "reason", + "type": 7, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool/ptc-dispatch" + } + ] + }, + "names": [ + "event:tool/ptc-dispatch" + ], + "sources": [] + }, { "digest": "b38f206d59bad891fa58c89f97a0036987b3defe7685acbd318fa55eb85f9751", "schema": { @@ -56165,398 +57389,6 @@ "names": [], "sources": [] }, - { - "digest": "b5d66eaebed4da391b13498623b11142222149fbf5e025975dbc6d94f0d06796", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 29, - "optional": true - }, - { - "name": "seq", - "type": 17, - "optional": false - }, - { - "name": "time", - "type": 17, - "optional": false - }, - { - "name": "type", - "type": 30, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 2, - "optional": false - }, - { - "name": "content", - "type": 3, - "optional": false - }, - { - "name": "error", - "type": 28, - "optional": true - }, - { - "name": "isError", - "type": 6, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "parentCallId", - "type": 7, - "optional": false - }, - { - "name": "rootCallId", - "type": 7, - "optional": false - }, - { - "name": "subCallId", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "array", - "element": 4 - }, - { - "kind": "union", - "types": [ - 5, - 9, - 11, - 13, - 15, - 25 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 3, - "optional": false - }, - { - "name": "isError", - "type": 6, - "optional": true - }, - { - "name": "toolCallId", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 7, - "optional": false - }, - { - "name": "id", - "type": 7, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 14, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 16, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 7, - "optional": false - }, - { - "name": "bytes", - "type": 17, - "optional": false - }, - { - "name": "height", - "type": 17, - "optional": false - }, - { - "name": "mediaType", - "type": 18, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": true - }, - { - "name": "originalDimensions", - "type": 23, - "optional": true - }, - { - "name": "width", - "type": 17, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 19, - 20, - 21, - 22 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 17, - "optional": false - }, - { - "name": "width", - "type": 17, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 26, - "optional": false - }, - { - "name": "type", - "type": 27, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 7, - "optional": false - }, - { - "name": "bytes", - "type": 17, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 7, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "reason", - "type": 7, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "literal", - "value": "tool/ptc-dispatch" - } - ] - }, - "names": [ - "event:tool/ptc-dispatch" - ], - "sources": [] - }, { "digest": "b6ef9c6210be8350cd465f485cf520031fffa6bf3809c61a1e713e834c5982d0", "schema": { @@ -57181,6 +58013,318 @@ ], "sources": [] }, + { + "digest": "bab768260853e13a7cf2e22e65af572a1e76a5b2c01f3cdc2ce09d39b87fb70b", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 29, + "optional": true + }, + { + "name": "seq", + "type": 14, + "optional": false + }, + { + "name": "time", + "type": 14, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 2, + "optional": false + }, + { + "name": "turn", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 3, + 5, + 7, + 9, + 11, + 16 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "blocked" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "completed" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "interrupted" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "error", + "type": 12, + "optional": false + }, + { + "name": "kind", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 13, + "optional": false + }, + { + "name": "message", + "type": 13, + "optional": false + }, + { + "name": "offloadImages", + "type": 14, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 14, + "optional": true + }, + { + "name": "requestId", + "type": 13, + "optional": true + }, + { + "name": "status", + "type": 14, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 17, + "optional": false + }, + { + "name": "reason", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "union", + "types": [ + 19, + 21, + 23, + 25, + 27 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "disposed" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "legacy" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "parent" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 28, + "optional": false + }, + { + "name": "reason", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hook" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "turn/end" + } + ] + }, + "names": [ + "event:turn/end" + ], + "sources": [] + }, { "digest": "babf9ee4d1af62bf6c3a8103737f7a5e4e78ce179be835ce05a38803e15884b7", "schema": { @@ -57381,6 +58525,308 @@ ], "sources": [] }, + { + "digest": "bb72d86bc9ef0b3908bfa57ef98ccccae24d67d945c9f6869ca3cb8577889e75", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "array", + "element": 1 + }, + { + "kind": "union", + "types": [ + 2, + 5, + 7, + 9, + 20, + 23 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 10, + "optional": false + }, + { + "name": "offloaded", + "type": 18, + "optional": true + }, + { + "name": "type", + "type": 19, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 3, + "optional": false + }, + { + "name": "bytes", + "type": 11, + "optional": false + }, + { + "name": "height", + "type": 11, + "optional": false + }, + { + "name": "mediaType", + "type": 12, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": true + }, + { + "name": "originalDimensions", + "type": 17, + "optional": true + }, + { + "name": "width", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 13, + 14, + 15, + 16 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 11, + "optional": false + }, + { + "name": "width", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 21, + "optional": false + }, + { + "name": "type", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 3, + "optional": false + }, + { + "name": "bytes", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 0, + "optional": false + }, + { + "name": "isError", + "type": 24, + "optional": true + }, + { + "name": "toolCallId", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + } + ] + }, + "names": [ + "event:agent/inbox/spliced.data.inserted[0].content", + "event:agent/inbox/spliced.data.inserted[0].content[0][5].content", + "event:assistant/message.data.message.content", + "event:browser-use/stagehand-llm-request.data.request.messages[0].content", + "event:browser-use/stagehand-llm-result.data.blocks", + "event:compaction/summary.data[0].rawOutput", + "event:compaction/summary.data[0].summary", + "event:compaction/summary.data[1].rawOutput", + "event:compaction/summary.data[1].summary", + "event:system/message.data.message.content", + "event:team/message/queued.data.message.content", + "event:tool/ptc-dispatch.data.content" + ], + "sources": [] + }, { "digest": "bbc5696e3f13c758237b316e3cf08cda0b9fd51ffe672539883457769faa185d", "schema": { @@ -57537,56 +58983,68 @@ ] }, { - "digest": "c0f91c064454e9ae25a4b5caacdc487712aee3acf414b07b7d5f7fd3530a0452", + "digest": "c170da8ab7c37635b10867b2571811d9e317ac3ec394e8ff8efe53f902fc41c1", "schema": { "root": 0, "nodes": [ + { + "kind": "union", + "types": [ + 1, + 6 + ] + }, { "kind": "object", "properties": [ { "name": "delayMs", - "type": 1, + "type": 2, "optional": false }, { "name": "failure", + "type": 3, + "optional": false + }, + { + "name": "maxRetries", "type": 2, "optional": false }, { "name": "mode", - "type": 4, + "type": 5, "optional": false }, { "name": "policyKey", - "type": 3, + "type": 4, "optional": false }, { "name": "provider", - "type": 3, + "type": 4, "optional": false }, { "name": "retry", - "type": 1, + "type": 2, "optional": false }, { "name": "retryId", - "type": 3, + "type": 4, "optional": false }, { "name": "step", - "type": 1, + "type": 2, "optional": false }, { "name": "turn", - "type": 1, + "type": 2, "optional": false } ], @@ -57601,27 +59059,32 @@ "properties": [ { "name": "code", - "type": 3, + "type": 4, "optional": false }, { "name": "message", - "type": 3, + "type": 4, "optional": false }, + { + "name": "offloadImages", + "type": 2, + "optional": true + }, { "name": "providerRetryAfterMs", - "type": 1, + "type": 2, "optional": true }, { "name": "requestId", - "type": 3, + "type": 4, "optional": true }, { "name": "status", - "type": 1, + "type": 2, "optional": true } ], @@ -57631,6 +59094,61 @@ "kind": "primitive", "type": "string" }, + { + "kind": "literal", + "value": "normal" + }, + { + "kind": "object", + "properties": [ + { + "name": "delayMs", + "type": 2, + "optional": false + }, + { + "name": "failure", + "type": 3, + "optional": false + }, + { + "name": "mode", + "type": 7, + "optional": false + }, + { + "name": "policyKey", + "type": 4, + "optional": false + }, + { + "name": "provider", + "type": 4, + "optional": false + }, + { + "name": "retry", + "type": 2, + "optional": false + }, + { + "name": "retryId", + "type": 4, + "optional": false + }, + { + "name": "step", + "type": 2, + "optional": false + }, + { + "name": "turn", + "type": 2, + "optional": false + } + ], + "indices": [] + }, { "kind": "literal", "value": "always" @@ -57638,10 +59156,10 @@ ] }, "names": [ - "event:llm/retry.data[1]" + "packages/llm/llm-retry/src/types.ts#LlmRetryEventData" ], "sources": [ - "packages/llm/llm-retry/src/types.ts:30" + "packages/llm/llm-retry/src/types.ts:16" ] }, { @@ -57689,7 +59207,7 @@ "packages/llm/llm/src/types.ts#ToolCallBlock" ], "sources": [ - "packages/llm/llm/src/types.ts:91" + "packages/llm/llm/src/types.ts:104" ] }, { @@ -58080,6 +59598,1201 @@ ], "sources": [] }, + { + "digest": "c7b24be059d34d08929593049a1a81eaa3855166aacfe9d7603c0bc8bb728d56", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "union", + "types": [ + 1, + 4, + 6, + 8, + 19, + 22 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 2, + "optional": false + }, + { + "name": "id", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 9, + "optional": false + }, + { + "name": "offloaded", + "type": 17, + "optional": true + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 2, + "optional": false + }, + { + "name": "bytes", + "type": 10, + "optional": false + }, + { + "name": "height", + "type": 10, + "optional": false + }, + { + "name": "mediaType", + "type": 11, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": true + }, + { + "name": "originalDimensions", + "type": 16, + "optional": true + }, + { + "name": "width", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 12, + 13, + 14, + 15 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 10, + "optional": false + }, + { + "name": "width", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 20, + "optional": false + }, + { + "name": "type", + "type": 21, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 2, + "optional": false + }, + { + "name": "bytes", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 23, + "optional": false + }, + { + "name": "isError", + "type": 24, + "optional": true + }, + { + "name": "toolCallId", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 0 + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + } + ] + }, + "names": [ + "packages/llm/llm/src/types.ts#ContentBlock" + ], + "sources": [ + "packages/llm/llm/src/types.ts:137" + ] + }, + { + "digest": "c80c89da83c46db7a454f034c10f969e03cfb574859c7316e5bff683f5a14b0e", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 26, + "optional": true + }, + { + "name": "seq", + "type": 2, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 72, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "step", + "type": 2, + "optional": false + }, + { + "name": "stream", + "type": 3, + "optional": false + }, + { + "name": "turn", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 5, + 10, + 68, + 70 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "args", + "type": 6, + "optional": false + }, + { + "name": "dt", + "type": 8, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "time0", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "literal", + "value": "tool-call-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "chunk", + "type": 11, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 12, + 14, + 40, + 43, + 45, + 47, + 64 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "argumentsDelta", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "block", + "type": 15, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 39, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 16, + 18, + 28, + 31, + 35, + 37 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 19, + "optional": false + }, + { + "name": "offloaded", + "type": 26, + "optional": true + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "mediaType", + "type": 20, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 25, + "optional": true + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 21, + 22, + 23, + 24 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 29, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 32, + "optional": false + }, + { + "name": "isError", + "type": 33, + "optional": true + }, + { + "name": "toolCallId", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 15 + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "block-end" + }, + { + "kind": "object", + "properties": [ + { + "name": "blockType", + "type": 41, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 17, + 27, + 30, + 34, + 36, + 38 + ] + }, + { + "kind": "literal", + "value": "block-start" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 48, + "optional": false + }, + { + "name": "replayState", + "type": 60, + "optional": true + }, + { + "name": "type", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 49, + 52, + 54, + 56, + 58 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 50, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 7, + "optional": false + }, + { + "name": "message", + "type": 7, + "optional": false + }, + { + "name": "offloadImages", + "type": 2, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 2, + "optional": true + }, + { + "name": "requestId", + "type": 7, + "optional": true + }, + { + "name": "status", + "type": 2, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 50, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 61, + "optional": true + }, + { + "name": "response", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 62 + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "literal", + "value": "finish" + }, + { + "kind": "object", + "properties": [ + { + "name": "type", + "type": 65, + "optional": false + }, + { + "name": "usage", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "usage" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 2, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 2, + "optional": true + }, + { + "name": "inputTokens", + "type": 2, + "optional": false + }, + { + "name": "outputTokens", + "type": 2, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 2, + "optional": true + }, + { + "name": "totalTokens", + "type": 2, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "chunk" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 8, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "texts", + "type": 6, + "optional": false + }, + { + "name": "time0", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 69, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-chunks" + }, + { + "kind": "object", + "properties": [ + { + "name": "dt", + "type": 8, + "optional": false + }, + { + "name": "index", + "type": 2, + "optional": false + }, + { + "name": "texts", + "type": 6, + "optional": false + }, + { + "name": "time0", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 71, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-chunks" + }, + { + "kind": "literal", + "value": "assistant/attempt" + } + ] + }, + "names": [ + "event:assistant/attempt" + ], + "sources": [] + }, { "digest": "c8e6f48878cc2a2f0dc59682280d81f407943fb503f32c64bf9ff3d9393c3eff", "schema": { @@ -58094,6 +60807,718 @@ "names": [], "sources": [] }, + { + "digest": "c93b6f51b399607349b4bcc66b58e848a44250ffc1e2537207bd092dfff94192", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "chunk", + "type": 1, + "optional": false + }, + { + "name": "time", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 6, + 32, + 35, + 37, + 39, + 56 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "argumentsDelta", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "index", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": true + }, + { + "name": "type", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "tool-call-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "block", + "type": 7, + "optional": false + }, + { + "name": "index", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 31, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 8, + 10, + 20, + 23, + 27, + 29 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 3, + "optional": false + }, + { + "name": "id", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 11, + "optional": false + }, + { + "name": "offloaded", + "type": 18, + "optional": true + }, + { + "name": "type", + "type": 19, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 3, + "optional": false + }, + { + "name": "bytes", + "type": 4, + "optional": false + }, + { + "name": "height", + "type": 4, + "optional": false + }, + { + "name": "mediaType", + "type": 12, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": true + }, + { + "name": "originalDimensions", + "type": 17, + "optional": true + }, + { + "name": "width", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 13, + 14, + 15, + 16 + ] + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 4, + "optional": false + }, + { + "name": "width", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 21, + "optional": false + }, + { + "name": "type", + "type": 22, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 3, + "optional": false + }, + { + "name": "bytes", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 24, + "optional": false + }, + { + "name": "isError", + "type": 25, + "optional": true + }, + { + "name": "toolCallId", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 7 + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "literal", + "value": "block-end" + }, + { + "kind": "object", + "properties": [ + { + "name": "blockType", + "type": 33, + "optional": false + }, + { + "name": "index", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 9, + 19, + 22, + 26, + 28, + 30 + ] + }, + { + "kind": "literal", + "value": "block-start" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 4, + "optional": false + }, + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "index", + "type": 4, + "optional": false + }, + { + "name": "text", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text-delta" + }, + { + "kind": "object", + "properties": [ + { + "name": "reason", + "type": 40, + "optional": false + }, + { + "name": "replayState", + "type": 52, + "optional": true + }, + { + "name": "type", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 41, + 44, + 46, + 48, + 50 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 3, + "optional": false + }, + { + "name": "message", + "type": 3, + "optional": false + }, + { + "name": "offloadImages", + "type": 4, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 4, + "optional": true + }, + { + "name": "requestId", + "type": 3, + "optional": true + }, + { + "name": "status", + "type": 4, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "aborted" + }, + { + "kind": "object", + "properties": [ + { + "name": "failure", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 45, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "error" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 47, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "stop" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-calls" + }, + { + "kind": "object", + "properties": [ + { + "name": "blocks", + "type": 53, + "optional": true + }, + { + "name": "response", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 54 + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "literal", + "value": "finish" + }, + { + "kind": "object", + "properties": [ + { + "name": "type", + "type": 57, + "optional": false + }, + { + "name": "usage", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "usage" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 4, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 4, + "optional": true + }, + { + "name": "inputTokens", + "type": 4, + "optional": false + }, + { + "name": "outputTokens", + "type": 4, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 4, + "optional": true + }, + { + "name": "totalTokens", + "type": 4, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "chunk" + } + ] + }, + "names": [ + "event:assistant/attempt.data.stream[0][3]" + ], + "sources": [ + "packages/llm/llm/src/assistant-stream.ts:44" + ] + }, { "digest": "c99e20441bf15341e9db54385e558e5c03d21dff2eb1ac08ccbf47c2d8747954", "schema": { @@ -58122,79 +61547,6 @@ "names": [], "sources": [] }, - { - "digest": "ca13866102dfc53a503fccddeebc9caf667b8a06ef27217dd65de29f0495e65d", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 1, - "optional": false - }, - { - "name": "kind", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 2, - "optional": false - }, - { - "name": "message", - "type": 2, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 3, - "optional": true - }, - { - "name": "requestId", - "type": 2, - "optional": true - }, - { - "name": "status", - "type": 3, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "error" - } - ] - }, - "names": [ - "event:assistant/attempt.data.stream[0][3].chunk[6].reason[1]" - ], - "sources": [ - "packages/llm/llm/src/types.ts:135" - ] - }, { "digest": "ca59530d1a4f3e191b4032bc4a20eded0d827db8b5a7efb27b3af9e9b456c6f2", "schema": { @@ -58455,6 +61807,124 @@ "names": [], "sources": [] }, + { + "digest": "cdee10e87dbed8440d8b6935888624884878efc7af95402e94171b29f173dced", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "delayMs", + "type": 1, + "optional": false + }, + { + "name": "failure", + "type": 2, + "optional": false + }, + { + "name": "maxRetries", + "type": 1, + "optional": false + }, + { + "name": "mode", + "type": 4, + "optional": false + }, + { + "name": "policyKey", + "type": 3, + "optional": false + }, + { + "name": "provider", + "type": 3, + "optional": false + }, + { + "name": "retry", + "type": 1, + "optional": false + }, + { + "name": "retryId", + "type": 3, + "optional": false + }, + { + "name": "step", + "type": 1, + "optional": false + }, + { + "name": "turn", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 3, + "optional": false + }, + { + "name": "message", + "type": 3, + "optional": false + }, + { + "name": "offloadImages", + "type": 1, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 1, + "optional": true + }, + { + "name": "requestId", + "type": 3, + "optional": true + }, + { + "name": "status", + "type": 1, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "normal" + } + ] + }, + "names": [ + "event:llm/retry.data[0]" + ], + "sources": [ + "packages/llm/llm-retry/src/types.ts:17" + ] + }, { "digest": "cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331", "schema": { @@ -58770,7 +62240,7 @@ "sources": [] }, { - "digest": "d489162978ec04f1f76f40a68c7715326f46ec4f696edf1f321efdf88545fc18", + "digest": "d3e447062a2847dad847dad5168a9508d92c484363dfc76fd82605aa7bd46285", "schema": { "root": 0, "nodes": [ @@ -58778,15 +62248,10 @@ "kind": "object", "properties": [ { - "name": "message", + "name": "reason", "type": 1, "optional": false }, - { - "name": "step", - "type": 9, - "optional": false - }, { "name": "turn", "type": 9, @@ -58795,69 +62260,95 @@ ], "indices": [] }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "role", - "type": 27, - "optional": false - }, - { - "name": "source", - "type": 28, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 3 - }, { "kind": "union", "types": [ + 2, 4, - 7, - 17, - 20, - 23, - 25 + 6, + 11, + 24, + 26 ] }, { "kind": "object", "properties": [ { - "name": "arguments", + "name": "kind", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "interrupted" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", "type": 5, "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "max-tokens" + }, + { + "kind": "object", + "properties": [ + { + "name": "error", + "type": 7, + "optional": false }, { - "name": "id", - "type": 5, + "name": "kind", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 8, "optional": false }, { - "name": "name", - "type": 5, + "name": "message", + "type": 8, "optional": false }, { - "name": "type", - "type": 6, - "optional": false + "name": "offloadImages", + "type": 9, + "optional": true + }, + { + "name": "providerRetryAfterMs", + "type": 9, + "optional": true + }, + { + "name": "requestId", + "type": 8, + "optional": true + }, + { + "name": "status", + "type": 9, + "optional": true } ], "indices": [] @@ -58866,107 +62357,25 @@ "kind": "primitive", "type": "string" }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 8, - "optional": false - }, - { - "name": "type", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 9, - "optional": false - }, - { - "name": "height", - "type": 9, - "optional": false - }, - { - "name": "mediaType", - "type": 10, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": true - }, - { - "name": "originalDimensions", - "type": 15, - "optional": true - }, - { - "name": "width", - "type": 9, - "optional": false - } - ], - "indices": [] - }, { "kind": "primitive", "type": "number" }, - { - "kind": "union", - "types": [ - 11, - 12, - 13, - 14 - ] - }, { "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" + "value": "error" }, { "kind": "object", "properties": [ { - "name": "height", - "type": 9, + "name": "kind", + "type": 12, "optional": false }, { - "name": "width", - "type": 9, + "name": "reason", + "type": 13, "optional": false } ], @@ -58974,94 +62383,74 @@ }, { "kind": "literal", - "value": "image" + "value": "aborted" + }, + { + "kind": "union", + "types": [ + 14, + 16, + 18, + 20, + 22 + ] }, { "kind": "object", "properties": [ { - "name": "attachment", - "type": 18, + "name": "kind", + "type": 15, "optional": false - }, + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "disposed" + }, + { + "kind": "object", + "properties": [ { - "name": "type", + "name": "kind", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "legacy" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", "type": 19, "optional": false } ], "indices": [] }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, { "kind": "literal", - "value": "file" + "value": "parent" }, { "kind": "object", "properties": [ { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "isError", + "name": "kind", "type": 21, - "optional": true - }, - { - "name": "toolCallId", - "type": 5, "optional": false }, { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 24, + "name": "reason", + "type": 8, "optional": false } ], @@ -59069,251 +62458,60 @@ }, { "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "system" - }, - { - "kind": "union", - "types": [ - 29, - 34, - 36, - 38, - 40, - 42, - 44 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 30, - "optional": false - }, - { - "name": "kind", - "type": 31, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - }, - { - "name": "sections", - "type": 32, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 33 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "text", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 35, - "optional": false - }, - { - "name": "kind", - "type": 31, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 37, - "optional": false - }, - { - "name": "kind", - "type": 31, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 39, - "optional": false - }, - { - "name": "kind", - "type": 31, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 31, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 43, - "optional": false - }, - { - "name": "kind", - "type": 31, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "instructions" + "value": "hook" }, { "kind": "object", "properties": [ { "name": "kind", - "type": 31, - "optional": false - }, - { - "name": "plugin", - "type": 5, + "type": 23, "optional": false } ], "indices": [] + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "blocked" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "completed" } ] }, "names": [ - "event:system/message.data" + "event:turn/end.data" ], "sources": [ - "packages/core/session/src/types.ts:310" + "packages/core/session/src/types.ts:285" ] }, { @@ -59489,6 +62687,299 @@ ], "sources": [] }, + { + "digest": "d61574d68558c5a184429e70b04da393dd5460bf5cc3ddef814546e659bd5127", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "isError", + "type": 24, + "optional": true + }, + { + "name": "toolCallId", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "union", + "types": [ + 0, + 3, + 6, + 8, + 10, + 21 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 11, + "optional": false + }, + { + "name": "offloaded", + "type": 19, + "optional": true + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 4, + "optional": false + }, + { + "name": "bytes", + "type": 12, + "optional": false + }, + { + "name": "height", + "type": 12, + "optional": false + }, + { + "name": "mediaType", + "type": 13, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "originalDimensions", + "type": 18, + "optional": true + }, + { + "name": "width", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 14, + 15, + 16, + 17 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 12, + "optional": false + }, + { + "name": "width", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 22, + "optional": false + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 4, + "optional": false + }, + { + "name": "bytes", + "type": 12, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + } + ] + }, + "names": [ + "packages/llm/llm/src/types.ts#ToolResultBlock" + ], + "sources": [ + "packages/llm/llm/src/types.ts:114" + ] + }, { "digest": "d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9", "schema": { @@ -60233,32 +63724,151 @@ "sources": [] }, { - "digest": "e1161343902a8da654d4d1bb66d729065cc93a2710ebfe2717a242a5c2ec709a", + "digest": "e0b5bf44c27bbfc6ab144239e3e4169c4d21146645e0e3121bbb359c3bc8591d", "schema": { "root": 0, "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 19, + "optional": true + }, + { + "name": "seq", + "type": 2, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 91, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "maxTokens", + "type": 2, + "optional": false + }, + { + "name": "messageSeqs", + "type": 3, + "optional": false + }, + { + "name": "messages", + "type": 4, + "optional": false + }, + { + "name": "route", + "type": 90, + "optional": false + }, + { + "name": "system", + "type": 9, + "optional": false + }, + { + "name": "titleProvider", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 9, + "optional": false + }, + { + "name": "role", + "type": 31, + "optional": false + }, + { + "name": "source", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 7 + }, { "kind": "union", "types": [ - 1, - 4, - 6, 8, - 18, - 21 + 11, + 21, + 24, + 27, + 29 ] }, { "kind": "object", "properties": [ { - "name": "text", - "type": 2, + "name": "arguments", + "type": 9, + "optional": false + }, + { + "name": "id", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 9, "optional": false }, { "name": "type", - "type": 3, + "type": 10, "optional": false } ], @@ -60268,56 +63878,6 @@ "kind": "primitive", "type": "string" }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 2, - "optional": false - }, - { - "name": "id", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": false - }, - { - "name": "type", - "type": 7, - "optional": false - } - ], - "indices": [] - }, { "kind": "literal", "value": "tool-call" @@ -60327,114 +63887,13 @@ "properties": [ { "name": "attachment", - "type": 9, + "type": 12, "optional": false }, { - "name": "type", - "type": 17, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 2, - "optional": false - }, - { - "name": "bytes", - "type": 10, - "optional": false - }, - { - "name": "height", - "type": 10, - "optional": false - }, - { - "name": "mediaType", - "type": 11, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": true - }, - { - "name": "originalDimensions", - "type": 16, - "optional": true - }, - { - "name": "width", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 12, - 13, - 14, - 15 - ] - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 10, - "optional": false - }, - { - "name": "width", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", + "name": "offloaded", "type": 19, - "optional": false + "optional": true }, { "name": "type", @@ -60449,22 +63908,128 @@ "properties": [ { "name": "attachmentId", - "type": 2, + "type": 9, "optional": false }, { "name": "bytes", - "type": 10, + "type": 2, + "optional": false + }, + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "mediaType", + "type": 13, "optional": false }, { "name": "name", + "type": 9, + "optional": true + }, + { + "name": "originalDimensions", + "type": 18, + "optional": true + }, + { + "name": "width", "type": 2, "optional": false } ], "indices": [] }, + { + "kind": "union", + "types": [ + 14, + 15, + 16, + 17 + ] + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 22, + "optional": false + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 9, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + } + ], + "indices": [] + }, { "kind": "literal", "value": "file" @@ -60474,31 +64039,27 @@ "properties": [ { "name": "content", - "type": 22, + "type": 6, "optional": false }, { "name": "isError", - "type": 23, + "type": 25, "optional": true }, { "name": "toolCallId", - "type": 2, + "type": 9, "optional": false }, { "name": "type", - "type": 24, + "type": 26, "optional": false } ], "indices": [] }, - { - "kind": "array", - "element": 0 - }, { "kind": "primitive", "type": "boolean" @@ -60506,15 +64067,839 @@ { "kind": "literal", "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 9, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "union", + "types": [ + 32, + 33, + 34 + ] + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 36, + 45, + 47, + 48, + 51, + 56, + 58, + 63, + 64, + 65, + 67, + 69, + 70, + 77, + 79, + 81, + 83, + 85, + 88, + 89 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 19, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 9, + "optional": true + }, + { + "name": "changes", + "type": 37, + "optional": false + }, + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 38 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 39, + "optional": false + }, + { + "name": "digest", + "type": 9, + "optional": true + }, + { + "name": "path", + "type": 9, + "optional": false + }, + { + "name": "scope", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 40, + 41, + 42 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 9, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 9, + "optional": true + }, + { + "name": "kind", + "type": 34, + "optional": false + }, + { + "name": "rpcId", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 9, + "optional": false + }, + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + }, + { + "name": "provider", + "type": 9, + "optional": false + }, + { + "name": "ruleId", + "type": 9, + "optional": false + }, + { + "name": "source", + "type": 9, + "optional": false + }, + { + "name": "summary", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 52, + "optional": false + }, + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "update", + "type": 19, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 53 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "name", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 59, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + }, + { + "name": "sections", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "plugin" + }, + { + "kind": "array", + "element": 62 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 9, + "optional": false + }, + { + "name": "text", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + }, + { + "name": "summary", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 66, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 68, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 66, + "optional": false + }, + { + "name": "kind", + "type": 71, + "optional": false + }, + { + "name": "references", + "type": 72, + "optional": false + }, + { + "name": "version", + "type": 76, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 73 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 2, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 74, + "optional": false + }, + { + "name": "compacted", + "type": 25, + "optional": false + }, + { + "name": "inputIndex", + "type": 2, + "optional": false + }, + { + "name": "label", + "type": 9, + "optional": false + }, + { + "name": "omittedBytes", + "type": 2, + "optional": false + }, + { + "name": "omittedMessages", + "type": 2, + "optional": false + }, + { + "name": "originalMessages", + "type": 2, + "optional": false + }, + { + "name": "retainedMessages", + "type": 2, + "optional": false + }, + { + "name": "sessionId", + "type": 9, + "optional": false + }, + { + "name": "truncated", + "type": 25, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 75 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 49, + "optional": false + }, + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "senderSessionId", + "type": 9, + "optional": false + }, + { + "name": "summary", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 68, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "senderSessionId", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 9, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "revision", + "type": 2, + "optional": false + }, + { + "name": "round", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 84, + "optional": false + }, + { + "name": "messageId", + "type": 9, + "optional": false + }, + { + "name": "senderId", + "type": 9, + "optional": false + }, + { + "name": "senderName", + "type": 9, + "optional": false + }, + { + "name": "teamId", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 86, + "optional": false + }, + { + "name": "model", + "type": 9, + "optional": false + }, + { + "name": "provider", + "type": 9, + "optional": false + }, + { + "name": "replayState", + "type": 87, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "plugin", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "model", + "type": 9, + "optional": false + }, + { + "name": "provider", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session/title-llm-request" } ] }, "names": [ - "packages/llm/llm/src/types.ts#ContentBlock" + "event:session/title-llm-request" ], - "sources": [ - "packages/llm/llm/src/types.ts:124" - ] + "sources": [] }, { "digest": "e1d0e52dcd1855ffb803efc18f8f7b776c48d1c25757e8a75c0281fa298c02e1", @@ -60788,260 +65173,6 @@ "names": [], "sources": [] }, - { - "digest": "e3528b95cff70d83a080a96666877da3f9dc205e71955d6bc5b9b315b9e0da22", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "union", - "types": [ - 1, - 3, - 5, - 10, - 23, - 25 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "interrupted" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "error", - "type": 6, - "optional": false - }, - { - "name": "kind", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 7, - "optional": false - }, - { - "name": "message", - "type": 7, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 8, - "optional": true - }, - { - "name": "requestId", - "type": 7, - "optional": true - }, - { - "name": "status", - "type": 8, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 11, - "optional": false - }, - { - "name": "reason", - "type": 12, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "union", - "types": [ - 13, - 15, - 17, - 19, - 21 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 14, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "disposed" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "legacy" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 18, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "parent" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 20, - "optional": false - }, - { - "name": "reason", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "hook" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "blocked" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "completed" - } - ] - }, - "names": [ - "packages/core/session/src/types.ts#TurnEndReason" - ], - "sources": [ - "packages/core/session/src/types.ts:224" - ] - }, { "digest": "e3e77b26f0148755a505f5b8ea843a3be117827d4703bb5754d9dc8ddbef521f", "schema": { @@ -61122,7 +65253,7 @@ "SessionEventEnvelope[38]", "SessionEventEnvelope[39]", "SessionEventEnvelope[3]", - "SessionEventEnvelope[41]", + "SessionEventEnvelope[40]", "SessionEventEnvelope[42]", "SessionEventEnvelope[43]", "SessionEventEnvelope[44]", @@ -61135,9 +65266,10 @@ "SessionEventEnvelope[50]", "SessionEventEnvelope[51]", "SessionEventEnvelope[52]", - "SessionEventEnvelope[54]", + "SessionEventEnvelope[53]", "SessionEventEnvelope[55]", - "SessionEventEnvelope[57]", + "SessionEventEnvelope[56]", + "SessionEventEnvelope[58]", "SessionEventEnvelope[5]", "SessionEventEnvelope[7]", "SessionEventEnvelope[8]", @@ -61254,6 +65386,363 @@ "names": [], "sources": [] }, + { + "digest": "e50b0a3ed8e6c80b672e4f855c91984f03b5c3fdd5d475bf171171f391a86c83", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "role", + "type": 27, + "optional": false + }, + { + "name": "source", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "union", + "types": [ + 3, + 7, + 9, + 11, + 13, + 24 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 1, + "optional": false + }, + { + "name": "isError", + "type": 4, + "optional": true + }, + { + "name": "toolCallId", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 14, + "optional": false + }, + { + "name": "offloaded", + "type": 22, + "optional": true + }, + { + "name": "type", + "type": 23, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 15, + "optional": false + }, + { + "name": "height", + "type": 15, + "optional": false + }, + { + "name": "mediaType", + "type": 16, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 21, + "optional": true + }, + { + "name": "width", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 17, + 18, + 19, + 20 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 15, + "optional": false + }, + { + "name": "width", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 25, + "optional": false + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 15, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 29, + "optional": false + }, + { + "name": "model", + "type": 5, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "replayState", + "type": 30, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + } + ] + }, + "names": [ + "packages/llm/llm/src/message.ts#AssistantMessage" + ], + "sources": [ + "packages/llm/llm/src/message.ts:148" + ] + }, { "digest": "e5244812c495a80c2e78ba2c2690dca5f6b58ea8d3fe5531ff94a6f9ac5e5d66", "schema": { @@ -61698,7 +66187,612 @@ ] }, { - "digest": "eb1d32dcd3d76833392f351e039dcbd40686a1ab24b252a7a5003426f9cf4c89", + "digest": "ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "image/webp" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 4, + "optional": true + }, + { + "name": "seq", + "type": 5, + "optional": false + }, + { + "name": "time", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": false + }, + { + "name": "parentCallId", + "type": 3, + "optional": false + }, + { + "name": "rootCallId", + "type": 3, + "optional": false + }, + { + "name": "subCallId", + "type": 3, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "tool/ptc-dispatch-start" + } + ] + }, + "names": [ + "event:tool/ptc-dispatch-start" + ], + "sources": [] + }, + { + "digest": "eefbea819caeeea91ccb02dfafc16c68dd7f961cf9c1b2be25a2084509cf5c5a", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "block-end" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "ef11a3b7fa2033ae4279545ff8a8a2ee4e301b9b73e5b91654c48e6d08283545", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "step", + "type": 1, + "optional": false + }, + { + "name": "turn", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + } + ] + }, + "names": [ + "event:step/end.data", + "event:step/start.data" + ], + "sources": [ + "packages/core/session/src/types.ts:287", + "packages/core/session/src/types.ts:289" + ] + }, + { + "digest": "ef4f00923b9edbae15c7ef05320280bf61147ca17146cf59690359bfd5e5da45", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "compaction/summary" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "f05fb85e715af4ab4de0b6ba111166dd0435bd8bb74a0fa0f32b9d174113fc7c", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "task-result" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "model", + "type": 1, + "optional": false + }, + { + "name": "provider", + "type": 1, + "optional": false + }, + { + "name": "reasoningEffort", + "type": 1, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + } + ] + }, + "names": [ + "packages/api/session-controller/src/types.ts#ModelSelection" + ], + "sources": [ + "packages/api/session-controller/src/types.ts:86" + ] + }, + { + "digest": "f1996af2602e6f62b5ab179ca5c4af00d8753938b74cc6ef102de09e99e74a7e", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "command/run" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "f19bee84bf3d20904553cdcd57465232bc1c2a194f54c4d195292ba0487223a6", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 1, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 1, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + } + ] + }, + "names": [ + "packages/attachment/attachment/src/types.ts#FileAttachmentRef" + ], + "sources": [ + "packages/attachment/attachment/src/types.ts:39" + ] + }, + { + "digest": "f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "type", + "type": 1, + "optional": false + }, + { + "name": "usage", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "usage" + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 3, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 3, + "optional": true + }, + { + "name": "inputTokens", + "type": 3, + "optional": false + }, + { + "name": "outputTokens", + "type": 3, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 3, + "optional": true + }, + { + "name": "totalTokens", + "type": 3, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + } + ] + }, + "names": [ + "event:assistant/attempt.data.stream[0][3].chunk[5]" + ], + "sources": [ + "packages/llm/llm/src/types.ts:430" + ] + }, + { + "digest": "f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 1, + "optional": false + }, + { + "name": "type", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 2, + "optional": false + }, + { + "name": "bytes", + "type": 3, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "file" + } + ] + }, + "names": [ + "packages/llm/llm/src/types.ts#FileBlock" + ], + "sources": [ + "packages/llm/llm/src/types.ts:97" + ] + }, + { + "digest": "f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "in-history" + } + ] + }, + "names": [ + "packages/llm/llm/src/types.ts#SystemPromptUpdate" + ], + "sources": [ + "packages/llm/llm/src/types.ts:381" + ] + }, + { + "digest": "f56c5b59ca0161310c338d31dd16cf14287ccd602ff4181058df178c1c286d7f", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 1, + "optional": false + }, + { + "name": "kind", + "type": 2, + "optional": false + }, + { + "name": "references", + "type": 3, + "optional": false + }, + { + "name": "version", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 5, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 6, + "optional": false + }, + { + "name": "compacted", + "type": 8, + "optional": false + }, + { + "name": "inputIndex", + "type": 5, + "optional": false + }, + { + "name": "label", + "type": 9, + "optional": false + }, + { + "name": "omittedBytes", + "type": 5, + "optional": false + }, + { + "name": "omittedMessages", + "type": 5, + "optional": false + }, + { + "name": "originalMessages", + "type": 5, + "optional": false + }, + { + "name": "retainedMessages", + "type": 5, + "optional": false + }, + { + "name": "sessionId", + "type": 9, + "optional": false + }, + { + "name": "truncated", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 5, + 7 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": 1 + } + ] + }, + "names": [ + "packages/context/session-reference/src/types.ts#SessionReferenceSource" + ], + "sources": [ + "packages/context/session-reference/src/types.ts:13" + ] + }, + { + "digest": "f634871d1e18b1eb199497ac8ab72d3964f48d03fbf611dc7521a1970cf429d9", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "plan/mode" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "f6cb462f808309e315fea634f1ed86268b8346bbcee6774d9097ca6eed7ac1f8", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "literal", + "value": "replace" + } + ] + }, + "names": [], + "sources": [] + }, + { + "digest": "f6f3f30109e9008fccf7da2a3268a63da8646b59dd7cf7b75bcd9cf08ac08a59", "schema": { "root": 0, "nodes": [ @@ -61945,6 +67039,11 @@ "type": 18, "optional": false }, + { + "name": "offloaded", + "type": 4, + "optional": true + }, { "name": "type", "type": 25, @@ -62208,4755 +67307,6 @@ ], "sources": [] }, - { - "digest": "ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "image/webp" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 4, - "optional": true - }, - { - "name": "seq", - "type": 5, - "optional": false - }, - { - "name": "time", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": false - }, - { - "name": "parentCallId", - "type": 3, - "optional": false - }, - { - "name": "rootCallId", - "type": 3, - "optional": false - }, - { - "name": "subCallId", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "tool/ptc-dispatch-start" - } - ] - }, - "names": [ - "event:tool/ptc-dispatch-start" - ], - "sources": [] - }, - { - "digest": "ecaf9469000a15d46d269ea72994ceaab3652ed31507822906fc12397422da21", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "union", - "types": [ - 1, - 7, - 64, - 66 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "args", - "type": 2, - "optional": false - }, - { - "name": "dt", - "type": 4, - "optional": false - }, - { - "name": "id", - "type": 3, - "optional": false - }, - { - "name": "index", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": true - }, - { - "name": "time0", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "array", - "element": 5 - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "tool-call-chunks" - }, - { - "kind": "object", - "properties": [ - { - "name": "chunk", - "type": 8, - "optional": false - }, - { - "name": "time", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 63, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 9, - 11, - 36, - 39, - 41, - 43, - 60 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "argumentsDelta", - "type": 3, - "optional": false - }, - { - "name": "id", - "type": 3, - "optional": false - }, - { - "name": "index", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": true - }, - { - "name": "type", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "block", - "type": 12, - "optional": false - }, - { - "name": "index", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 35, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 13, - 15, - 24, - 27, - 31, - 33 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 3, - "optional": false - }, - { - "name": "id", - "type": 3, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 14, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 16, - "optional": false - }, - { - "name": "type", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 3, - "optional": false - }, - { - "name": "bytes", - "type": 5, - "optional": false - }, - { - "name": "height", - "type": 5, - "optional": false - }, - { - "name": "mediaType", - "type": 17, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": true - }, - { - "name": "originalDimensions", - "type": 22, - "optional": true - }, - { - "name": "width", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 18, - 19, - 20, - 21 - ] - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 5, - "optional": false - }, - { - "name": "width", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 25, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 3, - "optional": false - }, - { - "name": "bytes", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 3, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 28, - "optional": false - }, - { - "name": "isError", - "type": 29, - "optional": true - }, - { - "name": "toolCallId", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 30, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 12 - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 32, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 34, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "block-end" - }, - { - "kind": "object", - "properties": [ - { - "name": "blockType", - "type": 37, - "optional": false - }, - { - "name": "index", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 14, - 23, - 26, - 30, - 32, - 34 - ] - }, - { - "kind": "literal", - "value": "block-start" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 5, - "optional": false - }, - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "index", - "type": 5, - "optional": false - }, - { - "name": "text", - "type": 3, - "optional": false - }, - { - "name": "type", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-delta" - }, - { - "kind": "object", - "properties": [ - { - "name": "reason", - "type": 44, - "optional": false - }, - { - "name": "replayState", - "type": 56, - "optional": true - }, - { - "name": "type", - "type": 59, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 45, - 48, - 50, - 52, - 54 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 46, - "optional": false - }, - { - "name": "kind", - "type": 47, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "code", - "type": 3, - "optional": false - }, - { - "name": "message", - "type": 3, - "optional": false - }, - { - "name": "providerRetryAfterMs", - "type": 5, - "optional": true - }, - { - "name": "requestId", - "type": 3, - "optional": true - }, - { - "name": "status", - "type": 5, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "aborted" - }, - { - "kind": "object", - "properties": [ - { - "name": "failure", - "type": 46, - "optional": false - }, - { - "name": "kind", - "type": 49, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "error" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 51, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "max-tokens" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 53, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "stop" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 55, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-calls" - }, - { - "kind": "object", - "properties": [ - { - "name": "blocks", - "type": 57, - "optional": true - }, - { - "name": "response", - "type": 58, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 58 - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "literal", - "value": "finish" - }, - { - "kind": "object", - "properties": [ - { - "name": "type", - "type": 61, - "optional": false - }, - { - "name": "usage", - "type": 62, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "usage" - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 5, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 5, - "optional": true - }, - { - "name": "inputTokens", - "type": 5, - "optional": false - }, - { - "name": "outputTokens", - "type": 5, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 5, - "optional": true - }, - { - "name": "totalTokens", - "type": 5, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "chunk" - }, - { - "kind": "object", - "properties": [ - { - "name": "dt", - "type": 4, - "optional": false - }, - { - "name": "index", - "type": 5, - "optional": false - }, - { - "name": "texts", - "type": 2, - "optional": false - }, - { - "name": "time0", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 65, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning-chunks" - }, - { - "kind": "object", - "properties": [ - { - "name": "dt", - "type": 4, - "optional": false - }, - { - "name": "index", - "type": 5, - "optional": false - }, - { - "name": "texts", - "type": 2, - "optional": false - }, - { - "name": "time0", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 67, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text-chunks" - } - ] - }, - "names": [ - "packages/llm/llm/src/assistant-stream.ts#AssistantStreamRecord" - ], - "sources": [ - "packages/llm/llm/src/assistant-stream.ts:20" - ] - }, - { - "digest": "eefbea819caeeea91ccb02dfafc16c68dd7f961cf9c1b2be25a2084509cf5c5a", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "block-end" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "ef11a3b7fa2033ae4279545ff8a8a2ee4e301b9b73e5b91654c48e6d08283545", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "step", - "type": 1, - "optional": false - }, - { - "name": "turn", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - } - ] - }, - "names": [ - "event:step/end.data", - "event:step/start.data" - ], - "sources": [ - "packages/core/session/src/types.ts:287", - "packages/core/session/src/types.ts:289" - ] - }, - { - "digest": "ef4f00923b9edbae15c7ef05320280bf61147ca17146cf59690359bfd5e5da45", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "compaction/summary" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "f051068babb7ebab80e9fd4fff3a70cfe54430c2ef3ca9bb77bb54db8bc34476", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "role", - "type": 26, - "optional": false - }, - { - "name": "source", - "type": 30, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 2 - }, - { - "kind": "union", - "types": [ - 3, - 6, - 16, - 19, - 22, - 24 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 4, - "optional": false - }, - { - "name": "id", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 15, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 8, - "optional": false - }, - { - "name": "height", - "type": 8, - "optional": false - }, - { - "name": "mediaType", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": true - }, - { - "name": "originalDimensions", - "type": 14, - "optional": true - }, - { - "name": "width", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 10, - 11, - 12, - 13 - ] - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 8, - "optional": false - }, - { - "name": "width", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 17, - "optional": false - }, - { - "name": "type", - "type": 18, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 4, - "optional": false - }, - { - "name": "bytes", - "type": 8, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 1, - "optional": false - }, - { - "name": "isError", - "type": 20, - "optional": true - }, - { - "name": "toolCallId", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 4, - "optional": false - }, - { - "name": "type", - "type": 25, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "union", - "types": [ - 27, - 28, - 29 - ] - }, - { - "kind": "literal", - "value": "system" - }, - { - "kind": "literal", - "value": "assistant" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "union", - "types": [ - 31, - 41, - 43, - 44, - 47, - 52, - 54, - 59, - 60, - 61, - 63, - 65, - 66, - 73, - 75, - 77, - 79, - 81, - 84, - 85 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "baseline", - "type": 32, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 4, - "optional": true - }, - { - "name": "changes", - "type": 33, - "optional": false - }, - { - "name": "form", - "type": 39, - "optional": false - }, - { - "name": "kind", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 34 - }, - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 35, - "optional": false - }, - { - "name": "digest", - "type": 4, - "optional": true - }, - { - "name": "path", - "type": 4, - "optional": false - }, - { - "name": "scope", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 36, - 37, - 38 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 4, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "object", - "properties": [ - { - "name": "clientTimeZone", - "type": 4, - "optional": true - }, - { - "name": "kind", - "type": 29, - "optional": false - }, - { - "name": "rpcId", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 4, - "optional": false - }, - { - "name": "form", - "type": 45, - "optional": false - }, - { - "name": "kind", - "type": 46, - "optional": false - }, - { - "name": "provider", - "type": 4, - "optional": false - }, - { - "name": "ruleId", - "type": 4, - "optional": false - }, - { - "name": "source", - "type": 4, - "optional": false - }, - { - "name": "summary", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - }, - { - "kind": "object", - "properties": [ - { - "name": "entries", - "type": 48, - "optional": false - }, - { - "name": "form", - "type": 50, - "optional": false - }, - { - "name": "kind", - "type": 51, - "optional": false - }, - { - "name": "update", - "type": 32, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 49 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 4, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 39, - "optional": false - }, - { - "name": "kind", - "type": 53, - "optional": false - }, - { - "name": "name", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - }, - { - "name": "sections", - "type": 57, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 58 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 4, - "optional": false - }, - { - "name": "text", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 45, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - }, - { - "name": "summary", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 39, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 62, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 64, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 50, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 62, - "optional": false - }, - { - "name": "kind", - "type": 67, - "optional": false - }, - { - "name": "references", - "type": 68, - "optional": false - }, - { - "name": "version", - "type": 72, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 69 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 8, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 70, - "optional": false - }, - { - "name": "compacted", - "type": 20, - "optional": false - }, - { - "name": "inputIndex", - "type": 8, - "optional": false - }, - { - "name": "label", - "type": 4, - "optional": false - }, - { - "name": "omittedBytes", - "type": 8, - "optional": false - }, - { - "name": "omittedMessages", - "type": 8, - "optional": false - }, - { - "name": "originalMessages", - "type": 8, - "optional": false - }, - { - "name": "retainedMessages", - "type": 8, - "optional": false - }, - { - "name": "sessionId", - "type": 4, - "optional": false - }, - { - "name": "truncated", - "type": 20, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 8, - 71 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 45, - "optional": false - }, - { - "name": "kind", - "type": 74, - "optional": false - }, - { - "name": "senderSessionId", - "type": 4, - "optional": false - }, - { - "name": "summary", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 64, - "optional": false - }, - { - "name": "kind", - "type": 76, - "optional": false - }, - { - "name": "senderSessionId", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 4, - "optional": false - }, - { - "name": "kind", - "type": 78, - "optional": false - }, - { - "name": "revision", - "type": 8, - "optional": false - }, - { - "name": "round", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 80, - "optional": false - }, - { - "name": "messageId", - "type": 4, - "optional": false - }, - { - "name": "senderId", - "type": 4, - "optional": false - }, - { - "name": "senderName", - "type": 4, - "optional": false - }, - { - "name": "teamId", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 82, - "optional": false - }, - { - "name": "model", - "type": 4, - "optional": false - }, - { - "name": "provider", - "type": 4, - "optional": false - }, - { - "name": "replayState", - "type": 83, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 29, - "optional": false - } - ], - "indices": [] - } - ] - }, - "names": [ - "packages/llm/llm/src/message.ts#Message" - ], - "sources": [ - "packages/llm/llm/src/message.ts:131" - ] - }, - { - "digest": "f05fb85e715af4ab4de0b6ba111166dd0435bd8bb74a0fa0f32b9d174113fc7c", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "task-result" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "model", - "type": 1, - "optional": false - }, - { - "name": "provider", - "type": 1, - "optional": false - }, - { - "name": "reasoningEffort", - "type": 1, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - } - ] - }, - "names": [ - "packages/api/session-controller/src/types.ts#ModelSelection" - ], - "sources": [ - "packages/api/session-controller/src/types.ts:86" - ] - }, - { - "digest": "f1996af2602e6f62b5ab179ca5c4af00d8753938b74cc6ef102de09e99e74a7e", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "command/run" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "f19bee84bf3d20904553cdcd57465232bc1c2a194f54c4d195292ba0487223a6", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 1, - "optional": false - }, - { - "name": "bytes", - "type": 2, - "optional": false - }, - { - "name": "name", - "type": 1, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - } - ] - }, - "names": [ - "packages/attachment/attachment/src/types.ts#FileAttachmentRef" - ], - "sources": [ - "packages/attachment/attachment/src/types.ts:39" - ] - }, - { - "digest": "f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "type", - "type": 1, - "optional": false - }, - { - "name": "usage", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "usage" - }, - { - "kind": "object", - "properties": [ - { - "name": "cacheReadTokens", - "type": 3, - "optional": true - }, - { - "name": "cacheWriteTokens", - "type": 3, - "optional": true - }, - { - "name": "inputTokens", - "type": 3, - "optional": false - }, - { - "name": "outputTokens", - "type": 3, - "optional": false - }, - { - "name": "reasoningTokens", - "type": 3, - "optional": true - }, - { - "name": "totalTokens", - "type": 3, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - } - ] - }, - "names": [ - "event:assistant/attempt.data.stream[0][3].chunk[5]" - ], - "sources": [ - "packages/llm/llm/src/types.ts:396" - ] - }, - { - "digest": "f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 1, - "optional": false - }, - { - "name": "type", - "type": 4, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 2, - "optional": false - }, - { - "name": "bytes", - "type": 3, - "optional": false - }, - { - "name": "name", - "type": 2, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "literal", - "value": "file" - } - ] - }, - "names": [ - "packages/llm/llm/src/types.ts#FileBlock" - ], - "sources": [ - "packages/llm/llm/src/types.ts:84" - ] - }, - { - "digest": "f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "in-history" - } - ] - }, - "names": [ - "packages/llm/llm/src/types.ts#SystemPromptUpdate" - ], - "sources": [ - "packages/llm/llm/src/types.ts:347" - ] - }, - { - "digest": "f3cfc98798d8b0e726ee22b05b13ada5cb19e9cdbe105ef3529699de99ce357a", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "array", - "element": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "role", - "type": 27, - "optional": false - }, - { - "name": "source", - "type": 31, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "union", - "types": [ - 4, - 7, - 17, - 20, - 23, - 25 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 5, - "optional": false - }, - { - "name": "id", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 8, - "optional": false - }, - { - "name": "type", - "type": 16, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 9, - "optional": false - }, - { - "name": "height", - "type": 9, - "optional": false - }, - { - "name": "mediaType", - "type": 10, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": true - }, - { - "name": "originalDimensions", - "type": 15, - "optional": true - }, - { - "name": "width", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 11, - 12, - 13, - 14 - ] - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 9, - "optional": false - }, - { - "name": "width", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 18, - "optional": false - }, - { - "name": "type", - "type": 19, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 5, - "optional": false - }, - { - "name": "bytes", - "type": 9, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 2, - "optional": false - }, - { - "name": "isError", - "type": 21, - "optional": true - }, - { - "name": "toolCallId", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 22, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 5, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "union", - "types": [ - 28, - 29, - 30 - ] - }, - { - "kind": "literal", - "value": "system" - }, - { - "kind": "literal", - "value": "assistant" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "union", - "types": [ - 32, - 42, - 44, - 45, - 48, - 53, - 55, - 60, - 61, - 62, - 64, - 66, - 67, - 74, - 76, - 78, - 80, - 82, - 85, - 86 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "baseline", - "type": 33, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 5, - "optional": true - }, - { - "name": "changes", - "type": 34, - "optional": false - }, - { - "name": "form", - "type": 40, - "optional": false - }, - { - "name": "kind", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 35 - }, - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 36, - "optional": false - }, - { - "name": "digest", - "type": 5, - "optional": true - }, - { - "name": "path", - "type": 5, - "optional": false - }, - { - "name": "scope", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 37, - 38, - 39 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 5, - "optional": false - }, - { - "name": "kind", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "object", - "properties": [ - { - "name": "clientTimeZone", - "type": 5, - "optional": true - }, - { - "name": "kind", - "type": 30, - "optional": false - }, - { - "name": "rpcId", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 5, - "optional": false - }, - { - "name": "form", - "type": 46, - "optional": false - }, - { - "name": "kind", - "type": 47, - "optional": false - }, - { - "name": "provider", - "type": 5, - "optional": false - }, - { - "name": "ruleId", - "type": 5, - "optional": false - }, - { - "name": "source", - "type": 5, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - }, - { - "kind": "object", - "properties": [ - { - "name": "entries", - "type": 49, - "optional": false - }, - { - "name": "form", - "type": 51, - "optional": false - }, - { - "name": "kind", - "type": 52, - "optional": false - }, - { - "name": "update", - "type": 33, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 50 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 5, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 40, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "name", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 56, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - }, - { - "name": "sections", - "type": 58, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 59 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 5, - "optional": false - }, - { - "name": "text", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 46, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 40, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 63, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 65, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 51, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 63, - "optional": false - }, - { - "name": "kind", - "type": 68, - "optional": false - }, - { - "name": "references", - "type": 69, - "optional": false - }, - { - "name": "version", - "type": 73, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 70 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 9, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 71, - "optional": false - }, - { - "name": "compacted", - "type": 21, - "optional": false - }, - { - "name": "inputIndex", - "type": 9, - "optional": false - }, - { - "name": "label", - "type": 5, - "optional": false - }, - { - "name": "omittedBytes", - "type": 9, - "optional": false - }, - { - "name": "omittedMessages", - "type": 9, - "optional": false - }, - { - "name": "originalMessages", - "type": 9, - "optional": false - }, - { - "name": "retainedMessages", - "type": 9, - "optional": false - }, - { - "name": "sessionId", - "type": 5, - "optional": false - }, - { - "name": "truncated", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 9, - 72 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 46, - "optional": false - }, - { - "name": "kind", - "type": 75, - "optional": false - }, - { - "name": "senderSessionId", - "type": 5, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 65, - "optional": false - }, - { - "name": "kind", - "type": 77, - "optional": false - }, - { - "name": "senderSessionId", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 5, - "optional": false - }, - { - "name": "kind", - "type": 79, - "optional": false - }, - { - "name": "revision", - "type": 9, - "optional": false - }, - { - "name": "round", - "type": 9, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 81, - "optional": false - }, - { - "name": "messageId", - "type": 5, - "optional": false - }, - { - "name": "senderId", - "type": 5, - "optional": false - }, - { - "name": "senderName", - "type": 5, - "optional": false - }, - { - "name": "teamId", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 83, - "optional": false - }, - { - "name": "model", - "type": 5, - "optional": false - }, - { - "name": "provider", - "type": 5, - "optional": false - }, - { - "name": "replayState", - "type": 84, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "plugin", - "type": 5, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 30, - "optional": false - } - ], - "indices": [] - } - ] - }, - "names": [ - "event:browser-use/stagehand-llm-request.data.request.messages", - "event:session/title-llm-request.data.messages" - ], - "sources": [] - }, - { - "digest": "f56c5b59ca0161310c338d31dd16cf14287ccd602ff4181058df178c1c286d7f", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 1, - "optional": false - }, - { - "name": "kind", - "type": 2, - "optional": false - }, - { - "name": "references", - "type": 3, - "optional": false - }, - { - "name": "version", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 4 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 5, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 6, - "optional": false - }, - { - "name": "compacted", - "type": 8, - "optional": false - }, - { - "name": "inputIndex", - "type": 5, - "optional": false - }, - { - "name": "label", - "type": 9, - "optional": false - }, - { - "name": "omittedBytes", - "type": 5, - "optional": false - }, - { - "name": "omittedMessages", - "type": 5, - "optional": false - }, - { - "name": "originalMessages", - "type": 5, - "optional": false - }, - { - "name": "retainedMessages", - "type": 5, - "optional": false - }, - { - "name": "sessionId", - "type": 9, - "optional": false - }, - { - "name": "truncated", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 5, - 7 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": 1 - } - ] - }, - "names": [ - "packages/context/session-reference/src/types.ts#SessionReferenceSource" - ], - "sources": [ - "packages/context/session-reference/src/types.ts:13" - ] - }, - { - "digest": "f634871d1e18b1eb199497ac8ab72d3964f48d03fbf611dc7521a1970cf429d9", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "plan/mode" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "f6cb462f808309e315fea634f1ed86268b8346bbcee6774d9097ca6eed7ac1f8", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "literal", - "value": "replace" - } - ] - }, - "names": [], - "sources": [] - }, - { - "digest": "f757322e25914d63799c6235af4ed4b4f36d7d083fef40d6873a45e94fe6ad65", - "schema": { - "root": 0, - "nodes": [ - { - "kind": "object", - "properties": [ - { - "name": "data", - "type": 1, - "optional": false - }, - { - "name": "ignorable", - "type": 32, - "optional": true - }, - { - "name": "seq", - "type": 11, - "optional": false - }, - { - "name": "time", - "type": 11, - "optional": false - }, - { - "name": "type", - "type": 90, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "inserted", - "type": 2, - "optional": false - }, - { - "name": "outcome", - "type": 86, - "optional": true - }, - { - "name": "removedCount", - "type": 11, - "optional": true - }, - { - "name": "start", - "type": 11, - "optional": false - }, - { - "name": "target", - "type": 87, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 3 - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 4, - "optional": false - }, - { - "name": "id", - "type": 7, - "optional": false - }, - { - "name": "role", - "type": 29, - "optional": false - }, - { - "name": "source", - "type": 30, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 5 - }, - { - "kind": "union", - "types": [ - 6, - 9, - 19, - 22, - 25, - 27 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "arguments", - "type": 7, - "optional": false - }, - { - "name": "id", - "type": 7, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 8, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "literal", - "value": "tool-call" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 10, - "optional": false - }, - { - "name": "type", - "type": 18, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 7, - "optional": false - }, - { - "name": "bytes", - "type": 11, - "optional": false - }, - { - "name": "height", - "type": 11, - "optional": false - }, - { - "name": "mediaType", - "type": 12, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": true - }, - { - "name": "originalDimensions", - "type": 17, - "optional": true - }, - { - "name": "width", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "number" - }, - { - "kind": "union", - "types": [ - 13, - 14, - 15, - 16 - ] - }, - { - "kind": "literal", - "value": "image/jpeg" - }, - { - "kind": "literal", - "value": "image/png" - }, - { - "kind": "literal", - "value": "image/webp" - }, - { - "kind": "literal", - "value": "image/gif" - }, - { - "kind": "object", - "properties": [ - { - "name": "height", - "type": 11, - "optional": false - }, - { - "name": "width", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "image" - }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 20, - "optional": false - }, - { - "name": "type", - "type": 21, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 7, - "optional": false - }, - { - "name": "bytes", - "type": 11, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, - { - "kind": "object", - "properties": [ - { - "name": "content", - "type": 4, - "optional": false - }, - { - "name": "isError", - "type": 23, - "optional": true - }, - { - "name": "toolCallId", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 24, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "primitive", - "type": "boolean" - }, - { - "kind": "literal", - "value": "tool-result" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 26, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "reasoning" - }, - { - "kind": "object", - "properties": [ - { - "name": "text", - "type": 7, - "optional": false - }, - { - "name": "type", - "type": 28, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "text" - }, - { - "kind": "literal", - "value": "user" - }, - { - "kind": "union", - "types": [ - 31, - 41, - 43, - 44, - 47, - 52, - 54, - 59, - 60, - 61, - 63, - 65, - 66, - 73, - 75, - 77, - 79, - 81, - 84, - 85 - ] - }, - { - "kind": "object", - "properties": [ - { - "name": "baseline", - "type": 32, - "optional": true - }, - { - "name": "baselineIdentity", - "type": 7, - "optional": true - }, - { - "name": "changes", - "type": 33, - "optional": false - }, - { - "name": "form", - "type": 39, - "optional": false - }, - { - "name": "kind", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": true - }, - { - "kind": "array", - "element": 34 - }, - { - "kind": "object", - "properties": [ - { - "name": "action", - "type": 35, - "optional": false - }, - { - "name": "digest", - "type": 7, - "optional": true - }, - { - "name": "path", - "type": 7, - "optional": false - }, - { - "name": "scope", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 36, - 37, - 38 - ] - }, - { - "kind": "literal", - "value": "remove" - }, - { - "kind": "literal", - "value": "replace" - }, - { - "kind": "literal", - "value": "set" - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "agent-instructions" - }, - { - "kind": "object", - "properties": [ - { - "name": "callId", - "type": 7, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool" - }, - { - "kind": "object", - "properties": [ - { - "name": "clientTimeZone", - "type": 7, - "optional": true - }, - { - "name": "kind", - "type": 29, - "optional": false - }, - { - "name": "rpcId", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "deliveryId", - "type": 7, - "optional": false - }, - { - "name": "form", - "type": 45, - "optional": false - }, - { - "name": "kind", - "type": 46, - "optional": false - }, - { - "name": "provider", - "type": 7, - "optional": false - }, - { - "name": "ruleId", - "type": 7, - "optional": false - }, - { - "name": "source", - "type": 7, - "optional": false - }, - { - "name": "summary", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "notice" - }, - { - "kind": "literal", - "value": "webhook" - }, - { - "kind": "object", - "properties": [ - { - "name": "entries", - "type": 48, - "optional": false - }, - { - "name": "form", - "type": 50, - "optional": false - }, - { - "name": "kind", - "type": 51, - "optional": false - }, - { - "name": "update", - "type": 32, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "array", - "element": 49 - }, - { - "kind": "object", - "properties": [ - { - "name": "description", - "type": 7, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "catalog" - }, - { - "kind": "literal", - "value": "skill-catalog" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 39, - "optional": false - }, - { - "name": "kind", - "type": 53, - "optional": false - }, - { - "name": "name", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "skill-invocation" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - }, - { - "name": "sections", - "type": 57, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "literal", - "value": "plugin" - }, - { - "kind": "array", - "element": 58 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "text", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 45, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - }, - { - "name": "summary", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 39, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 62, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "recall" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 64, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 50, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 62, - "optional": false - }, - { - "name": "kind", - "type": 67, - "optional": false - }, - { - "name": "references", - "type": 68, - "optional": false - }, - { - "name": "version", - "type": 72, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "session-reference" - }, - { - "kind": "array", - "element": 69 - }, - { - "kind": "object", - "properties": [ - { - "name": "capturedFormatVersion", - "type": 11, - "optional": true - }, - { - "name": "capturedThroughSeq", - "type": 70, - "optional": false - }, - { - "name": "compacted", - "type": 23, - "optional": false - }, - { - "name": "inputIndex", - "type": 11, - "optional": false - }, - { - "name": "label", - "type": 7, - "optional": false - }, - { - "name": "omittedBytes", - "type": 11, - "optional": false - }, - { - "name": "omittedMessages", - "type": 11, - "optional": false - }, - { - "name": "originalMessages", - "type": 11, - "optional": false - }, - { - "name": "retainedMessages", - "type": 11, - "optional": false - }, - { - "name": "sessionId", - "type": 7, - "optional": false - }, - { - "name": "truncated", - "type": 23, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "union", - "types": [ - 11, - 71 - ] - }, - { - "kind": "primitive", - "type": "null" - }, - { - "kind": "literal", - "value": 1 - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 45, - "optional": false - }, - { - "name": "kind", - "type": 74, - "optional": false - }, - { - "name": "senderSessionId", - "type": 7, - "optional": false - }, - { - "name": "summary", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 64, - "optional": false - }, - { - "name": "kind", - "type": 76, - "optional": false - }, - { - "name": "senderSessionId", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "goalId", - "type": 7, - "optional": false - }, - { - "name": "kind", - "type": 78, - "optional": false - }, - { - "name": "revision", - "type": 11, - "optional": false - }, - { - "name": "round", - "type": 11, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "goal" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 80, - "optional": false - }, - { - "name": "messageId", - "type": 7, - "optional": false - }, - { - "name": "senderId", - "type": 7, - "optional": false - }, - { - "name": "senderName", - "type": 7, - "optional": false - }, - { - "name": "teamId", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "team-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 82, - "optional": false - }, - { - "name": "model", - "type": 7, - "optional": false - }, - { - "name": "provider", - "type": 7, - "optional": false - }, - { - "name": "replayState", - "type": 83, - "optional": true - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model" - }, - { - "kind": "opaque", - "reason": "unknown" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "plugin", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 29, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "canceled" - }, - { - "kind": "union", - "types": [ - 88, - 89 - ] - }, - { - "kind": "literal", - "value": "next-step" - }, - { - "kind": "literal", - "value": "next-turn" - }, - { - "kind": "literal", - "value": "agent/inbox/spliced" - } - ] - }, - "names": [ - "event:agent/inbox/spliced" - ], - "sources": [] - }, { "digest": "f789af5ab844827eb5a4f357c0afae1eeed108c6c22d8ca1a56e586b349a7c85", "schema": { @@ -67240,6 +67590,487 @@ "names": [], "sources": [] }, + { + "digest": "feb329074f0206ecc06d8a1aa0a0b3e2f47fd27524ba020fbc849ba63e938896", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "union", + "types": [ + 1, + 31 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 2, + "optional": false + }, + { + "name": "llmStreamCall", + "type": 3, + "optional": false + }, + { + "name": "maxTokens", + "type": 4, + "optional": true + }, + { + "name": "model", + "type": 2, + "optional": false + }, + { + "name": "provider", + "type": 2, + "optional": false + }, + { + "name": "rawOutput", + "type": 5, + "optional": false + }, + { + "name": "shadowedRange", + "type": 28, + "optional": false + }, + { + "name": "shadowedSeqs", + "type": 29, + "optional": false + }, + { + "name": "shadowedTokenCount", + "type": 4, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 2, + "optional": true + }, + { + "name": "summary", + "type": 5, + "optional": false + }, + { + "name": "usage", + "type": 30, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 6 + }, + { + "kind": "union", + "types": [ + 7, + 10, + 12, + 14, + 16, + 25 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 5, + "optional": false + }, + { + "name": "isError", + "type": 8, + "optional": true + }, + { + "name": "toolCallId", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool-result" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 2, + "optional": false + }, + { + "name": "id", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 17, + "optional": false + }, + { + "name": "offloaded", + "type": 3, + "optional": true + }, + { + "name": "type", + "type": 24, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 2, + "optional": false + }, + { + "name": "bytes", + "type": 4, + "optional": false + }, + { + "name": "height", + "type": 4, + "optional": false + }, + { + "name": "mediaType", + "type": 18, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": true + }, + { + "name": "originalDimensions", + "type": 23, + "optional": true + }, + { + "name": "width", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 19, + 20, + 21, + 22 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 4, + "optional": false + }, + { + "name": "width", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 26, + "optional": false + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 2, + "optional": false + }, + { + "name": "bytes", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "end", + "type": 4, + "optional": false + }, + { + "name": "start", + "type": 4, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "object", + "properties": [ + { + "name": "cacheReadTokens", + "type": 4, + "optional": true + }, + { + "name": "cacheWriteTokens", + "type": 4, + "optional": true + }, + { + "name": "inputTokens", + "type": 4, + "optional": false + }, + { + "name": "outputTokens", + "type": 4, + "optional": false + }, + { + "name": "reasoningTokens", + "type": 4, + "optional": true + }, + { + "name": "totalTokens", + "type": 4, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 2, + "optional": false + }, + { + "name": "maxTokens", + "type": 4, + "optional": true + }, + { + "name": "model", + "type": 2, + "optional": false + }, + { + "name": "provider", + "type": 2, + "optional": false + }, + { + "name": "rawOutput", + "type": 5, + "optional": true + }, + { + "name": "shadowedRange", + "type": 28, + "optional": false + }, + { + "name": "shadowedSeqs", + "type": 29, + "optional": false + }, + { + "name": "shadowedTokenCount", + "type": 4, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 2, + "optional": true + }, + { + "name": "summary", + "type": 5, + "optional": false + }, + { + "name": "usage", + "type": 30, + "optional": true + } + ], + "indices": [] + } + ] + }, + "names": [ + "event:compaction/summary.data" + ], + "sources": [] + }, { "digest": "fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394", "schema": { @@ -67291,7 +68122,7 @@ "packages/llm/llm/src/types.ts#TokenUsage" ], "sources": [ - "packages/llm/llm/src/types.ts:149" + "packages/llm/llm/src/types.ts:162" ] }, { diff --git a/docs/subsystems/compaction.i18n.yaml b/docs/subsystems/compaction.i18n.yaml index c6b7cd2682..671930e3d1 100644 --- a/docs/subsystems/compaction.i18n.yaml +++ b/docs/subsystems/compaction.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/compaction.md -compaction.md: 6a8743feac0481626f171e3926c7f924508db54a -compaction.zh.md: 1b8d2a2d1e47067583593ca3fae039bc78ed06f9 +compaction.md: 6e135025ab6095e2365e172847bc493d41c50774 +compaction.zh.md: 9b82fb7a7b9e55f2aefa6d561922948fd328c8e9 diff --git a/docs/subsystems/compaction.md b/docs/subsystems/compaction.md index 6a8743feac..6e135025ab 100644 --- a/docs/subsystems/compaction.md +++ b/docs/subsystems/compaction.md @@ -22,6 +22,21 @@ The markers are lock time points, not an exclusive container. An unrelated idle These variants are merged inside a `declare module '@deepseek-ai/dsh-session/types'` block, so — unlike the top-level types on the other subsystem pages — they are not pasted as a drift-checked ` ```ts type-equiv ` block (the `verify-type-equiv` extractor matches only top-level declarations by name). The payload table above is the catalog entry; follow the source link for the authoritative fields. + +## Image offload + +`compaction-image-offload` owns the `image/offload` declaration and its pure message projection. Each target identifies a current input node and exact depth-first image occurrences. The event preserves node and message identities and carries no `surfaceOp`. The [package README](../../packages/compaction/compaction-image-offload/README.md) owns recovery policy, registration, and detached replay. + +```ts type-equiv +/** Exact input-image occurrences selected by one durable offload decision. */ +interface ImageOffloadTarget { + /** Current message-producing event containing these occurrences. */ + seq: SessionSeq + /** Zero-based depth-first image indexes within the immutable message. */ + imageIndexes: number[] +} +``` + ## `CompactionResult` What a successful compaction returns to its caller: the bookkeeping-event seqs, safe summary projection, shadowed range and seqs, and estimated token count. @@ -81,7 +96,7 @@ type ManualCompactionErrorCode = | 'persistence' ``` -`changed` and `summary` leave the conversation surface unchanged but still close and persist the failed attempt in the log. `commit` may follow partial mutation; `persistence` means the in-memory bracket closed but its flush failed. Cancellation remains separate and throws the exact abort reason after required cleanup. +`changed` and `summary` close and persist the failed attempt without a summary replacement; image omissions recorded during recovery remain effective. `commit` may follow partial mutation; `persistence` means the in-memory bracket closed but its flush failed. Cancellation remains separate and throws the exact abort reason after required cleanup. Pressure compaction runs at the `agent/pre-step` waterfall before request derivation. Once pressure or canonical overflow qualifies, compaction-basic invokes optional [`ctx.toolResultPruner`](../../packages/compaction/compaction-tool-result-pruner/README.md) before range selection, remeasures through `ctx.tokenMeter`, and can advance the surface without a summary. Failed-request recovery runs through `agent/request-error` after the failed step closes and returns a retry action only when the surface replacement generation advances, even if later summary work throws after pruning; cancellation still wins. Region boundaries preserve tool-call/result pairing but not whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compaction-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling. @@ -235,4 +250,35 @@ pruneSession(session: Session): PruneResult Types: [ContentBlock](llm-streaming.md) · [Session](session.md) Source: [`packages/compaction/compaction-tool-result-pruner/src/index.ts`](../../packages/compaction/compaction-tool-result-pruner/src/index.ts) + + + +### `compaction/*` events + + + +#### `compaction/summary-error` — waterfall + +Recover a failed summary request by synchronously recording a durable change to its selected input. Return true only after making progress; the provider re-derives and re-prices the selection before retrying. Call next() when the failure cannot be recovered. Decisions survive a later summary failure or cancellation. + +```ts cordis-catalog +/** + * Recover a failed summary request by synchronously recording a durable + * change to its selected input. Return true only after making progress; + * the provider re-derives and re-prices the selection before retrying. + * Call next() when the failure cannot be recovered. Decisions survive a + * later summary failure or cancellation. + * @param payload.session - session containing the selected input. + * @param payload.sourceEventSeqs - selected message events in request order. + * @param payload.error - failure thrown by the summarizer. + * @param payload.signal - optional compaction cancellation signal. + * @param next - delegate to the next recovery listener. + * @mode waterfall + */ +'compaction/summary-error'(payload: { session: Session; sourceEventSeqs: readonly SessionSeq[]; error: unknown; signal?: AbortSignal }, next: () => boolean): boolean +``` + +Types: [Session](session.md) · [SessionSeq](session.md) + +Source: [`packages/compaction/compaction/src/index.ts`](../../packages/compaction/compaction/src/index.ts) diff --git a/docs/subsystems/compaction.zh.md b/docs/subsystems/compaction.zh.md index 1b8d2a2d1e..9b82fb7a7b 100644 --- a/docs/subsystems/compaction.zh.md +++ b/docs/subsystems/compaction.zh.md @@ -22,6 +22,21 @@ 这些变体在 `declare module '@deepseek-ai/dsh-session/types'` 块内合并,因此——与其他子系统页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威字段请循源码链接查看。 + +## 图片省略 + +`compaction-image-offload` 拥有 `image/offload` 声明及其纯消息投影。每个目标指定当前输入节点和按深度优先计数的确切图片位置。事件保留节点和消息身份,不携带 `surfaceOp`。[包 README](../../packages/compaction/compaction-image-offload/README.zh.md)负责恢复策略、注册和独立回放说明。 + +```ts type-equiv +/** Exact input-image occurrences selected by one durable offload decision. */ +interface ImageOffloadTarget { + /** Current message-producing event containing these occurrences. */ + seq: SessionSeq + /** Zero-based depth-first image indexes within the immutable message. */ + imageIndexes: number[] +} +``` + ## `CompactionResult` 成功压缩向调用方返回:记账事件 seq、安全摘要投影、被遮蔽的范围与 seq,以及估算 token 数。 @@ -81,7 +96,7 @@ type ManualCompactionErrorCode = | 'persistence' ``` -`changed` 和 `summary` 保持会话表层不变,但仍会闭合失败尝试并将其持久化到日志。`commit` 可能发生在部分变更之后;`persistence` 表示内存中的标记对已闭合,但 flush 失败。取消独立于这些失败,并在完成必要清理后抛出原始 abort 原因。 +`changed` 和 `summary` 闭合失败尝试并将其持久化到日志,不写入摘要替换;恢复过程中记录的图片省略仍然有效。`commit` 可能发生在部分变更之后;`persistence` 表示内存中的标记对已闭合,但 flush 失败。取消独立于这些失败,并在完成必要清理后抛出原始 abort 原因。 压力压缩在 `agent/pre-step` waterfall(瀑布式事件)中运行,先于请求推导。一旦压力或规范化溢出满足条件,compaction-basic 会在选择范围前调用可选的 [`ctx.toolResultPruner`](../../packages/compaction/compaction-tool-result-pruner/README.zh.md),再通过 `ctx.tokenMeter` 重新测量,并且可以在不生成摘要的情况下推进 surface。失败请求的恢复在失败的步骤关闭后通过 `agent/request-error` 运行;仅当 surface replacement generation 前进时才返回重试动作,即便后续摘要工作在剪枝后抛异常亦如此;取消仍然优先。区域边界保持工具调用/结果配对,但不保持整个轮次,因此一个过大轮次中较早关闭的步骤可以被压缩。`dsh-compaction-basic` 拥有阈值、保留尾部策略、溢出上限与失败处理。 @@ -235,4 +250,35 @@ pruneSession(session: Session): PruneResult Types: [ContentBlock](llm-streaming.zh.md) · [Session](session.zh.md) Source: [`packages/compaction/compaction-tool-result-pruner/src/index.ts`](../../packages/compaction/compaction-tool-result-pruner/src/index.ts) + + + +### `compaction/*` events + + + +#### `compaction/summary-error` — waterfall + +Recover a failed summary request by synchronously recording a durable change to its selected input. Return true only after making progress; the provider re-derives and re-prices the selection before retrying. Call next() when the failure cannot be recovered. Decisions survive a later summary failure or cancellation. + +```ts cordis-catalog +/** + * Recover a failed summary request by synchronously recording a durable + * change to its selected input. Return true only after making progress; + * the provider re-derives and re-prices the selection before retrying. + * Call next() when the failure cannot be recovered. Decisions survive a + * later summary failure or cancellation. + * @param payload.session - session containing the selected input. + * @param payload.sourceEventSeqs - selected message events in request order. + * @param payload.error - failure thrown by the summarizer. + * @param payload.signal - optional compaction cancellation signal. + * @param next - delegate to the next recovery listener. + * @mode waterfall + */ +'compaction/summary-error'(payload: { session: Session; sourceEventSeqs: readonly SessionSeq[]; error: unknown; signal?: AbortSignal }, next: () => boolean): boolean +``` + +Types: [Session](session.zh.md) · [SessionSeq](session.zh.md) + +Source: [`packages/compaction/compaction/src/index.ts`](../../packages/compaction/compaction/src/index.ts) diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml index 26ddf5d641..095a3db858 100644 --- a/docs/subsystems/llm-streaming.i18n.yaml +++ b/docs/subsystems/llm-streaming.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md -llm-streaming.md: e42d5f0c4a4e5273926ca8a58b3f060637da43dc -llm-streaming.zh.md: 727b327e31421477891955383bebbbe1a9e1b5fb +llm-streaming.md: 6d29664f4362a3196d3ba7526c6fb18e20b034ad +llm-streaming.zh.md: 3c60ae3935f745e11b850990c98a75208fb1c9f3 diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index e42d5f0c4a..6d29664f43 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -244,12 +244,19 @@ interface LlmFailure { readonly providerRetryAfterMs?: number /** Opaque provider-issued request identifier for diagnostics. */ readonly requestId?: ProviderRequestId + /** + * With code `IMAGE_OFFLOAD_REQUIRED`: how many more of the oldest retained + * image occurrences the route needs offloaded before the same request fits + * its exact byte accounting. `dsh-compaction-image-offload` records the + * selected occurrences in an `image/offload` event and retries the step. + */ + readonly offloadImages?: number } ``` ## Request-image pricing -An adapter whose provider charges visual tokens for request images declares per-route pricing by overriding `LlmAdapter.imageRequestPricing`, and `ctx.llm.imageRequestPricing(provider, model)` resolves it synchronously for consumers. The token meter resolves the routed model's pricing on every measurement so compaction pressure, retention, and range selection price image history as the routed request actually sends it; the DeepSeek adapter reproduces its own request projection (per-model request target, oldest-first offload) and prices retained images with the published vision accounting, while provider usage remains the authoritative anchor for completed requests. +An adapter whose provider charges visual tokens for request images declares per-route pricing by overriding `LlmAdapter.imageRequestPricing`, and `ctx.llm.imageRequestPricing(provider, model)` resolves it synchronously for consumers. The token meter resolves the routed model's pricing on every measurement so compaction pressure, retention, and range selection price image history as the routed request actually sends it; the DeepSeek adapter prices each retained occurrence at its per-model request target with the published vision accounting and each occurrence selected by a logged image-offload decision as its placeholder text, while provider usage remains the authoritative anchor for completed requests. ```ts type-equiv /** @@ -278,10 +285,11 @@ interface LlmImageRequestPrice { interface LlmImageRequestPricing { /** * Price every image occurrence of one request projection. - * @param images - durable image references in request order, one entry per occurrence. + * @param images - surface image blocks in request order, one entry per occurrence; an `offloaded` block + * is priced as its placeholder text. * @returns one price per occurrence, aligned by index with `images`. */ - priceImages(images: readonly ImageAttachmentRef[]): readonly LlmImageRequestPrice[] + priceImages(images: readonly ImageBlock[]): readonly LlmImageRequestPrice[] } ``` diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index 727b327e31..3c60ae3935 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -246,12 +246,19 @@ interface LlmFailure { readonly providerRetryAfterMs?: number /** Opaque provider-issued request identifier for diagnostics. */ readonly requestId?: ProviderRequestId + /** + * With code `IMAGE_OFFLOAD_REQUIRED`: how many more of the oldest retained + * image occurrences the route needs offloaded before the same request fits + * its exact byte accounting. `dsh-compaction-image-offload` records the + * selected occurrences in an `image/offload` event and retries the step. + */ + readonly offloadImages?: number } ``` ## 请求图片定价 -提供方对请求图片收取视觉 token 的适配器通过覆写 `LlmAdapter.imageRequestPricing` 声明按路由的定价,消费方经 `ctx.llm.imageRequestPricing(provider, model)` 同步解析。token 计量服务在每次计量时解析路由模型的定价,使 compaction 的压力、保留与选段都按路由请求实际发送的形式为图片历史计价;DeepSeek 适配器复现自身的请求投影(按模型的请求目标、最旧优先 offload),并用官方公布的视觉计量为保留图片定价,已完成请求仍以 provider usage 为权威锚点。 +提供方对请求图片收取视觉 token 的适配器通过覆写 `LlmAdapter.imageRequestPricing` 声明按路由的定价,消费方经 `ctx.llm.imageRequestPricing(provider, model)` 同步解析。token 计量服务在每次计量时解析路由模型的定价,使 compaction 的压力、保留与选段都按路由请求实际发送的形式为图片历史计价;DeepSeek 适配器按模型的请求目标用官方公布的视觉计量为每个保留的出现位置定价,并把日志中的图片省略决策选中的出现位置按其占位文本定价,已完成请求仍以 provider usage 为权威锚点。 ```ts type-equiv /** @@ -280,10 +287,11 @@ interface LlmImageRequestPrice { interface LlmImageRequestPricing { /** * Price every image occurrence of one request projection. - * @param images - durable image references in request order, one entry per occurrence. + * @param images - surface image blocks in request order, one entry per occurrence; an `offloaded` block + * is priced as its placeholder text. * @returns one price per occurrence, aligned by index with `images`. */ - priceImages(images: readonly ImageAttachmentRef[]): readonly LlmImageRequestPrice[] + priceImages(images: readonly ImageBlock[]): readonly LlmImageRequestPrice[] } ``` diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 4e7687c92d..142a9b6d7a 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/session.md -session.md: 4259988f23b7c64708b78dd62b69351c85d8e21b -session.zh.md: 9540766176262f06f25b8a99e460ad14e839ca80 +session.md: 3e4072cc5c11fabd669d152214ef02fb031ef61c +session.zh.md: 3d3aee700e100d6356abebdc9e982bb234a51d93 diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index 4259988f23..3e4072cc5c 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -344,11 +344,47 @@ Required for `SurfaceEventType` events — every message-producing event must de `assistant/message` cannot carry `sourceEventSeqs`; its `stream` owns exact provider evidence. Other surface events omit the field when they cite no earlier event and use a complete non-empty list when they do. + +### Plugin-owned message projections + +Content-changing plugins mark their event declaration with `@messageProjection` and register a pure definition through `ctx.sessions.registerMessageProjection()`. Session validates the complete decision through that definition before commit, applies its immutable message updates, and advances `contentGeneration`. It rejects missing interpreters, including on restore and detached folding; unloading a used definition blocks cached reads. Detached readers pass explicit definitions to `foldSurface(events, projections)` and apply `projectedMessages` through `deriveEventMessage()`. The installed format catalog assembles first-party definitions for offline readers. The [image-offload plugin](compaction.md#image-offload) owns its image-specific event and interpretation. + +```ts type-equiv +/** Readonly history immediately before a message-projection event. */ +interface SessionMessageProjectionContext { + /** Current message-producing event sequences in model-visible order. */ + nodes: readonly SessionSeq[] + /** Contiguous event window; entries at or beyond the candidate seq are not committed inputs. */ + events: readonly SessionEvent[] + /** Absolute sequence of the window's first event. */ + baseSeq: SessionLogOffset + /** Previously projected messages keyed by their original event sequences. */ + messages: ReadonlyMap +} +``` + +```ts type-equiv +/** Pure interpretation of one plugin-owned event that changes existing message content. */ +interface SessionMessageProjection { + /** Event interpreted by this definition; declare it with `@messageProjection` in SessionEventMap. */ + type: T + /** + * Validate the complete durable decision before returning any updates. Preserve + * message identities and publish immutable copies without mutating the input. + * @param event - candidate event, not yet applied to the supplied history. + * @param context - history preceding this decision. + * @returns changed current messages keyed by their original sequences. + * @throws when the durable decision cannot be applied to this history. + */ + project(event: SessionEvent, context: SessionMessageProjectionContext): ReadonlyMap +} +``` + ### `SessionSurface` — the live readonly surface projection `Session.surface` returns the session's stable `SessionSurface` view. The same incremental manager validates append candidates before commit and advances this projection from committed events; callers can observe membership and replacement generation but cannot invoke validation. -`SurfaceManager(log, baseSeq?)` can instead fold a contiguous loaded window whose first event has the absolute sequence `baseSeq`. Every event remains contiguous in that absolute sequence space, and a replacement that crosses the window head fails because its declared range is absent. +`SurfaceManager(log, baseSeq?, projections?)` can instead fold a contiguous loaded window whose first event has the absolute sequence `baseSeq`. Every event remains contiguous in that absolute sequence space, and a replacement that crosses the window head fails because its declared range is absent. ```ts type-equiv /** Readonly live projection of the message-producing session events. */ @@ -357,12 +393,14 @@ interface SessionSurface { readonly nodes: readonly SessionSeq[] /** Monotonic count of committed positional replacements. */ readonly replaceGeneration: number + /** Monotonic count of committed replacements and plugin-owned message changes. */ + readonly contentGeneration: number } ``` ### `SurfaceFoldReplacement` and `SurfaceFoldResult` — a complete surface replay -`foldSurface(events)` returns detached current event sequences together with the actual sequences shadowed by each declared replacement range. The live manager uses the same transitions without retaining replacement history. Its `replaceGeneration` increments for each committed replacement so incremental consumers can distinguish pure tail growth from a rewrite. +`foldSurface(events, projections)` returns detached current event sequences together with the actual sequences shadowed by each declared replacement range. The live manager uses the same transitions without retaining replacement history. Its `replaceGeneration` increments for each committed replacement so incremental consumers can distinguish pure tail growth from a rewrite. ```ts type-equiv /** One replacement operation observed while folding a session surface. */ @@ -385,6 +423,8 @@ interface SurfaceFoldResult { nodes: SessionSeq[] /** Replacement operations in event order. */ replacements: SurfaceFoldReplacement[] + /** Immutable projected messages, keyed by their original event sequences. */ + projectedMessages: ReadonlyMap } ``` @@ -439,7 +479,7 @@ declare class Session { * When this lifecycle appends the marker, it occupies this seq before the * store attaches and therefore does not publish either. Otherwise this seq * holds an ordinary published write. - */ + */ readonly firstLiveSeq: SessionLogOffset; /** * Create a detached session by validating and snapshotting borrowed seed @@ -448,14 +488,17 @@ declare class Session { * @param seed - optional borrowed replay or fork events. * @param header - optional borrowed storage metadata. * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header. + * @param projections - pure interpreters for plugin-owned message changes. * @returns a detached session. + * @throws when a seed event requires a missing message interpreter or fails validation. */ static create( id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader, inheritedEventCount?: SessionLogOffset, - ): Session; + projections?: readonly SessionMessageProjection[], + ): Session; /** * Restore a detached session by adopting an independently owned or deeply frozen seed. * Runtime-required event fields, event envelopes, sequence continuity, surface @@ -467,7 +510,9 @@ declare class Session { * @param header - independently owned storage metadata. * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage. * @param eventState - aliasing state carried from the operation that produced the seed. + * @param projections - pure interpreters for plugin-owned message changes. * @returns a restored detached session. + * @throws when a seed event requires a missing message interpreter or fails validation. */ static fromRestore( id: SessionId, @@ -475,7 +520,8 @@ declare class Session { header: SessionHeader, inheritedEventCount: SessionLogOffset, eventState: SessionSeedEventState, - ): Session; + projections?: readonly SessionMessageProjection[], + ): Session; /** * Return the immutable event stored at one exact sequence number. * @deprecated Existing logic may remain unmigrated for now, but new calls are prohibited. @@ -497,7 +543,7 @@ declare class Session { snapshotEvents( fromSeq: SessionLogOffset = SessionLogOffset(0), toSeqExclusive: SessionLogOffset = this.seq, - ): readonly SessionEvent[]; + ): readonly SessionEvent[]; /** * Return this Session's events after its fork-inherited prefix. * @deprecated Existing logic may remain unmigrated for now, but new calls are prohibited. @@ -554,7 +600,7 @@ declare class Session { type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : [] - ): SessionEvent; + ): SessionEvent; /** * The {@link EpochHeader} in force after the log's last header event — the * header the NEXT request will be compared against — or undefined before @@ -577,21 +623,21 @@ declare class Session { * append records its `surfaceOp`, so a raw event with no marker (a chunk, a * turn boundary) is correctly absent, and a compaction `replace` deletes the * shadowed nodes from the derivation. The projection rules are - * {@link deriveEventMessage}, folded per node. + * {@link deriveEventMessage}, with logged message projections applied + * without changing node membership or message identity. * - * CACHED: each surface node is projected exactly once, when first seen — a - * call costs O(new nodes), and a surface rewrite (a `replace`; - * {@link SessionSurface.replaceGeneration}) rebuilds. The returned array is + * CACHED: pure tail growth costs O(new nodes); a replacement or message projection + * ({@link SessionSurface.contentGeneration}) rebuilds. The returned array is * a fresh snapshot per call (later appends never grow an array a caller * already holds); the `Message` objects in it are SHARED and **deep-frozen**. - * Their content reuses the already frozen durable event data, so the cache - * needs no second deep clone and consumers still cannot mutate the log. + * Unchanged content reuses frozen event data; projected blocks are frozen + * derived copies. Consumers cannot mutate the log through either form. * @returns a fresh array of the shared, frozen derived history. */ deriveMessages(): Message[]; /** - * Instance face of the pure per-node `deriveEventMessage` export from - * `surface.ts`. + * Project one event with all committed message projections applied. + * The original durable event remains unchanged. * @param event - the event to project. * @returns the derived message, or null when the event produces none. */ @@ -861,6 +907,15 @@ In-memory session store (`ctx.sessions`). Persistence is intentionally not implemented here — the agent lifecycle attaches a session-log writer to each published session's write handle; a session published outside that lifecycle persists nothing. ```ts cordis-catalog +/** + * Register one event interpreter for live creation, restore, and fork. + * Disposing the contribution makes sessions that used it refuse further derivation. + * @param projection - pure definition owned by the event's plugin. + * @returns the fiber-owned disposer. + * @throws when another definition already owns this event type. + */ +registerMessageProjection(projection: SessionMessageProjection): () => Promise + /** * Create a session owned by the calling fiber: disposing that fiber stops * event notification and removes the session from the store. `options.seed` diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index 9540766176..3d3aee700e 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -346,11 +346,47 @@ type SurfaceIntent = { `assistant/message` 不能携带 `sourceEventSeqs`;它的 `stream` 拥有精确 provider 证据。其他 surface event 不引用较早 event 时省略该字段,需要引用时使用完整非空 list。 + +### 插件拥有的消息投影 + +修改内容的插件用 `@messageProjection` 标记事件声明,并通过 `ctx.sessions.registerMessageProjection()` 注册纯处理器。Session 在提交前通过处理器校验完整决策,应用其不可变消息更新,并推进 `contentGeneration`。缺少处理器时拒绝操作,包括恢复和独立折叠,卸载已经使用的处理器后也会拒绝读取缓存。独立读取器将处理器显式传给 `foldSurface(events, projections)`,并通过 `deriveEventMessage()` 应用 `projectedMessages`。当前格式目录为离线读取器装配第一方处理器。[图片省略插件](compaction.zh.md#image-offload)拥有图片专用的事件及解释逻辑。 + +```ts type-equiv +/** Readonly history immediately before a message-projection event. */ +interface SessionMessageProjectionContext { + /** Current message-producing event sequences in model-visible order. */ + nodes: readonly SessionSeq[] + /** Contiguous event window; entries at or beyond the candidate seq are not committed inputs. */ + events: readonly SessionEvent[] + /** Absolute sequence of the window's first event. */ + baseSeq: SessionLogOffset + /** Previously projected messages keyed by their original event sequences. */ + messages: ReadonlyMap +} +``` + +```ts type-equiv +/** Pure interpretation of one plugin-owned event that changes existing message content. */ +interface SessionMessageProjection { + /** Event interpreted by this definition; declare it with `@messageProjection` in SessionEventMap. */ + type: T + /** + * Validate the complete durable decision before returning any updates. Preserve + * message identities and publish immutable copies without mutating the input. + * @param event - candidate event, not yet applied to the supplied history. + * @param context - history preceding this decision. + * @returns changed current messages keyed by their original sequences. + * @throws when the durable decision cannot be applied to this history. + */ + project(event: SessionEvent, context: SessionMessageProjectionContext): ReadonlyMap +} +``` + ### `SessionSurface`:实时只读 surface 投影 `Session.surface` 返回会话稳定的 `SessionSurface` 视图。同一个增量管理器在提交前校验追加候选事件,并根据已提交事件推进该投影;调用方可以观察成员关系和替换代次,但不能调用校验。 -`SurfaceManager(log, baseSeq?)` 也可以折叠一个连续的已加载窗口,其第一个事件的绝对序号为 `baseSeq`。每个事件在该绝对序号空间中仍保持连续;如果替换跨过窗口头部,由于其声明的范围并不存在,该替换会失败。 +`SurfaceManager(log, baseSeq?, projections?)` 也可以折叠一个连续的已加载窗口,其第一个事件的绝对序号为 `baseSeq`。每个事件在该绝对序号空间中仍保持连续;如果替换跨过窗口头部,由于其声明的范围并不存在,该替换会失败。 ```ts type-equiv /** Readonly live projection of the message-producing session events. */ @@ -359,12 +395,14 @@ interface SessionSurface { readonly nodes: readonly SessionSeq[] /** Monotonic count of committed positional replacements. */ readonly replaceGeneration: number + /** Monotonic count of committed replacements and plugin-owned message changes. */ + readonly contentGeneration: number } ``` ### `SurfaceFoldReplacement` 与 `SurfaceFoldResult`:完整的 surface 回放 -`foldSurface(events)` 返回一份独立的当前事件 seq 列表,以及每个声明的替换范围实际遮蔽的 seq。实时管理器复用同一套状态转换,但不保留替换历史。每提交一次替换,其 `replaceGeneration` 就递增一次,使增量消费方能够区分纯尾部增长与重写。 +`foldSurface(events, projections)` 返回一份独立的当前事件 seq 列表,以及每个声明的替换范围实际遮蔽的 seq。实时管理器复用同一套状态转换,但不保留替换历史。每提交一次替换,其 `replaceGeneration` 就递增一次,使增量消费方能够区分纯尾部增长与重写。 ```ts type-equiv /** One replacement operation observed while folding a session surface. */ @@ -387,6 +425,8 @@ interface SurfaceFoldResult { nodes: SessionSeq[] /** Replacement operations in event order. */ replacements: SurfaceFoldReplacement[] + /** Immutable projected messages, keyed by their original event sequences. */ + projectedMessages: ReadonlyMap } ``` @@ -441,7 +481,7 @@ declare class Session { * When this lifecycle appends the marker, it occupies this seq before the * store attaches and therefore does not publish either. Otherwise this seq * holds an ordinary published write. - */ + */ readonly firstLiveSeq: SessionLogOffset; /** * Create a detached session by validating and snapshotting borrowed seed @@ -450,14 +490,17 @@ declare class Session { * @param seed - optional borrowed replay or fork events. * @param header - optional borrowed storage metadata. * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header. + * @param projections - pure interpreters for plugin-owned message changes. * @returns a detached session. + * @throws when a seed event requires a missing message interpreter or fails validation. */ static create( id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader, inheritedEventCount?: SessionLogOffset, - ): Session; + projections?: readonly SessionMessageProjection[], + ): Session; /** * Restore a detached session by adopting an independently owned or deeply frozen seed. * Runtime-required event fields, event envelopes, sequence continuity, surface @@ -469,7 +512,9 @@ declare class Session { * @param header - independently owned storage metadata. * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage. * @param eventState - aliasing state carried from the operation that produced the seed. + * @param projections - pure interpreters for plugin-owned message changes. * @returns a restored detached session. + * @throws when a seed event requires a missing message interpreter or fails validation. */ static fromRestore( id: SessionId, @@ -477,7 +522,8 @@ declare class Session { header: SessionHeader, inheritedEventCount: SessionLogOffset, eventState: SessionSeedEventState, - ): Session; + projections?: readonly SessionMessageProjection[], + ): Session; /** * Return the immutable event stored at one exact sequence number. * @deprecated Existing logic may remain unmigrated for now, but new calls are prohibited. @@ -499,7 +545,7 @@ declare class Session { snapshotEvents( fromSeq: SessionLogOffset = SessionLogOffset(0), toSeqExclusive: SessionLogOffset = this.seq, - ): readonly SessionEvent[]; + ): readonly SessionEvent[]; /** * Return this Session's events after its fork-inherited prefix. * @deprecated Existing logic may remain unmigrated for now, but new calls are prohibited. @@ -556,7 +602,7 @@ declare class Session { type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : [] - ): SessionEvent; + ): SessionEvent; /** * The {@link EpochHeader} in force after the log's last header event — the * header the NEXT request will be compared against — or undefined before @@ -579,21 +625,21 @@ declare class Session { * append records its `surfaceOp`, so a raw event with no marker (a chunk, a * turn boundary) is correctly absent, and a compaction `replace` deletes the * shadowed nodes from the derivation. The projection rules are - * {@link deriveEventMessage}, folded per node. + * {@link deriveEventMessage}, with logged message projections applied + * without changing node membership or message identity. * - * CACHED: each surface node is projected exactly once, when first seen — a - * call costs O(new nodes), and a surface rewrite (a `replace`; - * {@link SessionSurface.replaceGeneration}) rebuilds. The returned array is + * CACHED: pure tail growth costs O(new nodes); a replacement or message projection + * ({@link SessionSurface.contentGeneration}) rebuilds. The returned array is * a fresh snapshot per call (later appends never grow an array a caller * already holds); the `Message` objects in it are SHARED and **deep-frozen**. - * Their content reuses the already frozen durable event data, so the cache - * needs no second deep clone and consumers still cannot mutate the log. + * Unchanged content reuses frozen event data; projected blocks are frozen + * derived copies. Consumers cannot mutate the log through either form. * @returns a fresh array of the shared, frozen derived history. */ deriveMessages(): Message[]; /** - * Instance face of the pure per-node `deriveEventMessage` export from - * `surface.ts`. + * Project one event with all committed message projections applied. + * The original durable event remains unchanged. * @param event - the event to project. * @returns the derived message, or null when the event produces none. */ @@ -865,6 +911,15 @@ In-memory session store (`ctx.sessions`). Persistence is intentionally not implemented here — the agent lifecycle attaches a session-log writer to each published session's write handle; a session published outside that lifecycle persists nothing. ```ts cordis-catalog +/** + * Register one event interpreter for live creation, restore, and fork. + * Disposing the contribution makes sessions that used it refuse further derivation. + * @param projection - pure definition owned by the event's plugin. + * @returns the fiber-owned disposer. + * @throws when another definition already owns this event type. + */ +registerMessageProjection(projection: SessionMessageProjection): () => Promise + /** * Create a session owned by the calling fiber: disposing that fiber stops * event notification and removes the session from the store. `options.seed` diff --git a/docs/subsystems/subprocess.i18n.yaml b/docs/subsystems/subprocess.i18n.yaml index 4ea87e23f1..327e1ca6a2 100644 --- a/docs/subsystems/subprocess.i18n.yaml +++ b/docs/subsystems/subprocess.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/subprocess.md -subprocess.md: d51291cdc42a50d4ecd408064bb6fb770e3e3f2e -subprocess.zh.md: 054496701bc4be8f539ce0e1b5b573050d1bf961 +subprocess.md: e724903386b8c743da522a419401ac49ff1c1509 +subprocess.zh.md: b11a1cb0891123670be654d4c82105054979c911 diff --git a/docs/subsystems/subprocess.md b/docs/subsystems/subprocess.md index d51291cdc4..e724903386 100644 --- a/docs/subsystems/subprocess.md +++ b/docs/subsystems/subprocess.md @@ -244,7 +244,9 @@ interface SubprocessOutcome { `spawnTerminal(spec)` is the non-pipe process primitive. The provider allocates the controlling terminal and owns UTF-8 text transport, foreground-process-group inspection and signalling, and one awaited TERM-to-KILL operation that reaches quiescence for every session member the provider can still observe; providers document substrate-specific observability limits. The PTY backend remains responsible for prompt detection, readiness inference, scrollback, sandbox policy, and persistent-session ownership; ordinary `spawn()` cannot reconstruct controlling-terminal semantics. -The terminal spec fully specifies argv, cwd, environment overrides, dimensions, cleanup grace, and optional allocation cancellation. Its handle exposes `pid`, ordered output, `done`, `write`, `inspectForeground`, `signalForeground`, and awaited `terminate`; the exact public shapes are generated into the [`ctx.subprocess` service catalog](#ctxsubprocess--subprocessruntime-abstract-seam). +The terminal spec fully specifies argv, cwd, environment overrides, terminal type, dimensions, cleanup grace, and optional allocation cancellation. Its handle exposes `pid`, ordered output, `done`, `write`, `resize`, `inspectForeground`, `signalForeground`, and awaited `terminate`; [`SubprocessTerminalSpawnSpec` and `SubprocessTerminalHandle`](../../packages/subprocess/subprocess/src/types.ts) define these fields and operations. `resize(cols, rows)` updates the live PTY dimensions and rejects after process exit. + +`terminalEnvironment(signal?)` returns `SubprocessTerminalEnvironment`: the execution environment platform (`posix` or `windows`) and optional `defaultShell`. These facts come from the provider rather than the Web server or browser. `resolveExecutable` verifies shell candidates; `SubprocessExecutableNotFoundError` identifies a missing executable, while provider and transport failures remain errors. ## Service behavior @@ -287,6 +289,13 @@ Implementations must honor these semantics: */ abstract resolveExecutable( command: string, env?: Readonly>, signal?: AbortSignal, ): Promise +/** + * Inspect shell-selection facts in the provider's execution environment. + * @param signal - cancellation of remote environment inspection. + * @returns platform and preferred shell; executable lookup and allocation remain separate operations. + */ +abstract terminalEnvironment(signal?: AbortSignal): Promise + /** * Start one managed child process from a fully-specified spec; this seam * applies no defaults. diff --git a/docs/subsystems/subprocess.zh.md b/docs/subsystems/subprocess.zh.md index 054496701b..b11a1cb089 100644 --- a/docs/subsystems/subprocess.zh.md +++ b/docs/subsystems/subprocess.zh.md @@ -244,7 +244,9 @@ interface SubprocessOutcome { `spawnTerminal(spec)` 是非管道进程原语。提供方分配控制终端,并负责 UTF-8 文本传输、前台进程组检查与信号发送,以及一项须等待的 TERM→KILL 操作;该操作会使提供方仍可观察到的每个会话成员完全停稳,提供方则会记录执行基底特有的可观察性限制。PTY 后端仍负责提示符检测、就绪推断、scrollback、沙箱策略和持久会话所有权;普通 `spawn()` 无法重建控制终端语义。 -终端 spec 完全指定 argv、cwd、环境覆盖、尺寸、清理宽限期与可选的分配取消。其句柄公开 `pid`、有序输出、`done`、`write`、`inspectForeground`、`signalForeground` 和须等待的 `terminate`;确切的公共形状生成到 [`ctx.subprocess` 服务目录](#ctxsubprocess--subprocessruntime-abstract-seam)中。 +终端 spec 完全指定 argv、cwd、环境覆盖、终端类型、尺寸、清理宽限期与可选的分配取消。其句柄公开 `pid`、有序输出、`done`、`write`、`resize`、`inspectForeground`、`signalForeground` 和须等待的 `terminate`;[`SubprocessTerminalSpawnSpec` 与 `SubprocessTerminalHandle`](../../packages/subprocess/subprocess/src/types.ts) 定义这些字段和操作。`resize(cols, rows)` 更新正在运行的 PTY 尺寸,进程退出后拒绝调用。 + +`terminalEnvironment(signal?)` 返回 `SubprocessTerminalEnvironment`:执行环境平台(`posix` 或 `windows`)与可选的 `defaultShell`。这些事实来自提供方,而非 Web 服务器或浏览器。`resolveExecutable` 验证候选 shell;`SubprocessExecutableNotFoundError` 表示可执行文件不存在,提供方与传输故障仍作为错误报告。 ## 服务行为 @@ -287,6 +289,13 @@ Implementations must honor these semantics: */ abstract resolveExecutable( command: string, env?: Readonly>, signal?: AbortSignal, ): Promise +/** + * Inspect shell-selection facts in the provider's execution environment. + * @param signal - cancellation of remote environment inspection. + * @returns platform and preferred shell; executable lookup and allocation remain separate operations. + */ +abstract terminalEnvironment(signal?: AbortSignal): Promise + /** * Start one managed child process from a fully-specified spec; this seam * applies no defaults. diff --git a/docs/subsystems/workspace.i18n.yaml b/docs/subsystems/workspace.i18n.yaml index 72e034d222..b9004c4754 100644 --- a/docs/subsystems/workspace.i18n.yaml +++ b/docs/subsystems/workspace.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/workspace.md -workspace.md: 2c557c435c9eae9257e41ed2362fed832e437422 -workspace.zh.md: 76630506a2db6347e7155aedb12304a1c434959d +workspace.md: 77e5dd39afb6d2840751d6d56a7a50f9fea2adaa +workspace.zh.md: ad226e34b4fb508e9ce68e90349848d28632d559 diff --git a/docs/subsystems/workspace.md b/docs/subsystems/workspace.md index 2c557c435c..77e5dd39af 100644 --- a/docs/subsystems/workspace.md +++ b/docs/subsystems/workspace.md @@ -183,6 +183,97 @@ Host service backing the generated `ctx.remote.directoryPicker` namespace. The s Source: [`packages/api/workspace-controller/src/directory-picker.ts`](../../packages/api/workspace-controller/src/directory-picker.ts) + + +### `ctx.terminalController` — `TerminalController` + +Typed Remote control of transient Session-owned terminal processes. + +```ts cordis-catalog +/** + * Read the Session working directory and terminal limits without resolving a shell. + * @param agent - Session owner supplied by the Gateway. + * @param signal - request cancellation. + * @returns the Session workspace directory and terminal limits. + */ +@Remote environment(agent: Agent, signal: AbortSignal): TerminalEnvironment + +/** + * Discover installed shells in the Session's execution environment. + * @param agent - Session owner supplied by the Gateway. + * @param signal - request cancellation. + * @returns verified profiles, with the configured or system default first. + */ +@Remote shells(agent: Agent, signal: AbortSignal): Promise + +/** + * List retained terminals without resolving or activating an Agent. + * @param sessionId - displayed Session identity, including offline history. + * @returns terminals retained for this Host lifetime. + */ +@Remote list(sessionId: SessionId): WebTerminalInfo[] + +/** + * Allocate an interactive shell once for a caller-generated identity. + * @param agent - Session owner supplied by the Gateway. + * @param request - initial dimensions and idempotency identity. + * @param signal - allocation cancellation; committed terminals survive disconnection. + * @returns the existing or newly committed terminal. + */ +@Remote async create(agent: Agent, request: TerminalCreateRequest, signal: AbortSignal): Promise + +/** + * Attach to a terminal without binding its process lifetime to the transport. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param attachmentId - new exclusive input attachment. + * @param signal - physical stream cancellation. + * @returns screen recovery followed by output and metadata changes. + */ +@Remote({ mode: 'stream' }) follow(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, signal: AbortSignal): AsyncIterable + +/** + * Deliver raw input, including Tab completion and control characters. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param attachmentId - current writable attachment. + * @param data - input bytes represented as UTF-8 text. + * @returns after provider input acceptance. + */ +@Remote async write(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, data: string): Promise + +/** + * Update the dimensions of the PTY and recovery screen. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param attachmentId - current writable attachment. + * @param cols - column count. + * @param rows - row count. + * @returns after the resize completes. + */ +@Remote async resize(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, cols: number, rows: number): Promise + +/** + * Rename a terminal without changing its shell. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param title - nonempty display title, at most 120 characters. + */ +@Remote rename(agent: Agent, id: WebTerminalId, title: string): void + +/** + * Close an identity to future creation and kill its process range; repeated closes succeed. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @returns after provider cleanup succeeds. A failure retains the terminal for retry. + */ +@Remote async close(agent: Agent, id: WebTerminalId): Promise +``` + +Types: [Agent](core.md) · [SessionId](core.md) + +Source: [`packages/api/terminal-controller/src/index.ts`](../../packages/api/terminal-controller/src/index.ts) + ### `ctx.workspaceController` — `WorkspaceController` diff --git a/docs/subsystems/workspace.zh.md b/docs/subsystems/workspace.zh.md index 76630506a2..ad226e34b4 100644 --- a/docs/subsystems/workspace.zh.md +++ b/docs/subsystems/workspace.zh.md @@ -183,6 +183,97 @@ Host service backing the generated `ctx.remote.directoryPicker` namespace. The s Source: [`packages/api/workspace-controller/src/directory-picker.ts`](../../packages/api/workspace-controller/src/directory-picker.ts) + + +### `ctx.terminalController` — `TerminalController` + +Typed Remote control of transient Session-owned terminal processes. + +```ts cordis-catalog +/** + * Read the Session working directory and terminal limits without resolving a shell. + * @param agent - Session owner supplied by the Gateway. + * @param signal - request cancellation. + * @returns the Session workspace directory and terminal limits. + */ +@Remote environment(agent: Agent, signal: AbortSignal): TerminalEnvironment + +/** + * Discover installed shells in the Session's execution environment. + * @param agent - Session owner supplied by the Gateway. + * @param signal - request cancellation. + * @returns verified profiles, with the configured or system default first. + */ +@Remote shells(agent: Agent, signal: AbortSignal): Promise + +/** + * List retained terminals without resolving or activating an Agent. + * @param sessionId - displayed Session identity, including offline history. + * @returns terminals retained for this Host lifetime. + */ +@Remote list(sessionId: SessionId): WebTerminalInfo[] + +/** + * Allocate an interactive shell once for a caller-generated identity. + * @param agent - Session owner supplied by the Gateway. + * @param request - initial dimensions and idempotency identity. + * @param signal - allocation cancellation; committed terminals survive disconnection. + * @returns the existing or newly committed terminal. + */ +@Remote async create(agent: Agent, request: TerminalCreateRequest, signal: AbortSignal): Promise + +/** + * Attach to a terminal without binding its process lifetime to the transport. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param attachmentId - new exclusive input attachment. + * @param signal - physical stream cancellation. + * @returns screen recovery followed by output and metadata changes. + */ +@Remote({ mode: 'stream' }) follow(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, signal: AbortSignal): AsyncIterable + +/** + * Deliver raw input, including Tab completion and control characters. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param attachmentId - current writable attachment. + * @param data - input bytes represented as UTF-8 text. + * @returns after provider input acceptance. + */ +@Remote async write(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, data: string): Promise + +/** + * Update the dimensions of the PTY and recovery screen. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param attachmentId - current writable attachment. + * @param cols - column count. + * @param rows - row count. + * @returns after the resize completes. + */ +@Remote async resize(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, cols: number, rows: number): Promise + +/** + * Rename a terminal without changing its shell. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param title - nonempty display title, at most 120 characters. + */ +@Remote rename(agent: Agent, id: WebTerminalId, title: string): void + +/** + * Close an identity to future creation and kill its process range; repeated closes succeed. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @returns after provider cleanup succeeds. A failure retains the terminal for retry. + */ +@Remote async close(agent: Agent, id: WebTerminalId): Promise +``` + +Types: [Agent](core.zh.md) · [SessionId](core.zh.md) + +Source: [`packages/api/terminal-controller/src/index.ts`](../../packages/api/terminal-controller/src/index.ts) + ### `ctx.workspaceController` — `WorkspaceController` diff --git a/packages/api/README.i18n.yaml b/packages/api/README.i18n.yaml index ec53eaf4d8..86d8d25086 100644 --- a/packages/api/README.i18n.yaml +++ b/packages/api/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/api/README.md -README.md: 5bc878fa53b9ad0c3795c13e9442274b98820190 -README.zh.md: 8aa364850e2378c7361807ae1065cfdd6eeaa9cc +README.md: f1aefa7d4564539f94202e869bacb85857f939d5 +README.zh.md: 870bbd32552166fdbb06221baaf54b9a130c6069 diff --git a/packages/api/README.md b/packages/api/README.md index 5bc878fa53..f1aefa7d45 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -31,6 +31,7 @@ The packages below provide the Remote layer; the package READMEs own the exhaust | [`session-controller/`](session-controller/README.md) | Owns Session commands, history streams, live control state, and Agent/Session identity policy. | `ctx.sessionController` / `ctx.remote.session` | | [`settings-controller/`](settings-controller/README.md) | Owns the configuration-surface reads and writes over the settings-domain seams. | `ctx.settingsController`, `ctx.credentialsController` / `ctx.remote.settings`, `ctx.remote.credentials` | | [`workspace-controller/`](workspace-controller/README.md) | Owns Workspace mutations and the complete Client Workspace projection. | `ctx.workspaceController` / `ctx.remote.workspace` | +| [`terminal-controller/`](terminal-controller/README.md) | Session-owned interactive shells, screen recovery and browser terminal control. | `ctx.terminalController` / `ctx.remote.terminal` | | [`workspace-files/`](workspace-files/README.md) | Owns bounded workspace file access — `stat`, paged `read`, `list`, and the instrumented-operation `changes` feed — and the Client `file` resource provider over it. | `ctx.workspaceFiles` / `ctx.remote.workspaceFiles` | Remote calls run Client → Host over the application's shared Connection. API Gateway owns Remote transport, while the controller packages own Session, configuration-surface, and Workspace behavior. Feature packages register exact Connection Fetch routes for responses that do not fit Remote invocation, such as streamed downloads. diff --git a/packages/api/README.zh.md b/packages/api/README.zh.md index 8aa364850e..870bbd3255 100644 --- a/packages/api/README.zh.md +++ b/packages/api/README.zh.md @@ -31,6 +31,7 @@ kind: "package-group" | [`session-controller/`](session-controller/README.zh.md) | 拥有会话命令、历史记录流、实时控制状态与 Agent/Session 身份策略。 | `ctx.sessionController` / `ctx.remote.session` | | [`settings-controller/`](settings-controller/README.zh.md) | 拥有 settings 域各 seam 之上的配置界面读写。 | `ctx.settingsController`、`ctx.credentialsController` / `ctx.remote.settings`、`ctx.remote.credentials` | | [`workspace-controller/`](workspace-controller/README.zh.md) | 拥有 Workspace 变更与完整 Client Workspace 投影。 | `ctx.workspaceController` / `ctx.remote.workspace` | +| [`terminal-controller/`](terminal-controller/README.zh.md) | Session 拥有的交互式 shell、屏幕恢复和浏览器终端控制。 | `ctx.terminalController` / `ctx.remote.terminal` | | [`workspace-files/`](workspace-files/README.zh.md) | 拥有有界的工作区文件访问——`stat`、分页 `read`、`list` 与已埋点操作的 `changes` 流——以及其上的 Client `file` 资源提供方。 | `ctx.workspaceFiles` / `ctx.remote.workspaceFiles` | Remote 调用沿 Client → Host 方向运行在应用共享的 Connection 之上。API Gateway 拥有 Remote 传输,各控制器包分别拥有 Session、配置界面与 Workspace 行为。流式下载等不适合 Remote 调用的响应由功能包注册精确的 Connection Fetch 路由。 diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json index 985db89e9e..b84b7acb4e 100644 --- a/packages/api/gateway/package.json +++ b/packages/api/gateway/package.json @@ -64,6 +64,7 @@ "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-host-webserver": "workspace:^", + "@deepseek-ai/dsh-remote-mock": "workspace:^", "@deepseek-ai/dsh-typert-registry": "workspace:^", "@deepseek-ai/dsh-util-crypto": "workspace:^", "@types/ws": "^8.18.1", diff --git a/packages/api/gateway/tests/gateway.client.spec.ts b/packages/api/gateway/tests/gateway.client.spec.ts index 7fb0ec8bdc..eea7b53343 100644 --- a/packages/api/gateway/tests/gateway.client.spec.ts +++ b/packages/api/gateway/tests/gateway.client.spec.ts @@ -3,8 +3,10 @@ import { Context, Service } from '@deepseek-ai/cordis' import type { Fiber } from '@deepseek-ai/cordis' import { describe, expect, expectTypeOf, it, vi } from 'vitest' import { z } from 'zod' +import { RemoteMock } from '@deepseek-ai/dsh-remote-mock' import { apply as applyConnection, + type ClientTransportHooks, type ConnectionGeneration, type ConnectionGenerationSource, type ConnectionHandle, @@ -1975,11 +1977,17 @@ describe('Client Typert API', () => { }) }) - it('publishes the Fixture Host facts after Remote events report ready', async () => { + it('publishes injected Host facts after Remote events report ready', async () => { const locationDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'location') + const transportDescriptor = Object.getOwnPropertyDescriptor(globalThis, '__DSH_TRANSPORT__') + const mock = RemoteMock.create({ host: { home: '/home/mock' } }) Object.defineProperty(globalThis, 'location', { configurable: true, - value: { hostname: '127.0.0.1', search: '?fixture' }, + value: { hostname: '127.0.0.1', search: '' }, + }) + Object.defineProperty(globalThis, '__DSH_TRANSPORT__', { + configurable: true, + value: { rpc: mock.rpc } satisfies ClientTransportHooks, }) const ctx = new Context() try { @@ -1987,15 +1995,17 @@ describe('Client Typert API', () => { await ctx.plugin({ inject: [], apply: applyConnection }) await ctx.plugin({ inject, apply }) const connection = ctx.get('connection') as ConnectionHandle | undefined - if (connection === undefined) throw new Error('fixture Connection service is unavailable') + if (connection === undefined) throw new Error('injected Connection service is unavailable') await vi.waitFor(() => { - expect(connection.generation.getSnapshot()?.host.home).toBe('/home/fixture') + expect(connection.generation.getSnapshot()?.host.home).toBe('/home/mock') }) } finally { await ctx.fiber.dispose() if (locationDescriptor === undefined) Reflect.deleteProperty(globalThis, 'location') else Object.defineProperty(globalThis, 'location', locationDescriptor) + if (transportDescriptor === undefined) Reflect.deleteProperty(globalThis, '__DSH_TRANSPORT__') + else Object.defineProperty(globalThis, '__DSH_TRANSPORT__', transportDescriptor) } }) diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index 51c2ea252f..3aefa8bddc 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -86,6 +86,7 @@ "@deepseek-ai/dsh-api-session-controller": "workspace:^", "@deepseek-ai/dsh-api-workspace-files": "workspace:^", "zod": "^4.4.3", - "@deepseek-ai/dsh-command-feedback": "workspace:^" + "@deepseek-ai/dsh-command-feedback": "workspace:^", + "@deepseek-ai/dsh-api-terminal-controller": "workspace:^" } } diff --git a/packages/api/remotes/src/client/index.ts b/packages/api/remotes/src/client/index.ts index 88dc11c361..ea622cd552 100644 --- a/packages/api/remotes/src/client/index.ts +++ b/packages/api/remotes/src/client/index.ts @@ -16,6 +16,7 @@ import sessionReferencesRemote from '@deepseek-ai/dsh-session-reference/remote' import subagentsRemote from '@deepseek-ai/dsh-subagent/remote' import sessionRemote from '@deepseek-ai/dsh-api-session-controller/remote' import workspaceRemote from '@deepseek-ai/dsh-api-workspace-controller/remote' +import terminalRemote from '@deepseek-ai/dsh-api-terminal-controller/remote' import workspaceFilesRemote from '@deepseek-ai/dsh-api-workspace-files/remote' import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client' @@ -40,6 +41,8 @@ export type {} from '@deepseek-ai/dsh-api-workspace-controller/remote' export type * from '@deepseek-ai/dsh-api-workspace-controller/types' export type {} from '@deepseek-ai/dsh-api-workspace-files/remote' export type * from '@deepseek-ai/dsh-api-workspace-files/types' +export type {} from '@deepseek-ai/dsh-api-terminal-controller/remote' +export type * from '@deepseek-ai/dsh-api-terminal-controller/types' export type { SessionJob as JobView } from '@deepseek-ai/dsh-api-session-controller/types' // The forwarded-event allowlist's selection seat: without it in the consumer's // compilation face `TypertRemoteEvent` is `never` and every `$on` call fails. @@ -156,7 +159,7 @@ export async function apply(ctx: Context): Promise<() => Promise> { for (const contribution of [ agentPresetsRemote, commandsRemote, settingsControllerRemote, goalsRemote, llmRemote, dynamicRemote, pluginInventoryRemote, messageFeedbackRemote, sessionFeedbackRemote, fileUploadsRemote, sessionReferencesRemote, - permissionPresetsRemote, subagentsRemote, sessionRemote, workspaceRemote, workspaceFilesRemote, + permissionPresetsRemote, subagentsRemote, sessionRemote, workspaceRemote, workspaceFilesRemote, terminalRemote, ]) { disposers.push(await ctx.remote.$mount(contribution)) } diff --git a/packages/api/remotes/tsconfig.client.json b/packages/api/remotes/tsconfig.client.json index 31f11c38d5..c00198c04a 100644 --- a/packages/api/remotes/tsconfig.client.json +++ b/packages/api/remotes/tsconfig.client.json @@ -85,6 +85,9 @@ }, { "path": "../../feedback/command-feedback" + }, + { + "path": "../terminal-controller/tsconfig.client.json" } ] } diff --git a/packages/api/remotes/tsconfig.host.json b/packages/api/remotes/tsconfig.host.json index 55679eba43..66799352d2 100644 --- a/packages/api/remotes/tsconfig.host.json +++ b/packages/api/remotes/tsconfig.host.json @@ -70,6 +70,9 @@ }, { "path": "../../typert/protocol" + }, + { + "path": "../terminal-controller/tsconfig.host.json" } ] } diff --git a/packages/api/session-controller/tests/fake-api.client.ts b/packages/api/session-controller/tests/fake-api.client.ts deleted file mode 100644 index 50f702c5fa..0000000000 --- a/packages/api/session-controller/tests/fake-api.client.ts +++ /dev/null @@ -1,476 +0,0 @@ -// Test-local programmable Remote fake (NOT the fixture: fixture is a demo -// data source on a real clock; behavior tests need per-case responses and -// deferred-controlled timing). Session streams are hand pumps: pushFollow/pushControl. -import type { - MessageId, - SessionId, SessionSearchItem, - SubagentCatalog, SubagentInterruptReceipt, SubagentPromptReceipt, - WorkspaceId, WorkspaceView, -} from '@deepseek-ai/dsh-api-remotes/client' -import type { - SessionAddress, - SessionAssistantStreamBaseline, - SessionControlBaseline, - SessionControlFrame, - SessionFollowFrame, - SessionFollowRequest, - SessionPage, - SessionPageRequest, - SessionProjectionBaseline, - SessionSelectModelRequest, - SessionSelectModelValue, -} from '@deepseek-ai/dsh-api-session-controller/types' -import type { WorkspaceRemote } from '@deepseek-ai/dsh-api-workspace-controller/client' -import type { WorkspaceFollowFrame } from '@deepseek-ai/dsh-api-workspace-controller/types' -import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol' -import { - RemoteStream, - type RemoteStreamOptions, -} from '@deepseek-ai/dsh-api-gateway/client' -import type { SessionRemotes } from '../src/client/sessions/remotes.ts' -import { followSnapshot, pageThrough } from './remote/history.client.ts' - -const AVAILABLE_STREAM_CONNECTION = { - generation: { - getSnapshot: () => ({ id: 1, host: { home: '/h' } }), - subscribe: () => () => {}, - }, -} - -/** Programmable-default workspace row (branded id, ISO-ish times). */ -function fakeWorkspace(id: string, over: Partial = {}): WorkspaceView { - return { - workspaceId: id as WorkspaceId, - path: '/f/ws', - title: 'ws', - sessionIds: [], - createdAt: '2026-01-01T00:00:00.000Z', - updatedAt: '2026-01-01T00:00:00.000Z', - ...over, - } -} - -function addressSessionId(address: SessionAddress): SessionId { - return address.kind === 'session' ? address.sessionId : address.childSessionId -} - -export interface Deferred { - promise: Promise - resolve(value: T): void - reject(error: unknown): void -} - -/** Test-held settlement: the case decides when an RPC lands (history-pending injections etc.). */ -export function deferred(): Deferred { - let resolve!: (value: T) => void - let reject!: (error: unknown) => void - const promise = new Promise((res, rej) => { - resolve = res - reject = rej - }) - return { promise, resolve, reject } -} - -/** - * Successful generated Remote result for programmable domain fakes. - * @param value - the value the Host answers with. - * @returns the success branch of a Remote result. - */ -export function ok(value: T): RemoteResult { - return { ok: true, value } -} - -/** - * Failed generated Remote result carrying the owner's declared failure. - * @param error - the owner-declared failure. - * @returns the failure branch of a Remote result. - */ -export function err(error: RemoteFailure): RemoteResult { - return { ok: false, error } -} - -type ValueStreamItem = - | { kind: 'frame'; value: F; delivered?: () => void } - | { kind: 'end' } - | { kind: 'fail'; error: unknown } - -interface ValueStreamConn { - feed(item: ValueStreamItem): void -} - -interface OpenValueStream { - readonly values: AsyncGenerator - dispose(): void -} - -/** - * Commands Remote double: the generated face delivers the carrier's outcome, so - * a test that programs nothing sees an empty catalog and an unmatched line. - * @returns the Remote namespaces the session cluster calls. - */ -export type RuntimeRemotes = SessionRemotes & { readonly workspace: WorkspaceRemote } - -export function fakeRemote(api = new FakeApiClient()): RuntimeRemotes { - return api.sessionRemotes() -} - -export class FakeApiClient { - /** Chronological call record: [method, payload]. */ - readonly calls: { method: string; payload: unknown }[] = [] - /** Session ids in physical follow-generation opening order. */ - readonly followStarts: SessionId[] = [] - - // Programmable slots (defaults answer OK-empty); reassign per case. - onList: (payload: unknown) => Promise> = () => Promise.resolve(ok({ items: [] })) - onSearch: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ items: [], hasMore: false })) - onCreate: (payload: unknown) => Promise> = () => Promise.resolve(ok({ sessionId: 'fk-new' as SessionId })) - onSelectModel: (payload: SessionSelectModelRequest) => Promise> = - payload => Promise.resolve(ok({ - selected: { - provider: payload.provider, - model: payload.model, - ...(payload.reasoningEffort === undefined - ? {} - : { reasoningEffort: payload.reasoningEffort }), - }, - })) - onRename: (payload: unknown) => Promise> = () => Promise.resolve(ok({ title: 'fk-renamed', seq: 0 })) - onFork: (payload: unknown) => Promise> = () => Promise.resolve(ok({ sessionId: 'fk-fork' as SessionId })) - onHistory: (payload: { sessionId: SessionId; throughSeq?: number; beforeSeq?: number; maxMessages?: number }) - => Promise> = - () => Promise.resolve(ok({ records: [], hasMore: false })) - - onPrompt: (payload: unknown) => Promise> = () => Promise.resolve(ok({ accepted: true as const })) - onAttachment: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ attachment: { attachmentId: 'a' as never, mediaType: 'image/png', bytes: 1, width: 1, height: 1 }, data: 'AA==' })) - onUpdateQueue: (payload: unknown) => Promise> = () => Promise.resolve(ok({ accepted: true as const })) - onCancel: (payload: unknown) => Promise> = () => Promise.resolve(ok({ accepted: true as const })) - onOpenWorkspacePath: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ opened: true as const })) - - private readonly followConns = new Map[]>() - private readonly controlConns: ValueStreamConn[] = [] - private readonly workspaceConns: ValueStreamConn[] = [] - /** Optional Host opening cursor override for stale-page and reconnect tests. */ - followCursor: number | undefined - controlBaseline: SessionControlBaseline = { - queues: {}, - jobs: {}, - projections: {}, - } - assistantStreamBaseline: SessionAssistantStreamBaseline = { - revision: 0, - } - workspaceBaseline: Extract['value'] = { - items: [], - archivedSessionIds: [], - } - lastSearchSignal: AbortSignal | undefined - - onSubagentList: (payload: unknown) => Promise> - = () => Promise.resolve(ok({ entries: [], parentAvailable: true })) - onSubagentPrompt: (payload: unknown) => Promise> - = () => Promise.resolve(ok({ messageId: 'fake-message' as MessageId })) - - onSubagentInterrupt: (payload: unknown) => Promise> - = () => Promise.resolve(ok({ accepted: true as const })) - - onWorkspaceCreate: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws'), created: true })) - - onWorkspaceRename: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws') })) - - onWorkspaceDelete: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ deleted: true })) - - onWorkspaceInsertBefore: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ workspaceIds: [] })) - - onWorkspaceInsertSessionBefore: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws') })) - - onWorkspaceArchiveSession: (payload: unknown) => Promise> = - payload => Promise.resolve(ok({ archivedSessionIds: [(payload as { sessionId: SessionId }).sessionId] })) - - onWorkspaceUnarchiveSession: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ archivedSessionIds: [] })) - - /** Remote namespaces bound to this fake's programmable unary slots and stream pumps. */ - sessionRemotes(): RuntimeRemotes { - return { - $stream: (options: RemoteStreamOptions) => ( - new RemoteStream(AVAILABLE_STREAM_CONNECTION, options) - ), - commands: { - execute: () => Promise.resolve({ ok: true, value: undefined }), - }, - session: { - canOpenWorkspacePath: () => Promise.resolve(ok(true)), - list: payload => this.record('session.list', payload, this.onList(payload)), - modelCatalog: () => Promise.resolve({ - ok: true, - value: { - default: { provider: 'fixture', model: 'fixture' }, - routableProviders: [], - groups: [], - failures: [], - }, - }), - search: (payload, signal) => { - this.lastSearchSignal = signal - return this.record('session.search', payload, this.onSearch(payload)) - }, - create: payload => this.record('session.create', payload, this.onCreate(payload)), - selectModel: payload => this.record( - 'session.selectModel', - payload, - this.onSelectModel(payload), - ), - rename: payload => this.record('session.rename', payload, this.onRename(payload)), - fork: payload => this.record('session.fork', payload, this.onFork(payload)), - prompt: payload => this.record('session.prompt', payload, this.onPrompt(payload)), - attachment: payload => this.record('session.attachment', payload, this.onAttachment(payload)), - updateQueue: payload => this.record('session.updateQueue', payload, this.onUpdateQueue(payload)), - cancel: payload => this.record('session.cancel', payload, this.onCancel(payload)), - openWorkspacePath: payload => this.record( - 'session.openWorkspacePath', - payload, - this.onOpenWorkspacePath(payload), - ), - page: request => this.page(request), - follow: (request, signal) => this.openFollow(request, signal), - control: signal => this.openControl(signal), - }, - subagents: { - list: parentSessionId => this.record( - 'subagents.list', - parentSessionId, - this.onSubagentList(parentSessionId), - ), - prompt: request => this.record('subagents.prompt', request, this.onSubagentPrompt(request)), - interruptByParent: (childSessionId, parentSessionId, mode) => this.record( - 'subagents.interruptByParent', - { childSessionId, parentSessionId, mode }, - this.onSubagentInterrupt({ childSessionId, parentSessionId, mode }), - ), - }, - workspace: { - create: payload => this.record('workspace.create', payload, this.onWorkspaceCreate(payload)), - rename: payload => this.record('workspace.rename', payload, this.onWorkspaceRename(payload)), - delete: payload => this.record('workspace.delete', payload, this.onWorkspaceDelete(payload)), - insertBefore: payload => this.record( - 'workspace.insertBefore', - payload, - this.onWorkspaceInsertBefore(payload), - ), - insertSessionBefore: payload => this.record( - 'workspace.insertSessionBefore', - payload, - this.onWorkspaceInsertSessionBefore(payload), - ), - archiveSession: payload => this.record( - 'workspace.archiveSession', - payload, - this.onWorkspaceArchiveSession(payload), - ), - unarchiveSession: payload => this.record( - 'workspace.unarchiveSession', - payload, - this.onWorkspaceUnarchiveSession(payload), - ), - follow: signal => this.openWorkspace(signal), - }, - } - } - - /** Push one live Session event to every follower of that Session. */ - async pushFollow( - sessionId: SessionId, - frame: Exclude, - ): Promise { - await Promise.all([...(this.followConns.get(sessionId) ?? [])].map(conn => new Promise((resolve) => { - conn.feed({ kind: 'frame', value: frame, delivered: resolve }) - }))) - } - - /** Push one Host-wide control update. */ - pushControl(frame: Exclude): void { - for (const conn of [...this.controlConns]) conn.feed({ kind: 'frame', value: frame }) - } - - /** Push one Workspace projection increment. */ - pushWorkspace(frame: Exclude): void { - for (const conn of [...this.workspaceConns]) conn.feed({ kind: 'frame', value: frame }) - } - - /** End (clean close) or fail (throw) every open stream — reconnect-path material. */ - endStreams(): void { - for (const conns of this.followConns.values()) { - for (const conn of [...conns]) conn.feed({ kind: 'end' }) - } - for (const conn of [...this.controlConns]) conn.feed({ kind: 'end' }) - for (const conn of [...this.workspaceConns]) conn.feed({ kind: 'end' }) - } - - failStreams(error: unknown): void { - for (const conns of this.followConns.values()) { - for (const conn of [...conns]) conn.feed({ kind: 'fail', error }) - } - for (const conn of [...this.controlConns]) conn.feed({ kind: 'fail', error }) - for (const conn of [...this.workspaceConns]) conn.feed({ kind: 'fail', error }) - } - - callsOf(method: string): unknown[] { - return this.calls.filter(c => c.method === method).map(c => c.payload) - } - - /** Number of currently attached journal generations for one Session. */ - activeFollows(sessionId: SessionId): number { - return this.followConns.get(sessionId)?.length ?? 0 - } - - private record(method: string, payload: unknown, response: Promise): Promise { - this.calls.push({ method, payload }) - return response - } - - private page(request: SessionPageRequest): Promise> { - return this.fetchPage(request) - } - - private async fetchPage( - request: SessionPageRequest, - response?: Promise>, - ): Promise> { - const sessionId = addressSessionId(request.address) - const payload = request.address.kind === 'session' - ? { - sessionId, - throughSeq: request.throughSeq, - ...request.beforeSeq === undefined ? {} : { beforeSeq: request.beforeSeq }, - ...request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }, - } - : { - parentSessionId: request.address.parentSessionId, - childSessionId: request.address.childSessionId, - mode: request.address.mode, - throughSeq: request.throughSeq, - ...request.beforeSeq === undefined ? {} : { beforeSeq: request.beforeSeq }, - ...request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }, - } - const method = request.address.kind === 'session' ? 'session.history' : 'subagent.history' - const result = await this.record(method, payload, response ?? this.onHistory({ - sessionId, - throughSeq: request.throughSeq, - ...request.beforeSeq === undefined ? {} : { beforeSeq: request.beforeSeq }, - ...request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }, - })) - if (!result.ok) return result - return { - ok: true, - value: pageThrough(result.value, request.throughSeq), - } - } - - private async *openFollow( - request: SessionFollowRequest, - signal: AbortSignal = new AbortController().signal, - ): AsyncGenerator { - const sessionId = addressSessionId(request.address) - this.followStarts.push(sessionId) - this.calls.push({ method: 'session.follow', payload: request }) - const conns = this.followConns.get(sessionId) ?? [] - if (!this.followConns.has(sessionId)) this.followConns.set(sessionId, conns) - const stream = this.openValueStream(conns, signal) - try { - const response = await this.onHistory({ - sessionId, - maxMessages: request.maxMessages ?? 50, - }) - if (!response.ok) throw response.error - const page = response.value - yield followSnapshot(page, request, this.followCursor, this.assistantStreamBaseline) - yield* stream.values - } finally { - stream.dispose() - } - } - - private async *openControl( - signal: AbortSignal = new AbortController().signal, - ): AsyncGenerator { - const stream = this.openValueStream(this.controlConns, signal) - try { - yield { type: 'baseline', value: this.controlBaseline } - yield* stream.values - } finally { - stream.dispose() - } - } - - private async *openWorkspace( - signal: AbortSignal = new AbortController().signal, - ): AsyncGenerator { - const stream = this.openValueStream(this.workspaceConns, signal) - try { - yield { type: 'baseline', value: this.workspaceBaseline } - yield* stream.values - } finally { - stream.dispose() - } - } - - private openValueStream( - registry: ValueStreamConn[], - signal: AbortSignal, - ): OpenValueStream { - const inbox: ValueStreamItem[] = [] - let wake: (() => void) | null = null - let inFlightDelivered: (() => void) | undefined - let disposed = false - const conn: ValueStreamConn = { - feed: (item) => { - inbox.push(item) - wake?.() - }, - } - registry.push(conn) - const dispose = (): void => { - if (disposed) return - disposed = true - inFlightDelivered?.() - for (const item of inbox) { - if (item.kind === 'frame') item.delivered?.() - } - const index = registry.indexOf(conn) - if (index >= 0) registry.splice(index, 1) - wake?.() - } - const values = (async function* (): AsyncGenerator { - try { - while (!signal.aborted && !disposed) { - while (inbox.length > 0) { - const item = inbox.shift() as ValueStreamItem - if (item.kind === 'end') return - if (item.kind === 'fail') throw item.error - inFlightDelivered = item.delivered - yield item.value - inFlightDelivered?.() - inFlightDelivered = undefined - } - await new Promise((resolve) => { - wake = resolve - signal.addEventListener('abort', () => { resolve() }, { once: true }) - }) - wake = null - } - } finally { - dispose() - } - })() - return { values, dispose } - } - -} diff --git a/packages/api/session-controller/tests/manager.client.spec.ts b/packages/api/session-controller/tests/manager.client.spec.ts index 2608887137..e6ffa43850 100644 --- a/packages/api/session-controller/tests/manager.client.spec.ts +++ b/packages/api/session-controller/tests/manager.client.spec.ts @@ -3,18 +3,29 @@ * frame routing, and control baselines for uninstantiated sessions. */ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, vi } from 'vitest' import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client' import { SessionSeq } from '@deepseek-ai/dsh-session/types' import { RemoteError } from '@deepseek-ai/dsh-typert-protocol' import type { SessionControlFrame } from '@deepseek-ai/dsh-api-session-controller/types' +import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client' +import { ok, type RemoteMock } from '@deepseek-ai/dsh-remote-mock' +import { + createClientTest, type ClientTestFixtures, webApp, +} from '@deepseek-ai/dsh-client-test-runtime/src/assembly/index.ts' import type {} from '@deepseek-ai/dsh-session-title/client' import { SessionManager } from '../src/client/sessions/manager.ts' -import { FakeApiClient, deferred, err, fakeRemote, ok } from './fake-api.client.ts' +import type { SessionRemotes } from '../src/client/sessions/remotes.ts' import { entries, plainTurn } from './event-script.client.ts' +import { FOLLOW, err, followScript, sessionWorld } from './remote/session.client.ts' const S1 = 'fk-m1' as SessionId const S2 = 'fk-m2' as SessionId +/** Gateway Client cone used by the subagent-catalog and connected-generation cases. */ +const API_ROSTER = webApp.closure(['@deepseek-ai/dsh-api-gateway']) +const it = createClientTest({ roster: API_ROSTER }) +/** The first client boot pays the cold module transform of the api cone. */ +const COLD_BOOT_TIMEOUT_MS = 60_000 type SummaryOver = Partial<{ updatedAt: number @@ -29,16 +40,21 @@ function summary(sessionId: SessionId, over: SummaryOver = {}) { return { sessionId, updatedAt: 100, running: false, blank: false, ...over } } -function makeManager(): SessionManager { - const api = new FakeApiClient() - return new SessionManager(fakeRemote(api)) +function makeManager( + mock: RemoteMock, + remote: ClientTestFixtures['remote'], + restoredSelection?: SessionId, + restoredAddress?: SubagentAddress, +): SessionManager { + mock.load(sessionWorld) + // Cases using this helper never open a Session, so they do not need the broader Client Remote's $stream member. + return new SessionManager(remote as unknown as SessionRemotes, restoredSelection, restoredAddress) } describe('SessionManager instances', () => { - it('lazily builds one resident instance per id and syncs the running bit from the list', async () => { - const api = new FakeApiClient() - api.onList = () => Promise.resolve(ok({ items: [summary(S1, { running: true })] as never[] })) - const manager = new SessionManager(fakeRemote(api)) + it('lazily builds one resident instance per id and syncs the running bit from the list', async ({ mock, remote }) => { + remote.session.list.mockResolvedValue(ok({ items: [summary(S1, { running: true })] as never[] })) + const manager = makeManager(mock, remote) await manager.refreshList() const session = manager.get(S1) expect(manager.get(S1)).toBe(session) // resident: same instance forever @@ -48,97 +64,90 @@ describe('SessionManager instances', () => { }) describe('list lifecycle', () => { - it('single-flights refreshList and preserves the Host baseline order', async () => { - const api = new FakeApiClient() - const gate = deferred>>() - api.onList = () => gate.promise - const manager = new SessionManager(fakeRemote(api)) + it('single-flights refreshList and preserves the Host baseline order', async ({ mock, remote }) => { + const gate = Promise.withResolvers>>() + remote.session.list.mockReturnValue(gate.promise) + const manager = makeManager(mock, remote) const first = manager.refreshList() const second = manager.refreshList() expect(manager.getListSnapshot().state).toBe('loading') gate.resolve(ok({ items: [summary(S2, { updatedAt: 200 }), summary(S1)] as never[] })) await Promise.all([first, second]) - expect(api.callsOf('session.list')).toHaveLength(1) + expect(remote.session.list).toHaveBeenCalledOnce() const snapshot = manager.getListSnapshot() expect(snapshot.state).toBe('idle') expect(snapshot.items.map(i => i.sessionId)).toEqual([S2, S1]) }) - it('replays incremental frames over hydration and never batch-reorders established ids', async () => { - const api = new FakeApiClient() - const first = deferred>>() - api.onList = () => first.promise - const manager = new SessionManager(fakeRemote(api)) + it('replays incremental frames over hydration and never batch-reorders established ids', async ({ mock, remote }) => { + const first = Promise.withResolvers>>() + remote.session.list.mockReturnValue(first.promise) + const manager = makeManager(mock, remote) const hydration = manager.refreshList() manager.handleSessionAdded(summary(S2, { blank: true })) first.resolve(ok({ items: [summary(S1)] as never[] })) await hydration expect(manager.getListSnapshot().items.map(item => item.sessionId)).toEqual([S2, S1]) - api.onList = () => Promise.resolve(ok({ + remote.session.list.mockResolvedValue(ok({ items: [summary(S1, { updatedAt: 900 }), summary(S2, { updatedAt: 800 })] as never[], })) await manager.refreshList() expect(manager.getListSnapshot().items.map(item => item.sessionId)).toEqual([S2, S1]) }) - it('advances list activity from the filtered Host notification', async () => { - const api = new FakeApiClient() - api.onList = () => Promise.resolve(ok({ items: [summary(S1)] as never[] })) - const manager = new SessionManager(fakeRemote(api)) + it('advances list activity from the filtered Host notification', async ({ mock, remote }) => { + remote.session.list.mockResolvedValue(ok({ items: [summary(S1)] as never[] })) + const manager = makeManager(mock, remote) await manager.refreshList() manager.handleSessionActivity(S1, 500) expect(manager.getListSnapshot().items[0]?.updatedAt).toBe(500) }) - it('keeps the error in the list snapshot on failure', async () => { - const api = new FakeApiClient() - api.onList = () => Promise.resolve(err(new RemoteError('gateway/internal', 'boom', {}))) - const manager = new SessionManager(fakeRemote(api)) + it('keeps the error in the list snapshot on failure', async ({ mock, remote }) => { + remote.session.list.mockResolvedValue(err(new RemoteError('gateway/internal', 'boom', {}))) + const manager = makeManager(mock, remote) await manager.refreshList() expect(manager.getListSnapshot()).toMatchObject({ state: 'error', error: { code: 'gateway/internal' } }) // A failed pull does not step the arrival phase: still pending. expect(manager.getListSnapshot().phase).toBe('pending') }) - it('phase steps pending → ready on the first successful pull and never returns', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) + it('phase steps pending → ready on the first successful pull and never returns', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) expect(manager.getListSnapshot().phase).toBe('pending') await manager.refreshList() expect(manager.getListSnapshot().phase).toBe('ready') // Sticky across later failures: the pull-activity axis reports the error, // the arrival phase holds. - api.onList = () => Promise.resolve(err(new RemoteError('gateway/internal', 'down', {}))) + remote.session.list.mockResolvedValue(err(new RemoteError('gateway/internal', 'down', {}))) await manager.refreshList() expect(manager.getListSnapshot()).toMatchObject({ state: 'error', phase: 'ready' }) // And across an empty re-pull (empty-with-ready = truly no sessions). - api.onList = () => Promise.resolve(ok({ items: [] as never[] })) + remote.session.list.mockResolvedValue(ok({ items: [] as never[] })) await manager.refreshList() expect(manager.getListSnapshot()).toMatchObject({ state: 'idle', phase: 'ready' }) expect(manager.getListSnapshot().items).toEqual([]) }) - it('merges create into the list immediately without waiting for a refresh', async () => { - const api = new FakeApiClient() - api.onCreate = () => Promise.resolve(ok({ sessionId: S2 })) - const manager = new SessionManager(fakeRemote(api)) + it('merges create into the list immediately without waiting for a refresh', async ({ mock, remote }) => { + remote.session.create.mockResolvedValue(ok({ sessionId: S2 })) + const manager = makeManager(mock, remote) const result = await manager.create() expect(result).toMatchObject({ ok: true, value: { sessionId: S2 } }) expect(manager.getListSnapshot().items.map(i => i.sessionId)).toEqual([S2]) }) - it('retains title projections before list arrival, keeps last-wins by seq, and clears them on removal', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) + it('retains title projections before list arrival, keeps last-wins by seq, and clears them on removal', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) const titleFrame = (title: string, seq: number) => { manager.handleControlFrame({ type: 'projection', sessionId: S1, key: 'title', value: title, seq }) } titleFrame('Newest', 4) titleFrame('Stale', 3) titleFrame('Equal', 4) - api.onList = () => Promise.resolve(ok({ + remote.session.list.mockResolvedValue(ok({ items: [summary(S1), summary(S2, { updatedAt: 200 })] as never[], })) await manager.refreshList() @@ -153,14 +162,13 @@ describe('list lifecycle', () => { expect(manager.getListSnapshot().items.find(item => item.sessionId === S1)?.title).toBeUndefined() }) - it('seeds cold titles from the list rows\' projections block under higher-seq-wins', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) + it('seeds cold titles from the list rows\' projections block under higher-seq-wins', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) // A push frame landed before the list (S2's title is newer than the block's cut). manager.handleControlFrame({ type: 'projection', sessionId: S2, key: 'title', value: 'Pushed', seq: 9, }) - api.onList = () => Promise.resolve(ok({ + remote.session.list.mockResolvedValue(ok({ items: [ { ...summary(S1), projections: { asOfSeq: 4, values: { title: 'Cold cached' } } }, { ...summary(S2, { updatedAt: 200 }), projections: { asOfSeq: 5, values: { title: 'List stale' } } }, @@ -174,10 +182,9 @@ describe('list lifecycle', () => { expect(items.find(item => item.sessionId === S2)?.title).toBe('Pushed') }) - it('drops a projection row beyond the subscription baseline before accepting its durable replay', async () => { - const api = new FakeApiClient() - api.onList = () => Promise.resolve(ok({ items: [summary(S1)] as never[] })) - const manager = new SessionManager(fakeRemote(api)) + it('drops a projection row beyond the subscription baseline before accepting its durable replay', async ({ mock, remote }) => { + remote.session.list.mockResolvedValue(ok({ items: [summary(S1)] as never[] })) + const manager = makeManager(mock, remote) await manager.refreshList() const frame = (payload: SessionControlFrame) => { manager.handleControlFrame(payload) } frame({ type: 'projection', sessionId: S1, key: 'title', value: 'Unflushed', seq: 4 }) @@ -209,13 +216,12 @@ describe('list lifecycle', () => { }) describe('search', () => { - it('returns bounded Host results and forwards the caller signal', async () => { - const api = new FakeApiClient() - api.onSearch = () => Promise.resolve(ok({ + it('returns bounded Host results and forwards the caller signal', async ({ mock, remote }) => { + remote.session.search.mockResolvedValue(ok({ items: [{ sessionId: S1, snippet: 'matching excerpt' }], hasMore: true, })) - const manager = new SessionManager(fakeRemote(api)) + const manager = makeManager(mock, remote) const signal = new AbortController().signal await expect(manager.search('exact phrase', signal)).resolves.toEqual({ @@ -225,29 +231,26 @@ describe('search', () => { hasMore: true, }, }) - expect(api.callsOf('session.search')).toEqual([{ query: 'exact phrase' }]) - expect(api.lastSearchSignal).toBe(signal) + expect(remote.session.search).toHaveBeenCalledExactlyOnceWith({ query: 'exact phrase' }, signal) }) - it('preserves business errors and propagates a non-Remote throw', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) - api.onSearch = () => Promise.resolve(err(new RemoteError('gateway/internal', 'index unavailable', {}))) + it('preserves business errors and propagates a non-Remote throw', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) + remote.session.search.mockResolvedValue(err(new RemoteError('gateway/internal', 'index unavailable', {}))) const signal = new AbortController().signal await expect(manager.search('first', signal)).resolves.toMatchObject({ ok: false, error: { code: 'gateway/internal', message: 'index unavailable' }, }) - api.onSearch = () => Promise.reject(new Error('wire down')) + remote.session.search.mockRejectedValue(new Error('wire down')) await expect(manager.search('second', signal)).rejects.toThrow('wire down') }) }) describe('Host Remote event routing', () => { - it('adds/removes/flips sessions and keeps removed instances resident', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) + it('adds/removes/flips sessions and keeps removed instances resident', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) manager.handleSessionAdded(summary(S1, { blank: true })) manager.handleSessionAdded(summary(S1, { blank: true })) // dup: ignored expect(manager.getListSnapshot().items).toHaveLength(1) @@ -268,20 +271,21 @@ describe('Host Remote event routing', () => { }) describe('subagent catalogs', () => { - it('keeps a catalog-discovered child address across ordinary selection and status frames', async () => { - const api = new FakeApiClient() - api.onList = () => Promise.resolve(ok({ items: [ + it('keeps a catalog-discovered child address across ordinary selection and status frames', async ({ mock, remote, start }) => { + remote.session.list.mockResolvedValue(ok({ items: [ summary(S1), summary(S2, { parentSessionId: S1, origin: 'subagent' }), ] as never[] })) - api.onSubagentList = () => Promise.resolve(ok({ + remote.subagents.list.mockResolvedValue(ok({ entries: [{ kind: 'child', id: S2, mode: 'continuable', label: 'worker', activity: 'running', hasChildren: false, }] as never[], parentAvailable: true, })) - const manager = new SessionManager(fakeRemote(api)) + mock.load(sessionWorld) + const client = await start() + const manager = new SessionManager(client.ctx.remote) await manager.refreshList() await manager.refreshSubagents(S1) manager.selectSubagent({ parentSessionId: S1, childSessionId: S2, mode: 'continuable' }) @@ -305,7 +309,7 @@ describe('subagent catalogs', () => { }) await manager.get(S2).open() await manager.get(S2).prompt([{ type: 'text', text: 'continue' }], 'queue') - expect(api.callsOf('session.follow')).toEqual([ + expect(remote.session.follow.mock.calls.map(([request]) => request)).toEqual([ { address: { kind: 'subagent', parentSessionId: S1, childSessionId: S2, mode: 'continuable', @@ -314,8 +318,8 @@ describe('subagent catalogs', () => { maxMessages: 50, }, ]) - expect(api.callsOf('subagent.history')).toEqual([]) - expect(api.callsOf('subagents.prompt')).toEqual([ + expect(remote.session.page).not.toHaveBeenCalled() + expect(remote.subagents.prompt.mock.calls.map(([request]) => request)).toEqual([ { requestId: expect.any(String) as unknown as string, parentSessionId: S1, childSessionId: S2, @@ -325,14 +329,13 @@ describe('subagent catalogs', () => { clientTimeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone, }, ]) - expect(api.callsOf('session.history')).toEqual([]) - expect(api.callsOf('session.prompt')).toEqual([]) - const listCalls = api.callsOf('subagents.list').length + expect(remote.session.prompt).not.toHaveBeenCalled() + const listCalls = remote.subagents.list.mock.calls.length manager.handleSessionStatus(S2, false) expect(manager.getListSnapshot().subagentsByParent[S1]?.entries[0]).toMatchObject({ kind: 'child', id: S2, activity: 'inactive', }) - expect(api.callsOf('subagents.list')).toHaveLength(listCalls) + expect(remote.subagents.list).toHaveBeenCalledTimes(listCalls) manager.handleSessionRemoved(S2) expect(manager.getListSnapshot().items.find(item => item.sessionId === S2)).toMatchObject({ @@ -344,35 +347,33 @@ describe('subagent catalogs', () => { address: { parentSessionId: S1, childSessionId: S2, mode: 'continuable' }, }, }) - }) + }, COLD_BOOT_TIMEOUT_MS) - it('refetches debounced membership only while the parent catalog is open', async () => { + it('refetches debounced membership only while the parent catalog is open', async ({ mock, remote }) => { vi.useFakeTimers() try { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) + const manager = makeManager(mock, remote) await manager.refreshSubagents(S1) manager.setSubagentCatalogOpen(S1, true) await Promise.resolve() - const baseline = api.callsOf('subagents.list').length + const baseline = remote.subagents.list.mock.calls.length manager.handleSessionAdded(summary(S2, { parentSessionId: S1 })) manager.handleSessionAdded(summary('fk-m3' as SessionId, { parentSessionId: S1 })) await vi.advanceTimersByTimeAsync(50) - expect(api.callsOf('subagents.list')).toHaveLength(baseline + 1) + expect(remote.subagents.list).toHaveBeenCalledTimes(baseline + 1) manager.setSubagentCatalogOpen(S1, false) manager.handleSessionAdded(summary('fk-m4' as SessionId, { parentSessionId: S1 })) await vi.advanceTimersByTimeAsync(50) - expect(api.callsOf('subagents.list')).toHaveLength(baseline + 1) + expect(remote.subagents.list).toHaveBeenCalledTimes(baseline + 1) } finally { vi.useRealTimers() } }) - it('marks a loaded parent row expandable only for a direct subagent publication', async () => { - const api = new FakeApiClient() + it('marks a loaded parent row expandable only for a direct subagent publication', async ({ mock, remote }) => { const root = 'fk-root' as SessionId - api.onSubagentList = () => Promise.resolve(ok({ + remote.subagents.list.mockResolvedValue(ok({ entries: [ { kind: 'child', id: S1, mode: 'continuable', label: 'parent', @@ -385,7 +386,7 @@ describe('subagent catalogs', () => { ] as never[], parentAvailable: true, })) - const manager = new SessionManager(fakeRemote(api)) + const manager = makeManager(mock, remote) await manager.refreshSubagents(root) manager.handleSessionAdded(summary('fk-grandchild' as SessionId, { @@ -399,12 +400,11 @@ describe('subagent catalogs', () => { ]) }) - it('preserves a live expandability hint across only the older in-flight catalog response', async () => { - const api = new FakeApiClient() + it('preserves a live expandability hint across only the older in-flight catalog response', async ({ mock, remote }) => { const root = 'fk-root' as SessionId - const response = deferred>>() - api.onSubagentList = () => response.promise - const manager = new SessionManager(fakeRemote(api)) + const response = Promise.withResolvers>>() + remote.subagents.list.mockReturnValue(response.promise) + const manager = makeManager(mock, remote) const refresh = manager.refreshSubagents(root) manager.handleSessionAdded(summary('fk-grandchild' as SessionId, { @@ -423,7 +423,7 @@ describe('subagent catalogs', () => { { kind: 'child', id: S1, hasChildren: true }, ]) - api.onSubagentList = () => Promise.resolve(ok({ + remote.subagents.list.mockResolvedValue(ok({ entries: [{ kind: 'child', id: S1, mode: 'continuable', label: 'parent', activity: 'inactive', hasChildren: false, @@ -436,12 +436,11 @@ describe('subagent catalogs', () => { ]) }) - it('replays status frames over an older in-flight catalog response', async () => { - const api = new FakeApiClient() + it('replays status frames over an older in-flight catalog response', async ({ mock, remote }) => { const root = 'fk-root' as SessionId - const response = deferred>>() - api.onSubagentList = () => response.promise - const manager = new SessionManager(fakeRemote(api)) + const response = Promise.withResolvers>>() + remote.subagents.list.mockReturnValue(response.promise) + const manager = makeManager(mock, remote) const refresh = manager.refreshSubagents(root) manager.handleSessionStatus(S1, false) @@ -467,16 +466,15 @@ describe('subagent catalogs', () => { ]) }) - it('marks a detached catalog child inactive without requiring a selected address', async () => { - const api = new FakeApiClient() - api.onSubagentList = () => Promise.resolve(ok({ + it('marks a detached catalog child inactive without requiring a selected address', async ({ mock, remote }) => { + remote.subagents.list.mockResolvedValue(ok({ entries: [{ kind: 'child', id: S2, mode: 'continuable', label: 'worker', activity: 'running', hasChildren: false, }] as never[], parentAvailable: true, })) - const manager = new SessionManager(fakeRemote(api)) + const manager = makeManager(mock, remote) await manager.refreshSubagents(S1) manager.handleSessionRemoved(S2) @@ -486,31 +484,29 @@ describe('subagent catalogs', () => { ]) }) - it('coalesces overlapping catalog reads without scheduling a trailing pull', async () => { - const api = new FakeApiClient() + it('coalesces overlapping catalog reads without scheduling a trailing pull', async ({ mock, remote }) => { const root = 'fk-root' as SessionId - const first = deferred>>() - api.onSubagentList = () => first.promise - const manager = new SessionManager(fakeRemote(api)) + const first = Promise.withResolvers>>() + remote.subagents.list.mockReturnValue(first.promise) + const manager = makeManager(mock, remote) const refresh = manager.refreshSubagents(root) expect(manager.refreshSubagents(root)).toBe(refresh) - api.onSubagentList = () => Promise.resolve(ok({ entries: [], parentAvailable: true })) + remote.subagents.list.mockResolvedValue(ok({ entries: [], parentAvailable: true })) first.resolve(ok({ entries: [], parentAvailable: true })) await refresh - expect(api.callsOf('subagents.list')).toHaveLength(1) + expect(remote.subagents.list).toHaveBeenCalledOnce() }) - it('runs one trailing catalog refresh for a membership change coalesced into an in-flight pull', async () => { + it('runs one trailing catalog refresh for a membership change coalesced into an in-flight pull', async ({ mock, remote }) => { vi.useFakeTimers() try { - const api = new FakeApiClient() const root = 'fk-root' as SessionId - const first = deferred>>() - const second = deferred>>() - api.onSubagentList = () => first.promise - const manager = new SessionManager(fakeRemote(api), root) + const first = Promise.withResolvers>>() + const second = Promise.withResolvers>>() + remote.subagents.list.mockReturnValue(first.promise) + const manager = makeManager(mock, remote, root) const refresh = manager.refreshSubagents(root) manager.setSubagentCatalogOpen(root, true) @@ -520,7 +516,7 @@ describe('subagent catalogs', () => { // queue one trailing pull carrying the change. manager.handleSessionAdded(summary(S2, { parentSessionId: root })) await vi.advanceTimersByTimeAsync(50) - api.onSubagentList = () => second.promise + remote.subagents.list.mockReturnValueOnce(second.promise) first.resolve(ok({ entries: [{ kind: 'child', id: S1, mode: 'continuable', label: 'older', @@ -547,7 +543,7 @@ describe('subagent catalogs', () => { // The Remote face resolves one microtask after the response settles. await vi.advanceTimersByTimeAsync(0) - expect(api.callsOf('subagents.list')).toHaveLength(2) + expect(remote.subagents.list).toHaveBeenCalledTimes(2) expect(manager.getListSnapshot().subagentsByParent[root]?.entries).toMatchObject([ { kind: 'child', id: S1, label: 'older' }, { kind: 'child', id: S2, label: 'new child' }, @@ -557,16 +553,15 @@ describe('subagent catalogs', () => { } }) - it('keeps removal invalidation across a stale success and failed trailing pull', async () => { - const api = new FakeApiClient() + it('keeps removal invalidation across a stale success and failed trailing pull', async ({ mock, remote }) => { const root = 'fk-root' as SessionId const child = () => ({ kind: 'child' as const, id: S2, mode: 'continuable' as const, label: 'worker', activity: 'inactive' as const, hasChildren: false, }) - const first = deferred>>() - api.onSubagentList = () => first.promise - const manager = new SessionManager(fakeRemote(api)) + const first = Promise.withResolvers>>() + remote.subagents.list.mockReturnValue(first.promise) + const manager = makeManager(mock, remote) const refresh = manager.refreshSubagents(root) first.resolve(ok({ entries: [child()] as never[], parentAvailable: true })) await refresh @@ -574,12 +569,12 @@ describe('subagent catalogs', () => { // The removal lands while a second pull is in flight: the invalidation // must survive the pre-removal ok response, so one trailing pull runs. - const mid = deferred>>() - api.onSubagentList = () => mid.promise + const mid = Promise.withResolvers>>() + remote.subagents.list.mockReturnValueOnce(mid.promise) const midRefresh = manager.refreshSubagents(root) manager.handleSessionRemoved(root) - const trailing = deferred>>() - api.onSubagentList = () => trailing.promise + const trailing = Promise.withResolvers>>() + remote.subagents.list.mockReturnValueOnce(trailing.promise) mid.resolve(ok({ entries: [child()] as never[], parentAvailable: true })) await midRefresh expect(manager.getListSnapshot().subagentsByParent[root]?.parentAvailable).toBe(false) @@ -593,23 +588,22 @@ describe('subagent catalogs', () => { }) }) - const rootCalls = api.callsOf('subagents.list').filter(call => call === root) + const rootCalls = remote.subagents.list.mock.calls.filter(([call]) => call === root) expect(rootCalls).toHaveLength(3) expect(manager.getListSnapshot().subagentsByParent[root]?.parentAvailable).toBe(false) expect(manager.get(S2).getSnapshot().subagent).toMatchObject({ parentAvailable: false }) }) - it('invalidates catalog availability when the owning parent is removed', async () => { - const api = new FakeApiClient() + it('invalidates catalog availability when the owning parent is removed', async ({ mock, remote }) => { const root = 'fk-root' as SessionId - api.onSubagentList = () => Promise.resolve(ok({ + remote.subagents.list.mockResolvedValue(ok({ entries: [{ kind: 'child', id: S2, mode: 'continuable', label: 'worker', activity: 'inactive', hasChildren: false, }] as never[], parentAvailable: true, })) - const manager = new SessionManager(fakeRemote(api)) + const manager = makeManager(mock, remote) await manager.refreshSubagents(root) manager.selectSubagent({ parentSessionId: root, childSessionId: S2, mode: 'continuable' }) expect(manager.get(S2).getSnapshot().subagent).toMatchObject({ parentAvailable: true }) @@ -622,56 +616,51 @@ describe('subagent catalogs', () => { }) describe('remaining branches', () => { - it('refreshList propagates a non-Remote throw', async () => { - const api = new FakeApiClient() - api.onList = () => Promise.reject(new Error('list wire down')) - const manager = new SessionManager(fakeRemote(api)) + it('refreshList propagates a non-Remote throw', async ({ mock, remote }) => { + remote.session.list.mockRejectedValue(new Error('list wire down')) + const manager = makeManager(mock, remote) await expect(manager.refreshList()).rejects.toThrow('list wire down') }) - it('refreshList pushes running bits down to already-instantiated sessions', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) + it('refreshList pushes running bits down to already-instantiated sessions', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) const session = manager.get(S1) - api.onList = () => Promise.resolve(ok({ items: [summary(S1, { running: true })] as never[] })) + remote.session.list.mockResolvedValue(ok({ items: [summary(S1, { running: true })] as never[] })) await manager.refreshList() expect(session.getSnapshot().running).toBe(true) }) - it('create passes cwd and a preallocated id, folds transport throws, and deduplicates the echo', async () => { - const api = new FakeApiClient() - api.onCreate = () => Promise.resolve(ok({ sessionId: S1 })) - const manager = new SessionManager(fakeRemote(api)) + it('create passes cwd and a preallocated id, folds transport throws, and deduplicates the echo', async ({ mock, remote }) => { + remote.session.create.mockResolvedValue(ok({ sessionId: S1 })) + const manager = makeManager(mock, remote) await manager.create({ cwd: '/tmp/w', sessionId: S1 }) - expect(api.callsOf('session.create')).toEqual([{ cwd: '/tmp/w', sessionId: S1 }]) + expect(remote.session.create).toHaveBeenCalledExactlyOnceWith({ cwd: '/tmp/w', sessionId: S1 }) expect(manager.getListSnapshot().items[0]).toMatchObject({ sessionId: S1, cwd: '/tmp/w' }) await manager.create({ cwd: '/tmp/w' }) // same id returned: no duplicate row expect(manager.getListSnapshot().items).toHaveLength(1) - api.onCreate = () => Promise.reject(new Error('create wire down')) + remote.session.create.mockRejectedValue(new Error('create wire down')) await expect(manager.create()).rejects.toThrow('create wire down') // Business error passes through untouched. - api.onCreate = () => Promise.resolve(err(new RemoteError('gateway/internal', 'no', {}))) + remote.session.create.mockResolvedValue(err(new RemoteError('gateway/internal', 'no', {}))) expect(await manager.create()).toMatchObject({ ok: false }) }) - it('publishes a real Ungrouped summary from workspace-attach-failed', async () => { - const api = new FakeApiClient() - api.onCreate = () => Promise.resolve(err(new RemoteError('session/workspace-attach-failed', 'published but unattached', { + it('publishes a real Ungrouped summary from workspace-attach-failed', async ({ mock, remote }) => { + remote.session.create.mockResolvedValue(err(new RemoteError('session/workspace-attach-failed', 'published but unattached', { sessionId: S1, workspaceId: 'w1', }))) - const manager = new SessionManager(fakeRemote(api)) + const manager = makeManager(mock, remote) const result = await manager.create({ workspaceId: 'w1' as never, sessionId: S1 }) expect(result).toMatchObject({ ok: false, error: { code: 'session/workspace-attach-failed' } }) expect(manager.getListSnapshot().items).toEqual([expect.objectContaining({ sessionId: S1 })]) expect(manager.getListSnapshot().items[0]).not.toHaveProperty('cwd') }) - it('reconciles a fork child published before workspace attachment fails', async () => { - const api = new FakeApiClient() - api.onFork = () => Promise.resolve(err(new RemoteError('session/workspace-attach-failed', 'forked but unattached', { + it('reconciles a fork child published before workspace attachment fails', async ({ mock, remote }) => { + remote.session.fork.mockResolvedValue(err(new RemoteError('session/workspace-attach-failed', 'forked but unattached', { sessionId: S2, workspaceId: 'w1', }))) - const manager = new SessionManager(fakeRemote(api)) + const manager = makeManager(mock, remote) const result = await manager.fork({ sessionId: S1 }) expect(result).toMatchObject({ ok: false, error: { code: 'session/workspace-attach-failed' } }) expect(manager.getListSnapshot().items).toEqual([expect.objectContaining({ @@ -681,10 +670,9 @@ describe('remaining branches', () => { })]) }) - it('reconciles a preallocated id after an ordinary transport failure', async () => { - const api = new FakeApiClient() - api.onCreate = () => Promise.reject(new Error('response lost')) - const manager = new SessionManager(fakeRemote(api)) + it('reconciles a preallocated id after an ordinary transport failure', async ({ mock, remote }) => { + remote.session.create.mockRejectedValue(new Error('response lost')) + const manager = makeManager(mock, remote) await expect(manager.create({ workspaceId: 'w1' as never, sessionId: S1 })) .rejects.toThrow('response lost') expect(manager.getListSnapshot().items).toEqual([]) @@ -697,9 +685,8 @@ describe('remaining branches', () => { expect(manager.getListSnapshot().items).toHaveLength(1) }) - it('subscribe notifies on list changes and stops after unsubscribe', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) + it('subscribe notifies on list changes and stops after unsubscribe', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) let notified = 0 const unsubscribe = manager.subscribe(() => { notified++ }) await manager.refreshList() @@ -712,17 +699,15 @@ describe('remaining branches', () => { expect(notified).toBe(seen) }) - it('ignores Host status and error events for sessions without an instance', () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) + it('ignores Host status and error events for sessions without an instance', ({ mock, remote }) => { + const manager = makeManager(mock, remote) manager.handleSessionStatus(S2, true) manager.handleSessionError(S2, '无实例') }) - it('keeps list-entry identity for unchanged rows across an unrelated list change', async () => { - const api = new FakeApiClient() - api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 200 })] as never[] })) - const manager = new SessionManager(fakeRemote(api)) + it('keeps list-entry identity for unchanged rows across an unrelated list change', async ({ mock, remote }) => { + remote.session.list.mockResolvedValue(ok({ items: [summary(S1), summary(S2, { updatedAt: 200 })] as never[] })) + const manager = makeManager(mock, remote) await manager.refreshList() const before = manager.getListSnapshot() manager.handleSessionStatus(S2, true) @@ -736,9 +721,8 @@ describe('remaining branches', () => { expect(manager.getListSnapshot().items).toBe(after.items) }) - it('carries parentSessionId from the added event into the lineage row', () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) + it('carries parentSessionId from the added event into the lineage row', ({ mock, remote }) => { + const manager = makeManager(mock, remote) manager.handleSessionAdded(summary(S1, { blank: true })) manager.handleSessionAdded(summary(S2, { blank: true, parentSessionId: S1, origin: 'subagent', @@ -751,34 +735,34 @@ describe('remaining branches', () => { }) describe('connected generation', () => { - it('refreshes query baselines without rebuilding independently resumed Session sources', async () => { - const api = new FakeApiClient() - api.onHistory = () => Promise.resolve(ok({ + it('refreshes query baselines without rebuilding independently resumed Session sources', async ({ mock, remote, start }) => { + mock.stream(FOLLOW, followScript(ok({ records: entries(plainTurn(SessionSeq(0), 0, 'a', 'b')) as never[], hasMore: false, modelSelection: { provider: 'deepseek-official', model: 'deepseek-chat' }, - })) - const manager = new SessionManager(fakeRemote(api)) + }))) + const client = await start() + const manager = new SessionManager(client.ctx.remote) const openedSession = manager.get(S1) await openedSession.open() manager.get(S2) // instantiated but never opened - const historyCallsBefore = api.callsOf('session.history').length + const historyCallsBefore = remote.session.page.mock.calls.length manager.handleConnected() await vi.waitFor(() => { - expect(api.callsOf('session.list').length).toBe(1) + expect(remote.session.list).toHaveBeenCalledOnce() }) - expect(api.callsOf('session.history')).toHaveLength(historyCallsBefore) + expect(remote.session.follow).toHaveBeenCalledOnce() + expect(remote.session.page).toHaveBeenCalledTimes(historyCallsBefore) }) - it('retains the durable parent address and refreshes its catalogs across reconnect', async () => { - const api = new FakeApiClient() + it('retains the durable parent address and refreshes its catalogs across reconnect', async ({ mock, remote }) => { const address = { parentSessionId: S1, childSessionId: S2, mode: 'continuable' as const, } - const parent = deferred>>() - const child = deferred>>() - api.onSubagentList = payload => (payload === S1 ? parent.promise : child.promise) - const manager = new SessionManager(fakeRemote(api), S2, address) + const parent = Promise.withResolvers>>() + const child = Promise.withResolvers>>() + remote.subagents.list.mockImplementation(payload => (payload === S1 ? parent.promise : child.promise)) + const manager = makeManager(mock, remote, S2, address) manager.handleConnected() expect(manager.get(S2).getSnapshot().subagent).toEqual({ address }) @@ -786,10 +770,10 @@ describe('connected generation', () => { child.resolve(ok({ entries: [], parentAvailable: true })) await vi.waitFor(() => { - expect(api.callsOf('session.list')).toHaveLength(1) + expect(remote.session.list).toHaveBeenCalledOnce() }) await vi.waitFor(() => { - expect(api.callsOf('subagents.list')).toEqual([S1, S2]) + expect(remote.subagents.list.mock.calls.map(([parentSessionId]) => parentSessionId)).toEqual([S1, S2]) }) expect(manager.get(S2).getSnapshot().subagent).toEqual({ address, @@ -809,8 +793,8 @@ describe('completed reminder', () => { const entry = (manager: SessionManager, sessionId: SessionId) => manager.getListSnapshot().items.find(item => item.sessionId === sessionId) - it('arms on a running→idle flip of a non-selected session and clears on select', () => { - const manager = makeManager() + it('arms on a running→idle flip of a non-selected session and clears on select', ({ mock, remote }) => { + const manager = makeManager(mock, remote) added(manager, S1) added(manager, S2) manager.select(S1) @@ -823,8 +807,8 @@ describe('completed reminder', () => { expect(entry(manager, S2)?.completed).toBe(false) }) - it('never arms for the session being watched and re-arms after a switch-away re-run', () => { - const manager = makeManager() + it('never arms for the session being watched and re-arms after a switch-away re-run', ({ mock, remote }) => { + const manager = makeManager(mock, remote) added(manager, S1) added(manager, S2) manager.select(S2) @@ -838,8 +822,8 @@ describe('completed reminder', () => { expect(entry(manager, S2)?.completed).toBe(true) }) - it('a re-run disarms the reminder while running and re-arms on its completion', () => { - const manager = makeManager() + it('a re-run disarms the reminder while running and re-arms on its completion', ({ mock, remote }) => { + const manager = makeManager(mock, remote) added(manager, S1) added(manager, S2) manager.select(S1) @@ -853,8 +837,8 @@ describe('completed reminder', () => { expect(entry(manager, S2)?.completed).toBe(true) }) - it('session-removed drops the reminder and a re-add starts clean', () => { - const manager = makeManager() + it('session-removed drops the reminder and a re-add starts clean', ({ mock, remote }) => { + const manager = makeManager(mock, remote) added(manager, S1) added(manager, S2) manager.select(S1) @@ -867,35 +851,32 @@ describe('completed reminder', () => { expect(entry(manager, S2)?.completed).toBe(false) }) - it('a list refresh carrying the running→idle transition arms the reminder', async () => { - const api = new FakeApiClient() - api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 200, running: true })] as never[] })) - const manager = new SessionManager(fakeRemote(api)) + it('a list refresh carrying the running→idle transition arms the reminder', async ({ mock, remote }) => { + remote.session.list.mockResolvedValue(ok({ items: [summary(S1), summary(S2, { updatedAt: 200, running: true })] as never[] })) + const manager = makeManager(mock, remote) await manager.refreshList() manager.select(S1) expect(entry(manager, S2)?.completed).toBe(false) - api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 200, running: false })] as never[] })) + remote.session.list.mockResolvedValue(ok({ items: [summary(S1), summary(S2, { updatedAt: 200, running: false })] as never[] })) await manager.refreshList() expect(entry(manager, S2)?.completed).toBe(true) }) - it('never arms for sessions already idle at first observation', async () => { - const api = new FakeApiClient() - api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 200 })] as never[] })) - const manager = new SessionManager(fakeRemote(api)) + it('never arms for sessions already idle at first observation', async ({ mock, remote }) => { + remote.session.list.mockResolvedValue(ok({ items: [summary(S1), summary(S2, { updatedAt: 200 })] as never[] })) + const manager = makeManager(mock, remote) await manager.refreshList() manager.select(S1) expect(entry(manager, S2)?.completed).toBe(false) - api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 201 })] as never[] })) + remote.session.list.mockResolvedValue(ok({ items: [summary(S1), summary(S2, { updatedAt: 201 })] as never[] })) await manager.refreshList() expect(entry(manager, S2)?.completed).toBe(false) }) - it('arms a completion that happened during an in-flight first pull (baseline running, replayed idle)', async () => { - const api = new FakeApiClient() - const gate = deferred>>() - api.onList = () => gate.promise - const manager = new SessionManager(fakeRemote(api)) + it('arms a completion that happened during an in-flight first pull (baseline running, replayed idle)', async ({ mock, remote }) => { + const gate = Promise.withResolvers>>() + remote.session.list.mockReturnValue(gate.promise) + const manager = makeManager(mock, remote) const refresh = manager.refreshList() // The session finishes while the first pull is still in flight; the pull // response recorded it as running at pull time. @@ -905,11 +886,10 @@ describe('completed reminder', () => { expect(entry(manager, S2)?.completed).toBe(true) }) - it('arms when a session ran and completed entirely between in-flight mutations (baseline idle)', async () => { - const api = new FakeApiClient() - const gate = deferred>>() - api.onList = () => gate.promise - const manager = new SessionManager(fakeRemote(api)) + it('arms when a session ran and completed entirely between in-flight mutations (baseline idle)', async ({ mock, remote }) => { + const gate = Promise.withResolvers>>() + remote.session.list.mockReturnValue(gate.promise) + const manager = makeManager(mock, remote) const refresh = manager.refreshList() // The unknown session starts and finishes while the first pull is in // flight; the pull-time baseline recorded it idle, so the running→idle @@ -933,8 +913,8 @@ describe('background-job mirror', () => { type: 'jobs', sessionId, jobs: jobs as never, }) - it('mirrors the whole set last-wins, keyed per session, with no Session instance needed', () => { - const manager = makeManager() + it('mirrors the whole set last-wins, keyed per session, with no Session instance needed', ({ mock, remote }) => { + const manager = makeManager(mock, remote) manager.handleControlFrame(tasksFrame(S1, [view()])) manager.handleControlFrame(tasksFrame(S2, [view({ id: 'pwsh-1', label: 'other' })])) const first = manager.getListSnapshot().jobsBySession @@ -946,16 +926,16 @@ describe('background-job mirror', () => { expect(manager.getListSnapshot().jobsBySession[S1]).toEqual([view({ status: 'completed' })]) }) - it('stores an emptied set as an absent key so absence and [] read alike', () => { - const manager = makeManager() + it('stores an emptied set as an absent key so absence and [] read alike', ({ mock, remote }) => { + const manager = makeManager(mock, remote) manager.handleControlFrame(tasksFrame(S1, [view()])) expect(S1 in manager.getListSnapshot().jobsBySession).toBe(true) manager.handleControlFrame(tasksFrame(S1, [])) expect(S1 in manager.getListSnapshot().jobsBySession).toBe(false) }) - it('clears the mirror when the next control baseline has no jobs', () => { - const manager = makeManager() + it('clears the mirror when the next control baseline has no jobs', ({ mock, remote }) => { + const manager = makeManager(mock, remote) manager.handleControlFrame(tasksFrame(S1, [view()])) manager.handleControlFrame({ type: 'baseline', @@ -964,16 +944,16 @@ describe('background-job mirror', () => { expect(S1 in manager.getListSnapshot().jobsBySession).toBe(false) }) - it('drops the rows when the session is removed, whichever stream lands first', () => { - const manager = makeManager() + it('drops the rows when the session is removed, whichever stream lands first', ({ mock, remote }) => { + const manager = makeManager(mock, remote) manager.handleSessionAdded(summary(S1, { blank: true })) manager.handleControlFrame(tasksFrame(S1, [view()])) manager.handleSessionRemoved(S1) expect(S1 in manager.getListSnapshot().jobsBySession).toBe(false) }) - it('notifies list subscribers so an open header re-renders without a poll', async () => { - const manager = makeManager() + it('notifies list subscribers so an open header re-renders without a poll', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) const seen = vi.fn() manager.subscribe(seen) manager.handleControlFrame(tasksFrame(S1, [view()])) diff --git a/packages/api/session-controller/tests/projection-store.client.spec.ts b/packages/api/session-controller/tests/projection-store.client.spec.ts index 74f37c57cd..b5a04e659c 100644 --- a/packages/api/session-controller/tests/projection-store.client.spec.ts +++ b/packages/api/session-controller/tests/projection-store.client.spec.ts @@ -7,14 +7,19 @@ * seeding, control-stream projection routing pre- and post-instantiation, the * list rows' title projection). */ -import { describe, expect, it } from 'vitest' +import { describe, expect } from 'vitest' import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client' import { SessionSeq } from '@deepseek-ai/dsh-session/types' +import { ok, type RemoteMock } from '@deepseek-ai/dsh-remote-mock' +import { + createClientTest, type ClientTestFixtures, webApp, +} from '@deepseek-ai/dsh-client-test-runtime/src/assembly/index.ts' import { ProjectionValueStore } from '../src/client/sessions/projection-store.ts' -import { Session } from '../src/client/sessions/session.ts' import { SessionManager } from '../src/client/sessions/manager.ts' -import { FakeApiClient, fakeRemote, ok } from './fake-api.client.ts' +import type { SessionRemotes } from '../src/client/sessions/remotes.ts' import { entries, plainTurn } from './event-script.client.ts' +import { sessionBench } from './remote/bench.client.ts' +import { FOLLOW, followScript, sessionWorld } from './remote/session.client.ts' // Test-domain keys merged into the projection map (the Service Definition package's // pure-type outlet), the same way domain host plugins merge theirs. @@ -25,6 +30,17 @@ declare module '@deepseek-ai/dsh-session-projection/types' { } const SID = 'fk-s1' as SessionId +/** A Session talks through the Gateway client; its dependency cone is the Typert registry and the Connection. */ +const API_ROSTER = webApp.closure(['@deepseek-ai/dsh-api-gateway']) +const it = createClientTest({ roster: API_ROSTER }) +/** The first client boot pays the cold module transform of the api cone. */ +const COLD_BOOT_TIMEOUT_MS = 60_000 + +function makeManager(mock: RemoteMock, remote: ClientTestFixtures['remote']): SessionManager { + mock.load(sessionWorld) + // Manager-routing cases never open a Session, so they do not need the broader Client Remote's $stream member. + return new SessionManager(remote as unknown as SessionRemotes) +} describe('Session projection value semantics', () => { it('reads undefined until a value lands (capability absence)', () => { @@ -102,34 +118,31 @@ describe('Session projection value semantics', () => { }) describe('Session tail-page seeding', () => { - it('seeds the store from a history response carrying a projections block', async () => { - const api = new FakeApiClient() - const session = new Session(SID, fakeRemote(api)) - api.onHistory = () => Promise.resolve(ok({ + it('seeds the store from a history response carrying a projections block', async ({ mock, start }) => { + const session = await sessionBench(mock, start, SID) + mock.stream(FOLLOW, followScript(ok({ records: entries(plainTurn(SessionSeq(0), 0, '问', '答')) as never[], hasMore: false, projections: { asOfSeq: 5, values: { 'test/marks': { marks: ['from-baseline'] } } }, - } as never)) + } as never))) await session.open() expect(session.projections.get('test/marks')).toEqual({ marks: ['from-baseline'] }) - }) + }, COLD_BOOT_TIMEOUT_MS) - it('a resync serving a stale block keeps the newer pushed value (seq rule end to end)', async () => { - const api = new FakeApiClient() - const session = new Session(SID, fakeRemote(api)) - api.onHistory = () => Promise.resolve(ok({ + it('a resync serving a stale block keeps the newer pushed value (seq rule end to end)', async ({ mock, start }) => { + const session = await sessionBench(mock, start, SID) + mock.stream(FOLLOW, followScript(ok({ records: entries(plainTurn(SessionSeq(0), 0, 'a', 'b')) as never[], hasMore: false, projections: { asOfSeq: 5, values: { 'test/marks': { marks: ['baseline'] } } }, - } as never)) + } as never))) await session.open() session.projections.apply('test/marks', { marks: ['pushed-9'] }, SessionSeq(9)) await session.resync() expect(session.projections.get('test/marks')).toEqual({ marks: ['pushed-9'] }) }) - it('treats a blockless response as no reset: pushed values survive', async () => { - const api = new FakeApiClient() - const session = new Session(SID, fakeRemote(api)) - api.onHistory = () => Promise.resolve(ok({ records: entries(plainTurn(SessionSeq(0), 0, 'a', 'b')) as never[], hasMore: false })) + it('treats a blockless response as no reset: pushed values survive', async ({ mock, start }) => { + const session = await sessionBench(mock, start, SID) + mock.stream(FOLLOW, followScript(ok({ records: entries(plainTurn(SessionSeq(0), 0, 'a', 'b')) as never[], hasMore: false }))) await session.open() session.projections.apply('test/marks', { marks: ['pushed'] }, SessionSeq(9)) await session.resync() @@ -140,9 +153,8 @@ describe('Session tail-page seeding', () => { describe('manager frame routing', () => { const sid = (s: string): SessionId => s as SessionId - it('lands projection frames before instantiation and the Session adopts the same store', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) + it('lands projection frames before instantiation and the Session adopts the same store', ({ mock, remote }) => { + const manager = makeManager(mock, remote) manager.handleControlFrame({ type: 'projection', sessionId: sid('s1'), key: 'test/marks', value: { marks: ['early'] }, seq: 7, }) @@ -155,12 +167,11 @@ describe('manager frame routing', () => { expect(session.projections.get('test/marks')).toEqual({ marks: ['later'] }) }) - it('projects the title key into list rows and truncates phantom rows on the control baseline', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) - api.onList = () => Promise.resolve(ok({ + it('projects the title key into list rows and truncates phantom rows on the control baseline', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) + remote.session.list.mockResolvedValue(ok({ items: [{ sessionId: sid('s1'), updatedAt: 1, running: false, blank: false }], - }) as never) + })) await manager.refreshList() manager.handleControlFrame({ type: 'projection', sessionId: sid('s1'), key: 'title', value: 'Projected title', seq: 4, @@ -180,10 +191,9 @@ describe('manager frame routing', () => { expect(manager.getListSnapshot().items[0]?.title).toBeUndefined() }) - it('projects every retained value into list rows with stable snapshot identity', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) - api.onList = () => Promise.resolve(ok({ + it('projects every retained value into list rows with stable snapshot identity', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) + remote.session.list.mockResolvedValue(ok({ items: [{ sessionId: sid('s1'), updatedAt: 1, running: false, blank: false, projections: { @@ -191,7 +201,7 @@ describe('manager frame routing', () => { values: { 'test/marks': { marks: ['baseline'] } }, }, }], - }) as never) + })) await manager.refreshList() const baseline = manager.getListSnapshot().items[0]?.projectionValues expect(baseline).toEqual({ 'test/marks': { marks: ['baseline'] } }) @@ -207,12 +217,11 @@ describe('manager frame routing', () => { expect(manager.getListSnapshot().items[0]?.projectionValues).not.toBe(baseline) }) - it('drops the projection store with the removed session', async () => { - const api = new FakeApiClient() - const manager = new SessionManager(fakeRemote(api)) - api.onList = () => Promise.resolve(ok({ + it('drops the projection store with the removed session', async ({ mock, remote }) => { + const manager = makeManager(mock, remote) + remote.session.list.mockResolvedValue(ok({ items: [{ sessionId: sid('s1'), updatedAt: 1, running: false, blank: false }], - }) as never) + })) await manager.refreshList() manager.handleControlFrame({ type: 'projection', sessionId: sid('s1'), key: 'title', value: 'Doomed', seq: 4, diff --git a/packages/api/session-controller/tests/remote/history.client.ts b/packages/api/session-controller/tests/remote/history.client.ts index 3a194f4e99..e954f89a2b 100644 --- a/packages/api/session-controller/tests/remote/history.client.ts +++ b/packages/api/session-controller/tests/remote/history.client.ts @@ -1,4 +1,4 @@ -/** Pure history responses shared by the local Remote fake and assembled Session tests. */ +/** Pure history response builders for assembled Session tests. */ import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session/types' import type { SessionAssistantStreamBaseline, SessionFollowFrame, SessionFollowRequest, SessionPage, SessionProjectionBaseline, diff --git a/packages/api/session-controller/tests/remote/session.client.ts b/packages/api/session-controller/tests/remote/session.client.ts index d2e0b3c135..e35b845f2e 100644 --- a/packages/api/session-controller/tests/remote/session.client.ts +++ b/packages/api/session-controller/tests/remote/session.client.ts @@ -1,6 +1,6 @@ /** * The Remote side of one Session under test: default answers for every - * `session/*` and `subagents/*` endpoint a `Session` calls, builders for + * `session/*` and `subagents/*` endpoint a `Session` or its manager calls, builders for * the two history-shaped answers, the `session/follow` opening snapshot and * the `session/page` page, both derived from event lists the way the Host * derives them from its log, and builders for the `session/control` queue @@ -13,8 +13,8 @@ import { ok, type RemoteMock, type RemoteTable, type StreamScript, type UnaryRul import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types' import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol' import type { - SessionControlFrame, SessionFollowFrame, SessionFollowRequest, SessionPage, SessionPageRequest, - SessionQueuedItem, SessionRequestId, + SessionAssistantStreamBaseline, SessionControlFrame, SessionFollowFrame, SessionFollowRequest, + SessionPage, SessionPageRequest, SessionQueuedItem, SessionRequestId, } from '../../src/types.ts' import { entries, historyValue } from '../event-script.client.ts' import { followSnapshot, pageThrough } from './history.client.ts' @@ -73,10 +73,16 @@ export async function pushEvent(mock: RemoteMock, event: SessionEvent): Promise< * then open for pushes. A failed answer fails the stream with its error; a * rejected one fails it with the rejection. * @param history - history answer or a function of the follow request. - * @param options - `cursor` overrides the snapshot cursor. + * @param options - snapshot cursor and initial Assistant stream state; the latter may be read per opening. * @returns the script. */ -export function followScript(history: HistorySource, options: { cursor?: number } = {}): StreamScript { +export function followScript( + history: HistorySource, + options: { + cursor?: number + assistantStream?: SessionAssistantStreamBaseline | (() => SessionAssistantStreamBaseline) + } = {}, +): StreamScript { return async ([request], stream) => { const follow = request as SessionFollowRequest const result = await answer(history, follow) @@ -84,7 +90,10 @@ export function followScript(history: HistorySource, optio stream.fail(result.error) return } - stream.push(followSnapshot(result.value, follow, options.cursor)) + const assistantStream = typeof options.assistantStream === 'function' + ? options.assistantStream() + : options.assistantStream + stream.push(followSnapshot(result.value, follow, options.cursor, assistantStream)) } } @@ -167,7 +176,7 @@ export function queueFrame( return { type: 'queue', sessionId, items: items.map(queueItem) } } -/** Default answers: every command accepted, an empty history, one attachment of one zero byte. */ +/** Default answers: every command accepted, empty history and subagent catalog, one attachment of one zero byte. */ export const sessionWorld: RemoteTable = { unary: { 'session/prompt': ok({ accepted: true }), @@ -179,6 +188,7 @@ export const sessionWorld: RemoteTable = { data: 'AA==', }), 'session/page': pageRule(ok({ records: [], hasMore: false })), + 'subagents/list': ok({ entries: [], parentAvailable: true }), 'subagents/prompt': ok({ messageId: 'fake-message' }), 'subagents/interruptByParent': ok({ accepted: true }), }, diff --git a/packages/api/session-controller/tests/sessions-service.client.spec.ts b/packages/api/session-controller/tests/sessions-service.client.spec.ts index 81de34198f..f49116158b 100644 --- a/packages/api/session-controller/tests/sessions-service.client.spec.ts +++ b/packages/api/session-controller/tests/sessions-service.client.spec.ts @@ -7,39 +7,52 @@ * projection, create. */ import { Context } from '@deepseek-ai/cordis' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, vi } from 'vitest' import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client' import { RemoteError } from '@deepseek-ai/dsh-typert-protocol' import { LlmAttemptId } from '@deepseek-ai/dsh-llm' import { RemoteStreamCarrierError } from '@deepseek-ai/dsh-api-gateway/client' import { SESSION_FORMAT_VERSION, SessionSeq } from '@deepseek-ai/dsh-session/types' +import { ok, type RemoteMock } from '@deepseek-ai/dsh-remote-mock' +import { createClientTest, webApp } from '@deepseek-ai/dsh-client-test-runtime/src/assembly/index.ts' import { ClientSessions, SessionCreateError } from '../src/client/sessions/service.ts' import { scopeOf } from '../src/client/scope.ts' -import type { SessionFollowFrame } from '../src/types.ts' -import { - FakeApiClient, - deferred, - err, - fakeRemote, - ok, - type RuntimeRemotes, -} from './fake-api.client.ts' +import type { + SessionAssistantStreamBaseline, SessionFollowFrame, SessionFollowRequest, +} from '../src/types.ts' +import { FOLLOW, err, followScript, sessionWorld } from './remote/session.client.ts' const sid = (s: string): SessionId => s as SessionId +/** ClientSessions uses the Gateway client for stream supervision and the native Remote mocks for responses. */ +const API_ROSTER = webApp.closure(['@deepseek-ai/dsh-api-gateway']) +/** The first client boot pays the cold module transform of the api cone. */ +const COLD_BOOT_TIMEOUT_MS = 60_000 interface Bench { ctx: Context - api: FakeApiClient + mock: RemoteMock svc: ClientSessions } -function bench(configureRemote?: (remote: RuntimeRemotes) => RuntimeRemotes): Bench { - const ctx = new Context() - const api = new FakeApiClient() - const remote = fakeRemote(api) - const svc = new ClientSessions(ctx, configureRemote?.(remote) ?? remote) - return { ctx, api, svc } -} +type BenchFactory = () => Bench + +const it = createClientTest({ roster: API_ROSTER }).extend<{ bench: BenchFactory }>({ + bench: async ({ mock, start }, use) => { + mock.load(sessionWorld) + const client = await start() + const contexts: Context[] = [] + try { + await use(() => { + const ctx = new Context() + contexts.push(ctx) + const svc = new ClientSessions(ctx, client.ctx.remote) + return { ctx, mock, svc } + }) + } finally { + await Promise.all(contexts.map(ctx => ctx.fiber.dispose())) + } + }, +}) /** Refresh the manager list from programmable rows and flush the microtask batch. */ type FeedRow = { @@ -53,7 +66,7 @@ type FeedRow = { } async function feedList(b: Bench, rows: FeedRow[]): Promise { - b.api.onList = () => Promise.resolve(ok({ + b.mock.remote.session.list.mockResolvedValue(ok({ items: rows.map(r => ({ sessionId: sid(r.id), updatedAt: 1, running: r.running ?? false, blank: r.blank ?? false, ...(r.cwd !== undefined ? { cwd: r.cwd } : {}), @@ -69,7 +82,7 @@ async function feedList(b: Bench, rows: FeedRow[]): Promise { } describe('list store projection', () => { - it('projects durable titles separately from cwd/id display fallbacks and parent links', async () => { + it('projects durable titles separately from cwd/id display fallbacks and parent links', async ({ bench }) => { const b = bench() b.svc.handleControlFrame({ type: 'projection', sessionId: sid('s1'), key: 'title', value: 'Durable title', seq: 2, @@ -85,9 +98,9 @@ describe('list store projection', () => { displayTitle: 's2', parentId: 's1', origin: 'subagent', running: true, }) expect(state.byId[sid('s2')]?.title).toBeUndefined() - }) + }, COLD_BOOT_TIMEOUT_MS) - it('reprojects a blank session from the generic agent-preset projection', async () => { + it('reprojects a blank session from the generic agent-preset projection', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1', blank: true, projections: { agentPreset: 'standard' } }]) expect(b.svc.list.getSnapshot().byId[sid('s1')]?.projectionValues?.agentPreset).toBe('standard') @@ -100,7 +113,7 @@ describe('list store projection', () => { expect(b.svc.list.getSnapshot().byId[sid('s1')]?.projectionValues?.agentPreset).toBe('minimal') }) - it('reflects live increments (host stream via manager) into the store', async () => { + it('reflects live increments (host stream via manager) into the store', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }]) b.svc.handleSessionAdded({ @@ -112,15 +125,16 @@ describe('list store projection', () => { }) describe('search', () => { - it('delegates transient content search without changing the list snapshot', async () => { + it('delegates transient content search without changing the list snapshot', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }]) const before = b.svc.list.getSnapshot() - b.api.onSearch = () => Promise.resolve(ok({ + b.mock.remote.session.search.mockResolvedValue(ok({ items: [{ sessionId: sid('s1'), snippet: 'matching excerpt' }], hasMore: false, })) const signal = new AbortController().signal + const call = vi.spyOn(b.mock.rpc, 'call') await expect(b.svc.search('needle', signal)).resolves.toEqual({ ok: true, @@ -129,13 +143,13 @@ describe('search', () => { hasMore: false, }, }) - expect(b.api.lastSearchSignal).toBe(signal) + expect(call.mock.calls.find(([, endpoint]) => endpoint === 'session/search')?.[3]).toBe(signal) expect(b.svc.list.getSnapshot()).toBe(before) }) }) describe('scope tree', () => { - it('publishes transient Assistant chunks and the named durable v2 settlement through one event source', async () => { + it('publishes transient Assistant chunks and the named durable v2 settlement through one event source', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }]) b.svc.open(sid('s1')) @@ -168,14 +182,15 @@ describe('scope tree', () => { publications.push(binding.eventSource.getSnapshot().entries.map(entry => entry.event.type)) }) - await b.api.pushFollow(sid('s1'), { + b.mock.streams.push(FOLLOW, { type: 'assistant-stream', frame: { type: 'start', attemptId, revision: 1, startedAfterSeq: -1, turn: 1, step: 1, }, }) - await b.api.pushFollow(sid('s1'), { + await b.mock.streams.drained(FOLLOW) + b.mock.streams.push(FOLLOW, { type: 'assistant-stream', frame: { type: 'chunk', attemptId, revision: 2, index: 0, @@ -183,20 +198,23 @@ describe('scope tree', () => { chunk: { type: 'text-delta', index: 0, text: 'live' }, }, }) + await b.mock.streams.drained(FOLLOW) await vi.waitFor(() => { expect(binding.eventSource.getSnapshot().entries).toHaveLength(1) }) - await b.api.pushFollow(sid('s1'), durableMessage) + b.mock.streams.push(FOLLOW, durableMessage) + await b.mock.streams.drained(FOLLOW) await Promise.resolve() expect(binding.eventSource.getSnapshot().entries).toHaveLength(1) - await b.api.pushFollow(sid('s1'), { + b.mock.streams.push(FOLLOW, { type: 'assistant-stream', frame: { type: 'end', attemptId, revision: 3, index: 1, outcome: { kind: 'committed', eventType: 'assistant/message', seq: 0 }, }, }) + await b.mock.streams.drained(FOLLOW) await vi.waitFor(() => { expect(binding.eventSource.getSnapshot().entries).toHaveLength(1) }) @@ -208,12 +226,11 @@ describe('scope tree', () => { dispose() }) - it('replaces an active assistant baseline on reconnect without duplicate chunks', async () => { + it('replaces an active assistant baseline on reconnect without duplicate chunks', async ({ bench }) => { const b = bench() const attemptId = LlmAttemptId('reconnect-attempt') let records: never[] = [] - b.api.onHistory = () => Promise.resolve(ok({ records, hasMore: false })) - b.api.assistantStreamBaseline = { + let assistantStreamBaseline: SessionAssistantStreamBaseline = { revision: 2, activeAttempt: { attemptId, startedAfterSeq: -1, turn: 1, step: 1, @@ -221,6 +238,10 @@ describe('scope tree', () => { stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['a'] }], }, } + b.mock.stream(FOLLOW, followScript( + () => ok({ records, hasMore: false }), + { assistantStream: () => assistantStreamBaseline }, + )) await feedList(b, [{ id: 's1' }]) b.svc.open(sid('s1')) const binding = b.svc.binding(sid('s1')) @@ -230,7 +251,7 @@ describe('scope tree', () => { }) records = [] - b.api.assistantStreamBaseline = { + assistantStreamBaseline = { revision: 3, activeAttempt: { attemptId, startedAfterSeq: -1, turn: 1, step: 1, @@ -238,9 +259,9 @@ describe('scope tree', () => { stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [1], texts: ['a', 'b'] }], }, } - b.api.failStreams(new RemoteStreamCarrierError('lost')) + b.mock.streams.fail(FOLLOW, new RemoteStreamCarrierError('lost')) await vi.waitFor(() => { - expect(b.api.followStarts.filter(id => id === sid('s1'))).toHaveLength(2) + expect(b.mock.log.requests(FOLLOW)).toHaveLength(2) expect(binding.eventSource.getSnapshot().entries).toHaveLength(2) }) @@ -251,7 +272,7 @@ describe('scope tree', () => { ))).toEqual(['a', 'b']) }) - it('stages a post-opening assistant settlement behind its exact active attempt', async () => { + it('stages a post-opening assistant settlement behind its exact active attempt', async ({ bench }) => { const b = bench() const attemptId = LlmAttemptId('reconnect-settlement-attempt') const priorMessage = { @@ -290,11 +311,11 @@ describe('scope tree', () => { surfaceOp: 'append' as const, }, } - b.api.onHistory = () => Promise.resolve(ok({ + const history = ok({ records: [priorMessage] as never[], hasMore: false, - })) - b.api.assistantStreamBaseline = { + }) + const assistantStreamBaseline: SessionAssistantStreamBaseline = { revision: 2, activeAttempt: { attemptId, @@ -305,6 +326,7 @@ describe('scope tree', () => { stream: currentMessage.event.data.stream, }, } + b.mock.stream(FOLLOW, followScript(history, { assistantStream: assistantStreamBaseline })) await feedList(b, [{ id: 's1' }]) b.svc.open(sid('s1')) const binding = b.svc.binding(sid('s1')) @@ -317,18 +339,20 @@ describe('scope tree', () => { .toEqual(['assistant/message', 'assistant/live-chunk']) expect(binding.eventSource.getSnapshot().entries[0]?.event).toBe(priorMessage.event) - await b.api.pushFollow(sid('s1'), currentMessage) + b.mock.streams.push(FOLLOW, currentMessage) + await b.mock.streams.drained(FOLLOW) await Promise.resolve() expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.type)) .toEqual(['assistant/message', 'assistant/live-chunk']) - await b.api.pushFollow(sid('s1'), { + b.mock.streams.push(FOLLOW, { type: 'assistant-stream', frame: { type: 'end', attemptId, revision: 3, index: 1, outcome: { kind: 'committed', eventType: 'assistant/message', seq: 1 }, }, }) + await b.mock.streams.drained(FOLLOW) await vi.waitFor(() => { expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.type)) .toEqual(['assistant/message', 'assistant/message']) @@ -338,7 +362,7 @@ describe('scope tree', () => { }) }) - it('replaces an invalid settlement with the authoritative post-end baseline', async () => { + it('replaces an invalid settlement with the authoritative post-end baseline', async ({ bench }) => { const b = bench() const attemptId = LlmAttemptId('reconnect-end-index-attempt') const prior = { @@ -364,11 +388,7 @@ describe('scope tree', () => { }, } let records = [prior] as never[] - b.api.onHistory = () => Promise.resolve(ok({ - records, - hasMore: false, - })) - b.api.assistantStreamBaseline = { + let assistantStreamBaseline: SessionAssistantStreamBaseline = { revision: 2, activeAttempt: { attemptId, @@ -379,6 +399,10 @@ describe('scope tree', () => { stream: message.event.data.stream, }, } + b.mock.stream(FOLLOW, followScript( + () => ok({ records, hasMore: false }), + { assistantStream: () => assistantStreamBaseline }, + )) await feedList(b, [{ id: 's1' }]) b.svc.open(sid('s1')) const binding = b.svc.binding(sid('s1')) @@ -389,29 +413,31 @@ describe('scope tree', () => { }) const openingRevision = binding.eventSource.getSnapshot().revision - await b.api.pushFollow(sid('s1'), message) + b.mock.streams.push(FOLLOW, message) + await b.mock.streams.drained(FOLLOW) await Promise.resolve() expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.type)) .toEqual(['turn/start', 'assistant/live-chunk']) records = [prior, message] as never[] - b.api.assistantStreamBaseline = { revision: 3 } - await b.api.pushFollow(sid('s1'), { + assistantStreamBaseline = { revision: 3 } + b.mock.streams.push(FOLLOW, { type: 'assistant-stream', frame: { type: 'end', attemptId, revision: 3, index: 0, outcome: { kind: 'committed', eventType: 'assistant/message', seq: 0 }, }, }) + await b.mock.streams.drained(FOLLOW) await vi.waitFor(() => { - expect(b.api.followStarts.filter(id => id === sid('s1'))).toHaveLength(2) - expect(b.api.activeFollows(sid('s1'))).toBe(1) + expect(b.mock.log.requests(FOLLOW)).toHaveLength(2) + expect(b.mock.log.streams(FOLLOW).filter(stream => stream.state === 'open')).toHaveLength(1) expect(binding.eventSource.getSnapshot().revision).toBeGreaterThan(openingRevision) expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.type)) .toEqual(['turn/start', 'assistant/message']) }) }) - it('retains a Host-addressed scope until the first Session baseline owns pruning', async () => { + it('retains a Host-addressed scope until the first Session baseline owns pruning', async ({ bench }) => { const b = bench() const scoped = b.svc.resolveAgentScope(sid('s-early')) expect(scopeOf(scoped)).toBe('s-early') @@ -427,7 +453,7 @@ describe('scope tree', () => { expect(b.svc.scope(sid('s-early'))).toBeUndefined() }) - it('mints lazily on first resolution, tags the ctx, and keeps binding identity stable', async () => { + it('mints lazily on first resolution, tags the ctx, and keeps binding identity stable', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }]) expect(b.svc.scope(sid('unknown'))).toBeUndefined() @@ -442,7 +468,7 @@ describe('scope tree', () => { expect(binding?.ctx).toBe(scoped) }) - it('tears down an off-stage removed session but defers the staged one until the stage moves', async () => { + it('tears down an off-stage removed session but defers the staged one until the stage moves', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }, { id: 's2' }]) const ctx1 = b.svc.scope(sid('s1')) @@ -460,7 +486,7 @@ describe('scope tree', () => { expect(b.svc.scope(sid('s1'))).toBeUndefined() }) - it('keeps the scope when the session merely stops running (frozen ≠ removed)', async () => { + it('keeps the scope when the session merely stops running (frozen ≠ removed)', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1', running: true }]) const scoped = b.svc.scope(sid('s1')) @@ -468,7 +494,7 @@ describe('scope tree', () => { expect(b.svc.scope(sid('s1'))).toBe(scoped) }) - it('cancels a deferred teardown when the id reappears in the list', async () => { + it('cancels a deferred teardown when the id reappears in the list', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }]) const scoped = b.svc.scope(sid('s1')) @@ -479,13 +505,17 @@ describe('scope tree', () => { expect(b.svc.scope(sid('s1'))).toBe(scoped) }) - it('closes an opened journal when its removed scope drops', async () => { + it('closes an opened journal when its removed scope drops', async ({ bench }) => { const b = bench() + const follows = () => b.mock.log.streams(FOLLOW).filter(({ args }) => { + const request = args[0] as SessionFollowRequest + return request.address.kind === 'session' && request.address.sessionId === sid('s1') + }) await feedList(b, [{ id: 's1' }]) b.svc.open(sid('s1')) const session = b.svc.binding(sid('s1'))?.session if (session === undefined) throw new Error('expected the selected Session binding') - await vi.waitFor(() => { expect(b.api.activeFollows(sid('s1'))).toBe(1) }) + await vi.waitFor(() => { expect(follows().filter(stream => stream.state === 'open')).toHaveLength(1) }) const notified = vi.fn() session.subscribe(notified) @@ -493,20 +523,24 @@ describe('scope tree', () => { await feedList(b, [{ id: 's2' }]) b.svc.open(sid('s2')) - await vi.waitFor(() => { expect(b.api.activeFollows(sid('s1'))).toBe(0) }) + await vi.waitFor(() => { expect(follows().filter(stream => stream.state === 'open')).toHaveLength(0) }) notified.mockClear() - await b.api.pushFollow(sid('s1'), { + expect(b.mock.streams.push(FOLLOW, { type: 'event', event: { seq: 0, timestamp: 0, type: 'turn/start', data: { turn: 0 } } as never, - }) + }, ([request]) => { + const address = (request as SessionFollowRequest).address + return address.kind === 'session' && address.sessionId === sid('s1') + })).toBe(0) + await b.mock.streams.drained(FOLLOW) await Promise.resolve() - expect(b.api.followStarts.filter(id => id === sid('s1'))).toHaveLength(1) + expect(follows()).toHaveLength(1) expect(notified).not.toHaveBeenCalled() }) }) describe('Agent scope disposal lifecycle', () => { - it('root disposal runs Agent scope effects', async () => { + it('root disposal runs Agent scope effects', async ({ bench }) => { const b = bench() const readiness = b.ctx.plugin(() => undefined) await readiness @@ -525,59 +559,54 @@ describe('Agent scope disposal lifecycle', () => { expect(b.svc.sessionOf(scoped)).toBeUndefined() }) - it('root disposal waits for an opened Session source to finish closing', async () => { - const closeGate = deferred() + it('root disposal waits for an opened Session source to finish closing', async ({ bench }) => { + const closeGate = Promise.withResolvers() const abortObserved = vi.fn() let followSignal: AbortSignal | undefined - const b = bench(remote => ({ - ...remote, - session: { - ...remote.session, - follow: (request, signal) => { - if (signal === undefined) throw new Error('fixture requires a signal') - followSignal = signal - let opened = false - return { - [Symbol.asyncIterator]: () => ({ - next: () => { - if (!opened) { - opened = true - return Promise.resolve({ - done: false, - value: { - type: 'snapshot', - header: { - version: SESSION_FORMAT_VERSION, - id: request.address.kind === 'session' - ? request.address.sessionId - : request.address.childSessionId, - createdAt: 0, - isSeeded: false, - }, - cursor: -1, - records: [], - hasMore: false, - projections: { asOfSeq: -1, values: {} }, - assistantStream: { revision: 0 }, - } as const, - }) - } - return new Promise((_resolve, reject) => { - signal.addEventListener('abort', () => { - abortObserved() - void closeGate.promise.then(() => { - reject(signal.reason instanceof Error - ? signal.reason - : new Error(String(signal.reason))) - }) - }, { once: true }) + const b = bench() + b.mock.remote.session.follow.mockImplementation((request, signal) => { + if (signal === undefined) throw new Error('fixture requires a signal') + followSignal = signal + let opened = false + return { + [Symbol.asyncIterator]: () => ({ + next: () => { + if (!opened) { + opened = true + return Promise.resolve({ + done: false, + value: { + type: 'snapshot', + header: { + version: SESSION_FORMAT_VERSION, + id: request.address.kind === 'session' + ? request.address.sessionId + : request.address.childSessionId, + createdAt: 0, + isSeeded: false, + }, + cursor: -1, + records: [], + hasMore: false, + projections: { asOfSeq: -1, values: {} }, + assistantStream: { revision: 0 }, + } as const, + }) + } + return new Promise((_resolve, reject) => { + signal.addEventListener('abort', () => { + abortObserved() + void closeGate.promise.then(() => { + reject(signal.reason instanceof Error + ? signal.reason + : new Error(String(signal.reason))) }) - }, - }), - } - }, - }, - })) + }, { once: true }) + }) + }, + }), + } + }) const readiness = b.ctx.plugin(() => undefined) await readiness await feedList(b, [{ id: 's1' }]) @@ -599,55 +628,50 @@ describe('Agent scope disposal lifecycle', () => { expect(settled).toHaveBeenCalledOnce() }) - it('root disposal joins every Session drop already started by pruning under load', async () => { - const closeGates = new Map>>() + it('root disposal joins every Session drop already started by pruning under load', async ({ bench }) => { + const closeGates = new Map>() const aborted = new Set() - const b = bench(remote => ({ - ...remote, - session: { - ...remote.session, - follow: (request, signal) => { - if (signal === undefined) throw new Error('fixture requires a signal') - const sessionId = request.address.kind === 'session' - ? request.address.sessionId - : request.address.childSessionId - const closeGate = deferred() - closeGates.set(sessionId, closeGate) - let opened = false - return { - [Symbol.asyncIterator]: () => ({ - next: () => { - if (!opened) { - opened = true - return Promise.resolve({ - done: false, - value: { - type: 'snapshot', - header: { version: SESSION_FORMAT_VERSION, id: sessionId, createdAt: 0, isSeeded: false }, - cursor: -1, - records: [], - hasMore: false, - projections: { asOfSeq: -1, values: {} }, - assistantStream: { revision: 0 }, - } as const, - }) - } - return new Promise>((_resolve, reject) => { - signal.addEventListener('abort', () => { - aborted.add(sessionId) - void closeGate.promise.then(() => { - reject(signal.reason instanceof Error - ? signal.reason - : new Error(String(signal.reason))) - }) - }, { once: true }) + const b = bench() + b.mock.remote.session.follow.mockImplementation((request, signal) => { + if (signal === undefined) throw new Error('fixture requires a signal') + const sessionId = request.address.kind === 'session' + ? request.address.sessionId + : request.address.childSessionId + const closeGate = Promise.withResolvers() + closeGates.set(sessionId, closeGate) + let opened = false + return { + [Symbol.asyncIterator]: () => ({ + next: () => { + if (!opened) { + opened = true + return Promise.resolve({ + done: false, + value: { + type: 'snapshot', + header: { version: SESSION_FORMAT_VERSION, id: sessionId, createdAt: 0, isSeeded: false }, + cursor: -1, + records: [], + hasMore: false, + projections: { asOfSeq: -1, values: {} }, + assistantStream: { revision: 0 }, + } as const, + }) + } + return new Promise>((_resolve, reject) => { + signal.addEventListener('abort', () => { + aborted.add(sessionId) + void closeGate.promise.then(() => { + reject(signal.reason instanceof Error + ? signal.reason + : new Error(String(signal.reason))) }) - }, - }), - } - }, - }, - })) + }, { once: true }) + }) + }, + }), + } + }) const readiness = b.ctx.plugin(() => undefined) await readiness const sessionIds = Array.from({ length: 24 }, (_, index) => sid(`load-${String(index)}`)) @@ -691,7 +715,7 @@ describe('Agent scope disposal lifecycle', () => { describe('current selection (migrated from ui-layout, arbitrated into the list snapshot)', () => { afterEach(() => { vi.unstubAllGlobals() }) - it('open() writes list.current; unknown ids fail loud', async () => { + it('open() writes list.current; unknown ids fail loud', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }]) expect(b.svc.list.getSnapshot().current).toBeUndefined() @@ -701,7 +725,7 @@ describe('current selection (migrated from ui-layout, arbitrated into the list s expect(b.svc.list.getSnapshot().current).toBe('s1') // failed open leaves the selection alone }) - it('clear() blanks list.current and the persisted selection', async () => { + it('clear() blanks list.current and the persisted selection', async ({ bench }) => { const storage = new Map() vi.stubGlobal('localStorage', { getItem: (k: string) => storage.get(k) ?? null, @@ -721,7 +745,7 @@ describe('current selection (migrated from ui-layout, arbitrated into the list s expect(again.svc.list.getSnapshot().current).toBeUndefined() }) - it('masks (not destroys) the selection while its session is off the list', async () => { + it('masks (not destroys) the selection while its session is off the list', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }, { id: 's2' }]) b.svc.open(sid('s1')) @@ -731,7 +755,7 @@ describe('current selection (migrated from ui-layout, arbitrated into the list s expect(b.svc.list.getSnapshot().current).toBe('s1') }) - it('persists the selection under dsh.sessions.current and rehydrates it into a fresh service', async () => { + it('persists the selection under dsh.sessions.current and rehydrates it into a fresh service', async ({ bench }) => { const storage = new Map() vi.stubGlobal('localStorage', { getItem: (k: string) => storage.get(k) ?? null, @@ -749,7 +773,7 @@ describe('current selection (migrated from ui-layout, arbitrated into the list s }) describe('binding and stage lifecycle', () => { - it('binding() is pure resolution: no staging, no deferred sweep', async () => { + it('binding() is pure resolution: no staging, no deferred sweep', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }, { id: 's2' }]) b.svc.open(sid('s1')) // staged @@ -758,10 +782,13 @@ describe('binding and stage lifecycle', () => { expect(b.svc.scope(sid('s1'))).toBeDefined() }) - it('staging (current write) opens the session event window; resolution and re-staging do not re-pull', async () => { + it('staging (current write) opens the session event window; resolution and re-staging do not re-pull', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }, { id: 's2' }]) - const followStarts = () => b.api.followStarts.map(String) + const followStarts = () => b.mock.log.requests(FOLLOW).map((request) => { + const address = (request as SessionFollowRequest).address + return String(address.kind === 'session' ? address.sessionId : address.childSessionId) + }) // Resolution is addressing, not staging: no window pull. b.svc.scope(sid('s1')) b.svc.binding(sid('s1')) @@ -780,7 +807,7 @@ describe('binding and stage lifecycle', () => { }) }) - it('startup restore: a persisted selection validated by the first projection opens its window unprompted', async () => { + it('startup restore: a persisted selection validated by the first projection opens its window unprompted', async ({ bench }) => { const storage = new Map([ ['dsh.sessions.current', JSON.stringify({ sessionId: 's1' })], ]) @@ -790,10 +817,13 @@ describe('binding and stage lifecycle', () => { }) try { const b = bench() - expect(b.api.followStarts).toEqual([]) + expect(b.mock.log.requests(FOLLOW)).toEqual([]) await feedList(b, [{ id: 's1' }]) // projection validates the persisted id → current lands → stage follows await vi.waitFor(() => { - expect(b.api.followStarts.map(String)).toEqual(['s1']) + expect(b.mock.log.requests(FOLLOW).map((request) => { + const address = (request as SessionFollowRequest).address + return String(address.kind === 'session' ? address.sessionId : address.childSessionId) + })).toEqual(['s1']) }) } finally { vi.unstubAllGlobals() @@ -802,10 +832,10 @@ describe('binding and stage lifecycle', () => { }) describe('catalog-addressed navigation', () => { - it('uses catalog labels for a listed addressed route', async () => { + it('uses catalog labels for a listed addressed route', async ({ bench }) => { const b = bench() - b.api.onSubagentList = (payload) => { - const parentSessionId = payload as SessionId + b.mock.remote.subagents.list.mockImplementation((payload) => { + const parentSessionId = payload if (parentSessionId === sid('root')) { return Promise.resolve(ok({ entries: [{ @@ -825,7 +855,7 @@ describe('catalog-addressed navigation', () => { })) } return Promise.resolve(ok({ entries: [], parentAvailable: false })) - } + }) await feedList(b, [ { id: 'root' }, { id: 'child', cwd: '/summary-child', parentId: 'root', origin: 'subagent' }, @@ -841,10 +871,10 @@ describe('catalog-addressed navigation', () => { expect(b.svc.list.getSnapshot().byId[sid('grandchild')]?.displayTitle).toBe('Grandchild') }) - it('projects a directly opened descendant route without retaining ancestor scopes or addresses', async () => { + it('projects a directly opened descendant route without retaining ancestor scopes or addresses', async ({ bench }) => { const b = bench() - b.api.onSubagentList = (payload) => { - const parentSessionId = payload as SessionId + b.mock.remote.subagents.list.mockImplementation((payload) => { + const parentSessionId = payload if (parentSessionId === sid('root')) { return Promise.resolve(ok({ entries: [{ @@ -864,7 +894,7 @@ describe('catalog-addressed navigation', () => { })) } return Promise.resolve(ok({ entries: [], parentAvailable: false })) - } + }) await feedList(b, [{ id: 'root' }]) await b.svc.refreshSubagents(sid('root')) await b.svc.refreshSubagents(sid('child')) @@ -888,12 +918,12 @@ describe('catalog-addressed navigation', () => { }) describe('create', () => { - it('passes a preallocated id and preserves it on ordinary failure', async () => { + it('passes a preallocated id and preserves it on ordinary failure', async ({ bench }) => { const b = bench() - b.api.onCreate = () => Promise.resolve(ok({ sessionId: sid('fresh') })) + b.mock.remote.session.create.mockResolvedValue(ok({ sessionId: sid('fresh') })) await expect(b.svc.create({ cwd: '/w', sessionId: sid('fresh') })).resolves.toBe('fresh') - expect(b.api.callsOf('session.create')).toEqual([{ cwd: '/w', sessionId: 'fresh' }]) - b.api.onCreate = () => Promise.resolve(err(new RemoteError('gateway/internal', '爆了', {}))) + expect(b.mock.remote.session.create).toHaveBeenCalledExactlyOnceWith({ cwd: '/w', sessionId: 'fresh' }) + b.mock.remote.session.create.mockResolvedValue(err(new RemoteError('gateway/internal', '爆了', {}))) const failure = await b.svc.create({ sessionId: sid('candidate') }).catch((error: unknown) => error) expect(failure).toBeInstanceOf(SessionCreateError) expect(failure).toMatchObject({ @@ -902,9 +932,9 @@ describe('create', () => { }) }) - it('resolves with the session already listed and binding-resolvable (no flush wait)', async () => { + it('resolves with the session already listed and binding-resolvable (no flush wait)', async ({ bench }) => { const b = bench() - b.api.onCreate = () => Promise.resolve(ok({ sessionId: sid('born') })) + b.mock.remote.session.create.mockResolvedValue(ok({ sessionId: sid('born') })) const born = await b.svc.create({ workspaceId: 'ws' as never }) // Synchronously after resolution — the draft hand-off contract: the // create echo IS the entity entering the client's view (blank row + @@ -914,9 +944,9 @@ describe('create', () => { expect(b.svc.scope(born)).toBeDefined() }) - it('lists the published id after Workspace attachment fails (publication precedes attachment)', async () => { + it('lists the published id after Workspace attachment fails (publication precedes attachment)', async ({ bench }) => { const b = bench() - b.api.onCreate = () => Promise.resolve(err(new RemoteError( + b.mock.remote.session.create.mockResolvedValue(err(new RemoteError( 'session/workspace-attach-failed', 'ledger unavailable', { sessionId: sid('published'), workspaceId: 'ws' }, @@ -936,29 +966,29 @@ describe('create', () => { }) describe('fork', () => { - it.each([ + it.for([ ['Roadmap', 'Roadmap (1)'], ['Roadmap (1)', 'Roadmap (2)'], ['计划(1)', '计划(2)'], ['计划 (9)', '计划 (10)'], - ])('increments the durable title %j after the child is published', async (sourceTitle, childTitle) => { + ] as const)('increments the durable title %j after the child is published', async ([sourceTitle, childTitle], { bench }) => { const b = bench() b.svc.handleControlFrame({ type: 'projection', sessionId: sid('source'), key: 'title', value: sourceTitle, seq: 2, }) await feedList(b, [{ id: 'source', cwd: '/work' }]) - b.api.onFork = () => Promise.resolve(ok({ sessionId: sid('child') })) - b.api.onRename = (payload) => { + b.mock.remote.session.fork.mockResolvedValue(ok({ sessionId: sid('child') })) + b.mock.remote.session.rename.mockImplementation((payload) => { const { title } = payload as { title: string } return Promise.resolve(ok({ title, seq: 3 })) - } + }) await expect(b.svc.fork({ sessionId: sid('source'), atSeq: 7, increaseTitle: true, })).resolves.toBe('child') - expect(b.api.callsOf('session.fork')).toEqual([{ sessionId: 'source', atSeq: 7 }]) - expect(b.api.callsOf('session.rename')).toEqual([{ sessionId: 'child', title: childTitle }]) + expect(b.mock.remote.session.fork).toHaveBeenCalledExactlyOnceWith({ sessionId: 'source', atSeq: 7 }) + expect(b.mock.remote.session.rename).toHaveBeenCalledExactlyOnceWith({ sessionId: 'child', title: childTitle }) await Promise.resolve() expect(b.svc.list.getSnapshot().byId[sid('child')]).toMatchObject({ title: childTitle, @@ -967,37 +997,37 @@ describe('fork', () => { }) }) - it('floors a fractional anchor to the real event seq the wire accepts', async () => { + it('floors a fractional anchor to the real event seq the wire accepts', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 'source', cwd: '/work' }]) - b.api.onFork = () => Promise.resolve(ok({ sessionId: sid('child') })) + b.mock.remote.session.fork.mockResolvedValue(ok({ sessionId: sid('child') })) // The frozen node of an interrupted turn carries turnEnd.seq - 0.9. await expect(b.svc.fork({ sessionId: sid('source'), atSeq: 41.1 })).resolves.toBe('child') - expect(b.api.callsOf('session.fork')).toEqual([{ sessionId: 'source', atSeq: 41 }]) + expect(b.mock.remote.session.fork).toHaveBeenCalledExactlyOnceWith({ sessionId: 'source', atSeq: 41 }) }) - it('does not rename without the title policy or a durable source title', async () => { + it('does not rename without the title policy or a durable source title', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 'source', cwd: '/work' }]) - b.api.onFork = () => Promise.resolve(ok({ sessionId: sid('child') })) + b.mock.remote.session.fork.mockResolvedValue(ok({ sessionId: sid('child') })) await expect(b.svc.fork({ sessionId: sid('source'), increaseTitle: true })).resolves.toBe('child') - expect(b.api.callsOf('session.rename')).toEqual([]) + expect(b.mock.remote.session.rename).not.toHaveBeenCalled() - b.api.onFork = () => Promise.resolve(ok({ sessionId: sid('child-2') })) + b.mock.remote.session.fork.mockResolvedValue(ok({ sessionId: sid('child-2') })) await expect(b.svc.fork({ sessionId: sid('source') })).resolves.toBe('child-2') - expect(b.api.callsOf('session.rename')).toEqual([]) + expect(b.mock.remote.session.rename).not.toHaveBeenCalled() }) - it('rejects when child rename fails while keeping the published child addressable', async () => { + it('rejects when child rename fails while keeping the published child addressable', async ({ bench }) => { const b = bench() b.svc.handleControlFrame({ type: 'projection', sessionId: sid('source'), key: 'title', value: 'Roadmap', seq: 2, }) await feedList(b, [{ id: 'source' }]) - b.api.onFork = () => Promise.resolve(ok({ sessionId: sid('child') })) - b.api.onRename = () => Promise.resolve(err(new RemoteError('session/title-invalid', 'rejected', { sessionId: sid('child') }))) + b.mock.remote.session.fork.mockResolvedValue(ok({ sessionId: sid('child') })) + b.mock.remote.session.rename.mockResolvedValue(err(new RemoteError('session/title-invalid', 'rejected', { sessionId: sid('child') }))) await expect(b.svc.fork({ sessionId: sid('source'), increaseTitle: true })) .rejects.toThrow('fork child rename failed: session/title-invalid: rejected') @@ -1006,7 +1036,7 @@ describe('fork', () => { }) describe('scope lifecycle rides the list mirror (entity parity: no client-side pre-birth)', () => { - it('a session-added frame births the row (blank) and makes the scope resolvable; removal prunes it', async () => { + it('a session-added frame births the row (blank) and makes the scope resolvable; removal prunes it', async ({ bench }) => { const b = bench() await feedList(b, []) expect(b.svc.scope(sid('s-new'))).toBeUndefined() // not in view: no scope, no exceptions @@ -1024,7 +1054,7 @@ describe('scope lifecycle rides the list mirror (entity parity: no client-side p }) describe('blank mirror', () => { - it('flips blank=false from the running:true status frame (cross-client conversion)', async () => { + it('flips blank=false from the running:true status frame (cross-client conversion)', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1', blank: true }]) expect(b.svc.list.getSnapshot().byId[sid('s1')]).toMatchObject({ blank: true }) @@ -1035,13 +1065,13 @@ describe('blank mirror', () => { expect(b.svc.binding(sid('s1'))?.session.getSnapshot().blank).toBe(false) }) - it('flips blank=false on prompt ACCEPTANCE, not on the attempt', async () => { + it('flips blank=false on prompt ACCEPTANCE, not on the attempt', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1', blank: true, cwd: '/w/a' }]) const session = b.svc.binding(sid('s1'))!.session expect(session.getSnapshot().blank).toBe(true) - const gate = deferred>>() - b.api.onPrompt = () => gate.promise + const gate = Promise.withResolvers>>() + b.mock.remote.session.prompt.mockReturnValue(gate.promise) const send = session.prompt([{ type: 'text', text: 'hi' }], 'queue') // In flight: still blank (the flip point is the success response, which // proves the user message reached the host log). @@ -1053,11 +1083,11 @@ describe('blank mirror', () => { expect(b.svc.list.getSnapshot().byId[sid('s1')]).toMatchObject({ blank: false }) }) - it('keeps a rejected first prompt blank: hidden and still reusable', async () => { + it('keeps a rejected first prompt blank: hidden and still reusable', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1', blank: true, cwd: '/w/a' }]) const session = b.svc.binding(sid('s1'))!.session - b.api.onPrompt = () => Promise.resolve(err(new RemoteError('gateway/internal', 'agent busy', {}))) + b.mock.remote.session.prompt.mockResolvedValue(err(new RemoteError('gateway/internal', 'agent busy', {}))) const result = await session.prompt([{ type: 'text', text: 'hi' }], 'queue') expect(result.ok).toBe(false) // No flip on failure: local stays aligned with the host authority @@ -1067,7 +1097,7 @@ describe('blank mirror', () => { expect(b.svc.list.getSnapshot().byId[sid('s1')]).toMatchObject({ blank: true }) }) - it('takes session-added blank=true as the hidden birth and list blank as reconnect authority', async () => { + it('takes session-added blank=true as the hidden birth and list blank as reconnect authority', async ({ bench }) => { const b = bench() await feedList(b, []) b.svc.handleSessionAdded({ @@ -1080,7 +1110,7 @@ describe('blank mirror', () => { expect(b.svc.list.getSnapshot().byId[sid('s-new')]).toMatchObject({ blank: false }) }) - it('never re-blanks: a stale blank=true summary cannot hide an engaged session', async () => { + it('never re-blanks: a stale blank=true summary cannot hide an engaged session', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1', blank: true }]) const session = b.svc.binding(sid('s1'))!.session @@ -1094,7 +1124,7 @@ describe('blank mirror', () => { }) describe('coverage tails (branch duals)', () => { - it('displayTitleOf falls back to the id for empty and separator-only cwd', async () => { + it('displayTitleOf falls back to the id for empty and separator-only cwd', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 'no-base', cwd: '///' }, { id: 'empty-cwd', cwd: '' }]) const { byId } = b.svc.list.getSnapshot() @@ -1103,7 +1133,7 @@ describe('coverage tails (branch duals)', () => { expect(byId[sid('no-base')]?.title).toBeUndefined() }) - it('binding for an unknown session returns undefined and leaves the staged scope intact', async () => { + it('binding for an unknown session returns undefined and leaves the staged scope intact', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }]) b.svc.open(sid('s1')) @@ -1113,20 +1143,20 @@ describe('coverage tails (branch duals)', () => { expect(b.svc.scope(sid('s1'))).toBeDefined() }) - it('a masked current gap holds the stage (no teardown, no re-open) until the stage moves', async () => { + it('a masked current gap holds the stage (no teardown, no re-open) until the stage moves', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 's1' }]) b.svc.open(sid('s1')) - await vi.waitFor(() => { expect(b.api.followStarts).toHaveLength(1) }) + await vi.waitFor(() => { expect(b.mock.log.requests(FOLLOW)).toHaveLength(1) }) await feedList(b, []) // removed while staged: current masks to undefined, stage holds → deferred expect(b.svc.scope(sid('s1'))).toBeDefined() // Resurfacing re-projects current = s1: same stage occupant, no second pull. await feedList(b, [{ id: 's1' }]) - expect(b.api.followStarts).toHaveLength(1) + expect(b.mock.log.requests(FOLLOW)).toHaveLength(1) expect(b.svc.list.getSnapshot().current).toBe('s1') }) - it('sweep hits both deferral edges: staged-id skip and an already-vacated scope record', async () => { + it('sweep hits both deferral edges: staged-id skip and an already-vacated scope record', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 'a' }, { id: 'b' }]) b.svc.scope(sid('a')) diff --git a/packages/api/terminal-controller/README.i18n.yaml b/packages/api/terminal-controller/README.i18n.yaml new file mode 100644 index 0000000000..0a1f8343a9 --- /dev/null +++ b/packages/api/terminal-controller/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/api/terminal-controller/README.md +README.md: c302a2b7269bdb6a76d0c6056d5851a84a876e9a +README.zh.md: dfc50e30f3a35b70c40bb76f9832cbd9548ba2cf diff --git a/packages/api/terminal-controller/README.md b/packages/api/terminal-controller/README.md new file mode 100644 index 0000000000..c302a2b726 --- /dev/null +++ b/packages/api/terminal-controller/README.md @@ -0,0 +1,93 @@ +--- +description: "Interactive user terminals with execution-environment shell defaults, bounded screen recovery and typed Remote control." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-api-terminal-controller + +English | [中文](README.zh.md) + +## Summary + +Open the execution environment's default shell in a Session workspace from the Web sidebar. Reconnect to existing processes and close their complete provider-owned process ranges. Terminal output stays outside the Agent transcript. Keeping a terminal open retains its process and a bounded screen buffer. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +The Web bundle mounts this package with the subprocess provider, sandbox policy and Typert Gateway. `remote.terminal` exposes `environment`, `shells`, `list`, `create`, `follow`, `write`, `resize`, `rename` and `close`; each operation is scoped by Session identity. Listing reads retained Host terminals directly, so viewing an offline Session neither activates an Agent nor produces a recovery error. + +Shell discovery lists the execution environment's declared default shell first. Only when the provider omits that default does resolution use `/bin/sh` on POSIX or `cmd.exe` on Windows. An optional `shell` profile overrides that choice with executable `path`, display `name` and `args` (default `[]`). The selector also probes `shellCandidates` through the execution provider and omits only confirmed lookup misses. Creation accepts a discovered `shellPath` and verifies it again; resolution or transport failure is reported without launching a different shell. Environment lookup returns the working directory and limits without resolving a shell, so an unavailable default does not prevent reattaching to an existing process. Automatic POSIX profiles start interactively, and PowerShell uses `-NoLogo`, so completion and startup configuration remain shell-owned. The Session workspace supplies the initial directory; its sandbox policy also applies to the terminal. + +| Configuration | Default | Meaning | +|---|---|---| +| `shell` | omitted | Use the execution environment's default shell, or one explicit profile | +| `shellCandidates` | `zsh`, `bash`, `fish`, `sh`, `ksh`, `tcsh`, `csh`, `pwsh`, `powershell`, `cmd`, `nu` | Additional executable names or paths offered when installed | +| `maxTerminals` | `8` | Retained terminals and pending allocations per Session | +| `maxCols`, `maxRows` | `500`, `200` | Maximum PTY dimensions | +| `scrollback` | `1000` | Retained screen history rows | +| `maxBufferedBytes` | `2097152` | Output queued for one follower | +| `maxInputBytes` | `65536` | Maximum input request bytes | +| `disposeGraceMs` | `1000` | Provider termination grace in milliseconds | + + +## Understand the implementation + +
+Implementation internals — click to expand + +The Host uses `ctx.subprocess.spawnTerminal` with `TERM=xterm-256color`; it never launches a desktop terminal application. Streaming UTF-8 decoding preserves split characters and leading BOMs, and replaces incomplete trailing bytes at EOF. Unary control uses the Gateway, and `follow` uses its multiplexed Remote stream transport. Headless xterm and its serializer produce each opening screen after all preceding output writes, then monotone output sequences identify subsequent frames. Slow followers fail explicitly; a new attachment restores the current screen. + +The latest attachment owns input and resize. Detachment releases input control without killing the process. Explicit close awaits process cleanup and final output; cleanup failure retains the resource for retry. The Session remembers closed identities and rejects their delayed or repeated creation, including creation already in progress when close arrives. A new terminal uses a new identity. Pending allocations remain owned even if cancellation and cleanup both fail. Session owner disposal and controller disposal also terminate owned processes. An open or pending terminal prevents changing that Session's sandbox mode. Input or resize refused after control transfer or process exit leaves the output attachment intact and disables input; rejected input is not replayed. + +Client views keep the association between sidebar tabs and terminal identities in memory. Recovery queries the Host for retained terminals; a new view may create a process, while a recovered view reports a missing target without creating a replacement. The Client model acknowledges screen writes after the browser emulator processes them, serializes input and ignores stale attachment responses. Client-owned errors carry locale keys. Plugin disposal awaits active and previously detached output streams without closing Host processes. + +New views wait for explicit shell selection and start. The last selected shell path is stored under `dsh.terminal.shell` in origin-scoped localStorage. A saved path is preselected only while Host discovery still lists it; otherwise the current default is selected. Storage failures do not prevent startup. Restoring existing terminals neither reads this preference nor discovers shells. + +Closing saves an unfinished cleanup request before releasing the tab, then awaits Host cleanup in the background. A failure exposes a retry notification. Each request has its own terminal-ID localStorage key and is removed after successful cleanup or a definitive `session/not-found` response; startup retries saved requests. Transport failures retain the request. This stores cleanup intent, not sidebar layout, open-tab mappings, selected tabs or process PIDs. If browser storage is unavailable, cleanup remains usable in memory but cannot be recovered after reload. + +
+ + +## Further Exploration + +- [Subprocess](../../subprocess/subprocess/README.md) +- [Right Sidebar](../../client/ui-sidebar-right/README.md) +- [Web terminal decision](../../../.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md) + + +## Model Experience + +None, as this package handles user terminal interaction without adding model input. + +#### KV Cache effect + +None; terminal output travels only between the browser and Host. + +## Known Limitations and Deferred Work + + + +- Processes and screens survive browser reloads, but not Host or Session owner disposal. There is no durable terminal restoration or automatic shell respawn. Exited terminals count toward `maxTerminals`; close unused tabs to release their screens and quota. +- The subprocess provider determines native PTY availability and process-tree cleanup guarantees. Finding an executable does not prove PTY allocation will succeed. +- Screen recovery retains bounded history, not a complete transcript. Only one attachment at a time can write or resize. + + +### Dev Note + +
+Working context for maintainers — click to expand + +No runtime invariant companion is published. One owner orders terminal metadata and screen updates; the provider exposes no independently observed dimensions to compare. + +
diff --git a/packages/api/terminal-controller/README.zh.md b/packages/api/terminal-controller/README.zh.md new file mode 100644 index 0000000000..dfc50e30f3 --- /dev/null +++ b/packages/api/terminal-controller/README.zh.md @@ -0,0 +1,93 @@ +--- +description: "用户交互式终端:执行环境默认 shell、有界屏幕恢复和类型化 Remote 控制。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-api-terminal-controller + +[English](README.md) | 中文 + +## 概述 + +从 Web 侧栏在会话工作区打开执行环境的默认 shell。重新连接已有进程,并关闭 provider 管理的完整进程范围。终端输出不进入 Agent 对话记录。保留终端会占用进程和有界屏幕缓存。 + +## 目录 + +- [使用此包](#use-this-package) +- [了解实现](#understand-the-implementation) +- [延伸阅读](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与延期工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用此包 + +Web bundle 将此包与 subprocess provider、sandbox policy 和 Typert Gateway 一起挂载。`remote.terminal` 提供 `environment`、`shells`、`list`、`create`、`follow`、`write`、`resize`、`rename` 和 `close`;每个操作均按 Session 标识限定范围。列表直接读取 Host 保留的终端,因此查看离线 Session 不会激活 Agent,也不会产生恢复错误。 + +Shell 探测结果首先列出执行环境声明的默认 shell。仅当 provider 未声明默认值时,才在 POSIX 使用 `/bin/sh`,在 Windows 使用 `cmd.exe`。可选的 `shell` profile 通过可执行路径 `path`、显示名称 `name` 和参数 `args`(默认 `[]`)覆盖这一选择。选择器还会通过执行 provider 探测 `shellCandidates`,仅省略确定未找到的候选。创建请求接受探测返回的 `shellPath` 并再次验证;解析或传输失败会直接报告,不启动其他 shell。环境查询只返回工作目录和限制,不解析 shell,因此默认 shell 不可用时仍可重新连接已有进程。POSIX 自动 profile 以交互模式启动,PowerShell 使用 `-NoLogo`,补全和启动配置仍由 shell 提供。初始目录来自 Session 工作区,终端遵循同一 sandbox policy。 + +| 配置 | 默认值 | 含义 | +|---|---|---| +| `shell` | 省略 | 使用执行环境默认 shell,或指定一个 profile | +| `shellCandidates` | `zsh`、`bash`、`fish`、`sh`、`ksh`、`tcsh`、`csh`、`pwsh`、`powershell`、`cmd`、`nu` | 已安装时供用户选择的额外可执行名称或路径 | +| `maxTerminals` | `8` | 每个 Session 保留的终端和创建请求上限 | +| `maxCols`、`maxRows` | `500`、`200` | PTY 最大尺寸 | +| `scrollback` | `1000` | 屏幕历史行数 | +| `maxBufferedBytes` | `2097152` | 单个订阅者的输出排队上限 | +| `maxInputBytes` | `65536` | 单次输入请求的字节上限 | +| `disposeGraceMs` | `1000` | provider 终止宽限期,单位毫秒 | + + +## 了解实现 + +
+实现细节 + +Host 通过 `ctx.subprocess.spawnTerminal` 创建 `TERM=xterm-256color` 的终端,不启动桌面终端应用。流式 UTF-8 解码保留跨块字符和开头的 BOM,并在 EOF 将不完整的尾部字节替换为替代字符。控制请求走 Gateway,`follow` 使用其复用的 Remote stream。Headless xterm 和序列化 addon 在此前输出写入后生成初始屏幕,后续增量携带单调序号。过慢的订阅者明确失败;重新连接恢复当前屏幕。 + +最新连接持有输入和尺寸控制权。断开连接只释放输入权,不结束进程。显式关闭等待进程清理和最后输出;清理失败时保留资源以便重试。Session 记住已关闭的标识并拒绝迟到或重复的创建请求,包括关闭到达时仍在进行的创建。新终端使用新标识。取消创建且清理失败时,已分配的进程仍有所有者。Session owner 和 controller 卸载也会终止所拥有的进程。存在终端或创建请求时不能改变该 Session 的 sandbox mode。 控制权转移或进程退出后被拒绝的输入和尺寸请求保留输出连接并禁用输入,不重发被拒绝的输入。 + +Client 视图只在内存中关联侧栏标签页与终端标识。恢复操作查询 Host 保留的终端;新视图可以创建进程,恢复视图在目标缺失时显示错误,不创建替代进程。Client 模型在浏览器完成屏幕解析后确认帧,按序发送输入,并忽略旧连接迟到的响应。 Client 自产错误携带本地化键。插件卸载等待活跃及先前断开的输出流结束,不关闭 Host 进程。 + +新视图等待用户选择 shell 并启动。上次选择的 shell 路径保存在当前站点 localStorage 的 `dsh.terminal.shell` 中。仅当 Host 探测仍列出该路径时才预选它,否则选择当前默认项。存储失败不影响启动。恢复已有终端既不读取这一偏好,也不探测 shell。 + +关闭时先保存未完成的清理请求并释放标签页,再在后台等待 Host 清理。失败时提供重试通知。每个请求使用独立的终端 ID localStorage key,清理成功或收到明确的 `session/not-found` 响应后删除;启动时重试已保存的请求。传输失败时保留请求。保存的是清理意图,不是侧栏布局、打开标签页映射、选中标签页或进程 PID。浏览器存储不可用时,内存中的清理仍可工作,但刷新后无法恢复该请求。 + +
+ + +## 延伸阅读 + +- [Subprocess](../../subprocess/subprocess/README.zh.md) +- [Right Sidebar](../../client/ui-sidebar-right/README.zh.md) +- [Web terminal decision](../../../.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md) + + +## 模型体验 + +无;此包只处理用户交互式终端,不向模型请求添加内容。 + +#### KV 缓存影响 + +无;终端输出只在浏览器与 Host 之间传输。 + +## 已知限制与延期工作 + + + +- 浏览器刷新保留进程和屏幕;Host 或 Session owner 卸载不保留。不提供持久终端恢复或自动重启 shell。 已退出的终端仍计入 `maxTerminals`;关闭不用的标签页可释放其屏幕和名额。 +- 原生 PTY 可用性和进程树清理保证由 subprocess provider 决定。找到可执行文件并不保证 PTY 分配成功。 +- 屏幕恢复只保留有界历史,不保留完整记录。同一时刻只有一个连接可输入或调整尺寸。 + + +### 开发备注 + +
+维护说明 + +不发布运行时 invariant companion。终端元数据与屏幕更新由同一对象按序写入,没有独立的进程尺寸观测可供比较。 + +
diff --git a/packages/api/terminal-controller/package.json b/packages/api/terminal-controller/package.json new file mode 100644 index 0000000000..0504a7dfcc --- /dev/null +++ b/packages/api/terminal-controller/package.json @@ -0,0 +1,93 @@ +{ + "name": "@deepseek-ai/dsh-api-terminal-controller", + "description": "Session-owned interactive terminals with shell discovery, screen recovery and typed Remote control", + "version": "0.1.5-rc.2", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/api/terminal-controller" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./types": { + "types": "./lib/types/types.d.ts", + "default": "./lib/types/types.js" + }, + "./client": { + "types": "./lib/types/client/index.d.ts", + "default": "./lib/client.js" + }, + "./typert": { + "types": "./lib/typert.host.d.ts", + "default": "./lib/typert.host.js" + }, + "./remote": { + "types": "./lib/typert.remote-client.d.ts", + "default": "./lib/typert.remote-client.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "dsh": { + "client": { + "inject": [ + "@deepseek-ai/dsh-api-gateway" + ], + "external": [ + "@deepseek-ai/dsh-api-gateway/client" + ], + "platform": "web" + } + }, + "scripts": { + "bundle": "tsdown", + "watch": "tsdown --watch" + }, + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-deque": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", + "@deepseek-ai/schemastery": "workspace:^", + "@xterm/headless": "^6.0.0", + "@xterm/addon-serialize": "^0.14.0", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/dsh-subprocess": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-api-gateway": "workspace:^", + "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-client-store": "workspace:^", + "@deepseek-ai/dsh-fs": "workspace:^", + "@deepseek-ai/dsh-sandbox": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/dsh-subprocess": "workspace:^", + "@deepseek-ai/dsh-subprocess-local": "workspace:^", + "@deepseek-ai/dsh-util-crypto": "workspace:^" + }, + "files": [ + "lib/index.js", + "lib/client.js", + "lib/types/**/*.js", + "lib/types/**/*.d.ts", + "lib/typert.host.js", + "lib/typert.host.d.ts", + "lib/typert.remote-client.js", + "lib/typert.remote-client.d.ts" + ] +} diff --git a/packages/api/terminal-controller/src/client/close-requests.ts b/packages/api/terminal-controller/src/client/close-requests.ts new file mode 100644 index 0000000000..f91581a4b5 --- /dev/null +++ b/packages/api/terminal-controller/src/client/close-requests.ts @@ -0,0 +1,73 @@ +/** Unfinished close requests survive reload independently of the removed sidebar tabs. */ +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { WebTerminalId } from '../types.ts' + +/** An explicit cleanup request; no process or open-tab metadata is mirrored here. */ +export interface TerminalCloseRequest { + readonly sessionId: SessionId + readonly id: WebTerminalId + readonly title: string +} + +const PREFIX = 'dsh.terminal.close.v1.' + +/** Each request has its own storage key, so other browser windows cannot overwrite its cleanup. */ +export class TerminalCloseRequests { + private readonly requests = new Map() + + constructor() { + try { + if (typeof localStorage === 'undefined') return + for (let index = 0; index < localStorage.length; index++) { + const key = localStorage.key(index) + if (key?.startsWith(PREFIX)) this.load(key) + } + } catch (error) { console.error('Terminal cleanup recovery failed:', error) } + } + + /** + * Read cleanup work still awaiting Host confirmation. + * @returns unfinished requests owned by this browser instance. + */ + pending(): readonly TerminalCloseRequest[] { return [...this.requests.values()] } + + /** + * Retain cleanup across reload before removing a tab. + * @param request - close intent to save before removing its tab. + */ + save(request: TerminalCloseRequest): void { + this.requests.set(request.id, request) + try { + if (typeof localStorage !== 'undefined') localStorage.setItem(PREFIX + request.id, JSON.stringify(request)) + } catch (error) { console.error('Terminal cleanup persistence failed:', error) } + } + + /** + * Forget confirmed cleanup in memory and browser storage. + * @param id - terminal whose Host cleanup succeeded. + */ + remove(id: WebTerminalId): void { + this.requests.delete(id) + try { + if (typeof localStorage !== 'undefined') localStorage.removeItem(PREFIX + id) + } catch (error) { console.error('Terminal cleanup persistence failed:', error) } + } + + private load(key: string): void { + try { + const raw = localStorage.getItem(key) + if (raw === null) return + const parsed: unknown = JSON.parse(raw) + if (!isRequest(parsed) || key !== PREFIX + parsed.id) throw new Error('Invalid terminal cleanup request') + this.requests.set(parsed.id, parsed) + } catch (error) { console.error('Terminal cleanup recovery failed:', error) } + } +} + +function isRequest(value: unknown): value is TerminalCloseRequest { + if (typeof value !== 'object' || value === null) return false + const request = value as Record + return typeof request.sessionId === 'string' && request.sessionId.length > 0 + && typeof request.id === 'string' && /^[\w-]{1,128}$/u.test(request.id) + && typeof request.title === 'string' +} diff --git a/packages/api/terminal-controller/src/client/index.ts b/packages/api/terminal-controller/src/client/index.ts new file mode 100644 index 0000000000..1daec7edd8 --- /dev/null +++ b/packages/api/terminal-controller/src/client/index.ts @@ -0,0 +1,150 @@ +/** Client terminal model service; views are keyed independently from Host terminal identities. */ +import { Service, type Context } from '@deepseek-ai/cordis' +import { remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type {} from '@deepseek-ai/dsh-api-gateway/client' +import { TerminalView, type TerminalRemote } from './model.ts' +import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store' +import { randomUUID } from '@deepseek-ai/dsh-util-crypto' +import type { WebTerminalId, WebTerminalInfo } from '../types.ts' +import { TerminalCloseRequests, type TerminalCloseRequest } from './close-requests.ts' + +export type { TerminalView, TerminalViewState, TerminalViewIssue, TerminalRenderFrame, TerminalRemote } from './model.ts' + +declare module '@deepseek-ai/cordis' { + interface Context { + /** React-free browser terminal views and explicit process cleanup. */ + webTerminals: ClientTerminals + } +} + +/** A failed background close that can be retried without restoring its tab. */ +export interface TerminalCloseFailure { + readonly id: WebTerminalId + readonly title: string + readonly message: string +} + +/** Session and occurrence lookup, independent tab and terminal identities and background cleanup. */ +export class ClientTerminals extends Service { + /** Failed cleanup tasks; successful and in-progress closes have no visible notification. */ + readonly closeFailures: SnapshotStore = createSnapshotStore([]) + private readonly requests = new TerminalCloseRequests() + private readonly closing = new Map>() + private readonly closed = new Set(this.requests.pending().map(request => request.id)) + private disposed = false + private readonly views = new Map>() + + /** + * @param ctx - Client root Context with Gateway and terminal Remote namespace. + * @param remote - generated terminal namespace. + */ + constructor(ctx: Context, private readonly remote: TerminalRemote) { + super(ctx, 'webTerminals') + ctx.effect(() => async () => { + this.disposed = true + const detaching = [...this.views.values()].flatMap(views => [...views.values()].map(view => view.dispose())) + this.views.clear() + await Promise.all([...detaching, ...this.closing.values()]) + }, 'terminal-controller.client.views') + for (const request of this.requests.pending()) this.cleanup(request) + } + + /** + * Return the stable model for one sidebar occurrence. + * @param sessionId - owning Session. + * @param key - sidebar occurrence key. + * @param terminalId - existing Host identity when restoring a listed terminal. + * @returns its observable state and terminal commands. + */ + view(sessionId: SessionId, key: string, terminalId?: WebTerminalId): TerminalView { + let views = this.views.get(sessionId) + if (views === undefined) { views = new Map(); this.views.set(sessionId, views) } + let view = views.get(key) + if (view === undefined) { + const id = terminalId ?? randomUUID() as WebTerminalId + view = new TerminalView(sessionId, this.remote, this.ctx.remote, id, terminalId === undefined) + views.set(key, view) + void view.refresh() + } + return view + } + + /** + * Save a close intent and release the tab immediately; cleanup outlives DOM unmount and reload. + * @param sessionId - owning Session. + * @param key - sidebar occurrence key, including an inactive restored tab. + * @param terminalId - restored identity if the tab has no model yet. + */ + close(sessionId: SessionId, key: string, terminalId?: WebTerminalId): void { + const views = this.views.get(sessionId) + const view = views?.get(key) + const id = view?.id ?? terminalId + if (id === undefined) return + const request: TerminalCloseRequest = { sessionId, id, title: view?.state.getSnapshot().title ?? key } + this.closed.add(id) + this.requests.save(request) + views?.delete(key) + if (views?.size === 0) this.views.delete(sessionId) + this.cleanup(request, view) + } + + /** + * Query Host terminals that have neither a tab in this page nor an unfinished close. + * @param sessionId - Session being displayed. + * @returns terminals available for opening as recovered tabs. + */ + async recover(sessionId: SessionId): Promise { + const result = await this.remote.list(sessionId) + if (!result.ok) throw new Error(result.error.message) + const held = new Set([...(this.views.get(sessionId)?.values() ?? [])].map(view => view.id)) + const closing = new Set(this.requests.pending().map(request => request.id)) + return result.value.filter(info => !held.has(info.id) && !closing.has(info.id) && !this.closed.has(info.id)) + } + + /** + * Retry a saved close request without reopening its tab. + * @param id - failed terminal identity. + */ + retryClose(id: WebTerminalId): void { + const record = this.requests.pending().find(item => item.id === id) + if (record !== undefined) this.cleanup(record) + } + + private cleanup(record: TerminalCloseRequest, view?: TerminalView): void { + if (this.closing.has(record.id) || this.disposed) return + this.closeFailures.set(this.closeFailures.getSnapshot().filter(failure => failure.id !== record.id)) + const pending = (async () => { + if (view !== undefined) await view.close() + else { + const result = await this.remote.close(record.sessionId, record.id) + if (!result.ok) throw result.error + } + this.requests.remove(record.id) + })().catch((error: unknown) => { + if (remoteErrorOf(error)?.code === 'session/not-found') { + this.requests.remove(record.id) + return + } + if (!this.disposed) this.closeFailures.set([...this.closeFailures.getSnapshot(), { + id: record.id, title: record.title, + message: error instanceof Error ? error.message : String(error), + }]) + }).then(async () => { + await view?.dispose() + this.closing.delete(record.id) + }) + this.closing.set(record.id, pending) + } +} + +/** Required Client transport and terminal namespace. */ +export const inject = ['remote', 'remote.terminal'] + +/** + * Install the Client terminal models. + * @param ctx - Client root Context. + */ +export function apply(ctx: Context): void { + new ClientTerminals(ctx, ctx.remote.terminal) +} diff --git a/packages/api/terminal-controller/src/client/model.ts b/packages/api/terminal-controller/src/client/model.ts new file mode 100644 index 0000000000..f6c1a350ad --- /dev/null +++ b/packages/api/terminal-controller/src/client/model.ts @@ -0,0 +1,352 @@ +/** React-free browser terminal state and reconnecting Remote-stream ownership. */ +import { preferredShell, rememberShell } from './shell-preference.ts' +import { randomUUID } from '@deepseek-ai/dsh-util-crypto' +import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store' +import { RemoteStreamCarrierError, type ClientRemote, type RemoteStream } from '@deepseek-ai/dsh-api-gateway/client' +import { RemoteError, remoteErrorOf, type RemoteResult } from '@deepseek-ai/dsh-typert-protocol' +import type {} from '@deepseek-ai/dsh-api-terminal-controller/remote' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { + TerminalShell, TerminalAttachmentId, TerminalEnvironment, TerminalFrame, + WebTerminalId, WebTerminalInfo, +} from '../types.ts' + +/** The generated terminal namespace's browser-facing operations. */ +export type TerminalRemote = ClientRemote['terminal'] + +/** Product error identifiers translated by the terminal UI. */ +export type TerminalViewIssue = 'missingTerminal' | 'inputFull' | 'attachmentEnded' | 'invalidOutput' | 'terminalLimit' + +declare module '@deepseek-ai/dsh-typert-protocol' { + interface RemoteErrorDetailsMap { + /** Client terminal failure preserved through the Remote stream supervisor. */ + 'terminal/view': { readonly issue: TerminalViewIssue } + } +} + +class TerminalViewError extends RemoteError<'terminal/view'> { + constructor(issue: TerminalViewIssue, message: string = issue) { super('terminal/view', message, { issue }) } +} + +/** One screen write awaiting the DOM emulator's callback. */ +export interface TerminalRenderFrame { + readonly revision: number + readonly frame: Extract +} + +/** Observable state of one sidebar occurrence. */ +export interface TerminalViewState { + readonly phase: 'idle' | 'loading' | 'selecting' | 'creating' | 'connecting' | 'connected' | 'disconnected' | 'closing' | 'closed' | 'failed' + readonly shells?: readonly TerminalShell[] | undefined + readonly selectedShell?: string | undefined + readonly environment?: TerminalEnvironment | undefined + readonly title?: string | undefined + readonly info?: WebTerminalInfo | undefined + readonly writable: boolean + readonly render?: TerminalRenderFrame | undefined + readonly error?: string | undefined + readonly issue?: TerminalViewIssue | undefined +} + +/** A view survives DOM unmount; its process only ends on explicit close. */ +export class TerminalView { + /** Observable controls, process metadata and the next screen update awaiting acknowledgement. */ + readonly state: SnapshotStore = createSnapshotStore({ phase: 'idle', writable: false }) + private readonly lifetime = new AbortController() + private stream: RemoteStream | undefined + private mounted = false + private attachmentId: TerminalAttachmentId | undefined + private pendingRender: { revision: number; resolve: () => void } | undefined + private revision = 0 + private creation: Promise | undefined + private loading: Promise | undefined + private closing: Promise | undefined + private writes: Promise = Promise.resolve() + private queuedInput = 0 + private readonly detaching = new Set>() + + /** + * @param sessionId - Session owning the terminal. + * @param remote - typed terminal Remote operations. + * @param gateway - reconnecting stream factory. + * @param id - Host terminal identity, reused when recovering an item from its Session list. + * @param createWhenMissing - allow allocation only for a new tab, never a listed terminal. + */ + constructor( + private readonly sessionId: SessionId, + private readonly remote: TerminalRemote, + private readonly gateway: Pick, + readonly id: WebTerminalId, + private readonly createWhenMissing = true, + ) {} + + /** + * Attach the DOM lifetime, loading shell choices or reconnecting the saved process. + * @returns a detach callback that leaves the terminal process alive. + */ + mount(): () => void { + this.mounted = true + if (this.state.getSnapshot().info === undefined) void this.refresh() + else this.connect() + return () => { + this.mounted = false + this.detach() + } + } + + /** + * Start or recover this tab, deduplicating mounts and retries during allocation. + * Only a new tab may allocate a shell; listed terminals cannot be silently replaced. + * @returns after environment lookup and creation or recovery settle. + */ + refresh(): Promise { + if (this.creation !== undefined) return this.creation + if (this.loading !== undefined) return this.loading + if (this.closing !== undefined || this.lifetime.signal.aborted) return Promise.resolve() + this.patch({ phase: 'loading', error: undefined, issue: undefined }) + this.loading = (async () => { + const [environment, available] = await Promise.all([ + this.remote.environment(this.sessionId, this.lifetime.signal), this.remote.list(this.sessionId), + ]) + if (this.stopped()) return + this.patch({ environment: valueOf(environment) }) + const info = valueOf(available).find(item => item.id === this.id) + if (info !== undefined) this.adopt(info) + else if (this.createWhenMissing) { + const shells = valueOf(await this.remote.shells(this.sessionId, this.lifetime.signal)) + if (this.stopped()) return + const previous = this.state.getSnapshot().selectedShell ?? preferredShell() + const selectedShell = shells.find(shell => shell.path === previous)?.path ?? shells[0]?.path + this.patch({ phase: 'selecting', shells, selectedShell }) + } + else throw new TerminalViewError('missingTerminal') + })().catch((error: unknown) => { this.fail(error) }).finally(() => { this.loading = undefined }) + return this.loading + } + + /** + * Select and remember one verified shell before starting the terminal. + * @param path - executable path offered by Host discovery. + */ + selectShell(path: string): void { + const state = this.state.getSnapshot() + if (state.phase === 'selecting' && state.shells?.some(shell => shell.path === path)) { + this.patch({ selectedShell: path }) + rememberShell(path) + } + } + + /** + * Start the selected shell once; failed attempts retain their selection for retry. + * @returns after allocation settles; failures are exposed in observable state. + */ + start(): Promise { + if (this.creation !== undefined) return this.creation + const state = this.state.getSnapshot() + if (state.phase !== 'selecting' || state.environment === undefined || state.selectedShell === undefined + || this.closing !== undefined || this.lifetime.signal.aborted) return Promise.resolve() + rememberShell(state.selectedShell) + return this.create(state.environment, state.selectedShell) + } + + private stopped(): boolean { return this.lifetime.signal.aborted || this.closing !== undefined } + + private async create(environment: TerminalEnvironment, shellPath: string): Promise { + this.patch({ phase: 'creating', error: undefined, issue: undefined }) + this.creation = (async () => { + const info = valueOf(await this.remote.create(this.sessionId, { + id: this.id, shellPath, cols: Math.min(80, environment.maxCols), rows: Math.min(24, environment.maxRows), + }, this.lifetime.signal)) + if (!this.lifetime.signal.aborted) { + this.adopt(info) + } + })().catch((error: unknown) => { this.fail(error) }).finally(() => { this.creation = undefined }) + await this.creation + } + + private adopt(info: WebTerminalInfo): void { + this.patch({ info, title: info.title }) + if (this.mounted && this.closing === undefined) this.connect() + } + + /** Reattach with a fresh screen and regain input control. */ + connect(): void { + const info = this.state.getSnapshot().info + if (info === undefined || !this.mounted || this.closing !== undefined || this.lifetime.signal.aborted) return + this.detach() + const stream = this.gateway.$stream({ + name: 'Browser terminal output', + open: (signal) => { + const attachmentId = randomUUID() as TerminalAttachmentId + this.attachmentId = attachmentId + return this.remote.follow(this.sessionId, info.id, attachmentId, signal) + }, + ended: () => new TerminalViewError('attachmentEnded'), + carrierFailed: () => { if (this.stream === stream) this.patch({ phase: 'disconnected', writable: false }) }, + }) + this.stream = stream + this.patch({ phase: 'connecting', writable: false, error: undefined, issue: undefined, render: undefined }) + void this.consume(stream) + } + + /** + * Release the next stream item after xterm has parsed this frame. + * @param revision - locally delivered render revision. + */ + acknowledge(revision: number): void { + if (this.pendingRender?.revision !== revision) return + this.pendingRender.resolve() + this.pendingRender = undefined + } + + /** + * Serialize raw input so concurrent RPC requests cannot reorder keystrokes. + * @param data - input from the terminal emulator. + */ + write(data: string): void { + const state = this.state.getSnapshot() + const attachmentId = this.attachmentId + if (!state.writable || state.info === undefined || attachmentId === undefined) return + const bytes = new TextEncoder().encode(data).byteLength + if (this.queuedInput + bytes > (state.environment?.maxInputBytes ?? 0)) { this.fail(new TerminalViewError('inputFull')); return } + this.queuedInput += bytes + const id = state.info.id + this.writes = this.writes.then(async () => { + if (this.attachmentId !== attachmentId || !this.state.getSnapshot().writable) return + valueOf(await this.remote.write(this.sessionId, id, attachmentId, data)) + }).catch((error: unknown) => { if (this.attachmentId === attachmentId) this.fail(error) }).finally(() => { this.queuedInput -= bytes }) + } + + /** + * Resize only from the currently writable view. + * @param cols - measured column count. + * @param rows - measured row count. + */ + resize(cols: number, rows: number): void { + const state = this.state.getSnapshot() + const attachmentId = this.attachmentId + if (!state.writable || state.info === undefined || attachmentId === undefined) return + if (state.info.cols === cols && state.info.rows === rows) return + const id = state.info.id + cols = Math.min(cols, state.environment?.maxCols ?? cols) + rows = Math.min(rows, state.environment?.maxRows ?? rows) + this.writes = this.writes.then(async () => { + if (this.attachmentId !== attachmentId || !this.state.getSnapshot().writable) return + valueOf(await this.remote.resize(this.sessionId, id, attachmentId, cols, rows)) + }).catch((error: unknown) => { if (this.attachmentId === attachmentId) this.fail(error) }) + } + + /** + * Update the Host terminal's display name. + * @param title - user-entered terminal title. + * @returns after the rename settles and its result is reflected in view state. + */ + async rename(title: string): Promise { + if (title.trim() === this.state.getSnapshot().title || this.lifetime.signal.aborted) return + try { + valueOf(await this.remote.rename(this.sessionId, this.id, title)) + const current = this.state.getSnapshot().info + this.patch({ ...(current === undefined ? {} : { info: { ...current, title: title.trim() } }), title: title.trim() }) + } catch (error) { this.fail(error) } + } + + /** + * Explicitly terminate this view's process independently of its DOM lifetime. + * @returns after Host process cleanup succeeds; failures remain retryable by the owner. + */ + close(): Promise { + if (this.closing !== undefined) return this.closing + this.patch({ phase: 'closing', writable: false, error: undefined, issue: undefined }) + this.detach() + this.closing = (async () => { + // Even a refused or lost creation response may leave an allocation to close. + await this.creation + valueOf(await this.remote.close(this.sessionId, this.id)) + this.detach() + this.patch({ phase: 'closed', writable: false }) + })().catch((error: unknown) => { this.closing = undefined; this.fail(error); throw error }) + return this.closing + } + + /** + * Stop Client work on plugin unload without closing Host terminals. + * @returns after active and previously detached stream iterators have closed. + */ + async dispose(): Promise { + this.mounted = false + this.lifetime.abort() + this.detach() + await Promise.all(this.detaching) + } + + private detach(): void { + const previous = this.stream + this.stream = undefined + this.attachmentId = undefined + this.pendingRender?.resolve() + this.pendingRender = undefined + if (previous !== undefined) { + const cleanup = previous.dispose().finally(() => { this.detaching.delete(cleanup) }) + this.detaching.add(cleanup) + } + } + + private async consume(stream: RemoteStream): Promise { + let generation = 0 + let sequence = 0 + try { + for await (const item of stream) { + if (this.stream !== stream) return + const frame = item.value + if (generation !== item.generation) { + if (frame.type !== 'snapshot') throw new TerminalViewError('invalidOutput', 'Terminal output generation is missing its screen snapshot') + generation = item.generation + sequence = frame.sequence + item.accept() + } else if (frame.type === 'output') { + if (frame.sequence !== sequence + 1) throw new TerminalViewError('invalidOutput', 'Terminal output sequence has a gap') + sequence = frame.sequence + } else if (frame.type === 'snapshot') throw new TerminalViewError('invalidOutput', 'Unexpected terminal screen snapshot') + if (frame.type !== 'output') { + this.patch({ info: frame.info, title: frame.info.title, phase: 'connected', writable: frame.info.state === 'running' && frame.info.controllerId === this.attachmentId }) + } + if (frame.type !== 'state') { + const revision = ++this.revision + await new Promise((resolve) => { + this.pendingRender = { revision, resolve } + const aborted = (): void => { this.acknowledge(revision) } + item.signal.addEventListener('abort', aborted, { once: true }) + this.pendingRender.resolve = () => { item.signal.removeEventListener('abort', aborted); resolve() } + this.patch({ render: { revision, frame } }) + if (item.signal.aborted) aborted() + }) + } + } + } catch (error) { + if (this.stream === stream) { + if (this.state.getSnapshot().info?.state === 'exited') this.patch({ phase: 'closed', writable: false }) + else this.fail(error) + } + } + } + + private patch(patch: Partial): void { + if (this.lifetime.signal.aborted) return + this.state.set({ ...this.state.getSnapshot(), ...patch }) + } + + private fail(error: unknown): void { + const failure = remoteErrorOf(error) + if (failure?.code === 'terminal/control-unavailable') { + this.patch({ writable: false, error: undefined, issue: undefined }) + return + } + const issue = failure?.code === 'terminal/view' ? failure.details.issue : failure?.code === 'terminal/limit-reached' ? 'terminalLimit' : undefined + this.patch({ phase: error instanceof RemoteStreamCarrierError ? 'disconnected' : 'failed', writable: false, issue, error: error instanceof Error ? error.message : String(error) }) + } +} + +function valueOf(result: RemoteResult): T { + if (!result.ok) throw result.error + return result.value +} diff --git a/packages/api/terminal-controller/src/client/shell-preference.ts b/packages/api/terminal-controller/src/client/shell-preference.ts new file mode 100644 index 0000000000..8e8a461591 --- /dev/null +++ b/packages/api/terminal-controller/src/client/shell-preference.ts @@ -0,0 +1,20 @@ +/** Browser-local shell preference; Host discovery decides whether the saved path is usable. */ +const KEY = 'dsh.terminal.shell' + +/** + * Read the browser preference. + * @returns the last selected shell path, or null when storage is unavailable. + */ +export function preferredShell(): string | null { + try { return typeof localStorage === 'undefined' ? null : localStorage.getItem(KEY) } + catch (_storageUnavailable) { return null } +} + +/** + * Remember the selected shell without making storage a startup dependency. + * @param path - verified executable path offered by the Host. + */ +export function rememberShell(path: string): void { + try { if (typeof localStorage !== 'undefined') localStorage.setItem(KEY, path) } + catch (_storageUnavailable) { /* Private browsing or quota failure leaves this launch usable. */ } +} diff --git a/packages/api/terminal-controller/src/index.ts b/packages/api/terminal-controller/src/index.ts new file mode 100644 index 0000000000..920982897e --- /dev/null +++ b/packages/api/terminal-controller/src/index.ts @@ -0,0 +1,360 @@ +/** Session-scoped browser terminals over the composed subprocess and sandbox providers. */ +import type { Context } from '@deepseek-ai/cordis' +import z from '@deepseek-ai/schemastery' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session' +import type { SubprocessTerminalHandle } from '@deepseek-ai/dsh-subprocess' +import type {} from '@deepseek-ai/dsh-sandbox-policy' +import type {} from '@deepseek-ai/dsh-sandbox' +import type {} from '@deepseek-ai/dsh-session-projection' +import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' +import { discoverShells, resolveShell } from './shells.ts' +import { BrowserTerminal } from './terminal.ts' +import type { + TerminalShell, TerminalAttachmentId, TerminalCreateRequest, TerminalEnvironment, TerminalFrame, + WebTerminalId, WebTerminalInfo, +} from './types.ts' + +export type * from './types.ts' + +declare module '@deepseek-ai/cordis' { + interface Context { + /** Interactive user terminals, separate from the Agent terminal tool registry. */ + terminalController: TerminalController + } +} + +/** Deployment limits and an optional shell profile. */ +export interface Config { + /** Explicit shell profile; omission uses the execution environment's default shell. */ + readonly shell?: { + /** Executable path or PATH name, verified by the subprocess provider. */ + path: string + /** User-visible profile name. */ + name: string + /** Arguments passed to the interactive shell. */ + args: string[] + } | undefined + /** Executable names or paths checked for the new-terminal shell selector. */ + readonly shellCandidates: string[] + /** Maximum retained terminals and pending allocations per Session. */ + readonly maxTerminals: number + /** Maximum terminal width in columns. */ + readonly maxCols: number + /** Maximum terminal height in rows. */ + readonly maxRows: number + /** Screen history rows retained for reconnecting clients. */ + readonly scrollback: number + /** Maximum queued UTF-8 frame bytes per output follower before disconnection. */ + readonly maxBufferedBytes: number + /** Maximum UTF-8 bytes in one input request. */ + readonly maxInputBytes: number + /** Provider process-termination grace period in milliseconds. */ + readonly disposeGraceMs: number +} + +interface OwnedSession { + readonly lifetime: AbortController + readonly closedIds: Set + cleanup?: Promise + readonly terminals: Map + readonly pending: Map> + readonly allocations: Map +} + +/** Typed Remote control of transient Session-owned terminal processes. */ +export class TerminalController extends TypertRemoteService { + static inject = ['subprocess', 'sandboxPolicy', 'sessionProjections', 'typert'] + static Config: z = z.object({ + shell: z.union([z.object({ + path: z.string().required(), name: z.string().required(), args: z.array(z.string()).default([]), + }), z.const(undefined)]), + shellCandidates: z.array(z.string().min(1)).default(['zsh', 'bash', 'fish', 'sh', 'ksh', 'tcsh', 'csh', 'pwsh', 'powershell', 'cmd', 'nu']), + maxTerminals: z.number().step(1).min(1).default(8), + maxCols: z.number().step(1).min(2).default(500), + maxRows: z.number().step(1).min(1).default(200), + scrollback: z.number().step(1).min(0).default(1000), + maxBufferedBytes: z.number().step(1).min(1024).default(2 * 1024 * 1024), + maxInputBytes: z.number().step(1).min(1).default(64 * 1024), + disposeGraceMs: z.number().step(1).min(1).default(1000), + }) + + private readonly owners = new Map() + private readonly lifetime = new AbortController() + + /** + * @param ctx - Host context carrying typed Remote and execution providers. + * @param config - validated terminal limits and optional shell profile. + */ + constructor(ctx: Context, private readonly config: Config) { + super(ctx, 'terminalController', { namespace: 'terminal' }) + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'session/event') return + const [session, event] = args as [Session, SessionEvent] + if (event.type !== 'sandbox/mode') return + const owner = this.owners.get(session.id) + if (owner === undefined || owner.terminals.size + owner.pending.size + owner.allocations.size === 0) return + const current = ctx.sessionProjections.stateOf(session, 'sandboxMode') ?? ctx.sandboxPolicy.defaultMode + if (event.data.mode !== current) throw new Error('Close browser terminals before changing the Session sandbox mode') + }, { global: true }) + ctx.effect(() => async () => { + this.lifetime.abort(new Error('Terminal controller disposed')) + const results = await Promise.allSettled([...this.owners].map(([id, owner]) => this.disposeOwner(id, owner))) + const errors = results.filter(result => result.status === 'rejected').map(result => result.reason as unknown) + if (errors.length > 0) throw new AggregateError(errors, 'Browser terminal cleanup failed') + }, 'terminal-controller.processes') + } + + /** + * Read the Session working directory and terminal limits without resolving a shell. + * @param agent - Session owner supplied by the Gateway. + * @param signal - request cancellation. + * @returns the Session workspace directory and terminal limits. + */ + @Remote + environment(agent: Agent, signal: AbortSignal): TerminalEnvironment { + signal.throwIfAborted() + const { sandboxPolicy } = this.execution(agent) + return { cwd: sandboxPolicy.resolve({ session: agent.session }).workspaceRoot, + maxInputBytes: this.config.maxInputBytes, maxCols: this.config.maxCols, + maxRows: this.config.maxRows, scrollback: this.config.scrollback } + } + + /** + * Discover installed shells in the Session's execution environment. + * @param agent - Session owner supplied by the Gateway. + * @param signal - request cancellation. + * @returns verified profiles, with the configured or system default first. + */ + @Remote + shells(agent: Agent, signal: AbortSignal): Promise { + signal.throwIfAborted() + return discoverShells(this.execution(agent).subprocess, this.config.shell, this.config.shellCandidates, signal) + } + + /** + * List retained terminals without resolving or activating an Agent. + * @param sessionId - displayed Session identity, including offline history. + * @returns terminals retained for this Host lifetime. + */ + @Remote + list(sessionId: SessionId): WebTerminalInfo[] { + const owner = this.owners.get(sessionId) + if (owner === undefined) return [] + return [...owner.terminals.values(), ...owner.allocations.values()].map(terminal => terminal.info) + } + + /** + * Allocate an interactive shell once for a caller-generated identity. + * @param agent - Session owner supplied by the Gateway. + * @param request - initial dimensions and idempotency identity. + * @param signal - allocation cancellation; committed terminals survive disconnection. + * @returns the existing or newly committed terminal. + */ + @Remote + async create(agent: Agent, request: TerminalCreateRequest, signal: AbortSignal): Promise { + this.lifetime.signal.throwIfAborted() + if (!/^[\w-]{1,128}$/u.test(request.id)) throw new Error('Invalid terminal identity') + this.dimensions(request.cols, request.rows) + const owner = this.owner(agent) + owner.lifetime.signal.throwIfAborted() + this.requireOpen(owner, request.id) + const existing = owner.terminals.get(request.id) + if (existing !== undefined) return existing.info + const pending = owner.pending.get(request.id) + if (pending !== undefined) { + const terminal = await pending + this.requireOpen(owner, request.id) + return terminal.info + } + if (owner.allocations.has(request.id)) throw new Error('Close the failed terminal allocation before creating it again') + if (new Set([...owner.terminals.keys(), ...owner.pending.keys(), ...owner.allocations.keys()]).size >= this.config.maxTerminals) throw new RemoteError('terminal/limit-reached', 'Session terminal limit reached', { limit: this.config.maxTerminals }) + const allocation = this.spawn(agent, owner, request, AbortSignal.any([signal, this.lifetime.signal, owner.lifetime.signal])) + owner.pending.set(request.id, allocation) + try { + const terminal = await allocation + owner.terminals.set(request.id, terminal) + owner.allocations.delete(request.id) + this.requireOpen(owner, request.id) + return terminal.info + } finally { + owner.pending.delete(request.id) + } + } + + /** + * Attach to a terminal without binding its process lifetime to the transport. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param attachmentId - new exclusive input attachment. + * @param signal - physical stream cancellation. + * @returns screen recovery followed by output and metadata changes. + */ + @Remote({ mode: 'stream' }) + follow(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, signal: AbortSignal): AsyncIterable { + if (!/^[\w-]{1,128}$/u.test(attachmentId)) throw new Error('Invalid terminal attachment identity') + return this.terminal(agent, id).follow(attachmentId, signal) + } + + /** + * Deliver raw input, including Tab completion and control characters. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param attachmentId - current writable attachment. + * @param data - input bytes represented as UTF-8 text. + * @returns after provider input acceptance. + */ + @Remote + async write(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, data: string): Promise { + if (Buffer.byteLength(data, 'utf8') > this.config.maxInputBytes) throw new Error('Terminal input exceeds the configured limit') + await this.terminal(agent, id).write(attachmentId, data) + } + + /** + * Update the dimensions of the PTY and recovery screen. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param attachmentId - current writable attachment. + * @param cols - column count. + * @param rows - row count. + * @returns after the resize completes. + */ + @Remote + async resize(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, cols: number, rows: number): Promise { + this.dimensions(cols, rows) + await this.terminal(agent, id).resize(attachmentId, cols, rows) + } + + /** + * Rename a terminal without changing its shell. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @param title - nonempty display title, at most 120 characters. + */ + @Remote + rename(agent: Agent, id: WebTerminalId, title: string): void { + if (title.trim().length === 0 || title.length > 120) throw new Error('Terminal title must contain 1–120 characters') + this.terminal(agent, id).rename(title.trim()) + } + + /** + * Close an identity to future creation and kill its process range; repeated closes succeed. + * @param agent - Session owner supplied by the Gateway. + * @param id - terminal identity. + * @returns after provider cleanup succeeds. A failure retains the terminal for retry. + */ + @Remote + async close(agent: Agent, id: WebTerminalId): Promise { + const owner = this.owner(agent) + owner.closedIds.add(id) + // create publishes the allocation before this wait settles; close owns it even if create then rejects. + await owner.pending.get(id)?.catch(() => { /* Creation reports its failure; close still owns any allocated process. */ }) + const terminal = owner.terminals.get(id) + if (terminal !== undefined) { + await terminal.close() + owner.terminals.delete(id) + } else { + const allocation = owner.allocations.get(id) + if (allocation === undefined) return + await allocation.handle.terminate() + owner.allocations.delete(id) + } + } + + private owner(agent: Agent): OwnedSession { + let owner = this.owners.get(agent.id) + if (owner === undefined) { + owner = { terminals: new Map(), pending: new Map(), allocations: new Map(), closedIds: new Set(), lifetime: new AbortController() } + this.owners.set(agent.id, owner) + const owned = owner + agent.ctx.effect(() => async () => { await this.disposeOwner(agent.id, owned) }, 'terminal-controller.owner') + } + return owner + } + + private disposeOwner(id: SessionId, owner: OwnedSession): Promise { + if (owner.cleanup !== undefined) return owner.cleanup + owner.lifetime.abort(new Error('Terminal Session owner disposed')) + owner.cleanup = (async () => { + await Promise.allSettled(owner.pending.values()) + const results = await Promise.allSettled([ + ...[...owner.terminals.values()].map(terminal => terminal.close()), + ...[...owner.allocations.values()].map(allocation => allocation.handle.terminate()), + ]) + const errors = results.filter(result => result.status === 'rejected').map(result => result.reason as unknown) + if (errors.length > 0) throw new AggregateError(errors, 'Session terminal cleanup failed') + owner.terminals.clear() + owner.allocations.clear() + this.owners.delete(id) + })().catch((error: unknown) => { delete owner.cleanup; throw error }) + return owner.cleanup + } + + private terminal(agent: Agent, id: WebTerminalId): BrowserTerminal { + const terminal = this.owners.get(agent.id)?.terminals.get(id) + if (terminal === undefined) throw new Error('Terminal no longer exists in this Session') + return terminal + } + + private requireOpen(owner: OwnedSession, id: WebTerminalId): void { + if (owner.closedIds.has(id)) throw new Error('Terminal was closed in this Session') + } + + private dimensions(cols: number, rows: number): void { + if (!Number.isSafeInteger(cols) || cols < 2 || cols > this.config.maxCols + || !Number.isSafeInteger(rows) || rows < 1 || rows > this.config.maxRows) throw new Error('Terminal dimensions exceed the configured limits') + } + + private execution(agent: Agent): { subprocess: Context['subprocess']; sandboxPolicy: Context['sandboxPolicy'] } { + // The Agent context selects execution providers but does not inject consumer services. + const subprocess = agent.ctx.get('subprocess') + const sandboxPolicy = agent.ctx.get('sandboxPolicy') + if (subprocess === undefined || sandboxPolicy === undefined) throw new Error('The Session execution environment requires subprocess and sandbox policy providers') + return { subprocess, sandboxPolicy } + } + + private async spawn(agent: Agent, owner: OwnedSession, request: TerminalCreateRequest, signal: AbortSignal): Promise { + const environment = this.environment(agent, signal) + const { subprocess, sandboxPolicy } = this.execution(agent) + const shell = request.shellPath === undefined + ? await resolveShell(subprocess, this.config.shell, signal) + : (await this.shells(agent, signal)).find(candidate => candidate.path === request.shellPath) + if (shell === undefined) throw new Error('Selected shell is not available in this execution environment') + const policy = sandboxPolicy.resolve({ session: agent.session }) + let argv = [shell.path, ...shell.args] + if (policy.mode !== 'danger-full-access') { + const sandbox = agent.ctx.get('sandbox') + if (sandbox === undefined) throw new Error('The Session sandbox mode requires an execution sandbox provider') + argv = (await sandbox.confine(argv, { ...policy, mode: policy.mode }, signal)).argv + } + const handle = await subprocess.spawnTerminal({ + argv, cwd: environment.cwd, cols: request.cols, rows: request.rows, + terminalType: 'xterm-256color', env: { DSH_SESSION_ID: agent.id }, + graceMs: this.config.disposeGraceMs, signal, + }) + const allocation = { + handle, + info: { + id: request.id, shell, title: shell.name, cwd: environment.cwd, + cols: request.cols, rows: request.rows, state: 'running', exitCode: null, + } as WebTerminalInfo, + } + owner.allocations.set(request.id, allocation) + try { + signal.throwIfAborted() + return new BrowserTerminal(handle, allocation.info, this.config.scrollback, this.config.maxBufferedBytes) + } catch (error) { + allocation.info = { ...allocation.info, state: 'failed', error: error instanceof Error ? error.message : String(error) } + try { + await handle.terminate() + owner.allocations.delete(request.id) + } catch (cleanupError) { + throw new AggregateError([error, cleanupError], 'Terminal allocation cleanup failed') + } + throw error + } + } +} + +/** Browser terminal service plugin. */ +export default TerminalController diff --git a/packages/api/terminal-controller/src/shells.ts b/packages/api/terminal-controller/src/shells.ts new file mode 100644 index 0000000000..c8010c38c4 --- /dev/null +++ b/packages/api/terminal-controller/src/shells.ts @@ -0,0 +1,57 @@ +/** Shell selection and executable verification use the target execution provider. */ +import { SubprocessExecutableNotFoundError, type SubprocessRuntime } from '@deepseek-ai/dsh-subprocess' +import type { TerminalShell } from './types.ts' + +/** + * Resolve the configured shell or the execution environment's default shell. + * @param subprocess - target execution provider. + * @param configured - optional profile overriding the environment's default shell. + * @param signal - resolution cancellation. + * @returns one verified shell; a declared default that cannot resolve rejects. + */ +export async function resolveShell( + subprocess: SubprocessRuntime, configured: TerminalShell | undefined, signal: AbortSignal, +): Promise { + let shell = configured + if (shell === undefined) { + const environment = await subprocess.terminalEnvironment(signal) + shell = profile(environment.defaultShell ?? (environment.platform === 'windows' ? 'cmd.exe' : '/bin/sh')) + } + const path = await subprocess.resolveExecutable(shell.path, undefined, signal) + return { ...shell, path } +} + +function profile(path: string): TerminalShell { + const name = path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1) + const kind = name.toLowerCase().replace(/\.exe$/u, '') + return { path, name, args: kind === 'cmd' ? [] : kind === 'pwsh' || kind === 'powershell' ? ['-NoLogo'] : ['-i'] } +} + +/** + * List verified candidates after the configured or environment-default shell. + * @param subprocess - target execution provider. + * @param configured - optional default profile. + * @param candidates - executable names or paths permitted for shell selection. + * @param signal - discovery cancellation. + * @returns unique installed shells, with the default first; transport failures reject. + */ +export async function discoverShells( + subprocess: SubprocessRuntime, configured: TerminalShell | undefined, + candidates: readonly string[], signal: AbortSignal, +): Promise { + const preferred = await resolveShell(subprocess, configured, signal) + const found = await Promise.all(candidates.map(async (candidate) => { + try { return await resolveShell(subprocess, profile(candidate), signal) } + catch (error) { + if (error instanceof SubprocessExecutableNotFoundError) return undefined + throw error + } + })) + const shells = new Map() + for (const shell of [preferred, ...found]) { + if (shell === undefined) continue + const key = shell.path.includes('\\') ? shell.path.toLowerCase() : shell.path + if (!shells.has(key)) shells.set(key, shell) + } + return [...shells.values()] +} diff --git a/packages/api/terminal-controller/src/stream.ts b/packages/api/terminal-controller/src/stream.ts new file mode 100644 index 0000000000..4ee89f254f --- /dev/null +++ b/packages/api/terminal-controller/src/stream.ts @@ -0,0 +1,75 @@ +/** A bounded output queue for one Remote stream generation. */ +import { Deque } from '@deepseek-ai/dsh-deque' +import type { TerminalFrame } from './types.ts' + +/** Slow followers fail explicitly; a later attachment recovers from the screen. */ +export class TerminalFollower { + private readonly queue = new Deque<{ frame: TerminalFrame; bytes: number }>() + private bytes = 0 + private wake: (() => void) | undefined + private closed = false + private finished = false + private failure: Error | undefined + + /** @param maxBytes - maximum queued UTF-8 bytes for this follower. */ + constructor(private readonly maxBytes: number) {} + + /** + * Queue a frame or fail this follower when its byte limit is exceeded. + * @param frame - next ordered frame. + */ + push(frame: TerminalFrame): void { + if (this.closed || this.finished) return + const bytes = Buffer.byteLength(JSON.stringify(frame), 'utf8') + if (this.bytes + bytes > this.maxBytes) { + this.failure = new Error('Terminal output consumer exceeded its buffer; reconnect to recover the current screen') + this.close() + return + } + this.queue.pushBack({ frame, bytes }) + this.bytes += bytes + this.wake?.() + } + + /** Finish after delivering every queued frame, including the final exit state. */ + finish(): void { + this.finished = true + this.wake?.() + } + + /** Stop this follower without stopping its terminal. */ + close(): void { + this.closed = true + this.queue.clear() + this.bytes = 0 + this.wake?.() + } + + /** + * Drain until detached or failed. + * @param signal - Remote generation cancellation. + * @returns ordered terminal frames. + */ + async *read(signal: AbortSignal): AsyncIterable { + const abort = (): void => { this.close() } + signal.addEventListener('abort', abort, { once: true }) + if (signal.aborted) abort() + try { + while (!this.closed) { + const next = this.queue.popFront() + if (next !== undefined) { + this.bytes -= next.bytes + yield next.frame + } else { + if (this.finished) break + await new Promise((resolve) => { this.wake = resolve }) + this.wake = undefined + } + } + if (this.failure !== undefined) throw this.failure + } finally { + signal.removeEventListener('abort', abort) + this.close() + } + } +} diff --git a/packages/api/terminal-controller/src/terminal.ts b/packages/api/terminal-controller/src/terminal.ts new file mode 100644 index 0000000000..9e8dfcafff --- /dev/null +++ b/packages/api/terminal-controller/src/terminal.ts @@ -0,0 +1,175 @@ +/** One PTY, a bounded terminal emulator and its detachable browser followers. */ +import { createRequire } from 'node:module' +import { RemoteError } from '@deepseek-ai/dsh-typert-protocol' +import type { Terminal as HeadlessTerminal } from '@xterm/headless' +import type { SerializeAddon as Serializer } from '@xterm/addon-serialize' +import type { SubprocessTerminalHandle } from '@deepseek-ai/dsh-subprocess' +import { TerminalFollower } from './stream.ts' +import type { TerminalAttachmentId, TerminalFrame, WebTerminalInfo } from './types.ts' + +const { Terminal, SerializeAddon } = loadXterm() + +function loadXterm() { + // The Preview's CommonJS wrapper owns its outer require binding; these literal calls also retain the CJS entries. + const require = createRequire(import.meta.url) + const { Terminal } = require('@xterm/headless') as typeof import('@xterm/headless') + const { SerializeAddon } = require('@xterm/addon-serialize') as typeof import('@xterm/addon-serialize') + return { Terminal, SerializeAddon } +} + +/** Process lifetime is independent of follower and component lifetimes. */ +export class BrowserTerminal { + private readonly screen: HeadlessTerminal + private readonly serializer: Serializer + private readonly followers = new Set() + private sequence = 0 + private operations: Promise = Promise.resolve() + private readonly drained: Promise + private closing: Promise | undefined + private controller: { id: TerminalAttachmentId; follower: TerminalFollower } | undefined + + /** + * @param handle - allocated terminal process range. + * @param info - initial metadata. + * @param scrollback - maximum retained scrollback rows. + * @param maxBufferedBytes - per-follower queue cap. + */ + constructor( + private readonly handle: SubprocessTerminalHandle, + public info: WebTerminalInfo, + scrollback: number, + private readonly maxBufferedBytes: number, + ) { + this.screen = new Terminal({ cols: info.cols, rows: info.rows, scrollback, allowProposedApi: true }) + this.serializer = new SerializeAddon() + this.screen.loadAddon(this.serializer) + this.drained = this.consume() + } + + /** + * Attach with exclusive input control; an older attachment becomes read-only. + * @param id - browser attachment identity. + * @param signal - attachment cancellation; never terminates the process. + * @returns a consistent screen followed by ordered output and state changes. + */ + async *follow(id: TerminalAttachmentId, signal: AbortSignal): AsyncIterable { + signal.throwIfAborted() + const follower = new TerminalFollower(this.maxBufferedBytes) + const baseline = await this.enqueue(() => { + signal.throwIfAborted() + this.controller = { id, follower } + this.info = { ...this.info, controllerId: id } + this.broadcast({ type: 'state', info: this.info }) + const snapshot: TerminalFrame = { type: 'snapshot', sequence: this.sequence, screen: this.serializer.serialize(), info: this.info } + this.followers.add(follower) + return snapshot + }) + try { + yield baseline + yield* follower.read(signal) + } finally { + this.followers.delete(follower) + follower.close() + if (this.controller?.follower === follower) { + this.controller = undefined + const { controllerId: _controllerId, ...info } = this.info + this.info = info + this.broadcast({ type: 'state', info }) + } + } + } + + /** + * Send raw terminal input without command interpretation. + * @param id - current writable attachment. + * @param data - UTF-8 input, including shell completion/control keys. + * @returns when the provider accepts the input. + */ + write(id: TerminalAttachmentId, data: string): Promise { + return this.enqueue(async () => { this.requireController(id); await this.handle.write(data) }) + } + + /** + * Resize the PTY and recovery screen in the same operation order as output. + * @param id - current writable attachment. + * @param cols - validated column count. + * @param rows - validated row count. + * @returns when the provider and emulator use the new dimensions. + */ + resize(id: TerminalAttachmentId, cols: number, rows: number): Promise { + return this.enqueue(async () => { + this.requireController(id) + await this.handle.resize(cols, rows) + this.screen.resize(cols, rows) + this.info = { ...this.info, cols, rows } + this.broadcast({ type: 'state', info: this.info }) + }) + } + + /** + * Publish a display name to every attached view. + * @param title - validated user title. + */ + rename(title: string): void { + this.info = { ...this.info, title } + this.broadcast({ type: 'state', info: this.info }) + } + + /** + * Terminate the complete provider-owned process range before releasing its screen. + * @returns after process cleanup and final output drainage; failures remain retryable. + */ + close(): Promise { + if (this.closing !== undefined) return this.closing + this.closing = (async () => { + await this.handle.terminate() + await this.drained + for (const follower of this.followers) follower.finish() + this.followers.clear() + this.screen.dispose() + })().catch((error: unknown) => { this.closing = undefined; throw error }) + return this.closing + } + + private requireController(id: TerminalAttachmentId): void { + if (this.closing !== undefined || this.info.state !== 'running') throw new RemoteError('terminal/control-unavailable', 'Terminal is not running', { reason: 'not-running' }) + if (this.controller?.id !== id) throw new RemoteError('terminal/control-unavailable', 'Terminal input is controlled by another attachment', { reason: 'read-only' }) + } + + private broadcast(frame: TerminalFrame): void { + for (const follower of this.followers) follower.push(frame) + } + + private enqueue(operation: () => T | Promise): Promise { + const pending = this.operations.then(operation) + this.operations = pending.catch(() => { /* The caller owns this operation's failure; later cleanup must still run. */ }) + return pending + } + + private async consume(): Promise { + const decoder = new TextDecoder('utf-8', { ignoreBOM: true }) + const outcome = this.handle.done.then(value => ({ value }), (error: unknown) => ({ error })) + try { + for await (const chunk of this.handle.output) { + // Node Readable's iterator is untyped; this provider explicitly emits Buffer chunks. + const data = decoder.decode(chunk as Buffer, { stream: true }) + await this.output(data) + } + await this.output(decoder.decode()) + const result = await outcome + if ('error' in result) throw result.error + this.info = { ...this.info, state: 'exited', exitCode: result.value.exitCode } + } catch (error) { + this.info = { ...this.info, state: 'failed', error: error instanceof Error ? error.message : String(error) } + } + this.broadcast({ type: 'state', info: this.info }) + } + + private async output(data: string): Promise { + if (data.length === 0) return + await this.enqueue(async () => { + await new Promise((resolve) => { this.screen.write(data, resolve) }) + this.broadcast({ type: 'output', sequence: ++this.sequence, data }) + }) + } +} diff --git a/packages/api/terminal-controller/src/types.ts b/packages/api/terminal-controller/src/types.ts new file mode 100644 index 0000000000..fac1f4363c --- /dev/null +++ b/packages/api/terminal-controller/src/types.ts @@ -0,0 +1,63 @@ +/** Browser terminal identities, metadata and screen-stream frames. */ +import type { Branded } from '@deepseek-ai/dsh-brand' +import type {} from '@deepseek-ai/dsh-typert-protocol' + +declare module '@deepseek-ai/dsh-typert-protocol' { + interface RemoteErrorDetailsMap { + /** Input or resize was refused without invalidating the output attachment. */ + 'terminal/control-unavailable': { readonly reason: 'read-only' | 'not-running' } + /** Retained screens and pending allocations consume the Session's terminal quota. */ + 'terminal/limit-reached': { readonly limit: number } + } +} + +/** A terminal identity scoped to one Session and one Host lifetime. */ +export type WebTerminalId = Branded<'WebTerminalId'> +/** An attachment allowed to write and resize one terminal. */ +export type TerminalAttachmentId = Branded<'TerminalAttachmentId'> + +/** An executable shell verified in the subprocess provider's execution environment. */ +export interface TerminalShell { + readonly path: string + readonly args: readonly string[] + readonly name: string +} + +/** Working directory and limits shared by new and restored terminals. */ +export interface TerminalEnvironment { + readonly cwd: string + readonly maxInputBytes: number + readonly maxCols: number + readonly maxRows: number + readonly scrollback: number +} + +/** Host-owned terminal state; process exit never creates a replacement shell. */ +export interface WebTerminalInfo { + readonly id: WebTerminalId + readonly title: string + readonly shell: TerminalShell + /** Initial working directory; shell directory changes do not update this field. */ + readonly cwd: string + readonly cols: number + readonly rows: number + readonly state: 'running' | 'exited' | 'failed' + readonly exitCode: number | null + readonly error?: string + readonly controllerId?: TerminalAttachmentId +} + +/** Create is idempotent for an open identity; closed identities cannot be recreated. */ +export interface TerminalCreateRequest { + /** A path returned by shell discovery; absent selects the execution default. */ + readonly shellPath?: string + readonly id: WebTerminalId + readonly cols: number + readonly rows: number +} + +/** Every attachment begins with a complete bounded screen, then ordered output. */ +export type TerminalFrame = + | { readonly type: 'snapshot'; readonly sequence: number; readonly screen: string; readonly info: WebTerminalInfo } + | { readonly type: 'output'; readonly sequence: number; readonly data: string } + | { readonly type: 'state'; readonly info: WebTerminalInfo } diff --git a/packages/api/terminal-controller/tests/controller.spec.ts b/packages/api/terminal-controller/tests/controller.spec.ts new file mode 100644 index 0000000000..a42ac6c44e --- /dev/null +++ b/packages/api/terminal-controller/tests/controller.spec.ts @@ -0,0 +1,543 @@ +/** Session identity, allocation races, confinement and real PTY behavior. */ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { PassThrough } from 'node:stream' +import { Context } from '@deepseek-ai/cordis' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session' +import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox' +import { SubprocessExecutableNotFoundError, type SubprocessRuntime, type SubprocessTerminalEnvironment, type SubprocessTerminalHandle } from '@deepseek-ai/dsh-subprocess' +import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { TerminalController, type Config } from '../src/index.ts' +import { resolveShell } from '../src/shells.ts' +import type { TerminalAttachmentId, WebTerminalId } from '../src/types.ts' + +const roots: Context[] = [] +afterEach(async () => { await Promise.all(roots.splice(0).map(ctx => ctx.fiber.dispose())) }) +const config: Config = { shellCandidates: ['zsh', 'bash', 'sh'], shell: { path: '/bin/bash', name: 'bash', args: ['--noprofile', '--norc', '-i'] }, maxTerminals: 2, maxCols: 200, maxRows: 100, scrollback: 100, maxBufferedBytes: 100_000, maxInputBytes: 1000, disposeGraceMs: 100 } +const id = 'test-terminal' as WebTerminalId +const request = { id, cols: 80, rows: 24 } +const signal = (): AbortSignal => new AbortController().signal + +function owner(ctx: Context, id = 'session'): Agent { + return { id: id as SessionId, ctx, session: { id: id as SessionId } } as unknown as Agent +} + +function fixture(overrides: Partial = {}) { + const ctx = new Context() + roots.push(ctx) + const effects = vi.spyOn(ctx.fiber, 'effect') + const sandboxPolicy = { defaultMode: 'danger-full-access', resolve: vi.fn((): SandboxExecutionPolicy => ({ mode: 'danger-full-access', workspaceRoot: '/workspace' })) } + const projections = { stateOf: vi.fn((): SandboxMode | null => null) } + ctx.provide('sandboxPolicy', sandboxPolicy as never) + ctx.provide('sessionProjections', projections as never) + const output = new PassThrough() + const done = Promise.withResolvers<{ exitCode: number; signal: null }>() + const handle = { + pid: 123, output, done: done.promise, write: vi.fn(async () => {}), resize: vi.fn(async () => {}), + inspectForeground: async () => undefined, signalForeground: async () => 123, + terminate: vi.fn(async () => { output.end(); done.resolve({ exitCode: 0, signal: null }) }) } + const checked: SubprocessTerminalHandle = handle + const subprocess = { terminalEnvironment: vi.fn(async (): Promise => ({ platform: 'posix', defaultShell: '/bin/bash' })), resolveExecutable: vi.fn(async (path: string) => path), spawnTerminal: vi.fn(async () => checked) } + ctx.provide('subprocess', subprocess as never) + const controller = new TerminalController(ctx, { ...config, ...overrides }) + const disposeEffect = (label: string): Promise => { + const index = effects.mock.calls.findIndex(call => call[1] === label) + const result = effects.mock.results[index] + if (result?.type !== 'return' || typeof result.value !== 'function') throw new Error(`Missing effect: ${label}`) + return result.value() + } + return { ctx, agent: owner(ctx), controller, subprocess, handle, sandboxPolicy, projections, disposeEffect } +} + +describe('TerminalController', () => { + it('resolves execution services from an Agent plugin context without consumer injections', async () => { + const { ctx, controller } = fixture() + let agent: Agent | undefined + await ctx.plugin((child) => { agent = owner(child) }) + if (agent === undefined) throw new Error('Agent plugin did not load') + expect(controller.environment(agent, signal())).toMatchObject({ cwd: '/workspace' }) + await controller.create(agent, request, signal()) + await controller.close(agent, id) + }) + + it('creates the environment default shell and keeps an existing identity when that default changes', async () => { + const { controller, agent, subprocess } = fixture({ shell: undefined }) + subprocess.terminalEnvironment.mockResolvedValue({ platform: 'posix', defaultShell: '/usr/local/bin/zsh' }) + expect(controller.environment(agent, signal())).toEqual({ cwd: '/workspace', maxCols: 200, maxRows: 100, maxInputBytes: 1000, scrollback: 100 }) + expect(subprocess.terminalEnvironment).not.toHaveBeenCalled() + const created = await controller.create(agent, request, signal()) + expect(created.shell.path).toBe('/usr/local/bin/zsh') + expect(subprocess.spawnTerminal).toHaveBeenCalledWith(expect.objectContaining({ argv: ['/usr/local/bin/zsh', '-i'] })) + subprocess.terminalEnvironment.mockResolvedValue({ platform: 'posix', defaultShell: '/bin/sh' }) + expect(await controller.create(agent, request, signal())).toBe(created) + expect(subprocess.terminalEnvironment).toHaveBeenCalledOnce() + expect(subprocess.spawnTerminal).toHaveBeenCalledOnce() + }) + + it('restores a running terminal after its default executable becomes unavailable', async () => { + const { controller, agent, subprocess } = fixture({ shell: undefined }) + await controller.create(agent, request, signal()) + subprocess.resolveExecutable.mockRejectedValue(new SubprocessExecutableNotFoundError('default shell was removed')) + expect(controller.environment(agent, signal())).toMatchObject({ cwd: '/workspace', maxInputBytes: 1000 }) + expect(controller.list(agent.id)).toMatchObject([{ id, state: 'running' }]) + const stream = controller.follow(agent, id, 'restored' as TerminalAttachmentId, signal())[Symbol.asyncIterator]() + try { + expect(await stream.next()).toMatchObject({ done: false, value: { type: 'snapshot', info: { id, state: 'running' } } }) + expect(subprocess.resolveExecutable).toHaveBeenCalledOnce() + await expect(controller.create(agent, { ...request, id: 'new-terminal' as WebTerminalId }, signal())).rejects.toThrow('default shell was removed') + expect(subprocess.spawnTerminal).toHaveBeenCalledOnce() + } finally { await stream.return?.() } + }) + + it('honors cancellation before reading environment limits', () => { + const { controller, agent, subprocess, sandboxPolicy } = fixture() + const abort = new AbortController() + abort.abort(new Error('request cancelled')) + expect(() => controller.environment(agent, abort.signal)).toThrow('request cancelled') + expect(subprocess.terminalEnvironment).not.toHaveBeenCalled() + expect(sandboxPolicy.resolve).not.toHaveBeenCalled() + }) + + it('deduplicates concurrent creates, scopes access by Session, and preserves terminals when a stream aborts', async () => { + const { controller, agent, ctx, subprocess, handle } = fixture() + const [first, second] = await Promise.all([controller.create(agent, request, signal()), controller.create(agent, request, signal())]) + expect(first.id).toBe(second.id) + expect(subprocess.spawnTerminal).toHaveBeenCalledOnce() + expect(subprocess.spawnTerminal).toHaveBeenCalledWith(expect.objectContaining({ terminalType: 'xterm-256color', cwd: '/workspace' })) + expect(controller.list('other' as SessionId)).toEqual([]) + expect(() => controller.follow(owner(ctx, 'other'), id, 'writer' as TerminalAttachmentId, signal())).toThrow('no longer exists') + const abort = new AbortController() + const stream = controller.follow(agent, id, 'writer' as TerminalAttachmentId, abort.signal)[Symbol.asyncIterator]() + const firstFrame = await stream.next() + if (firstFrame.done === true) throw new Error('Terminal stream ended before its snapshot') + expect(firstFrame.value.type).toBe('snapshot') + abort.abort() + await stream.return?.() + expect(handle.terminate).not.toHaveBeenCalled() + await controller.close(agent, id) + expect(handle.terminate).toHaveBeenCalledOnce() + expect(controller.list(agent.id)).toEqual([]) + await controller.close(agent, id) + }) + + it('waits for pending allocation when explicitly closing and rolls back cancellation before publication', async () => { + const { controller, agent, subprocess, handle } = fixture() + const allocation = Promise.withResolvers() + subprocess.spawnTerminal.mockImplementationOnce(() => allocation.promise) + const creating = controller.create(agent, request, signal()) + const rejected = expect(creating).rejects.toThrow('closed in this Session') + const closing = controller.close(agent, id) + allocation.resolve(handle) + await rejected + await closing + expect(controller.list(agent.id)).toEqual([]) + expect(handle.terminate).toHaveBeenCalledOnce() + }) + + it('rejects a delayed create after close even when that identity has never been allocated', async () => { + const { controller, agent, subprocess } = fixture() + await controller.close(agent, id) + await controller.close(agent, id) + await expect(controller.create(agent, request, signal())).rejects.toThrow('closed in this Session') + expect(subprocess.spawnTerminal).not.toHaveBeenCalled() + const nextId = 'next-terminal' as WebTerminalId + await expect(controller.create(agent, { ...request, id: nextId }, signal())).resolves.toMatchObject({ id: nextId }) + expect(subprocess.spawnTerminal).toHaveBeenCalledOnce() + }) + + it('rejects original, duplicate and later creates when close arrives during allocation', async () => { + const { controller, agent, subprocess, handle } = fixture() + const allocation = Promise.withResolvers() + const spawning = Promise.withResolvers() + subprocess.spawnTerminal.mockImplementationOnce(() => { spawning.resolve(undefined); return allocation.promise }) + const creating = controller.create(agent, request, signal()) + const duplicate = controller.create(agent, request, signal()) + const rejected = expect(creating).rejects.toThrow('closed in this Session') + const duplicateRejected = expect(duplicate).rejects.toThrow('closed in this Session') + try { + await spawning.promise + const closing = controller.close(agent, id) + await expect(controller.create(agent, request, signal())).rejects.toThrow('closed in this Session') + allocation.resolve(handle) + await Promise.all([rejected, duplicateRejected, closing]) + await expect(controller.create(agent, request, signal())).rejects.toThrow('closed in this Session') + expect(handle.terminate).toHaveBeenCalledOnce() + expect(subprocess.spawnTerminal).toHaveBeenCalledOnce() + expect(controller.list(agent.id)).toEqual([]) + } finally { allocation.resolve(handle) } + }) + + it('retains failed allocation cleanup for a later explicit close', async () => { + const { controller, agent, subprocess, handle } = fixture() + const abort = new AbortController() + subprocess.spawnTerminal.mockImplementationOnce(async () => { abort.abort(new Error('request disconnected')); return handle }) + vi.mocked(handle.terminate).mockRejectedValueOnce(new Error('cleanup failed')) + await expect(controller.create(agent, request, abort.signal)).rejects.toThrow('cleanup failed') + expect(controller.list(agent.id)).toMatchObject([{ id, state: 'failed' }]) + await controller.close(agent, id) + expect(handle.terminate).toHaveBeenCalledTimes(2) + expect(controller.list(agent.id)).toEqual([]) + }) + + it('retains a terminal when process cleanup fails so closing can be retried', async () => { + const { controller, agent, handle } = fixture() + await controller.create(agent, request, signal()) + vi.mocked(handle.terminate).mockRejectedValueOnce(new Error('process still alive')) + await expect(controller.close(agent, id)).rejects.toThrow('still alive') + expect(controller.list(agent.id)).toHaveLength(1) + await expect(controller.create(agent, request, signal())).rejects.toThrow('closed in this Session') + await controller.close(agent, id) + expect(controller.list(agent.id)).toHaveLength(0) + }) + + it('rejects invalid dimensions, unavailable shells and oversized input before executing them', async () => { + const { controller, agent, subprocess } = fixture() + await expect(controller.create(agent, { ...request, cols: 1 }, signal())).rejects.toThrow('dimensions') + subprocess.resolveExecutable.mockRejectedValueOnce(new SubprocessExecutableNotFoundError('shell unavailable')) + await expect(controller.create(agent, request, signal())).rejects.toThrow('unavailable') + expect(subprocess.spawnTerminal).not.toHaveBeenCalled() + await expect(controller.write(agent, id, 'writer' as TerminalAttachmentId, 'x'.repeat(1001))).rejects.toThrow('input') + }) + + it('keeps the committed identity on repeated creation and counts pending reservations toward the Session limit', async () => { + const { controller, agent, subprocess, handle } = fixture({ maxTerminals: 1 }) + const allocation = Promise.withResolvers() + const spawning = Promise.withResolvers() + subprocess.spawnTerminal.mockImplementationOnce(() => { spawning.resolve(undefined); return allocation.promise }) + const creating = controller.create(agent, request, signal()) + try { + await spawning.promise + await expect(controller.create(agent, { ...request, id: 'another' as WebTerminalId }, signal())).rejects.toMatchObject({ code: 'terminal/limit-reached', details: { limit: 1 } }) + } finally { allocation.resolve(handle) } + const first = await creating + expect(await controller.create(agent, request, signal())).toBe(first) + expect(subprocess.spawnTerminal).toHaveBeenCalledOnce() + }) + + it('keeps failed allocations reserved until their cleanup succeeds', async () => { + const { controller, agent, subprocess, handle } = fixture({ maxTerminals: 1 }) + const abort = new AbortController() + subprocess.spawnTerminal.mockImplementationOnce(async () => { abort.abort('lost request'); return handle }) + handle.terminate.mockRejectedValueOnce(new Error('still alive')) + await expect(controller.create(agent, request, abort.signal)).rejects.toThrow('cleanup failed') + expect(controller.list(agent.id)).toMatchObject([{ id, state: 'failed', error: 'lost request' }]) + await expect(controller.create(agent, request, signal())).rejects.toThrow('Close the failed terminal allocation') + await expect(controller.create(agent, { ...request, id: 'another' as WebTerminalId }, signal())).rejects.toMatchObject({ code: 'terminal/limit-reached', details: { limit: 1 } }) + await controller.close(agent, id) + expect(controller.list(agent.id)).toEqual([]) + }) + + it('lets close reclaim an allocation whose request failed while close was waiting', async () => { + const { controller, agent, subprocess, handle } = fixture() + const allocation = Promise.withResolvers() + const abort = new AbortController() + subprocess.spawnTerminal.mockImplementationOnce(() => allocation.promise) + const creating = controller.create(agent, request, abort.signal) + const rejected = expect(creating).rejects.toThrow('cancelled') + const closing = controller.close(agent, id) + abort.abort(new Error('cancelled')) + allocation.resolve(handle) + await rejected + await closing + expect(handle.terminate).toHaveBeenCalledOnce() + expect(controller.list(agent.id)).toEqual([]) + }) + + it('routes input, resize and trimmed names through the active attachment', async () => { + const { controller, agent, handle } = fixture({ maxInputBytes: 6 }) + await controller.create(agent, request, signal()) + const attachmentId = 'writer' as TerminalAttachmentId + const stream = controller.follow(agent, id, attachmentId, signal())[Symbol.asyncIterator]() + await stream.next() + try { + await controller.write(agent, id, attachmentId, '终端') + expect(handle.write).toHaveBeenCalledWith('终端') + await expect(controller.write(agent, id, attachmentId, '终端!')).rejects.toThrow('input exceeds') + await controller.resize(agent, id, attachmentId, 200, 100) + expect(handle.resize).toHaveBeenCalledWith(200, 100) + controller.rename(agent, id, ' server logs ') + expect(controller.list(agent.id)).toMatchObject([{ title: 'server logs', cols: 200, rows: 100 }]) + } finally { await stream.return?.() } + }) + + it.each([{ cols: 1.5, rows: 24 }, { cols: 201, rows: 24 }, { cols: 80, rows: 1.5 }, { cols: 80, rows: 0 }, { cols: 80, rows: 101 }])('rejects dimensions $cols × $rows before allocation or resize', async (dimensions) => { + const { controller, agent, subprocess, handle } = fixture() + await expect(controller.create(agent, { ...request, ...dimensions }, signal())).rejects.toThrow('dimensions') + await expect(controller.resize(agent, id, 'writer' as TerminalAttachmentId, dimensions.cols, dimensions.rows)).rejects.toThrow('dimensions') + expect(subprocess.spawnTerminal).not.toHaveBeenCalled() + expect(handle.resize).not.toHaveBeenCalled() + }) + + it('rejects invalid wire identities and names without changing retained terminals', async () => { + const { controller, agent } = fixture() + await expect(controller.create(agent, { ...request, id: '../terminal' as WebTerminalId }, signal())).rejects.toThrow('Invalid terminal identity') + expect(() => controller.follow(agent, id, '' as TerminalAttachmentId, signal())).toThrow('attachment identity') + expect(() => { controller.rename(agent, id, ' ') }).toThrow('1–120') + expect(() => { controller.rename(agent, id, 'x'.repeat(121)) }).toThrow('1–120') + await controller.close(agent, id) + expect(controller.list(agent.id)).toEqual([]) + }) + + it('uses the Session sandbox policy to confine its selected shell', async () => { + const { controller, agent, ctx, sandboxPolicy, subprocess } = fixture() + const policy: SandboxExecutionPolicy = { mode: 'workspace-write', workspaceRoot: '/workspace', sessionId: agent.id } + sandboxPolicy.resolve.mockReturnValue(policy) + const confine = vi.fn((argv: readonly string[]) => ({ argv: ['sandbox-runner', ...argv] })) + ctx.provide('sandbox', { confine } as never) + await controller.create(agent, request, signal()) + expect(confine).toHaveBeenCalledWith(['/bin/bash', '--noprofile', '--norc', '-i'], policy, expect.any(AbortSignal)) + expect(subprocess.spawnTerminal).toHaveBeenCalledWith(expect.objectContaining({ argv: ['sandbox-runner', '/bin/bash', '--noprofile', '--norc', '-i'], env: { DSH_SESSION_ID: agent.id }, graceMs: 100 })) + }) + + it('rejects a confined Session without a sandbox provider before spawning', async () => { + const { controller, agent, sandboxPolicy, subprocess } = fixture() + sandboxPolicy.resolve.mockReturnValue({ mode: 'read-only', workspaceRoot: '/workspace' }) + await expect(controller.create(agent, request, signal())).rejects.toThrow('requires an execution sandbox provider') + expect(subprocess.spawnTerminal).not.toHaveBeenCalled() + }) + + it.each(['subprocess', 'sandboxPolicy'] as const)('fails clearly when the Session lacks %s', (missing) => { + const { controller, subprocess, sandboxPolicy } = fixture() + const isolated = new Context() + roots.push(isolated) + if (missing !== 'subprocess') isolated.provide('subprocess', subprocess as never) + if (missing !== 'sandboxPolicy') isolated.provide('sandboxPolicy', sandboxPolicy as never) + expect(() => controller.environment(owner(isolated), signal())).toThrow('requires subprocess and sandbox policy providers') + }) + + it('blocks sandbox-mode changes only while that Session retains a terminal', async () => { + const { controller, agent, ctx, projections } = fixture() + const mode = (mode: SandboxMode): void => { ctx.emit('session/event', agent.session, { type: 'sandbox/mode', data: { mode } } as SessionEvent) } + ctx.emit('session/disposed', agent.session) + ctx.emit('session/event', agent.session, { type: 'turn/start', data: { turn: 1 } } as SessionEvent) + expect(() => { mode('workspace-write') }).not.toThrow() + await controller.create(agent, request, signal()) + expect(() => { mode('danger-full-access') }).not.toThrow() + expect(() => { mode('workspace-write') }).toThrow('Close browser terminals') + projections.stateOf.mockReturnValue('read-only') + expect(() => { mode('read-only') }).not.toThrow() + expect(() => { mode('danger-full-access') }).toThrow('Close browser terminals') + await controller.close(agent, id) + expect(() => { mode('workspace-write') }).not.toThrow() + }) + + it('terminates committed processes when the Session effect ends', async () => { + const { controller, agent, handle, disposeEffect } = fixture() + await controller.create(agent, request, signal()) + await disposeEffect('terminal-controller.owner') + expect(handle.terminate).toHaveBeenCalledOnce() + expect(controller.list(agent.id)).toEqual([]) + }) + + it('cleans up a Session when its real Agent plugin fiber is disposed', async () => { + const { controller, ctx, handle } = fixture() + let agent: Agent | undefined + const fiber = await ctx.plugin((child) => { agent = owner(child) }) + if (agent === undefined) throw new Error('Agent plugin did not load') + await controller.create(agent, request, signal()) + await fiber.dispose() + expect(handle.terminate).toHaveBeenCalledOnce() + expect(controller.list(agent.id)).toEqual([]) + }) + + it('waits for pending creation during Host disposal and rolls back its process before resolving', async () => { + const { controller, agent, subprocess, handle, disposeEffect } = fixture() + const allocation = Promise.withResolvers() + const spawning = Promise.withResolvers() + subprocess.spawnTerminal.mockImplementationOnce(() => { spawning.resolve(undefined); return allocation.promise }) + const creating = controller.create(agent, request, signal()) + const rejected = expect(creating).rejects.toThrow('disposed') + await spawning.promise + const disposing = disposeEffect('terminal-controller.processes') + allocation.resolve(handle) + await rejected + await disposing + expect(handle.terminate).toHaveBeenCalledOnce() + expect(controller.list(agent.id)).toEqual([]) + await expect(controller.create(agent, request, signal())).rejects.toThrow('controller disposed') + }) + + it('shares cleanup between the Session and Host effects while awaiting process termination', async () => { + const { controller, agent, handle, disposeEffect } = fixture() + await controller.create(agent, request, signal()) + const stopping = Promise.withResolvers() + const stopped = Promise.withResolvers() + const terminate = handle.terminate.getMockImplementation() + if (terminate === undefined) throw new Error('Missing process cleanup fixture') + handle.terminate.mockImplementationOnce(async () => { stopping.resolve(undefined); await stopped.promise; await terminate() }) + const ownerDisposal = disposeEffect('terminal-controller.owner') + try { + await stopping.promise + const hostDisposal = disposeEffect('terminal-controller.processes') + stopped.resolve(undefined) + await Promise.all([ownerDisposal, hostDisposal]) + expect(handle.terminate).toHaveBeenCalledOnce() + expect(controller.list(agent.id)).toEqual([]) + } finally { stopped.resolve(undefined) } + }) + + it('retains cleanup failures from both Session and Host disposal for explicit retry', async () => { + const { controller, agent, handle, disposeEffect } = fixture() + await controller.create(agent, request, signal()) + handle.terminate.mockRejectedValueOnce(new Error('still alive')) + await expect(disposeEffect('terminal-controller.owner')).rejects.toThrow('Session terminal cleanup failed') + await expect(controller.create(agent, request, signal())).rejects.toThrow('Session owner disposed') + handle.terminate.mockRejectedValueOnce(new Error('still alive again')) + await expect(disposeEffect('terminal-controller.processes')).rejects.toThrow('Browser terminal cleanup failed') + expect(controller.list(agent.id)).toHaveLength(1) + await controller.close(agent, id) + expect(controller.list(agent.id)).toEqual([]) + expect(handle.terminate).toHaveBeenCalledTimes(3) + }) + + it('reclaims retained failed allocations when the owning Session ends', async () => { + const { controller, agent, subprocess, handle, disposeEffect } = fixture() + const abort = new AbortController() + subprocess.spawnTerminal.mockImplementationOnce(async () => { abort.abort(new Error('disconnected')); return handle }) + handle.terminate.mockRejectedValueOnce(new Error('first cleanup failed')) + await expect(controller.create(agent, request, abort.signal)).rejects.toThrow('cleanup failed') + await disposeEffect('terminal-controller.owner') + expect(handle.terminate).toHaveBeenCalledTimes(2) + expect(controller.list(agent.id)).toEqual([]) + }) +}) + +describe('shell resolution', () => { + it('accepts an omitted shell profile and defaults configured arguments to an empty list', () => { + // Loader input is unvalidated; the schema's declared type describes its normalized output. + const parse = (input: unknown): Config => TerminalController.Config(input as Config) + expect(parse({}).shell).toBeUndefined() + expect(parse({ shell: { path: 'custom', name: 'Project shell' } }).shell).toEqual({ path: 'custom', name: 'Project shell', args: [] }) + expect(() => parse({ shell: { name: 'Project shell' } })).toThrow() + }) + + it.each([ + { platform: 'posix' as const, path: '/bin/sh', name: 'sh', args: ['-i'] }, + { platform: 'windows' as const, path: 'cmd.exe', name: 'cmd.exe', args: [] }, + ])('uses the conservative $platform fallback only when the provider omits its default shell', async ({ platform, path, name, args }) => { + const { subprocess } = fixture() + subprocess.terminalEnvironment.mockResolvedValue({ platform }) + const runtime = subprocess as unknown as SubprocessRuntime + const requestSignal = signal() + expect(await resolveShell(runtime, undefined, requestSignal)).toEqual({ path, name, args }) + expect(subprocess.resolveExecutable).toHaveBeenCalledExactlyOnceWith(path, undefined, requestSignal) + }) + + it.each([ + { platform: 'posix' as const, defaultShell: '/usr/local/bin/fish', name: 'fish', args: ['-i'] }, + { platform: 'windows' as const, defaultShell: 'C:\\Windows\\CMD.EXE', name: 'CMD.EXE', args: [] }, + { platform: 'windows' as const, defaultShell: 'C:\\PowerShell\\powershell.exe', name: 'powershell.exe', args: ['-NoLogo'] }, + { platform: 'posix' as const, defaultShell: '/usr/local/bin/pwsh', name: 'pwsh', args: ['-NoLogo'] }, + ])('resolves only the declared default $defaultShell with its interactive arguments', async ({ platform, defaultShell, name, args }) => { + const { subprocess } = fixture() + subprocess.terminalEnvironment.mockResolvedValue({ platform, defaultShell }) + subprocess.resolveExecutable.mockResolvedValue('/resolved/shell') + const runtime = subprocess as unknown as SubprocessRuntime + const requestSignal = signal() + expect(await resolveShell(runtime, undefined, requestSignal)).toEqual({ path: '/resolved/shell', name, args }) + expect(subprocess.terminalEnvironment).toHaveBeenCalledExactlyOnceWith(requestSignal) + expect(subprocess.resolveExecutable).toHaveBeenCalledExactlyOnceWith(defaultShell, undefined, requestSignal) + }) + + it('uses an explicit profile without querying the environment and preserves its name and arguments', async () => { + const { subprocess } = fixture() + subprocess.resolveExecutable.mockResolvedValue('/resolved/custom') + const runtime = subprocess as unknown as SubprocessRuntime + const requestSignal = signal() + const configured = { path: 'custom', name: 'Project shell', args: ['--project'] } + expect(await resolveShell(runtime, configured, requestSignal)).toEqual({ ...configured, path: '/resolved/custom' }) + expect(subprocess.terminalEnvironment).not.toHaveBeenCalled() + expect(subprocess.resolveExecutable).toHaveBeenCalledExactlyOnceWith('custom', undefined, requestSignal) + }) + + it.each([new SubprocessExecutableNotFoundError('missing default'), new Error('transport unavailable')])('reports default resolution failure without attempting fallback: %s', async (failure) => { + const { subprocess } = fixture() + subprocess.resolveExecutable.mockRejectedValue(failure) + const runtime = subprocess as unknown as SubprocessRuntime + await expect(resolveShell(runtime, undefined, signal())).rejects.toBe(failure) + expect(subprocess.resolveExecutable).toHaveBeenCalledOnce() + }) + + it('reports an unavailable configured shell without consulting the provider default', async () => { + const { subprocess } = fixture() + const failure = new SubprocessExecutableNotFoundError('missing configured shell') + subprocess.resolveExecutable.mockRejectedValue(failure) + const runtime = subprocess as unknown as SubprocessRuntime + await expect(resolveShell(runtime, { path: '/missing', name: 'missing', args: [] }, signal())).rejects.toBe(failure) + expect(subprocess.resolveExecutable).toHaveBeenCalledOnce() + expect(subprocess.terminalEnvironment).not.toHaveBeenCalled() + }) + + it('propagates environment cancellation before verifying an executable', async () => { + const { subprocess } = fixture() + const cancelled = new Error('request disconnected') + subprocess.terminalEnvironment.mockRejectedValue(cancelled) + const runtime = subprocess as unknown as SubprocessRuntime + await expect(resolveShell(runtime, undefined, signal())).rejects.toBe(cancelled) + expect(subprocess.resolveExecutable).not.toHaveBeenCalled() + }) +}) + +it.skipIf(process.platform === 'win32')('runs a real interactive shell with completion, TERM and live window dimensions', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'dsh-web-terminal-')) + const ctx = new Context() + const runtime = await ctx.plugin(LocalSubprocessRuntime) + try { + const handle = await ctx.subprocess.spawnTerminal({ argv: ['/bin/bash', '--noprofile', '--norc', '-i'], cwd, rows: 24, cols: 80, terminalType: 'xterm-256color', graceMs: 100, env: { PS1: 'READY> ', PS2: '' } }) + let output = '' + handle.output.on('data', (data: Buffer) => { output += data.toString('utf8') }) + try { + await expect.poll(() => output).toContain('READY>') + await handle.resize(100, 30) + await handle.write("printf 'TERM:%s\\n' \"$TERM\"; stty size\r") + await expect.poll(() => output).toContain('TERM:xterm-256color') + await expect.poll(() => output).toContain('30 100') + // Bash's builtin completion expands this unambiguous command before Enter. + await handle.write('histor\t') + await expect.poll(() => output).toContain('history') + } finally { await handle.terminate() } + await expect(handle.done).resolves.toBeDefined() + } finally { + await runtime.dispose() + await ctx.fiber.dispose() + await rm(cwd, { recursive: true, force: true }) + } +}) + +it('discovers installed shells once per path, preserves default arguments, and refuses unlisted paths', async () => { + const h = fixture({ shellCandidates: ['bash', 'zsh', 'missing'] }) + h.subprocess.resolveExecutable.mockImplementation(async (path) => { + if (path === 'missing') throw new SubprocessExecutableNotFoundError('absent') + return path.startsWith('/') ? path : `/bin/${path}` + }) + const shells = await h.controller.shells(h.agent, signal()) + expect(shells).toEqual([config.shell, { path: '/bin/zsh', name: 'zsh', args: ['-i'] }]) + expect(h.subprocess.spawnTerminal).not.toHaveBeenCalled() + await expect(h.controller.create(h.agent, { ...request, shellPath: '/bin/unlisted' }, signal())).rejects.toThrow('Selected shell is not available') + expect(h.subprocess.spawnTerminal).not.toHaveBeenCalled() + const created = await h.controller.create(h.agent, { ...request, shellPath: '/bin/zsh' }, signal()) + expect(created.shell.path).toBe('/bin/zsh') + expect(h.subprocess.spawnTerminal).toHaveBeenCalledWith(expect.objectContaining({ argv: ['/bin/zsh', '-i'] })) + h.subprocess.resolveExecutable.mockRejectedValue(new Error('SSH disconnected')) + await expect(h.controller.shells(h.agent, signal())).rejects.toThrow('SSH disconnected') + expect(await h.controller.create(h.agent, { ...request, shellPath: '/bin/bash' }, signal())).toBe(created) +}) + +it('propagates optional-shell discovery transport errors and cancellation', async () => { + const h = fixture({ shellCandidates: ['fish'] }) + h.subprocess.resolveExecutable.mockImplementation(async (path) => { + if (path === 'fish') throw new Error('lookup transport failed') + return path + }) + await expect(h.controller.shells(h.agent, signal())).rejects.toThrow('lookup transport failed') + const cancelled = AbortSignal.abort(new Error('cancelled discovery')) + expect(() => h.controller.shells(h.agent, cancelled)).toThrow('cancelled discovery') +}) + +it('deduplicates Windows executable paths regardless of letter case', async () => { + const h = fixture({ shell: { path: 'C:\\Windows\\cmd.exe', name: 'Command Prompt', args: [] }, shellCandidates: ['c:\\windows\\cmd.exe'] }) + expect(await h.controller.shells(h.agent, signal())).toEqual([{ path: 'C:\\Windows\\cmd.exe', name: 'Command Prompt', args: [] }]) +}) diff --git a/packages/api/terminal-controller/tests/model.client.spec.ts b/packages/api/terminal-controller/tests/model.client.spec.ts new file mode 100644 index 0000000000..ac1a7243f9 --- /dev/null +++ b/packages/api/terminal-controller/tests/model.client.spec.ts @@ -0,0 +1,567 @@ +/** Browser view ownership across slow RPCs, remounts and transport generations. */ +import { setImmediate } from 'node:timers/promises' +import { afterEach, expect, it, vi } from 'vitest' +import { RemoteStream, RemoteStreamCarrierError, type ClientRemote, type RemoteStreamOptions } from '@deepseek-ai/dsh-api-gateway/client' +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import { RemoteError, type RemoteResult } from '@deepseek-ai/dsh-typert-protocol' +import type {} from '@deepseek-ai/dsh-api-terminal-controller/remote' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { TerminalView, type TerminalRemote } from '../src/client/model.ts' +import type { TerminalEnvironment, TerminalFrame, WebTerminalId, WebTerminalInfo } from '../src/types.ts' + +const sessionId = 'session' as SessionId +const info: WebTerminalInfo = { id: 'terminal' as WebTerminalId, shell: { name: 'bash', path: '/bin/bash', args: ['-i'] }, title: 'bash', cwd: '/workspace', rows: 24, cols: 80, state: 'running', exitCode: null } +const environment: TerminalEnvironment = { + cwd: info.cwd, maxInputBytes: 1000, maxCols: 200, maxRows: 100, scrollback: 100, +} +const success = (value: T): RemoteResult => ({ ok: true, value }) +const cleanups: (() => void | Promise)[] = [] +afterEach(async () => { for (const close of cleanups.splice(0).reverse()) await close() }) + +function untilAborted(signal?: AbortSignal): Promise { + return new Promise((resolve) => { + if (signal?.aborted) resolve() + else signal?.addEventListener('abort', () => { resolve() }, { once: true }) + }) +} + +function failure(message: string): RemoteResult { + return { ok: false, error: new RemoteError('gateway/bad-request', message, {}) } +} + +function fixture(prepareStream?: (stream: RemoteStream) => void) { + const remote: TerminalRemote = { + shells: vi.fn(async () => success([info.shell])), + environment: vi.fn(async () => success(environment)), list: vi.fn(async () => success([])), + create: vi.fn(async (_sessionId, request) => success({ ...info, id: request.id })), + close: vi.fn(async () => success(undefined)), rename: vi.fn(async () => success(undefined)), + write: vi.fn(async () => success(undefined)), resize: vi.fn(async () => success(undefined)), + follow: vi.fn(async function* (_sessionId, id, attachmentId, signal) { + yield { type: 'snapshot', sequence: 0, screen: 'ready', info: { ...info, id, controllerId: attachmentId } } + await untilAborted(signal) + }), + } + const options: RemoteStreamOptions[] = [] + const streams: { dispose(): Promise; restart(): void }[] = [] + const generation = createSnapshotStore[0]['generation']['getSnapshot']>>(undefined) + const gateway: Pick = { + $stream: (option) => { + options.push(option) + const stream = new RemoteStream({ generation }, option) + streams.push(stream) + prepareStream?.(stream) + return stream + }, + } + cleanups.push(async () => { await Promise.all(streams.map(stream => stream.dispose())) }) + const model = new TerminalView(sessionId, remote, gateway, info.id) + cleanups.push(() => model.dispose()) + return { model, remote, gateway, options, streams, generation } +} + +async function mount(model: TerminalView) { + const detach = model.mount() + await model.refresh() + await model.start() + return detach +} + +function acknowledge(model: TerminalView): void { + const render = model.state.getSnapshot().render + if (render !== undefined) model.acknowledge(render.revision) +} + +async function connected(model: TerminalView): Promise { + await mount(model) + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + acknowledge(model) +} + +it('does not let a late input failure downgrade a newer connection', async () => { + const { model, remote } = fixture() + await mount(model) + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + acknowledge(model) + const input = Promise.withResolvers>() + vi.mocked(remote.write).mockReturnValueOnce(input.promise) + model.write('x') + await expect.poll(() => vi.mocked(remote.write).mock.calls.length).toBe(1) + model.connect() + await expect.poll(() => vi.mocked(remote.follow).mock.calls.length).toBe(2) + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + model.write('current input') + input.reject(new Error('old attachment was replaced')) + await expect.poll(() => vi.mocked(remote.write).mock.calls.some(call => call[3] === 'current input')).toBe(true) + expect(model.state.getSnapshot()).toMatchObject({ phase: 'connected', writable: true, error: undefined }) +}) + +it('waits for render acknowledgement before publishing subsequent output', async () => { + const { model, remote } = fixture() + vi.mocked(remote.follow).mockImplementation(async function* (_sessionId, id, controllerId, signal) { + const frames: TerminalFrame[] = [ + { type: 'snapshot', sequence: 0, screen: '', info: { ...info, id, controllerId } }, + { type: 'output', sequence: 1, data: 'first' }, + { type: 'output', sequence: 2, data: 'second' }, + ] + for (const frame of frames) yield frame + await untilAborted(signal) + }) + await mount(model) + await expect.poll(() => model.state.getSnapshot().render?.frame.type).toBe('snapshot') + acknowledge(model) + await expect.poll(() => model.state.getSnapshot().render?.frame).toMatchObject({ type: 'output', sequence: 1 }) + acknowledge(model) + await expect.poll(() => model.state.getSnapshot().render?.frame).toMatchObject({ type: 'output', sequence: 2 }) +}) + +it('serializes input and resizes, clamps geometry, and suppresses unchanged sizes', async () => { + const { model, remote } = fixture() + await connected(model) + const input = Promise.withResolvers>() + vi.mocked(remote.write).mockReturnValueOnce(input.promise) + model.resize(80, 24) + model.write('first') + model.write('second') + model.resize(500, 300) + await expect.poll(() => vi.mocked(remote.write).mock.calls.length).toBe(1) + expect(remote.resize).not.toHaveBeenCalled() + input.resolve(success(undefined)) + await expect.poll(() => vi.mocked(remote.resize).mock.calls.length).toBe(1) + const attachmentId = vi.mocked(remote.follow).mock.calls[0]![2] + const id = model.state.getSnapshot().info!.id + expect(vi.mocked(remote.write).mock.calls.map(call => call[3])).toEqual(['first', 'second']) + expect(remote.resize).toHaveBeenCalledWith(sessionId, id, attachmentId, 200, 100) + model.resize(90, 24) + await expect.poll(() => vi.mocked(remote.resize).mock.calls.length).toBe(2) +}) + +it('bounds queued input by UTF-8 bytes and releases the byte budget after settlement', async () => { + const { model, remote } = fixture() + vi.mocked(remote.environment).mockResolvedValue(success({ ...environment, maxInputBytes: 6 })) + await connected(model) + const input = Promise.withResolvers>() + vi.mocked(remote.write).mockReturnValueOnce(input.promise) + model.write('界界') + await expect.poll(() => vi.mocked(remote.write).mock.calls.length).toBe(1) + model.write('a') + expect(model.state.getSnapshot()).toMatchObject({ phase: 'failed', issue: 'inputFull' }) + input.resolve(success(undefined)) + model.connect() + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + model.write('界界') + await expect.poll(() => vi.mocked(remote.write).mock.calls.length).toBe(2) + expect(vi.mocked(remote.write).mock.calls.map(call => call[3])).toEqual(['界界', '界界']) +}) + +it('drops queued commands from a detached attachment and ignores its late resize failure', async () => { + const { model, remote } = fixture() + await connected(model) + const input = Promise.withResolvers>() + vi.mocked(remote.resize).mockReturnValueOnce(input.promise) + model.resize(100, 30) + await expect.poll(() => vi.mocked(remote.resize).mock.calls.length).toBe(1) + model.write('stale') + model.resize(110, 35) + model.connect() + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + model.write('current') + input.resolve(failure('old attachment')) + await expect.poll(() => vi.mocked(remote.write).mock.calls.length).toBe(1) + expect(vi.mocked(remote.write).mock.calls[0]?.[3]).toBe('current') + expect(remote.resize).toHaveBeenCalledOnce() + expect(model.state.getSnapshot().phase).toBe('connected') +}) + +it('publishes current input and resize failures and can reconnect for another attempt', async () => { + const { model, remote } = fixture() + await connected(model) + vi.mocked(remote.write).mockResolvedValueOnce(failure('input refused')) + model.write('x') + await expect.poll(() => model.state.getSnapshot().error).toBe('input refused') + model.connect() + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + vi.mocked(remote.resize).mockResolvedValueOnce(failure('resize refused')) + model.resize(100, 30) + await expect.poll(() => model.state.getSnapshot().error).toBe('resize refused') +}) + +it('treats carrier loss as disconnected only for the active attachment', async () => { + const { model, options } = fixture() + await connected(model) + const first = options[0]! + first.carrierFailed?.(new RemoteStreamCarrierError('offline')) + expect(model.state.getSnapshot()).toMatchObject({ phase: 'disconnected', writable: false }) + model.connect() + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + first.carrierFailed?.(new RemoteStreamCarrierError('late old failure')) + expect(model.state.getSnapshot().phase).toBe('connected') +}) + +it.each([ + ['output before snapshot', [{ type: 'output', sequence: 1, data: 'orphan' }], 'missing its screen snapshot'], + ['sequence gap', [{ type: 'snapshot', sequence: 4, screen: '', info }, { type: 'output', sequence: 6, data: 'gap' }], 'sequence has a gap'], + ['repeated snapshot', [{ type: 'snapshot', sequence: 4, screen: '', info }, { type: 'snapshot', sequence: 4, screen: '', info }], 'Unexpected terminal screen snapshot'], +] satisfies readonly (readonly [string, readonly TerminalFrame[], string])[])('refuses a malformed output stream: %s', async (_name, frames, message) => { + const { model, remote } = fixture() + vi.mocked(remote.follow).mockImplementation(async function* () { yield* frames }) + await mount(model) + if (frames[0]?.type === 'snapshot') { + await expect.poll(() => model.state.getSnapshot().render).toBeDefined() + acknowledge(model) + } + await expect.poll(() => model.state.getSnapshot().error).toContain(message) + expect(model.state.getSnapshot().writable).toBe(false) +}) + +it('retains an exited screen and closes controls when its stream ends', async () => { + const { model, remote } = fixture() + vi.mocked(remote.follow).mockImplementation(async function* (_sessionId, id, controllerId) { + yield { type: 'snapshot', sequence: 0, screen: 'last screen', info: { ...info, id, controllerId } } + yield { type: 'state', info: { ...info, id, controllerId, state: 'exited', exitCode: 3 } } + }) + await connected(model) + await expect.poll(() => model.state.getSnapshot().phase).toBe('closed') + expect(model.state.getSnapshot()).toMatchObject({ writable: false, info: { state: 'exited', exitCode: 3 }, render: { frame: { screen: 'last screen' } } }) +}) + +it('reports an attachment ending while the shell is still running and permits manual reconnect', async () => { + const { model, remote } = fixture() + vi.mocked(remote.follow).mockImplementationOnce(async function* (_sessionId, id, controllerId) { + yield { type: 'snapshot', sequence: 0, screen: '', info: { ...info, id, controllerId } } + }) + await connected(model) + await expect.poll(() => model.state.getSnapshot().issue).toBe('attachmentEnded') + model.connect() + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) +}) + +it('publishes remote control transfer without replacing the retained screen', async () => { + const { model, remote } = fixture() + vi.mocked(remote.follow).mockImplementation(async function* (_sessionId, id, controllerId, signal) { + yield { type: 'snapshot', sequence: 0, screen: 'retained', info: { ...info, id, controllerId } } + yield { type: 'state', info: { ...info, id } } + await untilAborted(signal) + }) + await connected(model) + await expect.poll(() => model.state.getSnapshot().writable).toBe(false) + expect(model.state.getSnapshot().render?.frame).toMatchObject({ type: 'snapshot', screen: 'retained' }) +}) + +/** Pause delivery after the real Gateway iterator settles, before the model observes it. */ +function deliveryBarrier() { + const ready = Promise.withResolvers() + const release = Promise.withResolvers() + let held = false + function prepare(stream: RemoteStream): void { + if (held) return + held = true + const iterator = stream[Symbol.asyncIterator]() + let first = true + vi.spyOn(stream, Symbol.asyncIterator).mockReturnValue({ + async next() { + if (!first) return iterator.next() + first = false + try { + return await iterator.next() + } finally { + ready.resolve(undefined) + await release.promise + } + }, + async return() { return iterator.return!() }, + }) + } + cleanups.push(() => { release.resolve(undefined) }) + return { ready: ready.promise, release: () => { release.resolve(undefined) }, prepare } +} + +it('ignores an already delivered screen when its attachment is replaced before the model receives it', async () => { + const barrier = deliveryBarrier() + const { model, remote } = fixture(barrier.prepare) + await mount(model) + await barrier.ready + model.connect() + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + const current = model.state.getSnapshot() + barrier.release() + await model.rename('settled') + expect(model.state.getSnapshot().render).toBe(current.render) + expect(remote.follow).toHaveBeenCalledTimes(2) +}) + +it('ignores an already settled stream error after a newer attachment has become writable', async () => { + const barrier = deliveryBarrier() + const { model, remote } = fixture(barrier.prepare) + vi.mocked(remote.follow).mockImplementationOnce(async function* () { + yield* [] + throw new Error('retired attachment failed') + }) + await mount(model) + await barrier.ready + model.connect() + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + barrier.release() + await model.rename('settled') + expect(model.state.getSnapshot()).toMatchObject({ phase: 'connected', writable: true, error: undefined }) +}) + +it('releases a pending screen render on carrier generation cancellation and accepts the replacement screen', async () => { + const { model, remote, streams } = fixture() + await mount(model) + await expect.poll(() => model.state.getSnapshot().render).toBeDefined() + const revision = model.state.getSnapshot().render!.revision + streams[0]!.restart() + await expect.poll(() => model.state.getSnapshot().render?.revision).toBe(revision + 1) + expect(remote.follow).toHaveBeenCalledTimes(2) + expect(model.state.getSnapshot().writable).toBe(true) +}) + +it('does not wait for the DOM callback of a screen whose generation was already cancelled at delivery', async () => { + const barrier = deliveryBarrier() + const { model, remote, streams } = fixture(barrier.prepare) + await mount(model) + await barrier.ready + streams[0]!.restart() + barrier.release() + await expect.poll(() => model.state.getSnapshot().render?.revision).toBe(2) + expect(remote.follow).toHaveBeenCalledTimes(2) + expect(model.state.getSnapshot().writable).toBe(true) +}) + +it.each(['write', 'resize'] as const)('keeps a fresh attachment writable when a pending %s fails after automatic transport recovery', async (operation) => { + const { model, remote, generation } = fixture() + generation.set({ id: 1, host: { home: '/home/fixture' } }) + const disconnected = Promise.withResolvers() + vi.mocked(remote.follow).mockImplementationOnce(async function* (_sessionId, id, controllerId, signal) { + yield { type: 'snapshot', sequence: 0, screen: 'before disconnect', info: { ...info, id, controllerId } } + await Promise.race([disconnected.promise, untilAborted(signal)]) + if (signal?.aborted) return + throw new RemoteStreamCarrierError('connection lost') + }) + await connected(model) + const pending = Promise.withResolvers>() + if (operation === 'write') { + vi.mocked(remote.write).mockReturnValueOnce(pending.promise) + model.write('old input') + } else { + vi.mocked(remote.resize).mockReturnValueOnce(pending.promise) + model.resize(100, 30) + } + await expect.poll(() => vi.mocked(remote[operation]).mock.calls.length).toBe(1) + const oldAttachmentId = vi.mocked(remote.follow).mock.calls[0]![2] + generation.set(undefined) + disconnected.resolve(undefined) + await expect.poll(() => model.state.getSnapshot().phase).toBe('disconnected') + expect(model.state.getSnapshot().writable).toBe(false) + expect(remote.follow).toHaveBeenCalledOnce() + + generation.set({ id: 2, host: { home: '/home/fixture' } }) + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + expect(remote.follow).toHaveBeenCalledTimes(2) + const attachmentId = vi.mocked(remote.follow).mock.calls[1]![2] + expect(attachmentId).not.toBe(oldAttachmentId) + expect(model.state.getSnapshot().info?.controllerId).toBe(attachmentId) + expect(model.state.getSnapshot().render?.frame).toMatchObject({ type: 'snapshot', screen: 'ready' }) + + model.write('fresh input') + pending.reject(new Error('old attachment was replaced')) + await expect.poll(() => vi.mocked(remote.write).mock.calls.some(call => call[3] === 'fresh input')).toBe(true) + expect(model.state.getSnapshot()).toMatchObject({ phase: 'connected', writable: true, error: undefined }) + expect(vi.mocked(remote.write).mock.calls.at(-1)?.[2]).toBe(attachmentId) + expect(remote.create).toHaveBeenCalledOnce() + expect(remote.close).not.toHaveBeenCalled() +}) + +it('ignores controls before discovery and reconnects an existing process after remount', async () => { + const { model, remote } = fixture() + model.connect() + model.write('early') + model.resize(100, 30) + model.acknowledge(100) + expect(remote.follow).not.toHaveBeenCalled() + expect(remote.write).not.toHaveBeenCalled() + expect(remote.resize).not.toHaveBeenCalled() + const unmount = await mount(model) + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + unmount() + model.connect() + model.write('detached') + model.resize(100, 30) + expect(remote.follow).toHaveBeenCalledOnce() + expect(remote.write).not.toHaveBeenCalled() + expect(remote.resize).not.toHaveBeenCalled() + model.mount() + await expect.poll(() => remote.follow).toHaveBeenCalledTimes(2) + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + expect(remote.create).toHaveBeenCalledOnce() +}) + +it('keeps controls inert when process metadata or the attachment is unavailable', async () => { + const { model, remote } = fixture() + await connected(model) + const state = model.state.getSnapshot() + model.state.set({ ...state, info: undefined }) + model.write('missing metadata') + model.resize(100, 30) + expect(remote.write).not.toHaveBeenCalled() + expect(remote.resize).not.toHaveBeenCalled() + model.state.set({ ...state, environment: undefined }) + model.resize(100, 30) + await expect.poll(() => remote.resize).toHaveBeenCalledOnce() + expect(vi.mocked(remote.resize).mock.calls[0]?.slice(-2)).toEqual([100, 30]) + model.write('unknown budget') + expect(model.state.getSnapshot().issue).toBe('inputFull') + expect(remote.write).not.toHaveBeenCalled() +}) + +it('renames an existing terminal, skips unchanged names, and exposes rename failures', async () => { + const { model, remote } = fixture() + await connected(model) + await model.rename(` ${info.title} `) + expect(remote.rename).not.toHaveBeenCalled() + await model.rename(' Build ') + expect(model.state.getSnapshot()).toMatchObject({ title: 'Build', info: { title: 'Build' } }) + vi.mocked(remote.rename).mockRejectedValueOnce('rename connection lost') + await model.rename('Other') + expect(model.state.getSnapshot()).toMatchObject({ phase: 'failed', error: 'rename connection lost', title: 'Build' }) + await model.dispose() + await model.rename('Ignored') + expect(remote.rename).toHaveBeenCalledTimes(2) +}) + +it('classifies a discovery carrier failure as disconnected and supports explicit retry', async () => { + const { model, remote } = fixture() + vi.mocked(remote.environment).mockRejectedValueOnce(new RemoteStreamCarrierError('offline')) + await model.refresh() + expect(model.state.getSnapshot()).toMatchObject({ phase: 'disconnected', error: 'offline' }) + expect(remote.create).not.toHaveBeenCalled() + await model.refresh() + await model.start() + expect(model.state.getSnapshot().info?.id).toBe(info.id) + expect(remote.create).toHaveBeenCalledOnce() +}) + +it('retains state when disposal overtakes successful allocation or a failed discovery', async () => { + for (const operation of ['creation', 'discovery'] as const) { + const { model, remote } = fixture() + const creation = Promise.withResolvers>() + const discovery = Promise.withResolvers>() + if (operation === 'creation') vi.mocked(remote.create).mockReturnValueOnce(creation.promise) + else vi.mocked(remote.environment).mockReturnValueOnce(discovery.promise) + const loading = model.refresh() + const starting = operation === 'creation' ? loading.then(() => model.start()) : undefined + if (operation === 'creation') await expect.poll(() => remote.create).toHaveBeenCalledOnce() + await model.dispose() + const before = model.state.getSnapshot() + if (operation === 'creation') creation.resolve(success(info)) + else discovery.reject(new Error('late discovery failure')) + await loading + await starting + expect(model.state.getSnapshot()).toBe(before) + expect(remote.follow).not.toHaveBeenCalled() + } +}) + +it('ignores a successful rename after the view is disposed', async () => { + const { model, remote } = fixture() + await connected(model) + const rename = Promise.withResolvers>() + vi.mocked(remote.rename).mockReturnValueOnce(rename.promise) + const pending = model.rename('Late') + await model.dispose() + const before = model.state.getSnapshot() + rename.resolve(success(undefined)) + await pending + expect(model.state.getSnapshot()).toBe(before) +}) + +it('allows retry after failed process cleanup and never reconnects while close is pending', async () => { + const { model, remote } = fixture() + await connected(model) + const closing = Promise.withResolvers>() + vi.mocked(remote.close).mockReturnValueOnce(closing.promise) + const first = model.close() + expect(model.close()).toBe(first) + model.connect() + expect(remote.follow).toHaveBeenCalledOnce() + closing.resolve(failure('close refused')) + await expect(first).rejects.toThrow('close refused') + expect(model.state.getSnapshot()).toMatchObject({ phase: 'failed', error: 'close refused' }) + await model.close() + expect(model.state.getSnapshot()).toMatchObject({ phase: 'closed', writable: false }) + expect(remote.close).toHaveBeenCalledTimes(2) +}) + +it('does not publish a close result or reconnect after disposal', async () => { + const { model, remote } = fixture() + await connected(model) + const closing = Promise.withResolvers>() + vi.mocked(remote.close).mockReturnValueOnce(closing.promise) + const pending = model.close() + await model.dispose() + const before = model.state.getSnapshot() + closing.resolve(success(undefined)) + await pending + model.mount() + model.connect() + expect(model.state.getSnapshot()).toBe(before) + expect(remote.follow).toHaveBeenCalledOnce() +}) + +it.each(['write', 'resize'] as const)('keeps the output connection when %s loses control before its state frame arrives', async (operation) => { + const { model, remote } = fixture() + const transfer = Promise.withResolvers() + const response = Promise.withResolvers>() + cleanups.push(() => { transfer.resolve(undefined); response.resolve(success(undefined)) }) + vi.mocked(remote.follow).mockImplementation(async function* (_session, id, controllerId, signal) { + yield { type: 'snapshot', sequence: 0, screen: 'retained screen', info: { ...info, id, controllerId } } + await transfer.promise + yield { type: 'state', info: { ...info, id } } + await untilAborted(signal) + }) + await connected(model) + vi.mocked(remote[operation]).mockReturnValueOnce(response.promise) + if (operation === 'write') model.write('before transfer') + else model.resize(100, 30) + model.write('queued before transfer') + await expect.poll(() => vi.mocked(remote[operation]).mock.calls.length).toBe(1) + response.resolve({ ok: false, error: new RemoteError('terminal/control-unavailable', 'Another window owns input', { reason: 'read-only' }) }) + await expect.poll(() => model.state.getSnapshot().writable).toBe(false) + expect(model.state.getSnapshot()).toMatchObject({ phase: 'connected', error: undefined, issue: undefined, render: { frame: { screen: 'retained screen' } } }) + transfer.resolve(undefined) + await expect.poll(() => model.state.getSnapshot().info?.controllerId).toBeUndefined() + expect(remote.follow).toHaveBeenCalledOnce() + expect(vi.mocked(remote.write).mock.calls.some(call => call[3] === 'queued before transfer')).toBe(false) +}) + +it('keeps an exited screen when a pending input is refused after the exit state arrives', async () => { + const { model, remote } = fixture() + const exit = Promise.withResolvers() + const response = Promise.withResolvers>() + cleanups.push(() => { exit.resolve(undefined); response.resolve(success(undefined)) }) + vi.mocked(remote.follow).mockImplementation(async function* (_session, id, controllerId, signal) { + yield { type: 'snapshot', sequence: 0, screen: 'final screen', info: { ...info, id, controllerId } } + await exit.promise + yield { type: 'state', info: { ...info, id, state: 'exited', exitCode: 0 } } + await untilAborted(signal) + }) + await connected(model) + vi.mocked(remote.write).mockReturnValueOnce(response.promise) + model.write('exit race') + await expect.poll(() => vi.mocked(remote.write).mock.calls.length).toBe(1) + exit.resolve(undefined) + await expect.poll(() => model.state.getSnapshot().info?.state).toBe('exited') + response.resolve({ ok: false, error: new RemoteError('terminal/control-unavailable', 'Terminal is not running', { reason: 'not-running' }) }) + await setImmediate() + expect(model.state.getSnapshot()).toMatchObject({ phase: 'connected', writable: false, error: undefined, issue: undefined, render: { frame: { screen: 'final screen' } } }) +}) + +it('exposes a localized quota error and clears it after a successful retry', async () => { + const { model, remote } = fixture() + vi.mocked(remote.create).mockResolvedValueOnce({ ok: false, error: new RemoteError('terminal/limit-reached', 'Session terminal limit reached', { limit: 8 }) }) + await model.refresh() + await model.start() + expect(model.state.getSnapshot()).toMatchObject({ phase: 'failed', issue: 'terminalLimit' }) + await connected(model) + expect(model.state.getSnapshot()).toMatchObject({ phase: 'connected', issue: undefined, error: undefined }) +}) diff --git a/packages/api/terminal-controller/tests/recovery.client.spec.ts b/packages/api/terminal-controller/tests/recovery.client.spec.ts new file mode 100644 index 0000000000..3839e2024a --- /dev/null +++ b/packages/api/terminal-controller/tests/recovery.client.spec.ts @@ -0,0 +1,515 @@ +/** Reloaded tab identities and nonblocking close requests use independent lifetimes. */ +import { setImmediate } from 'node:timers/promises' +import { afterEach, expect, it, vi } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import { RemoteError, type RemoteResult } from '@deepseek-ai/dsh-typert-protocol' +import { RemoteStream, type ClientRemote } from '@deepseek-ai/dsh-api-gateway/client' +import type {} from '@deepseek-ai/dsh-api-terminal-controller/remote' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { WebTerminalId, WebTerminalInfo, TerminalEnvironment } from '../src/types.ts' +import { TerminalView, type TerminalRemote } from '../src/client/model.ts' +import { TerminalCloseRequests } from '../src/client/close-requests.ts' +import * as TerminalClient from '../src/client/index.ts' + +const sessionId = 'session' as SessionId +const info: WebTerminalInfo = { id: 'terminal' as WebTerminalId, shell: { name: 'zsh', path: '/bin/zsh', args: ['-i'] }, title: 'zsh', cwd: '/workspace', rows: 24, cols: 80, state: 'running', exitCode: null } +const environment: TerminalEnvironment = { cwd: info.cwd, maxInputBytes: 1000, maxCols: 200, maxRows: 100, scrollback: 100 } +const success = (value: T): RemoteResult => ({ ok: true, value }) +const failure = (message: string): RemoteResult => ({ ok: false, error: new RemoteError('gateway/bad-request', message, {}) }) +const cleanups: (() => void | Promise)[] = [] +afterEach(async () => { for (const cleanup of cleanups.splice(0).reverse()) await cleanup(); vi.unstubAllGlobals(); vi.restoreAllMocks() }) + +function storage() { + const data = new Map() + vi.stubGlobal('localStorage', { get length() { return data.size }, key: (index: number) => [...data.keys()][index] ?? null, getItem: (key: string) => data.get(key) ?? null, setItem: (key: string, value: string) => { data.set(key, value) }, removeItem: (key: string) => { data.delete(key) } }) + return data +} + +function fixture() { + const remote: TerminalRemote = { + shells: vi.fn(async () => success([info.shell])), + environment: vi.fn(async () => success(environment)), list: vi.fn(async () => success([])), + create: vi.fn(async (_session, request) => success({ ...info, id: request.id })), + close: vi.fn(async () => success(undefined)), rename: vi.fn(async () => success(undefined)), + write: vi.fn(async () => success(undefined)), resize: vi.fn(async () => success(undefined)), + follow: vi.fn(async function* (_session, id, controllerId, signal) { + yield { type: 'snapshot', sequence: 0, screen: 'retained', info: { ...info, id, controllerId } } + await new Promise((resolve) => { + if (signal?.aborted) resolve() + else signal?.addEventListener('abort', () => { resolve() }, { once: true }) + }) + }), + } + const gateway: Pick = { $stream: options => new RemoteStream({ generation: createSnapshotStore(undefined) }, options) } + function view() { + const model = new TerminalView(sessionId, remote, gateway, info.id) + cleanups.push(() => model.dispose()) + return model + } + async function service() { + const ctx = new Context() + ctx.provide('remote', { ...gateway, terminal: remote } as never) + ctx.provide('remote.terminal', remote) + const fiber = ctx.plugin(TerminalClient) + cleanups.push(async () => { await ctx.fiber.dispose() }) + await fiber + return { service: ctx.webTerminals, dispose: () => fiber.dispose() } + } + return { remote, view, service } +} + +it('waits for shell selection and deduplicates overlapping starts', async () => { + const h = fixture() + const model = h.view() + const creation = Promise.withResolvers>() + vi.mocked(h.remote.create).mockReturnValueOnce(creation.promise) + model.mount() + const loading = model.refresh() + expect(model.refresh()).toBe(loading) + await loading + expect(h.remote.create).not.toHaveBeenCalled() + const starting = model.start() + const remounting = model.refresh() + void model.start() + expect(vi.mocked(h.remote.create).mock.calls[0]?.[1]).toEqual({ id: info.id, shellPath: info.shell.path, cols: 80, rows: 24 }) + creation.resolve(success(info)) + await starting + await remounting + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + expect(h.remote.create).toHaveBeenCalledOnce() +}) + +it('recovers a listed process without creating another, including its title and screen', async () => { + const h = fixture() + vi.mocked(h.remote.list).mockResolvedValue(success([{ ...info, title: 'Build' }])) + const model = h.view() + model.mount() + await model.refresh() + expect(h.remote.create).not.toHaveBeenCalled() + await expect.poll(() => model.state.getSnapshot().render?.frame).toMatchObject({ type: 'snapshot', screen: 'retained' }) + expect(h.remote.follow).toHaveBeenCalledWith(sessionId, info.id, expect.any(String), expect.any(AbortSignal)) +}) + +it('does not recreate a recovered terminal that disappeared after the recovery list was shown', async () => { + const h = fixture() + const { service } = await h.service() + vi.mocked(h.remote.list).mockResolvedValueOnce(success([info])) + expect(await service.recover(sessionId)).toEqual([info]) + const model = service.view(sessionId, 'recovered', info.id) + await model.refresh() + expect(model.state.getSnapshot()).toMatchObject({ phase: 'failed', writable: false }) + expect(model.state.getSnapshot().issue).toBe('missingTerminal') + expect(h.remote.create).not.toHaveBeenCalled() + model.mount() + await model.refresh() + expect(h.remote.create).not.toHaveBeenCalled() +}) + +it('retries lost create acknowledgements with the saved id and closes after a refused allocation', async () => { + const h = fixture() + const model = h.view() + vi.mocked(h.remote.create).mockResolvedValueOnce(failure('response lost')) + await model.refresh() + await model.start() + expect(model.state.getSnapshot().error).toBe('response lost') + await model.refresh() + await model.start() + expect(vi.mocked(h.remote.create).mock.calls.map(call => call[1].id)).toEqual([info.id, info.id]) + await model.close() + expect(h.remote.close).toHaveBeenCalledWith(sessionId, info.id) +}) + +it('waits for an in-flight creation while close detaches immediately and prevents a late connection', async () => { + const h = fixture() + const model = h.view() + const creation = Promise.withResolvers>() + vi.mocked(h.remote.create).mockReturnValueOnce(creation.promise) + const unmount = model.mount() + await model.refresh() + const starting = model.start() + await expect.poll(() => vi.mocked(h.remote.create).mock.calls.length).toBe(1) + const closing = model.close() + expect(model.close()).toBe(closing) + unmount() + expect(h.remote.close).not.toHaveBeenCalled() + creation.resolve(failure('cancelled allocation')) + await starting + await closing + expect(h.remote.close).toHaveBeenCalledWith(sessionId, info.id) + expect(h.remote.follow).not.toHaveBeenCalled() +}) + +it('does not allocate after close or disposal overtakes discovery', async () => { + for (const action of ['close', 'dispose'] as const) { + const h = fixture() + const model = h.view() + const discovery = Promise.withResolvers>() + vi.mocked(h.remote.environment).mockReturnValueOnce(discovery.promise) + const loading = model.refresh() + await model[action]() + discovery.resolve(success(environment)) + await loading + await model.refresh() + expect(h.remote.create).not.toHaveBeenCalled() + } +}) + +it('updates a restored inactive terminal title and ignores late results after disposal', async () => { + const h = fixture() + const model = h.view() + await model.rename(' Build ') + expect(h.remote.rename).toHaveBeenCalledWith(sessionId, info.id, ' Build ') + expect(model.state.getSnapshot().title).toBe('Build') + const rename = Promise.withResolvers>() + vi.mocked(h.remote.rename).mockReturnValueOnce(rename.promise) + const pending = model.rename('Late') + await model.dispose() + rename.resolve(success(undefined)) + await pending + expect(model.state.getSnapshot().title).toBe('Build') +}) + +it('removes a view synchronously, keeps cleanup retryable, and never deletes a replacement view', async () => { + storage() + const h = fixture() + const { service } = await h.service() + const first = service.view(sessionId, 'tab') + first.mount() + await first.refresh() + const closing = Promise.withResolvers>() + vi.mocked(h.remote.close).mockReturnValueOnce(closing.promise) + service.close(sessionId, 'tab') + const replacement = service.view(sessionId, 'tab') + expect(replacement).not.toBe(first) + expect(service.closeFailures.getSnapshot()).toEqual([]) + closing.resolve(failure('termination refused')) + await expect.poll(() => service.closeFailures.getSnapshot().length).toBe(1) + const failed = service.closeFailures.getSnapshot()[0]! + service.retryClose(failed.id) + service.retryClose(failed.id) + await expect.poll(() => service.closeFailures.getSnapshot()).toEqual([]) + expect(service.view(sessionId, 'tab')).toBe(replacement) + await expect.poll(() => new TerminalCloseRequests().pending()).toEqual([]) +}) + +it('closes an inactive restored tab by tab identity and retries saved close requests after reload', async () => { + storage() + const h = fixture() + const first = await h.service() + first.service.close(sessionId, 'inactive', 'inactive' as WebTerminalId) + await expect.poll(() => vi.mocked(h.remote.close).mock.calls.length).toBe(1) + expect(h.remote.close).toHaveBeenCalledWith(sessionId, 'inactive') + await first.dispose() + const pending = new TerminalCloseRequests() + pending.save({ sessionId, id: 'second' as WebTerminalId, title: 'Second' }) + await h.service() + await expect.poll(() => vi.mocked(h.remote.close).mock.calls.length).toBe(2) + expect(h.remote.close).toHaveBeenCalledWith(sessionId, 'second') + await expect.poll(() => new TerminalCloseRequests().pending()).toEqual([]) +}) + +it('keeps concurrent windows close requests independent and removes only the settled request', () => { + storage() + const a = new TerminalCloseRequests() + const b = new TerminalCloseRequests() + a.save({ sessionId, id: 'a' as WebTerminalId, title: 'A' }) + b.save({ sessionId, id: 'b' as WebTerminalId, title: 'B' }) + expect(new TerminalCloseRequests().pending().map(request => request.id)).toEqual(['a', 'b']) + a.remove('a' as WebTerminalId) + expect(new TerminalCloseRequests().pending().map(request => request.id)).toEqual(['b']) +}) + +it('recovers Host terminals by Session while excluding held, pending-close, and already-closed identities', async () => { + storage() + const h = fixture() + const { service } = await h.service() + const held = service.view(sessionId, 'held') + await held.refresh() + const closing = service.view(sessionId, 'closing') + await closing.refresh() + const done = service.view(sessionId, 'closed') + await done.refresh() + const unheld: WebTerminalInfo = { ...info, id: 'unheld' as WebTerminalId } + const pending = Promise.withResolvers>() + vi.mocked(h.remote.close).mockReturnValueOnce(pending.promise) + service.close(sessionId, 'closing') + service.close(sessionId, 'closed') + await expect.poll(() => new TerminalCloseRequests().pending().length).toBe(1) + vi.mocked(h.remote.list).mockResolvedValue(success([ + { ...info, id: held.id }, { ...info, id: closing.id }, { ...info, id: done.id }, unheld, + ])) + expect(await service.recover(sessionId)).toEqual([unheld]) + expect(h.remote.list).toHaveBeenLastCalledWith(sessionId) + const otherSession = 'other-session' as SessionId + vi.mocked(h.remote.list).mockResolvedValueOnce(success([info])) + expect(await service.recover(otherSession)).toEqual([info]) + pending.resolve(success(undefined)) + await expect.poll(() => new TerminalCloseRequests().pending()).toEqual([]) + expect(await service.recover(sessionId)).toEqual([unheld]) +}) + +it('queries current held views after a slow recovery response and reports discovery errors', async () => { + const h = fixture() + const { service } = await h.service() + const listed = Promise.withResolvers>() + vi.mocked(h.remote.list).mockReturnValueOnce(listed.promise) + const recovering = service.recover(sessionId) + vi.mocked(h.remote.list).mockResolvedValueOnce(success([info])) + const model = service.view(sessionId, 'recovered', info.id) + await model.refresh() + listed.resolve(success([info])) + expect(await recovering).toEqual([]) + expect(service.view(sessionId, 'recovered', info.id)).toBe(model) + expect(model.id).toBe(info.id) + vi.mocked(h.remote.list).mockResolvedValueOnce(failure('Session unavailable')) + await expect(service.recover(sessionId)).rejects.toThrow('Session unavailable') +}) + +it('ignores an unknown tab and retries only saved close requests', async () => { + storage() + const h = fixture() + const { service } = await h.service() + service.close(sessionId, 'unknown') + service.retryClose('unknown' as WebTerminalId) + expect(h.remote.close).not.toHaveBeenCalled() + expect(new TerminalCloseRequests().pending()).toEqual([]) +}) + +it('retains an inactive close failure with its tab title until retry succeeds', async () => { + const data = storage() + const h = fixture() + const { service } = await h.service() + vi.mocked(h.remote.close).mockResolvedValueOnce(failure('Host refused cleanup')) + service.close(sessionId, 'Build', info.id) + expect(data.has(`dsh.terminal.close.v1.${info.id}`)).toBe(true) + await expect.poll(() => service.closeFailures.getSnapshot()).toEqual([{ id: info.id, title: 'Build', message: 'Host refused cleanup' }]) + vi.mocked(h.remote.list).mockResolvedValueOnce(success([info])) + expect(await service.recover(sessionId)).toEqual([]) + const pending = Promise.withResolvers>() + vi.mocked(h.remote.close).mockReturnValueOnce(pending.promise) + service.retryClose(info.id) + service.retryClose(info.id) + expect(service.closeFailures.getSnapshot()).toEqual([]) + expect(h.remote.close).toHaveBeenCalledTimes(2) + pending.resolve(success(undefined)) + await expect.poll(() => data.size).toBe(0) +}) + +it('preserves a close failure from a non-Error rejection', async () => { + storage() + const h = fixture() + const { service } = await h.service() + vi.mocked(h.remote.close).mockRejectedValueOnce('carrier closed') + service.close(sessionId, 'Build', info.id) + await expect.poll(() => service.closeFailures.getSnapshot()).toEqual([{ id: info.id, title: 'Build', message: 'carrier closed' }]) +}) + +it('waits for pending cleanup during service disposal without publishing a late failure', async () => { + storage() + const h = fixture() + const { service, dispose } = await h.service() + const pending = Promise.withResolvers>() + vi.mocked(h.remote.close).mockReturnValueOnce(pending.promise) + service.close(sessionId, 'Build', info.id) + let finished = false + const disposing = dispose().then(() => { finished = true }) + expect(finished).toBe(false) + pending.resolve(failure('transport is stopping')) + await disposing + expect(service.closeFailures.getSnapshot()).toEqual([]) + expect(new TerminalCloseRequests().pending()).toEqual([{ sessionId, id: info.id, title: 'Build' }]) + service.retryClose(info.id) + expect(h.remote.close).toHaveBeenCalledOnce() +}) + +it('saves close intents without persisting any active terminal or sidebar state', async () => { + const data = storage() + const h = fixture() + const { service } = await h.service() + const model = service.view(sessionId, 'new-tab') + await model.refresh() + expect(model.id).toMatch(/^[0-9a-f-]{36}$/) + expect(data.size).toBe(0) + const pending = Promise.withResolvers>() + vi.mocked(h.remote.close).mockReturnValueOnce(pending.promise) + service.close(sessionId, 'new-tab') + const request = { sessionId, id: model.id, title: 'new-tab' } + expect([...data.entries()]).toEqual([[`dsh.terminal.close.v1.${model.id}`, JSON.stringify(request)]]) + pending.resolve(success(undefined)) + await expect.poll(() => data.size).toBe(0) +}) + +it.each(['{broken', 'null', '{}', '[{}]', '{"sessionId":"s","id":"bad/id","title":"x"}', '{"sessionId":"s","id":"different","title":"x"}'])('discards malformed saved cleanup: %s', (raw) => { + const data = storage() + data.set('dsh.terminal.close.v1.terminal', raw) + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + expect(new TerminalCloseRequests().pending()).toEqual([]) + expect(error).toHaveBeenCalledOnce() +}) + +it('skips unrelated storage and cleanup keys removed during enumeration', () => { + const getItem = vi.fn(() => null) + vi.stubGlobal('localStorage', { + length: 3, + key: (index: number) => ['unrelated', 'dsh.terminal.close.v1.gone', null][index], + getItem, + }) + expect(new TerminalCloseRequests().pending()).toEqual([]) + expect(getItem).toHaveBeenCalledExactlyOnceWith('dsh.terminal.close.v1.gone') +}) + +it('keeps close requests usable without browser storage', () => { + vi.stubGlobal('localStorage', undefined) + const requests = new TerminalCloseRequests() + requests.save({ sessionId, id: info.id, title: 'Build' }) + expect(requests.pending()).toEqual([{ sessionId, id: info.id, title: 'Build' }]) + requests.remove(info.id) + expect(requests.pending()).toEqual([]) +}) + +it('keeps cleanup usable in memory when storage access itself is denied', () => { + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + vi.stubGlobal('localStorage', undefined) + Object.defineProperty(globalThis, 'localStorage', { configurable: true, get() { throw new Error('storage denied') } }) + const requests = new TerminalCloseRequests() + requests.save({ sessionId, id: info.id, title: 'Build' }) + expect(requests.pending()).toHaveLength(1) + requests.remove(info.id) + expect(requests.pending()).toEqual([]) + expect(error).toHaveBeenCalledTimes(3) +}) + +it.each(['saved', 'view'] as const)('clears a %s close request after the Host confirms that its Session does not exist', async (source) => { + const data = storage() + const h = fixture() + const requests = new TerminalCloseRequests() + if (source === 'saved') requests.save({ sessionId, id: info.id, title: 'Build' }) + vi.mocked(h.remote.close).mockResolvedValue({ ok: false, error: new RemoteError('session/not-found', 'Deleted Session', { sessionId }) }) + const { service, dispose } = await h.service() + if (source === 'view') { + const view = service.view(sessionId, 'tab') + await view.refresh() + service.close(sessionId, 'tab') + } + await expect.poll(() => vi.mocked(h.remote.close).mock.calls.length).toBe(1) + await dispose() + expect(data.size).toBe(0) + expect(service.closeFailures.getSnapshot()).toEqual([]) + expect(new TerminalCloseRequests().pending()).toEqual([]) + await h.service() + expect(h.remote.close).toHaveBeenCalledOnce() +}) + +it('waits for both active and detached stream finalizers during plugin disposal without closing Host processes', async () => { + const h = fixture() + const { service, dispose } = await h.service() + const started = [Promise.withResolvers(), Promise.withResolvers()] + const release = [Promise.withResolvers(), Promise.withResolvers()] + const finished = [Promise.withResolvers(), Promise.withResolvers()] + cleanups.push(() => { for (const barrier of release) barrier.resolve(undefined) }) + let index = 0 + vi.mocked(h.remote.follow).mockImplementation(async function* (_session, id, controllerId, signal) { + const current = index++ + try { + yield { type: 'snapshot', sequence: 0, screen: 'screen', info: { ...info, id, controllerId } } + await new Promise((resolve) => { + if (signal?.aborted) resolve() + else signal?.addEventListener('abort', () => { resolve() }, { once: true }) + }) + } finally { + started[current]!.resolve(undefined) + await release[current]!.promise + finished[current]!.resolve(undefined) + } + }) + const model = service.view(sessionId, 'tab') + model.mount() + await model.refresh() + await model.start() + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + model.connect() + await started[0]!.promise + await expect.poll(() => model.state.getSnapshot().writable).toBe(true) + let disposed = false + const disposing = dispose().then(() => { disposed = true }) + await started[1]!.promise + release[1]!.resolve(undefined) + await finished[1]!.promise + // Drain runnable disposal continuations; only the held old finalizer may keep teardown pending. + await setImmediate() + expect(disposed).toBe(false) + release[0]!.resolve(undefined) + await disposing + expect(disposed).toBe(true) + expect(h.remote.close).not.toHaveBeenCalled() +}) + +it('remembers a shell choice immediately for new views and falls back when that shell is absent', async () => { + const data = storage() + const h = fixture() + const alternate = { name: 'bash', path: '/bin/bash', args: ['-i'] } + vi.mocked(h.remote.shells).mockResolvedValue(success([info.shell, alternate])) + vi.mocked(h.remote.create).mockImplementation(async (_sessionId, request) => success({ ...info, shell: alternate, id: request.id })) + const first = h.view() + await first.start() + first.selectShell(alternate.path) + await first.refresh() + expect(first.state.getSnapshot()).toMatchObject({ phase: 'selecting', selectedShell: info.shell.path }) + first.selectShell('/not-listed') + expect(first.state.getSnapshot().selectedShell).toBe(info.shell.path) + first.selectShell(alternate.path) + expect(data.get('dsh.terminal.shell')).toBe(alternate.path) + const beforeLaunch = h.view() + await beforeLaunch.refresh() + expect(beforeLaunch.state.getSnapshot().selectedShell).toBe(alternate.path) + expect(h.remote.create).not.toHaveBeenCalled() + await first.start() + expect(data.get('dsh.terminal.shell')).toBe(alternate.path) + expect(h.remote.create).toHaveBeenCalledWith(sessionId, expect.objectContaining({ shellPath: alternate.path }), expect.any(AbortSignal)) + const second = h.view() + await second.refresh() + expect(second.state.getSnapshot().selectedShell).toBe(alternate.path) + await second.dispose() + await second.start() + vi.mocked(h.remote.shells).mockResolvedValue(success([info.shell])) + const third = h.view() + await third.refresh() + expect(third.state.getSnapshot().selectedShell).toBe(info.shell.path) + expect(h.remote.create).toHaveBeenCalledOnce() +}) + +it('retains the last selection across a failed launch', async () => { + const data = storage() + const h = fixture() + vi.mocked(h.remote.create).mockResolvedValueOnce(failure('shell disappeared')) + const model = h.view() + await model.refresh() + await model.start() + expect(data.get('dsh.terminal.shell')).toBe(info.shell.path) + await model.refresh() + expect(model.state.getSnapshot().selectedShell).toBe(info.shell.path) + await model.close() + await model.start() + expect(h.remote.create).toHaveBeenCalledOnce() +}) + +it('keeps launch usable when browser storage is denied and stops late shell discovery after close', async () => { + vi.stubGlobal('localStorage', undefined) + Object.defineProperty(globalThis, 'localStorage', { configurable: true, get() { throw new Error('denied') } }) + const h = fixture() + const model = h.view() + await model.refresh() + await model.start() + expect(model.state.getSnapshot().info).toBeDefined() + const delayed = h.view() + const shells = Promise.withResolvers>>() + vi.mocked(h.remote.shells).mockReturnValueOnce(shells.promise) + const loading = delayed.refresh() + await expect.poll(() => h.remote.shells).toHaveBeenCalledTimes(2) + await delayed.close() + shells.resolve(success([info.shell])) + await loading + expect(delayed.state.getSnapshot().phase).toBe('closed') + expect(h.remote.create).toHaveBeenCalledOnce() +}) diff --git a/packages/api/terminal-controller/tests/stream.spec.ts b/packages/api/terminal-controller/tests/stream.spec.ts new file mode 100644 index 0000000000..a5fd9357ff --- /dev/null +++ b/packages/api/terminal-controller/tests/stream.spec.ts @@ -0,0 +1,80 @@ +/** Output followers drain on process exit and discard queued data on detach. */ +import { describe, expect, it } from 'vitest' +import { TerminalFollower } from '../src/stream.ts' +import type { TerminalFrame } from '../src/types.ts' + +const frame: TerminalFrame = { type: 'output', sequence: 1, data: '终端' } +const signal = (): AbortSignal => new AbortController().signal + +describe('TerminalFollower', () => { + it('accepts the exact byte limit and restores capacity after each frame is consumed', async () => { + const follower = new TerminalFollower(Buffer.byteLength(JSON.stringify(frame), 'utf8')) + const stream = follower.read(signal())[Symbol.asyncIterator]() + try { + follower.push(frame) + expect(await stream.next()).toEqual({ done: false, value: frame }) + follower.push(frame) + follower.finish() + follower.push({ ...frame, sequence: 2 }) + expect(await stream.next()).toEqual({ done: false, value: frame }) + expect(await stream.next()).toEqual({ done: true, value: undefined }) + } finally { follower.close(); await stream.return?.() } + }) + + it('fails accumulated overflow using encoded byte size', async () => { + const follower = new TerminalFollower(Buffer.byteLength(JSON.stringify(frame), 'utf8')) + follower.push(frame) + follower.push(frame) + await expect(follower.read(signal())[Symbol.asyncIterator]().next()).rejects.toThrow('reconnect to recover') + }) + + it('discards queued data when detached and ignores later output', async () => { + const follower = new TerminalFollower(1000) + follower.push(frame) + follower.close() + follower.push(frame) + expect(await follower.read(signal())[Symbol.asyncIterator]().next()).toEqual({ done: true, value: undefined }) + }) + + it('does not deliver buffered output to an already-aborted request', async () => { + const follower = new TerminalFollower(1000) + follower.push(frame) + const abort = new AbortController() + abort.abort() + expect(await follower.read(abort.signal)[Symbol.asyncIterator]().next()).toEqual({ done: true, value: undefined }) + }) + + it('wakes a waiting reader for output and then for graceful completion', async () => { + const follower = new TerminalFollower(1000) + const stream = follower.read(signal())[Symbol.asyncIterator]() + try { + const reading = stream.next() + follower.push(frame) + expect(await reading).toEqual({ done: false, value: frame }) + const finishing = stream.next() + follower.finish() + expect(await finishing).toEqual({ done: true, value: undefined }) + } finally { follower.close(); await stream.return?.() } + }) + + it('wakes a waiting reader on cancellation', async () => { + const follower = new TerminalFollower(1000) + const abort = new AbortController() + const stream = follower.read(abort.signal)[Symbol.asyncIterator]() + try { + const reading = stream.next() + abort.abort() + expect(await reading).toEqual({ done: true, value: undefined }) + } finally { follower.close(); await stream.return?.() } + }) + + it('closes the follower when its consumer returns before EOF', async () => { + const follower = new TerminalFollower(1000) + const stream = follower.read(signal())[Symbol.asyncIterator]() + follower.push(frame) + await stream.next() + await stream.return?.() + follower.push(frame) + expect(await follower.read(signal())[Symbol.asyncIterator]().next()).toEqual({ done: true, value: undefined }) + }) +}) diff --git a/packages/api/terminal-controller/tests/terminal.spec.ts b/packages/api/terminal-controller/tests/terminal.spec.ts new file mode 100644 index 0000000000..bf6bd16318 --- /dev/null +++ b/packages/api/terminal-controller/tests/terminal.spec.ts @@ -0,0 +1,228 @@ +/** Terminal screen continuity and process ownership under real output scheduling. */ +import { PassThrough } from 'node:stream' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { SubprocessOutcome, SubprocessTerminalHandle } from '@deepseek-ai/dsh-subprocess' +import { BrowserTerminal } from '../src/terminal.ts' +import { TerminalFollower } from '../src/stream.ts' +import type { TerminalAttachmentId, TerminalFrame, WebTerminalId, WebTerminalInfo } from '../src/types.ts' + +const info: WebTerminalInfo = { id: 'terminal-test' as WebTerminalId, title: 'bash', shell: { path: '/bin/bash', name: 'bash', args: ['-i'] }, cwd: '/workspace', cols: 80, rows: 24, state: 'running', exitCode: null } +const attachment = (id: string): TerminalAttachmentId => id as TerminalAttachmentId +const cleanups: (() => Promise)[] = [] +afterEach(async () => { await Promise.all(cleanups.splice(0).map(close => close())) }) + +function fixture() { + const output = new PassThrough() + const outcome = Promise.withResolvers() + const handle = { + pid: 123, output, done: outcome.promise, write: vi.fn(async () => {}), resize: vi.fn(async () => {}), + inspectForeground: async () => undefined, signalForeground: async () => 123, + terminate: vi.fn(async () => { output.end(); outcome.resolve({ exitCode: 0, signal: null }) }), + } + const checked: SubprocessTerminalHandle = handle + const terminal = new BrowserTerminal(checked, info, 100, 100_000) + cleanups.push(() => terminal.close()) + return { terminal, output, outcome, handle } +} + +async function attach(terminal: BrowserTerminal, id = 'first') { + const controller = new AbortController() + const iterator = terminal.follow(attachment(id), controller.signal)[Symbol.asyncIterator]() + const baseline = await readFrame(iterator) + cleanups.push(async () => { controller.abort(); await iterator.return?.() }) + return { controller, iterator, baseline } +} + +describe('BrowserTerminal', () => { + it('restores the screen after detach without re-executing the shell or replaying duplicate output', async () => { + const { terminal, output, handle } = fixture() + const first = await attach(terminal) + expect(first.baseline).toMatchObject({ type: 'snapshot', sequence: 0 }) + output.write(Buffer.from('hello\r\n')) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'output', sequence: 1, data: 'hello\r\n' }) + first.controller.abort() + await first.iterator.return?.() + expect(handle.terminate).not.toHaveBeenCalled() + output.write(Buffer.from('world')) + await expect.poll(() => terminal.info.state).toBe('running') + const second = await attach(terminal, 'second') + // The baseline and its output queue share the same ordered screen write queue. + const frames = [second.baseline] + if (second.baseline.type === 'snapshot' && !second.baseline.screen.includes('world')) frames.push(await readFrame(second.iterator)) + expect(JSON.stringify(frames)).toContain('world') + expect(JSON.stringify(frames)).toContain('hello') + expect(handle.terminate).not.toHaveBeenCalled() + }) + + it('preserves split UTF-8, gives the newest attachment input, and resizes both PTY and recovery screen', async () => { + const { terminal, output, handle } = fixture() + const first = await attach(terminal) + const encoded = Buffer.from('终端') + output.write(encoded.subarray(0, 2)) + await expect.poll(() => output.readableLength).toBe(0) + output.write(encoded.subarray(2)) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'output', data: '终端' }) + const second = await attach(terminal, 'second') + await expect(terminal.write(attachment('first'), 'ignored')).rejects.toMatchObject({ code: 'terminal/control-unavailable', details: { reason: 'read-only' } }) + await terminal.write(attachment('second'), '\t') + expect(handle.write).toHaveBeenCalledWith('\t') + await terminal.resize(attachment('second'), 100, 30) + expect(handle.resize).toHaveBeenCalledWith(100, 30) + expect(terminal.info).toMatchObject({ cols: 100, rows: 30 }) + second.controller.abort() + await second.iterator.return?.() + expect(terminal.info.controllerId).toBeUndefined() + }) + + it('keeps exit facts and screen until explicit cleanup and never starts a replacement process', async () => { + const { terminal, output, outcome, handle } = fixture() + const first = await attach(terminal) + output.end('done') + outcome.resolve({ exitCode: 7, signal: null }) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'output', data: 'done' }) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'state', info: { state: 'exited', exitCode: 7 } }) + expect(handle.terminate).not.toHaveBeenCalled() + const second = await attach(terminal, 'second') + expect(second.baseline).toMatchObject({ type: 'snapshot', info: { state: 'exited', exitCode: 7 } }) + await terminal.close() + expect(handle.terminate).toHaveBeenCalledOnce() + }) + + it('drains final output and exit state before closing followers', async () => { + const { terminal, handle, output, outcome } = fixture() + const first = await attach(terminal) + vi.mocked(handle.terminate).mockImplementationOnce(async () => { + output.end('FINAL OUTPUT\r\n') + outcome.resolve({ exitCode: 0, signal: null }) + }) + await terminal.close() + expect(await readFrame(first.iterator)).toMatchObject({ type: 'output', data: 'FINAL OUTPUT\r\n' }) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'state', info: { state: 'exited' } }) + expect((await first.iterator.next()).done).toBe(true) + }) + + it('rejects input before attachment, during close and after process exit', async () => { + const { terminal, handle, output, outcome } = fixture() + await expect(terminal.write(attachment('first'), 'ignored')).rejects.toMatchObject({ code: 'terminal/control-unavailable', details: { reason: 'read-only' } }) + const first = await attach(terminal) + output.end() + outcome.resolve({ exitCode: 0, signal: null }) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'state', info: { state: 'exited' } }) + await expect(terminal.write(attachment('first'), 'ignored')).rejects.toMatchObject({ code: 'terminal/control-unavailable', details: { reason: 'not-running' } }) + await expect(terminal.resize(attachment('first'), 100, 30)).rejects.toMatchObject({ code: 'terminal/control-unavailable', details: { reason: 'not-running' } }) + const closing = terminal.close() + await expect(terminal.write(attachment('first'), 'ignored')).rejects.toMatchObject({ code: 'terminal/control-unavailable', details: { reason: 'not-running' } }) + await closing + expect(handle.write).not.toHaveBeenCalled() + expect(handle.resize).not.toHaveBeenCalled() + }) + + it('preserves the input controller when an older follower detaches', async () => { + const { terminal, handle } = fixture() + const first = await attach(terminal) + const second = await attach(terminal, 'second') + first.controller.abort() + await first.iterator.return?.() + expect(terminal.info.controllerId).toBe(attachment('second')) + terminal.rename('build output') + expect(await readFrame(second.iterator)).toMatchObject({ type: 'state', info: { title: 'build output', controllerId: 'second' } }) + await terminal.write(attachment('second'), 'pwd\r') + expect(handle.write).toHaveBeenCalledWith('pwd\r') + }) + + it('does not grant input to attachments cancelled before their snapshot is ready', async () => { + const { terminal, handle } = fixture() + const aborted = new AbortController() + aborted.abort(new Error('already detached')) + await expect(terminal.follow(attachment('cancelled'), aborted.signal)[Symbol.asyncIterator]().next()).rejects.toThrow('already detached') + await attach(terminal) + const writing = Promise.withResolvers() + const written = Promise.withResolvers() + handle.write.mockImplementationOnce(async () => { writing.resolve(undefined); await written.promise }) + const pendingWrite = terminal.write(attachment('first'), 'pwd\r') + try { + await writing.promise + const abort = new AbortController() + const pendingAttachment = terminal.follow(attachment('late'), abort.signal)[Symbol.asyncIterator]().next() + const rejected = expect(pendingAttachment).rejects.toThrow('detached while waiting') + abort.abort(new Error('detached while waiting')) + written.resolve(undefined) + await pendingWrite + await rejected + expect(terminal.info.controllerId).toBe(attachment('first')) + } finally { written.resolve(undefined) } + }) + + it('continues accepting operations after a provider write or resize fails', async () => { + const { terminal, handle } = fixture() + await attach(terminal) + handle.write.mockRejectedValueOnce(new Error('input transport failed')) + handle.resize.mockRejectedValueOnce(new Error('resize transport failed')) + await expect(terminal.write(attachment('first'), 'failed')).rejects.toThrow('input transport failed') + await expect(terminal.resize(attachment('first'), 100, 30)).rejects.toThrow('resize transport failed') + expect(terminal.info).toMatchObject({ cols: 80, rows: 24 }) + await terminal.write(attachment('first'), 'accepted') + await terminal.resize(attachment('first'), 90, 25) + expect(handle.write).toHaveBeenLastCalledWith('accepted') + expect(terminal.info).toMatchObject({ cols: 90, rows: 25 }) + }) + + it.each([new Error('process wait failed'), 'remote process wait failed'])('publishes a failed process outcome and retains its recovery screen: %s', async (failure) => { + const { terminal, output, outcome } = fixture() + const first = await attach(terminal) + output.end('last output') + outcome.reject(failure) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'output', data: 'last output' }) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'state', info: { state: 'failed', error: failure instanceof Error ? failure.message : failure } }) + const second = await attach(terminal, 'second') + expect(second.baseline).toMatchObject({ type: 'snapshot', screen: 'last output', info: { state: 'failed' } }) + }) + + it('publishes output-stream failure even when the process wait succeeds', async () => { + const { terminal, output, outcome } = fixture() + const first = await attach(terminal) + output.destroy(new Error('output transport failed')) + outcome.resolve({ exitCode: 0, signal: null }) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'state', info: { state: 'failed', error: 'output transport failed' } }) + }) + + it('flushes incomplete UTF-8 at EOF before publishing the process exit', async () => { + const { terminal, output, outcome } = fixture() + const first = await attach(terminal) + output.end(Buffer.from([0xe7, 0xbb])) + outcome.resolve({ exitCode: 0, signal: null }) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'output', data: '�' }) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'state', info: { state: 'exited' } }) + }) + + it('preserves a leading UTF-8 BOM in the terminal output stream', async () => { + const { terminal, output } = fixture() + const first = await attach(terminal) + output.write(Buffer.from('\uFEFF终端')) + expect(await readFrame(first.iterator)).toMatchObject({ type: 'output', data: '\uFEFF终端' }) + }) + + it('shares concurrent close attempts and permits retry after termination fails', async () => { + const { terminal, handle } = fixture() + await attach(terminal) + handle.terminate.mockRejectedValueOnce(new Error('process range remains alive')) + const first = terminal.close() + expect(terminal.close()).toBe(first) + await expect(first).rejects.toThrow('remains alive') + await terminal.write(attachment('first'), 'retry cleanup next') + await terminal.close() + expect(handle.terminate).toHaveBeenCalledTimes(2) + }) + + it('reports overflow rather than silently discarding output', async () => { + const follower = new TerminalFollower(64) + follower.push({ type: 'output', sequence: 1, data: 'x'.repeat(128) }) + await expect(follower.read(new AbortController().signal)[Symbol.asyncIterator]().next()).rejects.toThrow('buffer') + }) +}) + +async function readFrame(iterator: AsyncIterator): Promise { + const result = await iterator.next() + if (result.done === true) throw new Error('Terminal stream ended before its expected frame') + return result.value +} diff --git a/packages/api/terminal-controller/tsconfig.client.json b/packages/api/terminal-controller/tsconfig.client.json new file mode 100644 index 0000000000..3f2a5ca277 --- /dev/null +++ b/packages/api/terminal-controller/tsconfig.client.json @@ -0,0 +1,38 @@ +{ + "extends": "../../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types", + "tsBuildInfoFile": "lib/tsconfig.client.tsbuildinfo" + }, + "files": [ + "src/client/index.ts", + "src/client/model.ts", + "src/client/shell-preference.ts", + "src/types.ts", + "src/client/close-requests.ts" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../gateway/tsconfig.client.json" + }, + { + "path": "../../client/store" + }, + { + "path": "../../core/session" + }, + { + "path": "../../util/brand" + }, + { + "path": "../../typert/protocol" + }, + { + "path": "../../util/crypto" + } + ] +} diff --git a/packages/api/terminal-controller/tsconfig.host.json b/packages/api/terminal-controller/tsconfig.host.json new file mode 100644 index 0000000000..132c120afa --- /dev/null +++ b/packages/api/terminal-controller/tsconfig.host.json @@ -0,0 +1,53 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types", + "tsBuildInfoFile": "lib/tsconfig.host.tsbuildinfo" + }, + "files": [ + "src/index.ts", + "src/types.ts", + "src/terminal.ts", + "src/stream.ts", + "src/shells.ts" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../core/agent" + }, + { + "path": "../../core/session" + }, + { + "path": "../../fs/fs" + }, + { + "path": "../../sandbox/sandbox" + }, + { + "path": "../../sandbox/sandbox-policy" + }, + { + "path": "../../session/session-projection" + }, + { + "path": "../../subprocess/subprocess" + }, + { + "path": "../../typert/protocol" + }, + { + "path": "../../util/brand" + }, + { + "path": "../../util/deque" + } + ] +} diff --git a/packages/api/terminal-controller/tsconfig.json b/packages/api/terminal-controller/tsconfig.json new file mode 100644 index 0000000000..2eca820546 --- /dev/null +++ b/packages/api/terminal-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.host.json" + }, + { + "path": "./tsconfig.client.json" + } + ] +} diff --git a/packages/api/terminal-controller/tsdown.config.ts b/packages/api/terminal-controller/tsdown.config.ts new file mode 100644 index 0000000000..cb9d252eb2 --- /dev/null +++ b/packages/api/terminal-controller/tsdown.config.ts @@ -0,0 +1,7 @@ +import { clientBundle } from '../../client/tsdown.client.ts' + +export default clientBundle( + '@deepseek-ai/dsh-api-terminal-controller', + ['lib/types/index.js'], + { hostPhase: true }, +) diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index ded4304cc1..97e5cf60da 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -401,6 +401,11 @@ headChars: 4096 tailChars: 1024 + # When an image-capable route rejects a request as over its image budget, + # replace the oldest images with placeholders on the surface and retry. + - id: image-offload + name: '@deepseek-ai/dsh-compaction-image-offload' + - id: tool-todo name: '@deepseek-ai/dsh-tool-todo' config: diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 71b0443dda..b3b09e7415 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -47,6 +47,7 @@ "@deepseek-ai/dsh-command-goal": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-compaction-basic": "workspace:^", + "@deepseek-ai/dsh-compaction-image-offload": "workspace:^", "@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^", "@deepseek-ai/dsh-credentials-local": "workspace:^", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^", diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 95ed42192f..1c13eb026b 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -104,6 +104,8 @@ # Workspace file service: bounded read, directory listing, and the # agent-write change feed inside the session workspace root. + - id: terminal-controller + name: '@deepseek-ai/dsh-api-terminal-controller' - id: workspace-files name: '@deepseek-ai/dsh-api-workspace-files' @@ -228,6 +230,8 @@ name: '@deepseek-ai/dsh-client-ui-sidebar-documentpreview' # The right Sidebar's workspace file tree tab type. + - id: ui-sidebar-terminal + name: '@deepseek-ai/dsh-client-ui-sidebar-terminal' - id: ui-sidebar-files name: '@deepseek-ai/dsh-client-ui-sidebar-files' diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 1e4da314cc..fbda35116d 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -118,7 +118,9 @@ "@deepseek-ai/dsh-workspace": "workspace:^", "@deepseek-ai/schemastery": "workspace:^", "commander": "^15.0.0", - "open": "^11.0.0" + "open": "^11.0.0", + "@deepseek-ai/dsh-api-terminal-controller": "workspace:^", + "@deepseek-ai/dsh-client-ui-sidebar-terminal": "workspace:^" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^", diff --git a/packages/client/connection/README.i18n.yaml b/packages/client/connection/README.i18n.yaml index 9793a90129..7e91c669e0 100644 --- a/packages/client/connection/README.i18n.yaml +++ b/packages/client/connection/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/connection/README.md -README.md: 606f27f37a80c11c105a1431858efae7b8e9d86a -README.zh.md: d7885ad873fb1d7d82c7a613c8f66bc7356b2542 +README.md: c8c2b2f6edb0ce43a07a664bcc2b3c9ece047b7b +README.zh.md: 2c25c0d3520fd7e5d3eeeec98c41cc1d3d4d25d8 diff --git a/packages/client/connection/README.md b/packages/client/connection/README.md index 606f27f37a..c8c2b2f6ed 100644 --- a/packages/client/connection/README.md +++ b/packages/client/connection/README.md @@ -25,9 +25,7 @@ The package carries browser-to-Host Remote calls, exact Fetch responses, and con ## Use this package -The browser uses HTTP POST for Remote unary calls. API Gateway owns the `/api/remote.mux` WebSocket and its logical streams; shell-owned compositions provide equivalent Remote streams through `connection.rpc.open` without opening a WebSocket. The Host half always provides the carrier-neutral RPC and exact `GET`/`HEAD`/`POST` route registries. When a Web carrier is present it also owns the sole `/api` route, Fetch bridge, browser authentication, and Host/Origin checks; a shell-owned carrier dispatches the shared Fetch handler directly. Each exact route declares buffered or streaming request-body handling before the bridge reads any bytes. Typert Gateway claims generated Remote endpoints, feature packages register non-JSON responses such as Session-log downloads and raw file uploads, and unclaimed requests return 404. Loopback hostname classification remains package-internal to the browser-facing Client state. Browser raw-body transfer is provided by [`dsh-client-file-upload`](../file-upload/README.md). - -The browser fixture follows the [Session Controller fork semantics](../../api/session-controller/README.md#use-this-package). +The browser uses HTTP POST for Remote unary calls. API Gateway owns the `/api/remote.mux` WebSocket and its logical streams; shell-owned compositions provide equivalent Remote streams through `connection.rpc.open` without opening a WebSocket. The browser plugin reads the page transport, recovery settings, and location, then delegates to `installConnection(ctx, options)`. A composition that owns its carrier may call the same installer directly; the whole-client test tier does so. Each invocation creates one Context-owned service, so several Client trees can use different carriers in one realm. The Host half always provides the carrier-neutral RPC and exact `GET`/`HEAD`/`POST` route registries. When a Web carrier is present it also owns the sole `/api` route, Fetch bridge, browser authentication, and Host/Origin checks; a shell-owned carrier dispatches the shared Fetch handler directly. Each exact route declares buffered or streaming request-body handling before the bridge reads any bytes. Typert Gateway claims generated Remote endpoints, feature packages register non-JSON responses such as Session-log downloads and raw file uploads, and unclaimed requests return 404. Loopback hostname classification remains package-internal to the browser-facing Client state. Browser raw-body transfer is provided by [`dsh-client-file-upload`](../file-upload/README.md). ----- diff --git a/packages/client/connection/README.zh.md b/packages/client/connection/README.zh.md index d7885ad873..2c25c0d352 100644 --- a/packages/client/connection/README.zh.md +++ b/packages/client/connection/README.zh.md @@ -25,9 +25,7 @@ kind: "package-reference" ## 使用本包 -浏览器通过 HTTP POST 执行 Remote 一元调用;API Gateway 自己拥有 `/api/remote.mux` WebSocket 及其逻辑流。由 shell 持有的组合通过 `connection.rpc.open` 提供等价的 Remote 流,不打开 WebSocket。Host half 始终提供与载体无关的 RPC 注册表和精确 `GET`/`HEAD`/`POST` 路由注册表。存在 Web 载体时,它还持有唯一 `/api` route、Fetch bridge、浏览器认证与 Host/Origin 校验;由 shell 持有的载体则直接分派共享 Fetch handler。每条精确路由会在 bridge 读取任何字节前声明缓冲或流式请求体处理方式。Typert Gateway 认领生成的 Remote endpoint,功能包注册 Session 日志下载、原始文件上传等非 JSON 响应,未认领的请求返回 404。Loopback hostname 判定只供浏览器侧当前页面状态使用,留在包内。浏览器原始请求体传输由 [`dsh-client-file-upload`](../file-upload/README.zh.md) 提供。 - -浏览器 fixture 遵循 [Session Controller 的 fork 语义](../../api/session-controller/README.zh.md#use-this-package)。 +浏览器通过 HTTP POST 执行 Remote 一元调用;API Gateway 自己拥有 `/api/remote.mux` WebSocket 及其逻辑流。由 shell 持有的组合通过 `connection.rpc.open` 提供等价的 Remote 流,不打开 WebSocket。浏览器插件读取页面 transport、恢复设置与 location,再委托 `installConnection(ctx, options)`。持有自身载体的组合可以直接调用同一个安装函数;整机客户端测试档就是这一消费者。每次调用都会创建一个归所属 Context 的服务,因此同一 realm 中的多棵 Client 树可以使用不同载体。Host half 始终提供与载体无关的 RPC 注册表和精确 `GET`/`HEAD`/`POST` 路由注册表。存在 Web 载体时,它还持有唯一 `/api` route、Fetch bridge、浏览器认证与 Host/Origin 校验;由 shell 持有的载体则直接分派共享 Fetch handler。每条精确路由会在 bridge 读取任何字节前声明缓冲或流式请求体处理方式。Typert Gateway 认领生成的 Remote endpoint,功能包注册 Session 日志下载、原始文件上传等非 JSON 响应,未认领的请求返回 404。Loopback hostname 判定只供浏览器侧当前页面状态使用,留在包内。浏览器原始请求体传输由 [`dsh-client-file-upload`](../file-upload/README.zh.md) 提供。 ----- diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index 4732dcdd71..913a7bf56e 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-client-connection", - "description": "Authenticated RPC transport, generation lifecycle, and browser fixture", + "description": "Authenticated RPC transport and generation lifecycle", "version": "0.1.5-rc.2", "publishConfig": { "access": "public" @@ -48,16 +48,10 @@ }, "devDependencies": { "@deepseek-ai/cordis": "workspace:^", - "@deepseek-ai/dsh-api-session-controller": "workspace:^", "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", - "@deepseek-ai/dsh-commands": "workspace:^", - "@deepseek-ai/dsh-host-directory-picker": "workspace:^", "@deepseek-ai/dsh-host-webserver": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", - "@deepseek-ai/dsh-session": "workspace:^", - "@deepseek-ai/dsh-settings": "workspace:^", - "@deepseek-ai/dsh-tool-todo": "workspace:^", - "@deepseek-ai/dsh-util-values": "workspace:^" + "@deepseek-ai/dsh-session": "workspace:^" } } diff --git a/packages/client/connection/src/client/api.ts b/packages/client/connection/src/client/api.ts index 41e384c662..a3de74c4fe 100644 --- a/packages/client/connection/src/client/api.ts +++ b/packages/client/connection/src/client/api.ts @@ -16,8 +16,8 @@ export type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm/types' import type { RpcResponse, RpcResult } from '../rpc.ts' /** - * Return the business result carried by a narrow fixture response. - * @param response - fixture response to unwrap. + * Return the business result carried by an RPC response. + * @param response - RPC response to unwrap. * @returns the response's business result. */ export function resultOf(response: RpcResponse): RpcResult { diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts deleted file mode 100644 index b270122a4a..0000000000 --- a/packages/client/connection/src/client/fixture.ts +++ /dev/null @@ -1,4053 +0,0 @@ -// Standalone browser fixture for UI development without a server. - -import { - createAssistantMessage, - createSystemMessage, - createToolResultMessage, - createUserMessage, -} from '@deepseek-ai/dsh-llm/message' -import { brandString } from '@deepseek-ai/dsh-brand' -import type { MessageId, ToolCallId } from '@deepseek-ai/dsh-llm/brand' -import type { - AssistantMessage, - ContentBlock, - MessageSource, - StreamChunk, - TokenUsage, - ToolResultMessage, - UserMessage, -} from '@deepseek-ai/dsh-llm' -import { LlmAttemptId } from '@deepseek-ai/dsh-llm/brand' -import { - AssistantStreamAccumulator, - expandAssistantStream, - type AssistantStreamRecord, -} from '@deepseek-ai/dsh-llm/assistant-stream' -import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' -import type { - SessionEvent, - SessionId, - SessionSeqCursor, -} from '@deepseek-ai/dsh-session/types' -import { SESSION_FORMAT_VERSION, SessionSeq } from '@deepseek-ai/dsh-session/types' -import type { JsonValue } from '@deepseek-ai/dsh-util-values' -import type { TodoItem } from '@deepseek-ai/dsh-tool-todo/client' -// Type-only: the brand constructor is host-side; the fixture casts at its -// wire-fabrication boundary (the schema layer's one-cast-point posture). -import type { CommandDefinitionId, CommandId } from '@deepseek-ai/dsh-commands/brand' -import type { CommandDescriptor, CommandExecution, CommandResult } from '@deepseek-ai/dsh-commands/types' -import type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types' -import type { DirectoryListing as FixtureDirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types' -import type { SettingsDescribeValue, SettingsNamespaceView } from '@deepseek-ai/dsh-settings/types' -import { deriveEventMessage, foldSurface } from '@deepseek-ai/dsh-session/surface' -import type { RpcResult } from './api.ts' -import { randomUuid } from './random-uuid.ts' -import type { - ClientConnectionRpc, ConnectionRpcFailure, ConnectionRpcResult, -} from '../rpc.ts' - -const FIXTURE_SESSION_SEARCH_RESULT_LIMIT = 20 - -interface ModelSelection { - readonly provider: string - readonly model: string - readonly reasoningEffort?: string -} - -interface ModelProviderGroup { - readonly id: string - readonly name: string - readonly models: readonly { - readonly id: string - readonly name: string - readonly description?: string - readonly reasoning?: { - readonly efforts: readonly { readonly id: string; readonly name: string; readonly description?: string }[] - readonly defaultEffort?: string - } - }[] -} - -/* jscpd:ignore-start -- The standalone fixture mirrors host timing without importing a target implementation. */ -function isFixtureTokenDelta(chunk: StreamChunk): boolean { - switch (chunk.type) { - case 'text-delta': - case 'reasoning-delta': - return chunk.text !== '' - case 'tool-call-delta': - return chunk.argumentsDelta !== '' || chunk.name !== undefined - default: - return false - } -} -/* jscpd:ignore-end */ - -interface FixtureSessionSummary { - readonly sessionId: SessionId - updatedAt: number - running: boolean - blank: boolean - readonly parentSessionId?: SessionId - readonly origin?: 'subagent' - readonly cwd?: string - readonly agentPreset?: string - readonly projections?: FixtureProjectionsBlock -} - -interface FixtureProjectionsBlock { - readonly asOfSeq: number - readonly values: Readonly> -} - -interface FixtureHistoryEntry { - readonly type: 'event' - readonly event: SessionEvent -} - -type FixtureHistoryRecord = FixtureHistoryEntry - -type FixtureSessionAddress = - | { readonly kind: 'session'; readonly sessionId: SessionId } - | { - readonly kind: 'subagent' - readonly parentSessionId: SessionId - readonly childSessionId: SessionId - readonly mode: 'one-shot' | 'continuable' - } - -interface FixtureFollowRequest { - readonly address: FixtureSessionAddress - readonly maxMessages?: number - readonly assistantStream?: true -} - -interface FixturePageRequest { - readonly address: FixtureSessionAddress - readonly throughSeq: number - readonly beforeSeq?: number - readonly maxMessages?: number -} - -interface FixtureSessionWireHeader { - readonly version: number - readonly id: SessionId - readonly createdAt: number - readonly cwd?: string - readonly parentSession?: SessionId - readonly isSeeded: boolean - readonly origin?: 'subagent' - readonly delegationDepth?: number - readonly agentPreset?: string -} - -type FixtureFollowFrame = - | { - readonly type: 'snapshot' - readonly header: FixtureSessionWireHeader - readonly cursor: number - readonly records: readonly FixtureHistoryRecord[] - readonly hasMore: boolean - readonly projections: FixtureProjectionsBlock - readonly assistantStream?: FixtureAssistantStreamBaseline - } - | FixtureHistoryEntry - | { readonly type: 'assistant-stream'; readonly frame: FixtureAssistantStreamFrame } - -type FixtureFollowEventFrame = Exclude - -interface FixtureAssistantStreamBaseline { - readonly revision: number - readonly activeAttempt?: { - readonly attemptId: ReturnType - readonly startedAfterSeq: number - readonly turn: number - readonly step: number - readonly nextIndex: number - readonly stream: readonly AssistantStreamRecord[] - } -} - -/** Assistant frame emitted by the standalone fixture; tests pin it to the controller wire type. */ -export type FixtureAssistantStreamFrame = - | { - readonly type: 'start' - readonly attemptId: ReturnType - readonly revision: number - readonly startedAfterSeq: SessionSeqCursor - readonly turn: number - readonly step: number - } - | { - readonly type: 'chunk' - readonly attemptId: ReturnType - readonly revision: number - readonly index: number - readonly time: number - readonly chunk: JsonValue - } - | { - readonly type: 'end' - readonly attemptId: ReturnType - readonly revision: number - readonly index: number - readonly outcome: - | { - readonly kind: 'committed' - readonly eventType: 'assistant/message' | 'assistant/attempt' - readonly seq: number - } - | { readonly kind: 'abandoned' } - } - -interface FixtureRemoteEventNotificationFrame { - readonly type: 'emit' - readonly event: string - readonly args: readonly unknown[] -} - -interface FixtureRemoteEventInvocationFrame { - readonly type: 'waterfall' - readonly event: string - readonly eventId: string - readonly agentId: SessionId - readonly request: Readonly> -} - -interface FixtureRemoteEventCancellationFrame { - readonly type: 'cancel' - readonly eventId: string -} - -type FixtureRemoteEventFrame = - | FixtureRemoteEventNotificationFrame - | FixtureRemoteEventInvocationFrame - | FixtureRemoteEventCancellationFrame - -interface FixtureRemoteEventResult { - readonly clientId: string - readonly eventId: string - readonly outcome: - | { readonly kind: 'next' } - | { readonly kind: 'result'; readonly value?: unknown } - | { - readonly kind: 'rejected' - readonly error: { - readonly name: string - readonly message: string - readonly code?: string - readonly details?: unknown - } - } -} - -interface FixtureRemoteEventReadyFrame { - readonly type: 'ready' - readonly clientId: string - readonly host: { readonly home: string } -} - -interface FixtureProjectionFrame { - readonly type: 'projection' - readonly sessionId: SessionId - readonly key: string - readonly value: unknown - readonly seq: number -} - -interface FixtureQuestionItem { - readonly id: string - readonly header?: string - readonly question: string - readonly detail?: string - readonly multiSelect?: boolean - readonly options?: readonly { readonly label: string; readonly description?: string }[] -} - -type FixtureControlFrame = - | { - readonly type: 'baseline' - readonly value: { - readonly queues: Readonly> - readonly jobs: Readonly> - readonly approvals: readonly never[] - readonly questions: readonly never[] - readonly projections: Readonly> - } - } - | FixtureProjectionFrame - -type FixturePromptPart = - | { readonly type: 'text'; readonly text: string } - | { - readonly type: 'image' - readonly mediaType: ImageAttachmentRef['mediaType'] - readonly data: string - readonly name?: string - } - -interface FixtureSessionApi { - list(request: { readonly cursor?: string }): Promise> - search( - request: { readonly query: string }, - signal: AbortSignal, - ): Promise> - create(request: { - readonly workspaceId?: WorkspaceId - readonly cwd?: string - readonly sessionId?: SessionId - readonly agentPreset?: string - }): Promise> - rename(request: { readonly sessionId: SessionId; readonly title: string }): Promise> - fork(request: { readonly sessionId: SessionId; readonly atSeq?: number }): Promise> - history(request: { - readonly sessionId: SessionId - readonly throughSeq?: number - readonly beforeSeq?: number - readonly maxMessages?: number - }): Promise> - selectModel(request: { - readonly sessionId: SessionId - readonly provider: string - readonly model: string - readonly reasoningEffort?: string - }): Promise> - prompt(request: { - readonly requestId: string - readonly sessionId: SessionId - readonly mode: 'queue' | 'steer' - readonly content: readonly FixturePromptPart[] - readonly clientTimeZone?: string - }): Promise> - attachment(request: { - readonly sessionId: SessionId - readonly attachmentId: AttachmentIdType - }): Promise> - updateQueue(request: { - readonly sessionId: SessionId - readonly itemId: MessageId - readonly action: unknown - }): Promise> - cancel(request: { readonly sessionId: SessionId }): Promise> -} - -type WorkspaceId = string & { readonly __fixtureWorkspaceId: 'WorkspaceId' } - -interface WorkspaceView { - readonly workspaceId: WorkspaceId - readonly path: string - readonly title: string - readonly sessionIds: readonly SessionId[] - readonly createdAt: string - readonly updatedAt: string -} - -interface WorkspaceCreateRequest { readonly path: string } -interface WorkspaceCreateValue { readonly workspace: WorkspaceView; readonly created: boolean } -interface WorkspaceRenameRequest { readonly workspaceId: WorkspaceId; readonly title: string } -interface WorkspaceValue { readonly workspace: WorkspaceView } -interface WorkspaceDeleteRequest { readonly workspaceId: WorkspaceId } -interface WorkspaceDeleteValue { readonly deleted: true } -interface WorkspaceInsertBeforeRequest { - readonly workspaceId: WorkspaceId - readonly beforeWorkspaceId?: WorkspaceId -} -interface WorkspaceOrderValue { readonly workspaceIds: readonly WorkspaceId[] } -interface WorkspaceInsertSessionBeforeRequest { - readonly workspaceId: WorkspaceId - readonly sessionId: SessionId - readonly beforeSessionId?: SessionId -} -interface WorkspaceArchiveSessionRequest { readonly sessionId: SessionId } -interface WorkspaceUnarchiveSessionRequest { readonly sessionId: SessionId } -interface WorkspaceArchiveValue { readonly archivedSessionIds: readonly SessionId[] } - -type WorkspaceFollowFrame = - | { - readonly type: 'baseline' - readonly value: { - readonly items: readonly WorkspaceView[] - readonly archivedSessionIds: readonly SessionId[] - } - } - | { readonly type: 'upsert'; readonly workspace: WorkspaceView } - | { readonly type: 'remove'; readonly workspaceId: WorkspaceId } - | { readonly type: 'order'; readonly workspaceIds: readonly WorkspaceId[] } - | { readonly type: 'archived'; readonly archivedSessionIds: readonly SessionId[] } - -interface FixtureWorkspaceApi { - create(request: WorkspaceCreateRequest): Promise> - rename(request: WorkspaceRenameRequest): Promise> - delete(request: WorkspaceDeleteRequest): Promise> - insertBefore(request: WorkspaceInsertBeforeRequest): Promise> - insertSessionBefore(request: WorkspaceInsertSessionBeforeRequest): Promise> - archiveSession(request: WorkspaceArchiveSessionRequest): Promise> - unarchiveSession(request: WorkspaceUnarchiveSessionRequest): Promise> -} - -interface FixtureWorkspace { - workspaceId: WorkspaceId - path: string - title: string - sessionIds: SessionId[] - createdAt: string - updatedAt: string -} - -function text(t: string): ContentBlock[] { - return [{ type: 'text', text: t }] -} - -function userMessage(content: ContentBlock[], source: MessageSource = { kind: 'user' }): UserMessage { - return createUserMessage({ content, source }) -} - -function assistantMessage(content: ContentBlock[], model = 'fx-1'): AssistantMessage { - return createAssistantMessage({ - content, - source: { provider: 'fixture', model }, - }) -} - -function toolResultMessage(callId: string, content: ContentBlock[], isError: boolean): ToolResultMessage { - return createToolResultMessage({ callId: brandString(callId), content, isError }) -} - -const MARKDOWN_FIXTURE = [ - '# Markdown fixture', - '', - 'Assistant output renders **strong text**, *emphasis*, and `inline code`.', - '', - '- first item', - ' - nested item', - '', - '| Area | State |', - '| --- | --- |', - '| history | rendered |', - '| streaming | stable |', - '', - '[DeepSeek](https://www.deepseek.com)', - '', - '```ts', - 'const markdown = true', - '```', -].join('\n') - -const USER_MARKDOWN_LITERAL = '用户字面量:# 不渲染 `code` [link](https://example.com)' - -/** - * SGR wrapper for the terminal output sample below: authoring the escapes as - * `\u001b` keeps literal control bytes out of this source file. - * @param code - the SGR parameter (an ANSI color or attribute number). - * @param body - the text the attribute applies to. - * @returns the body wrapped in the attribute and a reset. - */ -function sgr(code: number, body: string): string { - return `\u001b[${code}m${body}\u001b[0m` -} - -/** - * Terminal output sample for fixture turn 66, authored to carry every feature - * the terminal card draws that turn 60's two prompt rows cannot reach: - * basic-16 SGR foreground runs (green, red, bright-black) that must resolve to - * `--dsw-*` tokens, a bold run, column-aligned table rows that must scroll - * rather than fold, more than DEFAULT_TERMINAL_MAX_LINES (16) lines so the - * height cap collapses the middle. This constant is the visible body; the call - * site appends the shell result's `[exit code: N]` marker so Client derivation - * can consume it into the terminal status pill. - */ -const TERMINAL_OUTPUT_FIXTURE = [ - sgr(1, 'Running 4 checks'), - `${sgr(32, '\u2713')} typecheck 1.82s`, - `${sgr(32, '\u2713')} lint 0.94s`, - `${sgr(32, '\u2713')} duplication 2.10s`, - `${sgr(31, '\u2717')} unit 8.41s`, - '', - sgr(90, 'packages/client/ui-primitives/tests/terminal-block.client.spec.tsx'), - ` ${sgr(31, 'FAIL')} caps output at the configured line budget`, - ' expected 16 lines, received 24', - '', - 'NAME LINES BRANCHES FUNCTIONS UNCOVERED', - 'TerminalBlock.tsx 100% 100% 100% -', - 'ansi.ts 100% 100% 100% -', - 'clipboard.ts 100% 100% 100% -', - 'CodeBlock.tsx 98.4% 96.2% 100% 41-43', - 'highlight.ts 100% 100% 100% -', - 'Pill.tsx 100% 100% 100% -', - 'StateDot.tsx 100% 100% 100% -', - 'markdown/Markdown.tsx 100% 100% 100% -', - '', - sgr(31, '1 of 4 checks failed'), -].join('\n') - -/** - * Structured grep metadata for the search sample (turn 67). `truncated` with a - * larger `total` than the retained match count exercises the search card's - * capped indicator; the file with more than CHAT_SEARCH_MAX_LINES rows - * exercises its head/tail height cap. - */ -const SEARCH_MATCHES_FIXTURE: { path: string; matches: { lineNumber: number; line: string }[] }[] = [ - { - path: 'packages/client/ui-primitives/src/SearchBlock.tsx', - matches: [ - { lineNumber: 16, line: 'export const DEFAULT_SEARCH_MAX_LINES = 16' }, - { lineNumber: 138, line: 'export function SearchBlock(props: SearchBlockProps) {' }, - { lineNumber: 141, line: ' const [collapsed, setCollapsed] = useState>(() => new Set())' }, - ], - }, - { - path: 'packages/client/ui-tool/src/client/tool/models/search-card-model.ts', - matches: [ - { lineNumber: 45, line: 'export const CHAT_SEARCH_MAX_LINES = 8' }, - { lineNumber: 130, line: 'export function searchCardModel(block: ToolCallBlock): SearchCardModel | null {' }, - ], - }, - { - path: 'packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx', - matches: [ - { lineNumber: 34, line: 'export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) {' }, - { lineNumber: 36, line: ' const search = searchCardModel(block)' }, - { lineNumber: 56, line: ' search={search}' }, - { lineNumber: 78, line: " yield ctx.slots.register({ name: 'tool.call.toolview', key: 'grep', locale: NS }, SearchRow)" }, - ], - }, -] - -const SEARCH_MATCHES_TEXT = [ - 'Found 9 of 42 matches', - '', - ...SEARCH_MATCHES_FIXTURE.map(file => - [file.path, ...file.matches.map(m => `Line ${m.lineNumber}: ${m.line}`)].join('\n')), - '', - '(Full grep result stored at: fixture://spill/grep-66. Read it to see every match.)', -].join('\n') - -const SEARCH_PATHS_FIXTURE = [ - 'packages/client/ui-primitives/src/SearchBlock.tsx', - 'packages/client/ui-primitives/src/SearchBlock.module.css', - 'packages/client/ui-tool/src/client/tool/models/search-card-model.ts', - 'packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx', - 'packages/client/ui-tool/tests/search-card.client.spec.tsx', -] - -const SEARCH_PATHS_TEXT = [ - ...SEARCH_PATHS_FIXTURE, - '', - '(Showing 5 of 23 paths. Full sorted result stored at: fixture://spill/glob-67. Read it to see every path.)', -].join('\n') - -const READ_SAMPLE_FIRST_LINE = 41 -const READ_SAMPLE_SOURCE = [ - 'export interface ReadBlockProps {', - ' label?: string | undefined', - ' lines: readonly ReadBlockLine[]', - ' totalLines: number', - ' lang?: string | undefined', - ' maxLines?: number | undefined', - ' className?: string | undefined', - '}', - '', - '// A windowed read keeps the file line numbers in the gutter.', - 'const marker = "fixture read sample"', -] -const READ_SAMPLE_LINES = READ_SAMPLE_SOURCE.map((text, index) => ({ number: READ_SAMPLE_FIRST_LINE + index, text })) -const READ_SAMPLE_PATH = 'packages/client/ui-primitives/src/ReadBlock.tsx' -const READ_SAMPLE_TOTAL = 180 -const READ_SAMPLE_LAST_LINE = READ_SAMPLE_FIRST_LINE + READ_SAMPLE_SOURCE.length - 1 -const READ_SAMPLE_TEXT = [ - `${READ_SAMPLE_PATH}`, - 'file', - '', - ...READ_SAMPLE_SOURCE.map((text, index) => `${READ_SAMPLE_FIRST_LINE + index}: ${text}`), - '', - `(Showing lines ${READ_SAMPLE_FIRST_LINE}-${READ_SAMPLE_LAST_LINE} of ${READ_SAMPLE_TOTAL}. Use offset=${READ_SAMPLE_LAST_LINE + 1} to continue.)`, - '', -].join('\n') - -/** - * The `web_search` result metadata for the web-search turn. The sources cover a - * titled source with a snippet and date, a hostname-label fallback, and a - * titled source without a snippet; `truncated` exercises the capped indicator. - */ -const WEB_SEARCH_META = { - answer: 'DeepSeek Harness is a plugin-based agent harness on vendored Cordis where **every capability is a plugin**.', - sources: [ - { - url: 'https://github.com/deepseek-ai/deepseek-harness', - title: 'DeepSeek Harness — plugin-based agent harness', - snippet: 'Everything is a plugin: session, tools, agent-loop, and LLM adapters all mount on the same Cordis context.', - publishedAt: '2026-07-01', - }, - { - url: 'https://www.deepseek.com/blog/harness-architecture', - snippet: 'The capability-seam pattern splits each capability into interface, implementation, and consumer packages.', - }, - { - url: 'https://docs.deepseek.com/harness/plugins', - title: 'Writing a harness plugin', - publishedAt: '2026-06-15', - }, - ], - truncated: true, -} satisfies JsonValue - -/** The `web_fetch` result metadata for the web-fetch turn. */ -const WEB_FETCH_META = { - url: 'https://www.deepseek.com/blog/harness-architecture', - statusCode: 200, - truncated: false, -} satisfies JsonValue - -const DEEPSEEK_REASONING = { - efforts: [ - { id: 'off', name: 'Off' }, - { id: 'high', name: 'High' }, - { id: 'max', name: 'Max' }, - ], - defaultEffort: 'high', -} - -const OPENAI_REASONING = { - efforts: [ - { id: 'off', name: 'Off' }, - { id: 'medium', name: 'Medium' }, - { id: 'high', name: 'High' }, - { id: 'max', name: 'Max' }, - ], - defaultEffort: 'medium', -} - -/** Catalog served by `session/modelCatalog` (fresh copies per call). */ -function fixtureModelGroups(): ModelProviderGroup[] { - return [ - { - id: 'deepseek-official', - name: 'DeepSeek', - models: [ - { - id: 'deepseek-v4-flash', - name: 'DeepSeek-V4-Flash', - description: '快速响应', - reasoning: DEEPSEEK_REASONING, - }, - { - id: 'deepseek-v4-pro', - name: 'DeepSeek-V4-Pro', - description: '复杂任务', - reasoning: DEEPSEEK_REASONING, - }, - ], - }, - { - id: 'openai', - name: 'OpenAI', - models: [{ id: 'gpt-5', name: 'GPT-5', reasoning: OPENAI_REASONING }], - }, - ] -} - -function sid(id: string): SessionId { - return id as SessionId -} - -const FIXTURE_IMAGE_DATA = 'iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAYAAAA/xl1SAAAAvklEQVR42u3SMQ0AAAjAMIyhELM4AAe8PD1qYFlk9cCXEAEDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGxIBCYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIAYEAyIAcGAGBAMiAHBgBgQDIgBwYAYEAyIAcGAGBAMiAHBgBgQDIgB4bYWLb6pnOb1xAAAAABJRU5ErkJggg==' -const FIXTURE_IMAGE_REF: ImageAttachmentRef = { - attachmentId: 'fixture:image' as AttachmentIdType, - mediaType: 'image/png', - bytes: 247, - width: 160, - height: 90, - name: 'fixture-image.png', -} - -/** Deterministic provider billing attached to fixture assistant messages. */ -function fixtureUsage(turn: number, step: number): TokenUsage { - return { - inputTokens: 20 + turn % 5, - outputTokens: 8 + step, - cacheReadTokens: turn === 0 ? 0 : 80, - cacheWriteTokens: turn % 10 === 0 ? 4 : 0, - } -} - -/** Build a lossless settled stream for static fixture messages. */ -function fixtureSettledStream( - message: AssistantMessage, - usage: TokenUsage, - time: number, -): AssistantStreamRecord[] { - const stream: AssistantStreamRecord[] = [] - for (const [index, block] of message.content.entries()) { - stream.push( - { type: 'chunk', time, chunk: { type: 'block-start', index, blockType: block.type } }, - { type: 'chunk', time, chunk: { type: 'block-end', index, block } }, - ) - } - stream.push( - { type: 'chunk', time, chunk: { type: 'usage', usage } }, - { type: 'chunk', time, chunk: { type: 'finish', reason: { kind: 'stop' } } }, - ) - return stream -} - -/** Rendered system prompt of the fx-alpha history: surface node 0. */ -const FIXTURE_SYSTEM_PROMPT = '你是 DeepSeek Harness 的 fixture 助手。用简洁的中文回答,并在需要时调用工具。' - -/** fx-alpha history script: 75 turns (~150+ messages -> 4 pages at PAGE_MESSAGES=50), - * mixing reasoning blocks / tool call+result / context. */ -function buildAlphaLog(): SessionEvent[] { - const events: Record[] = [] - let time = Date.now() - 3_600_000 - const push = (e: Record): number => { - const seq = events.length - const data = e['data'] as Record | undefined - const nextTime = time + 800 - const authored = e['type'] === 'assistant/message' && data !== undefined - ? { - ...e, - data: { - ...data, - usage: fixtureUsage(data['turn'] as number, data['step'] as number), - stream: fixtureSettledStream( - data['message'] as AssistantMessage, - fixtureUsage(data['turn'] as number, data['step'] as number), - nextTime, - ), - }, - } - : e - events.push({ seq, time: (time = nextTime), ...authored }) - return seq - } - // Completed fixture requests retain the route capacity recorded with them. - push({ - type: 'request/context', - data: { provider: 'deepseek-official', model: 'deepseek-v4-flash', contextWindow: 128_000 }, - }) - for (let turn = 0; turn < 60; turn++) { - push({ type: 'turn/start', data: { turn } }) - // The rendered system prompt is surface node 0, ahead of the first user message. - if (turn === 0) { - push({ - type: 'system/message', surfaceOp: 'append', - data: { turn, step: 0, message: createSystemMessage(FIXTURE_SYSTEM_PROMPT, '@deepseek-ai/dsh-system-prompt') }, - }) - } - const userSeq = push({ - type: 'user/message', surfaceOp: 'append', - data: userMessage(text(turn === 59 ? USER_MARKDOWN_LITERAL : `问题 ${turn}:fixture 历史消息,用于翻页与渲染验收。`)), - }) - if (turn === 0) { - push({ - type: 'session/title', - data: { title: 'Fixture 历史会话', messageSeqs: [userSeq], source: { kind: 'fallback' } }, - }) - } - if (turn % 9 === 4) { - push({ type: 'user/message', surfaceOp: 'append', data: userMessage(text(`[fixture] 上下文注入(turn ${turn})`), { kind: 'plugin', plugin: 'fixture' }) }) - } - push({ type: 'step/start', data: { turn, step: 0 } }) - const withTool = turn % 5 === 2 - const withReasoning = turn % 3 === 1 - const blocks: ContentBlock[] = [] - if (withReasoning) blocks.push({ type: 'reasoning', text: `思考过程 ${turn}:这是一段可折叠的 reasoning 内容。` }) - blocks.push({ type: 'text', text: turn === 59 ? MARKDOWN_FIXTURE : `回答 ${turn}:这是 fixture 生成的历史回复正文。` }) - if (withTool) { - const callId = `fx-call-${turn}` - blocks.push({ type: 'tool-call', id: callId, name: 'echo', arguments: `{"text":"turn ${turn}"}` } as ContentBlock) - push({ type: 'assistant/message', surfaceOp: 'append', data: { turn, step: 0, message: assistantMessage(blocks) } }) - push({ type: 'tool/call', data: { turn, step: 0, callId, name: 'echo', arguments: `{"text":"turn ${turn}"}` } }) - push({ type: 'tool/result', surfaceOp: 'append', data: { turn, step: 0, message: toolResultMessage(callId, text(`ECHO: TURN ${turn}`), turn % 25 === 12) } }) - push({ type: 'step/end', data: { turn, step: 0 } }) - push({ type: 'step/start', data: { turn, step: 1 } }) - push({ type: 'assistant/message', surfaceOp: 'append', data: { turn, step: 1, message: assistantMessage(text(`工具结果已消化(turn ${turn})。`)) } }) - push({ type: 'step/end', data: { turn, step: 1 } }) - } else { - push({ type: 'assistant/message', surfaceOp: 'append', data: { turn, step: 0, message: assistantMessage(blocks) } }) - push({ type: 'step/end', data: { turn, step: 0 } }) - } - push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } }) - } - // The structured samples use real first-party names and result metadata so - // the fixture follows the same event-to-card path as a persisted Session. - // `echo` above remains the unknown-tool fallback. - const toolTurn = ( - turn: number, - name: string, - args: string, - resultText: string, - resultMeta?: JsonValue, - ): void => { - const callId = `fx-call-${turn}` - push({ type: 'turn/start', data: { turn } }) - push({ type: 'user/message', surfaceOp: 'append', data: userMessage(text(`问题 ${turn}:${name} 样本。`)) }) - push({ type: 'step/start', data: { turn, step: 0 } }) - push({ - type: 'assistant/message', surfaceOp: 'append', - data: { turn, step: 0, message: assistantMessage([{ type: 'tool-call', id: callId, name, arguments: args } as ContentBlock]) }, - }) - push({ type: 'tool/call', data: { turn, step: 0, callId, name, arguments: args } }) - push({ - type: 'tool/result', - surfaceOp: 'append', - data: { - turn, - step: 0, - message: toolResultMessage(callId, text(resultText), false), - ...resultMeta === undefined ? {} : { meta: resultMeta }, - }, - }) - push({ type: 'step/end', data: { turn, step: 0 } }) - push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } }) - } - // A two-line command, so the fixture covers the terminal card's one-row-per- - // command-line prompt (and that the card still marks the call exactly once). - toolTurn( - 60, - 'bash', - '{"command":"ls -la\\necho done","description":"fixture 终端样本","workdir":"/tmp/fixture"}', - 'total 2\ndrwxr-xr-x fixture\n-rw-r--r-- demo.txt', - ) - toolTurn( - 61, - 'write', - '{"file_path":"notes/demo.txt","content":"hello fixture\\n"}', - 'wrote notes/demo.txt', - { diffs: [{ path: 'notes/demo.txt', oldText: null, newText: 'hello fixture\n' }] }, - ) - toolTurn( - 62, - 'edit', - '{"file_path":"notes/demo.txt","old_string":"hello","new_string":"hello fixture"}', - '已编辑', - { diffs: [{ path: 'notes/demo.txt', oldText: 'hello', newText: 'hello fixture' }] }, - ) - toolTurn( - 63, - 'write', - '{"file_path":"notes/new-demo.txt","content":"hello fixture\\n"}', - '已写入', - { diffs: [{ path: 'notes/new-demo.txt', oldText: null, newText: 'hello fixture\n' }] }, - ) - // Turn 64: a multi-hunk edit — two scattered replacements in one file. Named - // `edit` so it lands on the keyed FileMutationRow (the resident diff card the - // single-hunk turn 62 also uses). Its result metadata carries two scattered - // hunks under one path header, so the card draws the first hunk, a `⋯` gap, - // then the second (the same-file - // second-hunk arm turns 62/63 cannot reach). - toolTurn( - 64, - 'edit', - '{"file_path":"src/config.ts","old_string":"const timeout = 30","new_string":"const timeout = 60"}', - '已编辑', - { - diffs: [ - { path: 'src/config.ts', oldText: 'const timeout = 30', newText: 'const timeout = 60' }, - { path: 'src/config.ts', oldText: 'retries: 1', newText: 'retries: 3' }, - ], - }, - ) - // Turn 65: one run_code turn with three logged sub-dispatches — the Code - // Mode acceptance surface (parent code row + nested native-identical rows, - // including an isError sub-call and a bash sub-call that must hit the same - // keyed registration a top-level bash row uses). - { - const turn = 65 - const callId = `fx-call-${turn}` - const program = 'const listing = await tools.bash({ command: "ls notes", description: "List notes" })\n' - + 'const demo = await tools.read({ file_path: "notes/demo.txt" })\n' - + 'await tools.read({ file_path: "notes/missing.txt" }).catch(() => "tolerated")\n' - + 'return { listing, demo }' - const args = JSON.stringify({ code: program, description: 'Read the notes files and summarize' }) - push({ type: 'turn/start', data: { turn } }) - push({ type: 'user/message', surfaceOp: 'append', data: userMessage(text(`问题 ${turn}:run_code 样本。`)) }) - push({ type: 'step/start', data: { turn, step: 0 } }) - push({ - type: 'assistant/message', surfaceOp: 'append', - data: { turn, step: 0, message: assistantMessage([{ type: 'tool-call', id: callId, name: 'run_code', arguments: args } as ContentBlock]) }, - }) - push({ type: 'tool/call', data: { turn, step: 0, callId, name: 'run_code', arguments: args } }) - const dispatchPair = (n: number, name: string, dispatchArgs: Record, resultText: string, isError = false): void => { - push({ - type: 'tool/ptc-dispatch-start', - data: { rootCallId: callId, parentCallId: callId, subCallId: `${callId}:ptc:${n}`, name, arguments: dispatchArgs }, - }) - push({ - type: 'tool/ptc-dispatch', - data: { - rootCallId: callId, parentCallId: callId, subCallId: `${callId}:ptc:${n}`, name, - arguments: dispatchArgs, isError, content: [{ type: 'text', text: resultText }], - }, - }) - } - dispatchPair(1, 'bash', { command: 'ls notes', description: 'List notes' }, 'demo.txt\nnew-demo.txt') - dispatchPair(2, 'read', { file_path: 'notes/demo.txt' }, 'hello fixture\n') - dispatchPair(3, 'read', { file_path: 'notes/missing.txt' }, 'Error: ENOENT: notes/missing.txt not found', true) - push({ - type: 'tool/result', surfaceOp: 'append', - data: { turn, step: 0, message: toolResultMessage(callId, text('{"listing":"demo.txt\\nnew-demo.txt","demo":"hello fixture\\n"}'), false) }, - }) - push({ type: 'step/end', data: { turn, step: 0 } }) - push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } }) - } - // Turn 74: todo_write sample — the TodoRow toolview in the flow plus the - // todo/write snapshot event feeding the TodoPanel plan strip. Two items are - // in_progress: this fixture chooses the parallel policy, so both surfaces - // must render a parallel plan rather than the first active item alone. - const fixtureTodos = [ - { content: '梳理需求', status: 'completed' }, - { content: '实现 fixture 样本', status: 'in_progress' }, - { content: '跑后台构建', status: 'in_progress' }, - { content: '浏览器验收', status: 'pending' }, - ] - // Turn 66: the terminal sample turn 60's two clean prompt rows cannot cover — - // ANSI SGR coloring, output past the terminal card's height cap, a nested cwd - // whose prompt label is its last segment, and a non-zero exit. The raw result - // includes an `[exit code: N]` marker below; Client - // derivation consumes it into the status pill before rendering the body. - // - // Ordered BEFORE the todo turn deliberately: the standing plan retires at the - // next `turn/start`, so a turn appended after it would leave the dock's plan - // strip empty and take the todo surfaces' own coverage with it. - toolTurn( - 66, - 'bash', - '{"command":"pnpm run check","description":"fixture 终端样本","workdir":"/tmp/fixture/deep/nested"}', - `${TERMINAL_OUTPUT_FIXTURE}\n[exit code: 1]`, - ) - - // Turns 67-68 carry the search card's two metadata variants: grouped matches - // and a flat path list, both truncated with a larger pre-cap total. Both use - // the keyed SearchRow registration. They stay before the todo turn for the - // same standing-plan reason as the bash turn. - toolTurn( - 67, - 'grep', - '{"pattern":"SEARCH_MAX_LINES","path":"packages/client"}', - SEARCH_MATCHES_TEXT, - { shape: 'matches', files: SEARCH_MATCHES_FIXTURE, truncated: true, total: 42 }, - ) - toolTurn( - 68, - 'glob', - '{"pattern":"**/SearchBlock*","path":"packages/client"}', - SEARCH_PATHS_TEXT, - { shape: 'paths', paths: SEARCH_PATHS_FIXTURE, truncated: true, total: 23 }, - ) - - // Turn 69: the read sample — a WINDOW past an offset so the card draws file - // line numbers starting above 1 and a "showing N of M" note (the window is - // shorter than READ_SAMPLE_TOTAL), with a `ts` language hint the shiki path - // highlights. Named `read`, so it exercises the keyed ReadRow registration. - // The run_code sub-dispatches above cover nested read calls without result - // metadata; this top-level result carries the structured window. - toolTurn( - 69, - 'read', - `{"file_path":${JSON.stringify(READ_SAMPLE_PATH)},"offset":${READ_SAMPLE_FIRST_LINE}}`, - READ_SAMPLE_TEXT, - { - path: READ_SAMPLE_PATH, - offset: READ_SAMPLE_FIRST_LINE, - lines: READ_SAMPLE_LINES, - totalLines: READ_SAMPLE_TOTAL, - lang: 'ts', - }, - ) - - // Turns 70-71 carry the web tools' result metadata. They stay before the todo - // turn because a later turn/start retires the standing plan projection. - toolTurn( - 70, - 'web_search', - '{"queries":["deepseek harness architecture"]}', - 'Search results for deepseek harness architecture.', - WEB_SEARCH_META, - ) - toolTurn( - 71, - 'web_fetch', - '{"url":"https://www.deepseek.com/blog/harness-architecture"}', - '# Harness architecture\n\nEverything is a plugin.', - WEB_FETCH_META, - ) - - // Turn 72: max-tokens sample — the provider ends the turn at its output cap - // mid-sentence, so the chat flow must render the turn-max-tokens notice - // instead of ending silently. Ordered before the todo turn for the same - // standing-plan reason the bash turn is. - push({ type: 'turn/start', data: { turn: 72 } }) - push({ type: 'user/message', surfaceOp: 'append', data: userMessage(text('问题 72:请完整列出全部一百条条目。')) }) - push({ type: 'step/start', data: { turn: 72, step: 0 } }) - push({ - type: 'assistant/message', - surfaceOp: 'append', - data: { turn: 72, step: 0, message: assistantMessage(text('条目 1:第一条。条目 2:第二条。条目 3:这一条写到一半被')) }, - }) - push({ type: 'step/end', data: { turn: 72, step: 0 } }) - push({ type: 'turn/end', data: { turn: 72, reason: { kind: 'max-tokens' } } }) - - // Turn 73: user and assistant images share one durable fixture object. - // The todo turn remains last so its standing projection stays visible. - push({ type: 'turn/start', data: { turn: 73 } }) - push({ - type: 'user/message', - surfaceOp: 'append', - data: userMessage([{ type: 'image', attachment: FIXTURE_IMAGE_REF }, ...text('历史用户图片')]), - }) - push({ type: 'step/start', data: { turn: 73, step: 0 } }) - push({ - type: 'assistant/message', - surfaceOp: 'append', - data: { - turn: 73, - step: 0, - message: assistantMessage( - [...text('结构化模型图片:'), { type: 'image', attachment: FIXTURE_IMAGE_REF }], - 'fx-vision', - ), - }, - }) - push({ type: 'step/end', data: { turn: 73, step: 0 } }) - push({ type: 'turn/end', data: { turn: 73, reason: { kind: 'completed' } } }) - - const todoArgs = JSON.stringify({ todos: fixtureTodos }) - toolTurn(74, 'todo_write', todoArgs, 'Updated todo list: 1 pending, 2 in progress, 1 completed.') - // The real tool appends the snapshot mid-execution — between tool/call and - // tool/result — so the fixture reproduces that exact ordering (the last - // toolTurn events run ... tool/call, tool/result, step/end, turn/end). - const callIndex = events.length - 4 - const callTime = events[callIndex]?.time as number - events.splice(callIndex + 1, 0, { type: 'todo/write', time: callTime + 400, data: { todos: fixtureTodos } }) - events.forEach((e, i) => { e.seq = i }) - return events as unknown as SessionEvent[] -} - -/** - * Fixture parallel of the plan unit's lifecycle fold. The paired - * `command/done` retains successful plan selections and drops failures; - * `plan/mode` commits one. `wanted` is exposed for the prompt boundary (the - * fixture's step/start parallel). - */ -function foldPlan(log: readonly SessionEvent[]): { active: boolean; pending: boolean; wanted: boolean | null } { - let active = false - let wanted: boolean | null = null - let running: { commandId: unknown; wanted: boolean } | null = null - for (const event of log) { - const item = event as unknown as { type: string; data?: Record } - if (item.type === 'command/run' && item.data?.['name'] === 'plan') { - const args = item.data['args'] - if (typeof args !== 'string') continue - running = { commandId: item.data['commandId'], wanted: args.trim() !== 'off' } - } else if (item.type === 'command/done' - && item.data !== undefined - && running !== null - && item.data['commandId'] === running.commandId) { - wanted = item.data['kind'] === 'success' && running.wanted !== active ? running.wanted : null - running = null - } else if (item.type === 'plan/mode') { - active = item.data?.['active'] === true - wanted = null - } - } - const selected = running?.wanted ?? wanted - return { active, pending: selected !== null && selected !== active, wanted: selected } -} - -/** The plan projection's wire view over the full log. */ -function planViewOf(log: readonly SessionEvent[]): { active: boolean; pending: boolean } { - const plan = foldPlan(log) - return { active: plan.active, pending: plan.pending } -} - -/** Fixture parallel of the host's projection units: whole current values per key over the full log. */ -/** Fixture preset table (the host PermissionPresetService defaults). */ -const PERMISSION_PRESETS: Record = { - 'workspace-write': { sandbox: 'workspace-write', approval: 'ask', description: 'Write inside the workspace and permitted temporary directories; wider retries require approval.' }, - 'danger-full-access': { sandbox: 'danger-full-access', approval: 'never', description: 'Full file access without approval prompts.' }, -} - -/** Host permissions-unit parallel: fold the three knob events, derive the select over the fixture defaults. */ -function permissionSelectOf( - log: readonly SessionEvent[], -): { options: { value: string; name: string; description?: string }[]; currentValue: string } { - let preset: string | null = null - let sandbox = 'workspace-write' - let approval = 'ask' - for (const event of log) { - const item = event as { type: string; data: Record } - if (item.type === 'permission/preset') preset = item.data['preset'] as string - else if (item.type === 'sandbox/mode') sandbox = item.data['mode'] as string - else if (item.type === 'approval/policy') approval = item.data['policy'] as string - } - const matches = (spec: { sandbox: string; approval: string }): boolean => spec.sandbox === sandbox && spec.approval === approval - let currentValue = 'custom' - const folded = preset === null ? undefined : PERMISSION_PRESETS[preset] - if (preset !== null && folded !== undefined && matches(folded)) { - currentValue = preset - } else { - for (const [name, spec] of Object.entries(PERMISSION_PRESETS)) { - if (matches(spec)) { currentValue = name; break } - } - } - return { - options: [ - ...Object.entries(PERMISSION_PRESETS).map(([value, spec]) => ({ value, name: value, description: spec.description })), - ...currentValue === 'custom' ? [{ value: 'custom', name: 'Custom', description: 'Current sandbox and approval settings do not match a preset.' }] : [], - ], - currentValue, - } -} - -interface FixtureTokenUsageProjection { - uncachedInputTokens: number - outputTokens: number - cacheReadTokens: number - cacheWriteTokens: number -} - -interface FixtureUsageSample { - turn: number - step: number - usage: TokenUsage -} - -/** Read one provider usage sample from either durable carrier. */ -function usageSampleOf(event: SessionEvent): FixtureUsageSample | undefined { - if (event.type !== 'assistant/message' && event.type !== 'assistant/attempt') return undefined - let usage = event.type === 'assistant/message' ? event.data.usage : undefined - for (const member of expandAssistantStream(event.data.stream)) { - if (member.chunk.type === 'usage') usage = member.chunk.usage - } - return usage === undefined - ? undefined - : { turn: event.data.turn, step: event.data.step, usage } -} - -/** Fixture parallel of token-meter's last-sample-replacing usage projection. */ -function tokenUsageOf(log: readonly SessionEvent[]): FixtureTokenUsageProjection { - const totals: FixtureTokenUsageProjection = { - uncachedInputTokens: 0, - outputTokens: 0, - cacheReadTokens: 0, - cacheWriteTokens: 0, - } - let last: { - turn: number - step: number - buckets: FixtureTokenUsageProjection - } | null = null - for (const event of log) { - const sample = usageSampleOf(event) - if (sample === undefined) continue - const buckets: FixtureTokenUsageProjection = { - uncachedInputTokens: sample.usage.inputTokens, - outputTokens: sample.usage.outputTokens, - cacheReadTokens: sample.usage.cacheReadTokens ?? 0, - cacheWriteTokens: sample.usage.cacheWriteTokens ?? 0, - } - const previous = last?.turn === sample.turn && last.step === sample.step - ? last.buckets - : undefined - totals.uncachedInputTokens += buckets.uncachedInputTokens - (previous?.uncachedInputTokens ?? 0) - totals.outputTokens += buckets.outputTokens - (previous?.outputTokens ?? 0) - totals.cacheReadTokens += buckets.cacheReadTokens - (previous?.cacheReadTokens ?? 0) - totals.cacheWriteTokens += buckets.cacheWriteTokens - (previous?.cacheWriteTokens ?? 0) - last = { turn: sample.turn, step: sample.step, buckets } - } - return totals -} - -/** Fixture parallel of session-stats' whole-log counting and wall-time fold. */ -function sessionStatsOf(log: readonly SessionEvent[]): { - turns: number - steps: number - llmMs: number - toolMs: number - ttftMs: number - ttftSteps: number - decodeMs: number - decodeTokens: number -} { - const value = { turns: 0, steps: 0, llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0 } - let lastTurn: number | null = null - let openStep: { turn: number; step: number; startTime: number; firstTokenTime: number | null } | null = null - const pendingCalls = new Map() - for (const event of log) { - switch (event.type) { - case 'step/start': - openStep = { turn: event.data.turn, step: event.data.step, startTime: event.time, firstTokenTime: null } - break - case 'assistant/attempt': { - if (openStep === null || openStep.turn !== event.data.turn || openStep.step !== event.data.step) break - const first = expandAssistantStream(event.data.stream) - .find(member => isFixtureTokenDelta(member.chunk))?.time - if (openStep.firstTokenTime === null && first !== undefined) openStep.firstTokenTime = first - break - } - case 'assistant/message': { - if (openStep === null || openStep.turn !== event.data.turn || openStep.step !== event.data.step) break - const first = expandAssistantStream(event.data.stream) - .find(member => isFixtureTokenDelta(member.chunk))?.time - if (openStep.firstTokenTime === null && first !== undefined) openStep.firstTokenTime = first - value.llmMs += Math.max(0, event.time - openStep.startTime) - if (openStep.firstTokenTime !== null) { - value.ttftMs += Math.max(0, openStep.firstTokenTime - openStep.startTime) - value.ttftSteps += 1 - const outputTokens = event.data.usage?.outputTokens - if (typeof outputTokens === 'number' && Number.isFinite(outputTokens) && outputTokens >= 0) { - value.decodeMs += Math.max(0, event.time - openStep.firstTokenTime) - value.decodeTokens += outputTokens - } - } - openStep = null - break - } - case 'tool/call': - pendingCalls.set(event.data.callId, event.time) - break - case 'tool/result': { - const callId = event.data.message.source.callId - const dispatched = pendingCalls.get(callId) - if (dispatched === undefined) break - pendingCalls.delete(callId) - value.toolMs += Math.max(0, event.time - dispatched) - break - } - case 'step/end': - if (event.data.turn !== lastTurn) { - value.turns += 1 - lastTurn = event.data.turn - } - value.steps += 1 - openStep = null - break - case 'turn/end': - pendingCalls.clear() - break - default: - break - } - } - return value -} - -interface FixtureRequestContext { - provider: string - model: string - contextWindow?: number -} - -interface FixtureContextBreakdownProjection { - systemTokens: number - toolsTokens: number - messageTokens: number -} - -/** Fixed token-meter heuristic constants mirrored by this client-only fixture. */ -const CHARS_PER_TOKEN = 4 -const BLOCK_OVERHEAD = 4 -const ROLE_OVERHEAD = 4 - -/** Price fixture content with token-meter's fixed-density heuristic. */ -function estimateFixtureContent(blocks: readonly ContentBlock[]): number { - const densityPrice = (value: string): number => Math.ceil(value.length / CHARS_PER_TOKEN) - return blocks.reduce((tokens, block) => { - if (block.type === 'text' || block.type === 'reasoning') { - return tokens + densityPrice(block.text) + BLOCK_OVERHEAD - } - if (block.type === 'tool-call') { - return tokens + densityPrice(block.name) + densityPrice(block.arguments) + BLOCK_OVERHEAD - } - // ContentBlockMap is merge-extensible: this client graph sees only the - // base four members, but fixture turns do carry extended blocks at - // runtime, so the structural JSON fallback below is live code. - if (block.type === 'tool-result') { - return tokens + estimateFixtureContent(block.content) + BLOCK_OVERHEAD - } - return tokens + densityPrice(JSON.stringify(block)) + BLOCK_OVERHEAD - }, 0) -} - -/** - * Fixture parallel of token-meter's heuristic context-composition projection. - * The system prompt is the system-role surface node; it prices as text plus - * role framing with no block overhead and stays out of the message figure. - */ -function contextBreakdownOf(log: readonly SessionEvent[]): FixtureContextBreakdownProjection { - const headerEvent = log.findLast(event => event.type === 'request/header') - const header = headerEvent === undefined - ? undefined - : headerEvent.data.header - let systemTokens = 0 - let messageTokens = 0 - for (const seq of foldSurface(log).nodes) { - const event = log[seq] - if (event === undefined) continue - const message = deriveEventMessage(event) - if (message === null) continue - if (message.role === 'system') { - const characters = message.content.reduce( - (total, block) => total + (block.type === 'text' ? block.text.length : JSON.stringify(block).length), - 0, - ) - systemTokens = Math.ceil(characters / CHARS_PER_TOKEN) + ROLE_OVERHEAD - continue - } - messageTokens += estimateFixtureContent(message.content) + ROLE_OVERHEAD - } - return { - systemTokens, - toolsTokens: header?.tools === undefined || header.tools.length === 0 - ? 0 - : Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD, - messageTokens, - } -} - -/** Latest log-only route context, or undefined before any request ran. */ -function lastRequestContext( - log: readonly SessionEvent[], -): FixtureRequestContext | undefined { - const event = log.findLast(item => (item as { type: string }).type === 'request/context') - return event === undefined - ? undefined - : (event as unknown as { data: FixtureRequestContext }).data -} - -/** - * Fixture parallel of token-meter's request-pressure projection: the last - * provider-reported prompt size paired with the last recorded capacity. The - * two need not come from one request — see the token-meter README. The host's - * `projectedTokens` is deliberately absent: reproducing it would mean - * reimplementing the estimator client-side, and every consumer falls back to - * the bare sample, so a fixture-driven view simply lags a compaction the way - * the projection did before that field existed. - */ -function contextPressureOf( - log: readonly SessionEvent[], -): { pressureTokens?: number; contextWindow?: number } { - let pressureTokens: number | undefined - for (const event of log) { - const sample = usageSampleOf(event) - if (sample === undefined) continue - pressureTokens = sample.usage.inputTokens - + (sample.usage.cacheReadTokens ?? 0) - + (sample.usage.cacheWriteTokens ?? 0) - } - const contextWindow = lastRequestContext(log)?.contextWindow - return { - ...pressureTokens === undefined ? {} : { pressureTokens }, - ...contextWindow === undefined ? {} : { contextWindow }, - } -} - -function projectionValuesOf(log: readonly SessionEvent[]): Record { - const values: Record = {} - values['modelSelection'] = modelSelectionProjectionOf(log) - const titleEvent = log.findLast(item => (item as { type: string }).type === 'session/title') - if (titleEvent !== undefined) { - values['title'] = (titleEvent as unknown as { data: { title: string } }).data.title - } - // Always present (tool-todo unit composed): null when no plan stands. - values['todos'] = backscanTodos(log) ?? null - // Always present (permission service composed): the whole select. - values['permissions'] = permissionSelectOf(log) - // Always present (plan-mode unit composed): the {active, pending} view. - values['plan'] = planViewOf(log) - // Always present (GoalService unit composed): null before create / after clear. - values['goal'] = backscanGoal(log) - // Always present (token-meter composed): full-log provider billing. - values['tokenUsage'] = tokenUsageOf(log) - // Always present (token-meter composed): last request pressure and capacity. - values['contextPressure'] = contextPressureOf(log) - // Always present (token-meter composed): heuristic request composition. - values['contextBreakdown'] = contextBreakdownOf(log) - // Always present (session-stats unit composed): whole-log turn/step counts. - values['sessionStats'] = sessionStatsOf(log) - // Always present (attachment service composed): the deployment image - // limits, constant per boot (mirrors the attachment-local defaults). - // Deliberate host divergence: the real gateway never pushes an imageLimits - // change frame (constant unit), but the fixture's uniform baseline replay - // frames every key here, incidentally exercising higher-seq-wins. - values['imageLimits'] = { - maxImageBytes: 5 * 1024 * 1024, - maxImagesPerMessage: 20, - maxMessageImageBytes: 100 * 1024 * 1024, - maxImagePixels: 40_000_000, - maxImageDimension: 2000, - mediaTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], - } - return values -} - -function modelSelectionProjectionOf(log: readonly SessionEvent[]): { - lastUsed: ModelSelection | null - next: ModelSelection | null -} { - let lastUsed: ModelSelection | null = null - let pending: ModelSelection | null = null - for (const event of log) { - if ((event as { type: string }).type === 'model/selection') { - pending = (event as unknown as { data: ModelSelection }).data - continue - } - if (event.type !== 'request/header') continue - lastUsed = { - provider: event.data.header.config.provider, - model: event.data.header.config.model, - ...(event.data.header.config.reasoningEffort === undefined - ? {} - : { reasoningEffort: event.data.header.config.reasoningEffort }), - } - if (sameModelSelection(pending, lastUsed)) pending = null - } - return { lastUsed, next: pending ?? lastUsed } -} - -function sameModelSelection(left: ModelSelection | null, right: ModelSelection | null): boolean { - return left === right || (left !== null && right !== null - && left.provider === right.provider - && left.model === right.model - && left.reasoningEffort === right.reasoningEffort) -} - -/** Host parallel: emit one Session control projection frame per key advanced by the event. */ -function projectionFramesOf( - id: SessionId, - log: readonly SessionEvent[], - event: SessionEvent, -): FixtureProjectionFrame[] { - const type = (event as { type: string }).type - const frames: FixtureProjectionFrame[] = [] - if (type === 'model/selection' || type === 'request/header') { - frames.push({ - type: 'projection', - sessionId: id, - key: 'modelSelection', - value: modelSelectionProjectionOf(log), - seq: event.seq, - }) - } - // One usage sample advances both token-meter units. - if (usageSampleOf(event) !== undefined) { - frames.push( - { type: 'projection', sessionId: id, key: 'tokenUsage', value: tokenUsageOf(log), seq: event.seq }, - { type: 'projection', sessionId: id, key: 'contextPressure', value: contextPressureOf(log), seq: event.seq }, - ) - } - if (type === 'request/context') { - frames.push({ - type: 'projection', - sessionId: id, - key: 'contextPressure', - value: contextPressureOf(log), - seq: event.seq, - }) - } - if (type === 'request/header' - || type === 'system/message' - || type === 'user/message' - || type === 'assistant/message' - || type === 'tool/result') { - frames.push({ - type: 'projection', - sessionId: id, - key: 'contextBreakdown', - value: contextBreakdownOf(log), - seq: event.seq, - }) - } - // The stats fold's view advances on message assembly and tool settlement - // (wall times) and on step close (counts). - if (type === 'assistant/message' || type === 'tool/result' || type === 'step/end') { - frames.push({ - type: 'projection', - sessionId: id, - key: 'sessionStats', - value: sessionStatsOf(log), - seq: event.seq, - }) - } - if (frames.length > 0) return frames - if (type === 'session/title') { - const values = projectionValuesOf(log) - /* v8 ignore next -- the advancing title event is in the log, so the key is present. */ - if (!Object.hasOwn(values, 'title')) return [] - return [{ type: 'projection', sessionId: id, key: 'title', value: values['title'], seq: event.seq }] - } - // The goal domain's own durable change advances its projection. - if (type === 'goal/change') { - return [{ type: 'projection', sessionId: id, key: 'goal', value: backscanGoal(log), seq: event.seq }] - } - // Standing-plan fold: writes replace the list; turn/start clears it (null). - if (type === 'todo/write' || type === 'turn/start') { - return [{ - type: 'projection', - sessionId: id, - key: 'todos', - value: backscanTodos(log) ?? null, - seq: event.seq, - }] - } - // Knob fold: any of the three whole-value knob events advances the select. - if (type === 'permission/preset' || type === 'sandbox/mode' || type === 'approval/policy') { - return [{ - type: 'projection', - sessionId: id, - key: 'permissions', - value: permissionSelectOf(log), - seq: event.seq, - }] - } - // The plan unit advances on its two folded event kinds when the command - // lifecycle contains the input that represents a plan selection. - const commandData = event as unknown as { data: { name?: string; args?: unknown } } - if (type === 'plan/mode' || (type === 'command/run' - && commandData.data.name === 'plan' && typeof commandData.data.args === 'string')) { - return [{ - type: 'projection', - sessionId: id, - key: 'plan', - value: planViewOf(log), - seq: event.seq, - }] - } - return [] -} - -/** - * Message-boundary paging mirrors the Host contract: count `maxMessages` - * backwards from the end and cut at a turn/start boundary. - */ -function pageOf( - log: readonly SessionEvent[], - beforeSeq: number | undefined, - maxMessages: number, -): { records: FixtureHistoryRecord[]; hasMore: boolean } { - const end = beforeSeq === undefined ? log.length : Math.max(0, Math.min(beforeSeq, log.length)) - let start = 0 - let messages = 0 - for (let i = end - 1; i >= 0; i--) { - const event = log[i] - /* v8 ignore next -- dense-array guard: log seqs are array indexes, i stays within [0, end). */ - if (event === undefined) break - if (event.type === 'user/message' || event.type === 'assistant/message') messages++ - if (event.type === 'turn/start' && messages >= maxMessages) { - start = i - break - } - } - const records = log.slice(start, end).map((event): FixtureHistoryRecord => ({ type: 'event', event })) - return { records, hasMore: start > 0 } -} - -/** Fixture mirror of host session-scoped attachment authorization. */ -function logReferencesAttachment(log: readonly SessionEvent[], attachmentId: string): boolean { - const visit = (value: unknown): boolean => { - if (Array.isArray(value)) return value.some(visit) - if (typeof value !== 'object' || value === null) return false - const record = value as Record - if (record.attachmentId === attachmentId) return true - return Object.values(record).some(visit) - } - return log.some(event => visit(event.data)) -} - -/** Fixture mirror of first-party message extraction used by session-query. */ -function searchBlockText(block: ContentBlock): string[] { - switch (block.type) { - case 'text': - return [block.text] - case 'reasoning': - return [] - case 'tool-call': - return [block.name, block.arguments] - case 'tool-result': - return block.content.flatMap(searchBlockText) - default: - return [] - } -} - -/** One current-surface user/assistant document, if searchable. */ -function searchEventText(event: SessionEvent): string { - const content = event.type === 'user/message' - ? event.data.content - : event.type === 'assistant/message' - ? event.data.message.content - : undefined - if (content === undefined) return '' - return content.flatMap(searchBlockText).map(part => part.trim()).filter(Boolean).join('\n') -} - -interface FixtureSearchToken { - value: string - /** Inclusive code-point offset in the whitespace-normalized display text. */ - start: number - /** Exclusive code-point offset in the whitespace-normalized display text. */ - end: number -} - -/** - * Browser-safe approximation of SQLite FTS5 unicode61 token boundaries. - * Keeping phrase matching token-based prevents the development fixture from - * promising arbitrary within-token substring behavior that production lacks. - */ -function searchTokenSpans(value: string): { text: string; tokens: FixtureSearchToken[] } { - const text = value.replace(/\s+/gu, ' ').trim() - const characters = Array.from(text) - const tokens: FixtureSearchToken[] = [] - let start: number | undefined - let raw = '' - const flush = (end: number): void => { - if (start !== undefined) { - const folded = raw.normalize('NFD').replace(/\p{M}+/gu, '').toLowerCase() - if (folded !== '') tokens.push({ value: folded, start, end }) - } - start = undefined - raw = '' - } - for (let index = 0; index < characters.length; index++) { - const character = characters[index] as string - const tokenBase = character.normalize('NFD').replace(/\p{M}+/gu, '') - if (tokenBase === '') { - if (start !== undefined) raw += character - continue - } - if (/^[\p{L}\p{N}\p{Co}]+$/u.test(tokenBase)) { - start ??= index - raw += character - } else { - flush(index) - } - } - flush(characters.length) - return { text, tokens } -} - -interface FixturePhraseMatch { - count: number - start: number - end: number -} - -/** Count exact contiguous token-phrase occurrences and retain the first display span. */ -function phraseMatch(document: readonly FixtureSearchToken[], phrase: readonly string[]): FixturePhraseMatch { - if (phrase.length === 0 || phrase.length > document.length) return { count: 0, start: 0, end: 0 } - let count = 0 - let firstStart = 0 - let firstEnd = 0 - for (let start = 0; start <= document.length - phrase.length; start++) { - if (!phrase.every((token, offset) => document[start + offset]?.value === token)) continue - count++ - if (count === 1) { - firstStart = document[start]?.start ?? 0 - firstEnd = document[start + phrase.length - 1]?.end ?? firstStart - } - } - return { count, start: firstStart, end: firstEnd } -} - -/** Match-centered fixture excerpt, bounded by Unicode code points for the sidebar. */ -function searchSnippet(value: string, matchStart: number, matchEnd: number): string { - const characters = Array.from(value) - if (characters.length <= 120) return value - const boundedStart = Math.min(Math.max(0, matchStart), characters.length - 1) - const boundedEnd = Math.min( - characters.length, - Math.max(boundedStart + 1, matchEnd), - ) - const center = Math.floor((boundedStart + boundedEnd) / 2) - let start = Math.min( - characters.length - 118, - Math.max(0, center - Math.floor(118 / 2)), - ) - let end = start + 118 - if (start === 0) { - end = 119 - } else if (end === characters.length) { - start = characters.length - 119 - } - return `${start > 0 ? '…' : ''}${characters.slice(start, end).join('')}${end < characters.length ? '…' : ''}` -} - -interface FixtureSearchCandidate { - sessionId: SessionId - seq: number - time: number - text: string - matchCount: number - matchStart: number - matchEnd: number - documentLength: number -} - -/** Mirrors `packages/session-query/session-query-sqlite/src/index.ts`; update both together. */ -function compareSearchCandidates(a: FixtureSearchCandidate, b: FixtureSearchCandidate): number { - if (a.matchCount !== b.matchCount) return b.matchCount - a.matchCount - if (a.documentLength !== b.documentLength) return a.documentLength - b.documentLength - if (a.time !== b.time) return b.time - a.time - if (a.sessionId !== b.sessionId) return a.sessionId < b.sessionId ? -1 : 1 - return b.seq - a.seq -} - -/** - * Current plan projection over the full log (host parallel: latest todo/write - * with no later turn/start; a new turn retires the previous plan). - */ -function backscanTodos(log: readonly SessionEvent[]): TodoItem[] | undefined { - for (let i = log.length - 1; i >= 0; i--) { - const event = log[i] - if (event === undefined) continue - if (event.type === 'turn/start') return undefined - if (event.type === 'todo/write') return event.data.todos - } - return undefined -} - -/** Fixture-local mirror of the goal projection value (dsh-goal's GoalProjection shape). */ -interface FxGoalProjection { - goal: { - id: string - revision: number - objective: string - phase: 'active' | 'paused' | 'blocked' | 'complete' - maxGoalRounds: number - } - roundsStarted: number - createdAt: number - updatedAt: number -} - -/** One durable goal change. */ -type FxGoalChange = - | { kind: 'goal/change'; version: 1; operation: 'clear'; cleared: { id: string; revision: number }; clearedAt: number } - | { - kind: 'goal/change' - version: 1 - operation: 'create' | 'edit' | 'pause' | 'resume' | 'complete' - goal: FxGoalProjection['goal'] - roundsStarted: number - createdAt: number - updatedAt: number - } - -/** - * Current goal projection over the full log (host parallel: the GoalService - * unit's last-wins fold of goal/change whole values; clear returns null). - */ -function backscanGoal(log: readonly SessionEvent[]): FxGoalProjection | null { - for (let i = log.length - 1; i >= 0; i--) { - const event = log[i] as unknown as { - type: string - data?: FxGoalChange - } | undefined - if (event === undefined || event.type !== 'goal/change' || event.data === undefined) continue - const change = event.data - if (change.operation === 'clear') return null - return { goal: change.goal, roundsStarted: change.roundsStarted, createdAt: change.createdAt, updatedAt: change.updatedAt } - } - return null -} - -interface StreamConn { - push(value: Value): void -} - -interface ReasoningChunkStormState { - sessionId: string - chunkCount: number - chunksPerInterval: number - intervalMs: number - emitted: number - marker: string - emitting: boolean -} - -/** Deterministic fixture branches used by keyless Web assembly tests. */ -export interface FixtureOptions { - /** Start with no real Workspace or Session. */ - empty?: boolean - /** Reject every prompt before appending its user event. */ - rejectPrompt?: boolean - /** Publish the Session but fail its Workspace account write. */ - failWorkspaceAttach?: boolean - /** Publish and frame the Session, then throw instead of returning create. */ - dropSessionCreateResponse?: boolean - /** Order of the two successful create frames. */ - createFrameOrder?: 'session-first' | 'workspace-first' - /** Announce one Agent write to `notes/demo.txt` shortly after a `workspaceFiles/changes` stream opens. */ - fileChanges?: boolean -} - -/** File observation payload, mirrored so this Client fixture names no Host package. */ -type FixtureWorkspaceFileChange = - | { readonly absolutePath: string; readonly version: string } - | { readonly absolutePath: string; readonly absent: true } - -/** Host subscription acknowledgement followed by file observations. */ -type FixtureWorkspaceFileWatchFrame = - | { readonly kind: 'ready' } - | { readonly kind: 'change'; readonly change: FixtureWorkspaceFileChange } - -/** Inbox pump shared by both stream generators (FrameQueue pattern: ONE abort listener hung - * outside the loop — a per-iteration {once:true} listener never fires for non-final rounds and - * piles up for the stream's lifetime). breakNow force-ends the stream without the - * client's signal (timing hook: simulated connection loss). */ -class FxInbox implements StreamConn { - private readonly inbox: Value[] = [] - private wake: (() => void) | null = null - private broken = false - - push(value: Value): void { - this.inbox.push(value) - this.wake?.() - } - - breakNow(): void { - this.broken = true - this.wake?.() - } - - /** Read through a method: breakNow()/abort flip state across yields, so narrowing from the loop condition must not stick. */ - private isLive(signal: AbortSignal): boolean { - return !signal.aborted && !this.broken - } - - async *drain(signal: AbortSignal): AsyncGenerator { - const onAbort = (): void => this.wake?.() - signal.addEventListener('abort', onAbort) - try { - while (this.isLive(signal)) { - while (this.inbox.length > 0) yield this.inbox.shift() as Value - if (!this.isLive(signal)) break - await new Promise((resolve) => { - this.wake = resolve - }) - this.wake = null - } - } finally { - signal.removeEventListener('abort', onAbort) - } - } -} - -/** Fixture RPC face over one in-memory state graph. */ -export interface FixtureWorld { - /** Generic Remote caller for the endpoints business services own. */ - readonly rpc: ClientConnectionRpc -} - -/** - * Build the fixture RPC face over one in-memory state graph. - * @param options - fixture branches for empty state and failure timing. - * @returns the Remote RPC face. - */ -export function createFixtureFaces(options: FixtureOptions = {}): FixtureWorld { - return createFixtureWorld(options) -} - -/** Build the fixture's Remote RPC face over one state graph. */ -function createFixtureWorld(options: FixtureOptions): FixtureWorld { - // The resident fixture sessions all carry history, so none of them is blank. - const sessions: FixtureSessionSummary[] = options.empty ? [] : [ - { sessionId: sid('fx-alpha'), updatedAt: Date.now(), running: true, blank: false, cwd: '/tmp/fixture' }, - { sessionId: sid('fx-beta'), updatedAt: Date.now() - 60_000, running: false, blank: false, parentSessionId: sid('fx-alpha'), cwd: '/tmp/fixture' }, - { sessionId: sid('fx-gamma'), updatedAt: Date.now() - 120_000, running: false, blank: false, cwd: '/tmp/fixture' }, - ] - const logs = new Map([[sid('fx-alpha'), buildAlphaLog()]]) - const goalActivations = new Map() - const modelSelections = new Map(sessions.map(session => [ - session.sessionId, - { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, - ])) - const attachments = new Map([[ - String(FIXTURE_IMAGE_REF.attachmentId), - { attachment: FIXTURE_IMAGE_REF, data: FIXTURE_IMAGE_DATA }, - ]]) - /** Credential store double: set/unset flip the describe badge, values never read back. */ - const fixtureCredentials = new Map([ - // The assembled fixture represents an already-configured shipped - // DeepSeek route so unrelated GUI journeys do not enter first-run setup. - ['DEEPSEEK_API_KEY', true], - ]) - - /** Canonical fixture implementation of the generated Settings Remote contract. */ - const settingsRemotes = { - // Only the resolved DeepSeek address needed by first-run readiness is - // represented here. Fixture-backed journeys do not open its Models editor; - // real schema-driven forms ride the HTTP transport. - describe(): RpcResult { - return { - ok: true, - value: { - writable: true, - hasDocument: true, - namespaces: [{ - ns: 'llm-deepseek', - schema: {}, - value: { apiKeyEnv: 'DEEPSEEK_API_KEY' }, - applies: 'live', - secrets: [{ path: ['apiKey'], set: false }], - revision: 0, - }], - }, - } - }, - update(ns: string): ConnectionRpcResult { - return { - ok: false, - error: { - code: 'settings/rejected', - message: 'fixture: the minimal readiness settings descriptor is read-only', - details: { ns }, - }, - } - }, - replace(ns: string): ConnectionRpcResult { - return { - ok: false, - error: { - code: 'settings/rejected', - message: 'fixture: the minimal readiness settings descriptor is read-only', - details: { ns }, - }, - } - }, - mutate(ns: string): ConnectionRpcResult { - // A Remote failure code is free-form, unlike the unary error vocabulary. - return { - ok: false, - error: { - code: 'settings/rejected', - message: 'fixture: no settings namespaces are registered', - details: { ns }, - }, - } - }, - openSettingsDocument(): RpcResult<{ opened: true }> { - return { ok: true, value: { opened: true } } - }, - openAgentPresetDirectory(agentPreset: string): RpcResult< - { opened: true } | { opened: false; path: string } - > { - const existing = fixturePresets.get(agentPreset) - if (existing === undefined || existing.trust === 'system') { - return { - ok: false, - error: { - code: 'agent-preset/read-only', - message: `agent preset "${agentPreset}" ships with the deployment`, - details: { agentPreset, reason: 'it ships with the deployment' }, - }, - } - } - return { ok: true, value: { opened: true } } - }, - } - - const credentialRemotes = { - describe(refs: readonly string[]): RpcResult> { - return { - ok: true, - value: Object.fromEntries(refs.map(ref => [ref, { - configured: fixtureCredentials.has(ref), - ...fixtureCredentials.has(ref) ? { source: 'file' } : {}, - writable: true, - }])), - } - }, - set(ref: string): RpcResult { - fixtureCredentials.set(ref, true) - return { ok: true, value: undefined } - }, - unset(ref: string): RpcResult { - fixtureCredentials.delete(ref) - return { ok: true, value: undefined } - }, - } - - /** - * Preset compositions the fixture serves. Held as state rather than - * constants so the settings editor's save and delete are exercisable: the - * roster a GUI journey sees after writing is the text it wrote. - */ - const fixturePresets = new Map([ - ['standard', { trust: 'system', content: "- id: tool-bash\n name: '@deepseek-ai/dsh-tool-bash'\n" }], - ['minimal', { trust: 'system', content: "- id: tool-web-search\n name: '@deepseek-ai/dsh-tool-web-search'\n" }], - ['my-agent', { trust: 'user', content: "- id: tool-read\n name: '@deepseek-ai/dsh-tool-read'\n" }], - ]) - let fixtureDefaultPreset = 'standard' - const nextTurn = new Map([[sid('fx-alpha'), 75]]) - let nextSession = 1 - // Workspace entities mirroring the host registry: the fixture sessions all - // live under one workspace, whose account carries them in attach order. - const wid = (raw: string): WorkspaceId => raw as WorkspaceId - const fixtureEpoch = new Date(Date.now() - 300_000).toISOString() - const FIXTURE_HOME = '/home/fixture' - const workspaces: FixtureWorkspace[] = options.empty ? [] : [{ - workspaceId: wid('fx-ws-fixture'), - path: '/tmp/fixture', - title: 'fixture', - sessionIds: [sid('fx-alpha'), sid('fx-beta'), sid('fx-gamma')], - createdAt: fixtureEpoch, - updatedAt: fixtureEpoch, - }, { - workspaceId: wid('fx-ws-home'), - path: `${FIXTURE_HOME}/Documents/project`, - title: 'project', - sessionIds: [], - createdAt: fixtureEpoch, - updatedAt: fixtureEpoch, - }] - let nextWorkspace = 1 - // Registry-global archive set mirroring the host: archived sessions keep - // their workspace accounting slot and only grouping surfaces hide them. - const archivedSessionIds: SessionId[] = [] - const workspaceSnapshot = (workspace: FixtureWorkspace): WorkspaceView => ({ - ...workspace, - sessionIds: [...workspace.sessionIds], - }) - const workspaceBaseline = (): Extract => ({ - type: 'baseline', - value: { - items: workspaces.map(workspaceSnapshot), - archivedSessionIds: [...archivedSessionIds], - }, - }) - - // In-memory browse tree behind the fixture's `browse` picker capability — - // deterministic content mirroring the design mock so assembled Web tests - // and snapshots can walk it. Leaves are materialized lazily: a child listed - // by its parent lists as empty until something is created inside it. - const directoryTree = new Map([ - ['/', ['home']], - ['/home', ['fixture']], - [FIXTURE_HOME, ['Documents', 'Downloads', '.config']], - [`${FIXTURE_HOME}/Documents`, [ - 'project', 'deepseek-iOS', 'deepseek-android', 'deepseek-platform', - 'deepseek-web', 'deepseek-harness', 'deepseek-app', 'deepseek-landing-blog', - ]], - ]) - const childrenOf = (path: string): string[] | undefined => { - const known = directoryTree.get(path) - if (known !== undefined) return known - const parent = path.slice(0, path.lastIndexOf('/')) || '/' - const name = path.slice(path.lastIndexOf('/') + 1) - return directoryTree.get(parent)?.includes(name) === true ? [] : undefined - } - const crumbsOf = (path: string): { name: string; path: string; hidden: boolean }[] => { - const crumbs = [{ name: '/', path: '/', hidden: false }] - let acc = '' - for (const segment of path.split('/').filter(Boolean)) { - acc += `/${segment}` - crumbs.push({ name: segment, path: acc, hidden: false }) - } - return crumbs - } - /** Resident waterfalls retain their event ids across Remote Event generations. */ - const pendingApprovalEventId = 'fx-interaction-approval' - let approvalPending = !options.empty - const pendingQuestionEventId = 'fx-interaction-question' - let questionPending = !options.empty - const fixtureQuestions: readonly FixtureQuestionItem[] = [ - { - id: 'harness-profile', - header: '偏好', - question: '你现在更想招哪类 Agent/Harness 候选人?', - options: [ - { label: '工程落地型 (Recommended)', description: '更看重能直接做 runtime、tool executor、sandbox、trace 和线上问题排查。' }, - { label: '研究潜力型', description: '更看重 Agent 理解、训练评测思路和长期成长空间。' }, - { label: '均衡型', description: '同时要求工程能力和 Agent 认知,但可能筛选门槛更高。' }, - ], - }, - { - id: 'work-mode', - header: '方式', - question: '你希望候选人优先展示哪种工作方式?', - options: [ - { label: '先做小型原型 (Recommended)', description: '用可运行结果尽快验证关键假设。' }, - { label: '先写完整设计', description: '先收敛边界、协议和风险,再开始实现。' }, - ], - }, - { - id: 'signals', - header: '信号', - question: '哪些面试信号最重要?', - detail: '按当前招聘目标选择;跳过则视为不设偏好。', - multiSelect: true, - options: [ - { label: '系统设计' }, - { label: '代码质量' }, - { label: 'Agent 产品判断' }, - ], - }, - ] - - const controlConns = new Set>() - const followConns = new Map>>() - interface FixtureAttemptState { - readonly attemptId: ReturnType - readonly startedAfterSeq: number - readonly turn: number - readonly step: number - readonly stream: AssistantStreamAccumulator - index: number - } - const activeAttempts = new Map() - const assistantRevisions = new Map() - const workspaceConns = new Set>() - const remoteEventConns = new Map>() - const emitControl = (frame: FixtureControlFrame): void => { - for (const conn of controlConns) conn.push(frame) - } - const emitWorkspace = (frame: Exclude): void => { - for (const conn of workspaceConns) conn.push(frame) - } - const emitRemote = (event: string, args: readonly unknown[]): void => { - for (const conn of remoteEventConns.values()) conn.push({ type: 'emit', event, args }) - } - const emitRemoteFrame = (frame: FixtureRemoteEventFrame): void => { - for (const conn of remoteEventConns.values()) conn.push(frame) - } - const emitFollow = (sessionId: SessionId, entry: FixtureHistoryEntry): void => { - for (const conn of followConns.get(sessionId) ?? []) conn.push(entry) - } - const emitAssistant = (sessionId: SessionId, frame: FixtureAssistantStreamFrame): void => { - for (const conn of followConns.get(sessionId) ?? []) conn.push({ type: 'assistant-stream', frame }) - } - const nextAssistantRevision = (sessionId: SessionId): number => { - const revision = (assistantRevisions.get(sessionId) ?? 0) + 1 - assistantRevisions.set(sessionId, revision) - return revision - } - const beginAssistant = (sessionId: SessionId, turn: number, step: number): FixtureAttemptState => { - const attemptId = LlmAttemptId(`${sessionId}:fixture:${String(nextAssistantRevision(sessionId))}`) - const lastSeq = logOf(sessionId).length - 1 - const startedAfterSeq = lastSeq < 0 ? -1 : SessionSeq(lastSeq) - const attempt = { - attemptId, startedAfterSeq, turn, step, - stream: new AssistantStreamAccumulator(), index: 0, - } - activeAttempts.set(sessionId, attempt) - emitAssistant(sessionId, { - type: 'start', attemptId, revision: assistantRevisions.get(sessionId) as number, - startedAfterSeq, turn, step, - }) - return attempt - } - const pushAssistant = (sessionId: SessionId, chunk: StreamChunk): void => { - const attempt = activeAttempts.get(sessionId) - if (attempt === undefined) throw new Error(`fixture: no active Assistant attempt for ${sessionId}`) - const timed = attempt.stream.push({ time: Date.now(), chunk }) - emitAssistant(sessionId, { - type: 'chunk', attemptId: attempt.attemptId, revision: nextAssistantRevision(sessionId), - index: attempt.index++, time: timed.time, chunk: timed.chunk as unknown as JsonValue, - }) - } - const commitAssistant = (sessionId: SessionId, event: SessionEvent): void => { - const attempt = activeAttempts.get(sessionId) - if (attempt === undefined) throw new Error(`fixture: no active Assistant attempt for ${sessionId}`) - activeAttempts.delete(sessionId) - emitAssistant(sessionId, { - type: 'end', attemptId: attempt.attemptId, revision: nextAssistantRevision(sessionId), - index: attempt.index, - outcome: { - kind: 'committed', - eventType: event.type === 'assistant/message' ? 'assistant/message' : 'assistant/attempt', - seq: event.seq, - }, - }) - } - - function sessionOk(value: T): Promise> { - return Promise.resolve({ ok: true, value }) - } - - function sessionErr(error: ConnectionRpcFailure): Promise> { - return Promise.resolve({ ok: false, error }) - } - - const summaryOf = (id: SessionId): FixtureSessionSummary | undefined => sessions.find(s => s.sessionId === id) - const requireRemoteSession = ( - request: { readonly sessionId: SessionId }, - ): Promise> | undefined => { - if (summaryOf(request.sessionId) !== undefined) return undefined - return sessionErr({ - code: 'session/not-found', - message: `no session ${request.sessionId}`, - details: { sessionId: request.sessionId }, - }) - } - const setRunning = (id: SessionId, running: boolean): void => { - const summary = summaryOf(id) - if (summary === undefined || summary.running === running) return - summary.running = running - emitRemote('api-session/status', [id, running]) - } - const logOf = (id: SessionId): SessionEvent[] => { - let log = logs.get(id) - if (log === undefined) { - log = [] - logs.set(id, log) - } - return log - } - const append = (id: SessionId, e: Record): SessionEvent => { - const log = logOf(id) - const event = { seq: SessionSeq(log.length), time: Date.now(), ...e } as unknown as SessionEvent - log.push(event) - emitFollow(id, { type: 'event', event }) - // Host eager-drive parallel: a unit-advancing event pushes its finished value. - for (const frame of projectionFramesOf(id, log, event)) emitControl(frame) - if (event.type === 'user/message' && event.data.source.kind === 'user') { - const summary = summaryOf(id) - if (summary !== undefined) summary.updatedAt = event.time - emitRemote('api-session/activity', [id, event.time]) - } - return event - } - - /** Append one durable goal/change (host GoalService parallel). */ - const appendGoalChange = (id: SessionId, change: FxGoalChange): FxGoalProjection => { - const log = logOf(id) - append(id, { - type: 'goal/change', - data: change, - }) - return backscanGoal(log) as FxGoalProjection - } - - /** Update process-local goal activation and publish the forwarded edge. */ - const setGoalActivation = (id: SessionId, activation: 'armed' | 'disarmed'): void => { - const current = backscanGoal(logOf(id)) - if (current === null) { - if (!goalActivations.delete(id)) return - emitRemote('goal/activation-changed', [{ sessionId: id }]) - return - } - const previous = goalActivations.get(id) - goalActivations.set(id, activation) - if (previous === activation) return - emitRemote('goal/activation-changed', [{ - sessionId: id, - goal: { id: current.goal.id, revision: current.goal.revision, activation }, - }]) - } - - type FxGoalRef = { id: string; revision: number } - type FxGoalView = FxGoalProjection['goal'] & { - roundsStarted: number - createdAt: number - updatedAt: number - activation: 'armed' | 'disarmed' - } - - const goalFailure = (message: string): RpcResult => ({ - ok: false, - error: { code: 'gateway/internal', message, details: {} }, - }) - - const requireGoalSession = (id: SessionId): RpcResult | undefined => ( - summaryOf(id) === undefined - ? { ok: false, error: { code: 'session/not-found', message: `no session ${id}`, details: { sessionId: id } } } - : undefined - ) - - /** Canonical fixture implementation of the generated Commands Remote contract. */ - const commandRemotes = { - list(id: SessionId): RpcResult { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - return { - ok: true, - value: [ - { name: 'compact', description: 'fixture:压缩当前会话上下文' }, - { name: 'echo', description: 'fixture:回显参数', input: { hint: 'text to echo' } }, - { definitionId: brandString('@deepseek-ai/dsh-command-goal'), name: 'goal', description: 'Set or view the goal for a long-running task', input: { hint: '', attachments: true } }, - { definitionId: brandString('@deepseek-ai/dsh-permission-presets'), name: 'permission', description: 'Switch the permission preset (sandbox mode + approval policy)', input: { hint: '' } }, - { definitionId: brandString('@deepseek-ai/dsh-plan-mode'), name: 'plan', description: 'Enter or leave plan mode', input: { hint: '[off|message]', attachments: true } }, - ], - } - }, - execute(id: SessionId, line: string, attachments: readonly unknown[] = []): RpcResult { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - // Structured split mirroring the Host parser: name + verbatim rawInput - // (separator whitespace included) — the run payload carries no line. - const match = /^\/(\S+)((?:\s.*)?)$/.exec(line.trim()) - const name = match?.[1] - const args = match?.[2] ?? '' - // Mirror the Host image policy AFTER command resolution, matching the - // executor's order (an unknown name answers undefined and logs no - // lifecycle): the declaration rejection covers every known command - // without `input.attachments`, and the two producer grammar rejections cover - // the declaring commands' control-only lines. The fixture stores no - // bytes, so an accepted batch is acknowledged and dropped. - const known = ['permission', 'goal', 'compact', 'echo', 'plan'] - if (attachments.length > 0 && name !== undefined && known.includes(name)) { - const rejection = name !== 'goal' && name !== 'plan' - ? `/${name} does not accept attachments` - : name === 'goal' && args.trim() === '' - ? 'Attachments only accompany a goal objective: /goal or /goal edit .' - : name === 'plan' && args.trim() === 'off' - ? 'Attachments cannot accompany /plan off.' - : undefined - if (rejection !== undefined) { - const commandId = `fx-cmd-${logOf(id).length}` as CommandId - append(id, { type: 'command/run', data: { commandId, name, args, source: { kind: 'user' } } }) - const result: CommandResult = { kind: 'error', text: rejection } - append(id, { type: 'command/done', data: { commandId, ...result } }) - return { ok: true, value: { commandId, result } } - } - } - if (name === 'permission') { - const preset = args.trim() - const commandId = `fx-cmd-${logOf(id).length}` as CommandId - append(id, { type: 'command/run', data: { commandId, name, args, source: { kind: 'user' } } }) - const spec = PERMISSION_PRESETS[preset] - let result: CommandResult - if (preset === '') { - const current = permissionSelectOf(logOf(id)).currentValue - result = { kind: 'success', text: `current preset ${current} (available: ${Object.keys(PERMISSION_PRESETS).join(', ')})` } - } else if (spec === undefined) { - result = { kind: 'error', text: `unknown preset "${preset}" (available: ${Object.keys(PERMISSION_PRESETS).join(', ')})` } - } else { - if (permissionSelectOf(logOf(id)).currentValue !== preset) append(id, { type: 'permission/preset', data: { preset } }) - append(id, { type: 'sandbox/mode', data: { mode: spec.sandbox } }) - append(id, { type: 'approval/policy', data: { policy: spec.approval } }) - result = { kind: 'success', text: `preset ${preset}` } - } - append(id, { type: 'command/done', data: { commandId, ...result } }) - return { ok: true, value: { commandId, result } } - } - if (name === 'goal') { - const commandId = `fx-cmd-${logOf(id).length}` as CommandId - append(id, { type: 'command/run', data: { commandId, name, args, source: { kind: 'user' } } }) - const objective = args.trim() - const current = backscanGoal(logOf(id)) - let text: string - if (objective === '') { - text = current === null ? 'No goal is set. Usage: /goal ' : `Current goal: ${current.goal.objective}` - } else if (current !== null && current.goal.phase !== 'complete') { - text = `A goal already exists (${current.goal.objective}). Clear it first.` - } else { - const created = appendGoalChange(id, { - kind: 'goal/change', version: 1, operation: 'create', - goal: { id: `fx-goal-${logOf(id).length}`, revision: 1, objective, phase: 'active', maxGoalRounds: 256 }, - roundsStarted: 0, createdAt: Date.now(), updatedAt: Date.now(), - }) - setGoalActivation(id, 'armed') - text = `Goal created: ${created.goal.objective}` - } - const result: CommandResult = { kind: 'success', text } - append(id, { type: 'command/done', data: { commandId, ...result } }) - return { ok: true, value: { commandId, result } } - } - const running = summaryOf(id)?.running === true - const outcomes: Record = { - compact: 'fixture:已压缩(假动作)', - echo: args.trim(), - plan: args.trim() === 'off' - ? (running ? 'Leaving plan mode (applies from the next step).' : 'Plan mode off.') - : (running - ? 'Entering plan mode (applies from the next step). Use /plan off to leave.' - : 'Plan mode on. Use /plan off to leave.'), - } - const text = name === undefined ? undefined : outcomes[name] - if (name === undefined || text === undefined) return { ok: true, value: undefined } - const commandId = `fx-cmd-${logOf(id).length}` as CommandId - append(id, { type: 'command/run', data: { commandId, name, args, source: { kind: 'user' } } }) - if (name === 'plan' && !running) { - const plan = foldPlan(logOf(id)) - if (plan.wanted !== null && plan.wanted !== plan.active) { - append(id, { type: 'plan/mode', data: { active: plan.wanted } }) - } - } - const result: CommandResult = { kind: 'success', ...text === '' ? {} : { text } } - append(id, { type: 'command/done', data: { commandId, ...result } }) - return { ok: true, value: { commandId, result } } - }, - } - - const goalView = (id: SessionId, projection: FxGoalProjection): FxGoalView => ({ - ...projection.goal, - roundsStarted: projection.roundsStarted, - createdAt: projection.createdAt, - updatedAt: projection.updatedAt, - activation: goalActivations.get(id) ?? (projection.goal.phase === 'active' ? 'armed' : 'disarmed'), - }) - - /** Canonical fixture implementation of the generated Goal Remote contract. */ - /** Canonical fixture implementation of the generated reference-discovery Remote contracts. */ - const referenceRemotes = { - files(id: SessionId, query: string): RpcResult<{ path: string; kind: 'file' | 'directory' }[]> { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - const needle = query.toLocaleLowerCase() - const items = [ - { path: 'notes', kind: 'directory' as const }, - { path: 'README.md', kind: 'file' as const }, - { path: 'notes/demo.txt', kind: 'file' as const }, - ].filter(item => item.path.toLocaleLowerCase().includes(needle)) - return { ok: true, value: items } - }, - sessions(id: SessionId, query: string): RpcResult<{ - sessionId: SessionId - label: string - cwd?: string - createdAt: number - mention: string - }[]> { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - const needle = query.toLocaleLowerCase() - const value = sessions - .filter(item => item.sessionId !== id) - .filter(item => String(item.sessionId).toLocaleLowerCase().includes(needle) - || item.cwd?.toLocaleLowerCase().includes(needle) === true) - .map((item) => { - const label = item.sessionId === sid('fx-beta') ? 'Fixture child session' : String(item.sessionId) - const encoded = btoa(JSON.stringify(item.sessionId)) - .replaceAll('+', '-') - .replaceAll('/', '_') - .replace(/=+$/u, '') - return { - sessionId: item.sessionId, - label, - ...item.cwd === undefined ? {} : { cwd: item.cwd }, - createdAt: item.updatedAt, - mention: `@[${label}](dsh-session:${encoded})`, - } - }) - return { ok: true, value } - }, - } - - /** - * Workspace text reads under `?fixture`. - * - * The sample content is deliberately more than one shape: the panel's states - * (text, oversized, unreadable) are only demonstrable if the fixture can - * produce each of them, and a preview that can only ever succeed hides its - * own failure rendering. - */ - // The Session workspace the replayed conversation writes into: `list` walks a - // fixed tree under it so the file-tree tab has directories, files, one entry - // of neither kind, and one cut listing to draw. - const WORKSPACE_FILES_ROOT = '/tmp/fixture' - type FixtureWorkspaceEntry = { name: string; type: 'file' | 'directory' | 'other'; size?: number } - const workspaceFileTree = new Map([ - ['', [ - { name: '.gitignore', type: 'file', size: 24 }, - { name: 'dev.sock', type: 'other' }, - { name: 'notes', type: 'directory' }, - { name: 'package.json', type: 'file', size: 512 }, - { name: 'README.md', type: 'file', size: 640 }, - { name: 'src', type: 'directory' }, - ]], - ['notes', [ - { name: 'demo.txt', type: 'file', size: 14 }, - { name: 'new-demo.txt', type: 'file', size: 14 }, - ]], - ['src', [ - { name: 'config.ts', type: 'file', size: 211 }, - { name: 'index.ts', type: 'file', size: 88 }, - { name: 'lib', type: 'directory' }, - ]], - ['src/lib', Array.from({ length: 24 }, (_, index) => ({ - name: `module-${String(index + 1).padStart(2, '0')}.ts`, - type: 'file' as const, - size: 96 + index, - }))], - ]) - /** Resolve a `list` argument to its workspace-relative path, or undefined when it leaves the root. */ - const workspaceFilePath = (path: string): string | undefined => { - const segments: string[] = [] - for (const segment of (path.startsWith('/') ? path : `${WORKSPACE_FILES_ROOT}/${path}`).split('/')) { - if (segment === '' || segment === '.') continue - if (segment === '..') { - segments.pop() - continue - } - segments.push(segment) - } - const absolute = `/${segments.join('/')}` - if (absolute !== WORKSPACE_FILES_ROOT && !absolute.startsWith(`${WORKSPACE_FILES_ROOT}/`)) return undefined - return absolute.slice(WORKSPACE_FILES_ROOT.length + 1) - } - // Page cap mirrored from the Host default so an over-limit request fails here too. - const WORKSPACE_FILE_PAGE_LINES = 5000 - /** - * Sample text for any readable path: a heading plus two lines of copy. The - * replayed conversation's `demo` files, and any `huge` path, run past two - * default pages so paging can be exercised without a real workspace. - */ - const workspaceFileLines = (path: string): string[] => { - const name = path.slice(path.lastIndexOf('/') + 1) - const head = [`# ${name}`, '', 'fixture 模式下的示例文本,用于验收侧栏的文本预览。', '真实构建从工作区读取同名文件。'] - return path.includes('demo') || path.includes('huge') - ? [...head, ...Array.from({ length: 12_000 }, (_, index) => `第 ${index + 5} 行:用于验收分页与滚动的长文本样本。`)] - : head - } - const workspaceFileRemotes = { - list(path: string): ConnectionRpcResult<{ - path: string - entries: readonly FixtureWorkspaceEntry[] - truncated: boolean - }> { - if (path.length === 0) { - return { ok: false, error: { code: 'gateway/bad-request', message: 'path is required', details: {} } } - } - const relative = workspaceFilePath(path) - if (relative === undefined) { - return { - ok: false, - error: { code: 'workspace-file/outside-workspace', message: `${path} is outside the workspace`, details: { path } }, - } - } - const entries = workspaceFileTree.get(relative) - if (entries === undefined) { - const cut = relative.lastIndexOf('/') - const name = relative.slice(cut + 1) - const sibling = workspaceFileTree.get(cut === -1 ? '' : relative.slice(0, cut))?.find(entry => entry.name === name) - if (sibling === undefined) { - return { ok: false, error: { code: 'workspace-file/not-found', message: `no entry at ${path}`, details: { path } } } - } - return { - ok: false, - error: { - code: 'workspace-file/not-directory', - message: `${path} is a ${sibling.type}`, - details: { path, kind: sibling.type === 'file' ? 'file' : 'other' }, - }, - } - } - return { ok: true, value: { path: relative, entries, truncated: relative === 'src/lib' } } - }, - read(path: string, range: { offset?: number; limit?: number }): ConnectionRpcResult<{ - absolutePath: string - version: string - bytes: number - offset: number - text: string - lines: number - eof: boolean - }> { - const located = workspaceFileRemotes.stat(path) - if (!located.ok) return located - const offset = range.offset ?? 1 - const limit = range.limit ?? WORKSPACE_FILE_PAGE_LINES - if (!Number.isInteger(offset) || offset < 1 || !Number.isInteger(limit) || limit < 1 || limit > WORKSPACE_FILE_PAGE_LINES) { - return { ok: false, error: { code: 'gateway/bad-request', message: 'offset and limit must be positive integers within the page cap', details: {} } } - } - const lines = workspaceFileLines(path) - const page = lines.slice(offset - 1, offset - 1 + limit) - return { - ok: true, - value: { - ...located.value, - offset, - text: page.join('\n'), - lines: page.length, - eof: offset - 1 + limit >= lines.length, - }, - } - }, - stat(path: string): ConnectionRpcResult<{ absolutePath: string; version: string; bytes: number }> { - if (path.length === 0) { - return { ok: false, error: { code: 'gateway/bad-request', message: 'path is required', details: {} } } - } - if (path.endsWith('.png') || path.endsWith('.bin')) { - return { - ok: false, - error: { code: 'workspace-file/not-text', message: `${path} is not UTF-8 text`, details: { path } }, - } - } - return { - ok: true, - value: { - absolutePath: path.startsWith('/') ? path : `/${path}`, - version: 'fx-v1', - bytes: new TextEncoder().encode(workspaceFileLines(path).join('\n')).byteLength, - }, - } - }, - } - - /** - * Canonical fixture implementation of the generated Directory Picker Remote - * contract. The pick is deterministic — the keyless lanes drive the full - * pick-then-adopt path without an OS chooser — over the same design-mock - * tree the browse primitives serve. - */ - const directoryPickerRemotes = { - pick(): ConnectionRpcResult { - return { ok: true, value: `${FIXTURE_HOME}/Documents/project` } - }, - list(path?: string): ConnectionRpcResult { - const target = path ?? FIXTURE_HOME - const children = childrenOf(target) - if (children === undefined) { - return { - ok: false, - error: { code: 'directory-picker/unreadable', message: `cannot list ${target}: not in the fixture tree`, details: { path: target } }, - } - } - return { - ok: true, - value: { - path: target, - home: FIXTURE_HOME, - crumbs: crumbsOf(target), - entries: [...children].sort((a, b) => a.localeCompare(b)) - .map(name => ({ name, path: target === '/' ? `/${name}` : `${target}/${name}`, hidden: name.startsWith('.') })), - // The fixture tree is tiny; no level ever reaches a backend bound. - truncated: false, - }, - } - }, - createDirectory(parent: string, name: string): ConnectionRpcResult { - const children = childrenOf(parent) - if (children === undefined) { - return { ok: false, error: { code: 'directory-picker/create-failed', message: `missing parent ${parent}`, details: { path: parent } } } - } - // Same root special case as list's entry paths: a plain join under '/' - // would mint '//name' and fork the tree's identity. - const target = parent === '/' ? `/${name}` : `${parent}/${name}` - if (children.includes(name)) { - return { ok: false, error: { code: 'directory-picker/exists', message: `${target} already exists`, details: { path: target } } } - } - directoryTree.set(parent, [...children, name]) - directoryTree.set(target, []) - return { ok: true, value: target } - }, - } - - const goalRemotes = { - get(id: SessionId): RpcResult { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - const current = backscanGoal(logOf(id)) - return { ok: true, value: current === null ? undefined : goalView(id, current) } - }, - create(id: SessionId, request: { objective: string; maxGoalRounds?: number }): RpcResult<{ ref: FxGoalRef }> { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - const current = backscanGoal(logOf(id)) - if (current !== null && current.goal.phase !== 'complete') { - return goalFailure(`goal "${current.goal.id}" already exists`) - } - const now = Date.now() - const projection = appendGoalChange(id, { - kind: 'goal/change', version: 1, operation: 'create', - goal: { - id: `fx-goal-${logOf(id).length}`, - revision: 1, - objective: request.objective, - phase: 'active', - maxGoalRounds: request.maxGoalRounds ?? 256, - }, - roundsStarted: 0, createdAt: now, updatedAt: now, - }) - setGoalActivation(id, 'armed') - return { ok: true, value: { ref: { id: projection.goal.id, revision: projection.goal.revision } } } - }, - edit(id: SessionId, ref: FxGoalRef, request: { objective?: string; maxGoalRounds?: number }): RpcResult { - return mutateGoal(id, ref, current => ({ - ...current.goal, - revision: current.goal.revision + 1, - ...request.objective === undefined ? {} : { objective: request.objective }, - ...request.maxGoalRounds === undefined ? {} : { maxGoalRounds: request.maxGoalRounds }, - })) - }, - pause(id: SessionId, ref: FxGoalRef): RpcResult { - return mutateGoal(id, ref, current => ( - current.goal.phase === 'active' - ? { ...current.goal, revision: current.goal.revision + 1, phase: 'paused' } - : undefined - )) - }, - resume(id: SessionId, ref: FxGoalRef): RpcResult { - return mutateGoal(id, ref, current => ( - current.goal.phase === 'paused' || current.goal.phase === 'blocked' || current.goal.phase === 'active' - ? { ...current.goal, revision: current.goal.revision + 1, phase: 'active' } - : undefined - )) - }, - complete(id: SessionId, ref: FxGoalRef): RpcResult { - return mutateGoal(id, ref, current => ( - current.goal.phase === 'complete' - ? undefined - : { ...current.goal, revision: current.goal.revision + 1, phase: 'complete' } - )) - }, - clear(id: SessionId, ref: FxGoalRef): RpcResult { - const resolved = resolveGoal(id, ref) - if (!resolved.ok) return resolved - const current = resolved.value - const tombstone = { id: current.goal.id, revision: current.goal.revision + 1 } - appendGoalChange(id, { - kind: 'goal/change', version: 1, operation: 'clear', cleared: tombstone, clearedAt: Date.now(), - }) - setGoalActivation(id, 'disarmed') - return { ok: true, value: tombstone } - }, - } - - /** Resolve one current goal revision for a canonical Remote mutation. */ - function resolveGoal(id: SessionId, ref: FxGoalRef): RpcResult { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - const current = backscanGoal(logOf(id)) - if (current === null || current.goal.id !== ref.id || current.goal.revision !== ref.revision) { - return goalFailure('stale or missing goal revision') - } - return { ok: true, value: current } - } - - /** Shared CAS mutation path behind the canonical Remote verbs. */ - function mutateGoal( - id: SessionId, - ref: FxGoalRef, - next: (current: FxGoalProjection) => FxGoalProjection['goal'] | undefined, - ): RpcResult { - const resolved = resolveGoal(id, ref) - if (!resolved.ok) return resolved - const current = resolved.value - const goal = next(current) - if (goal === undefined) { - return goalFailure(`invalid goal transition from "${current.goal.phase}"`) - } - const currentActivation = goalActivations.get(id) - ?? (current.goal.phase === 'active' ? 'armed' : 'disarmed') - const activation = goal.phase === 'active' - ? current.goal.phase === 'active' ? currentActivation : 'armed' - : 'disarmed' - const projection = appendGoalChange(id, { - kind: 'goal/change', version: 1, - operation: goal.phase === current.goal.phase ? 'edit' : goal.phase === 'paused' ? 'pause' : goal.phase === 'active' ? 'resume' : 'complete', - goal, roundsStarted: current.roundsStarted, createdAt: current.createdAt, updatedAt: Date.now(), - }) - setGoalActivation(id, activation) - return { ok: true, value: goalView(id, projection) } - } - - /** Canonical fixture implementation of the generated AgentPresets Remote contract. */ - const presetRemotes = { - // Both trusts appear, because a surface must present a locally authored - // preset differently from one the deployment vetted. - list(): RpcResult<{ - presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[] - authorable: boolean - modeSelectionEnabled: boolean - }> { - return { - ok: true, - value: { - presets: [...fixturePresets].map(([id, preset]) => ({ - id, - trust: preset.trust, - isDefault: id === fixtureDefaultPreset, - })), - authorable: true, - modeSelectionEnabled: true, - }, - } - }, - select(_id: SessionId, agentPreset: string): RpcResult { - fixtureDefaultPreset = agentPreset - return { ok: true, value: agentPreset } - }, - read(agentPreset: string): RpcResult<{ agentPreset: string; trust: 'system' | 'user'; content: string }> { - const preset = fixturePresets.get(agentPreset) - if (preset === undefined) { - return { - ok: false, - error: { - code: 'agent-preset/not-found', - message: `unknown agent preset "${agentPreset}"`, - details: { agentPreset, available: [...fixturePresets.keys()] }, - }, - } - } - return { ok: true, value: { agentPreset, trust: preset.trust, content: preset.content } } - }, - copy(from: string, id: string): RpcResult { - const source = fixturePresets.get(from) - if (source === undefined) { - return { - ok: false, - error: { - code: 'agent-preset/not-found', - message: `unknown agent preset "${from}"`, - details: { agentPreset: from, available: [...fixturePresets.keys()] }, - }, - } - } - if (fixturePresets.has(id)) { - return { - ok: false, - error: { - code: 'agent-preset/invalid', - message: `agent preset "${id}" already exists`, - details: { agentPreset: id, reason: 'already exists' }, - }, - } - } - fixturePresets.set(id, { trust: 'user', content: source.content }) - return { ok: true, value: undefined } - }, - deletePreset(id: string): RpcResult { - if (fixturePresets.get(id)?.trust === 'system') { - return { - ok: false, - error: { - code: 'agent-preset/read-only', - message: `agent preset "${id}" ships with the deployment`, - details: { agentPreset: id, reason: 'it ships with the deployment' }, - }, - } - } - fixturePresets.delete(id) - return { ok: true, value: undefined } - }, - } - - /** At most one in-flight replay per session; cancel clears it. */ - const replays = new Map; finish(aborted: boolean): void }>() - - /** History transit delay; the page snapshot is taken at request time. */ - let historyDelayMs = 0 - /** One-shot history failure (timing hook: a pre-disconnect history request already doomed when reconnect lands). */ - let failNextHistory = false - /** Force-enders for currently open stream generators (timing hook: simulated connection loss). */ - const streamBreakers = new Set<() => void>() - /** Retry scenarios opened by timing hooks and completed in a later browser assertion phase. */ - const retryScenarios = new Map() - /** The single opt-in browser stress producer; normal fixture journeys never start it. */ - let activeReasoningChunkStorm: ReasoningChunkStormState | null = null - - // Browser-only timing hooks for slow history, lost frames, and reconnects. - const timingHooks = { - setHistoryDelay(ms: number): void { - historyDelayMs = ms - }, - /** Fail the NEXT history call (after its transit delay) with a transport-level throw. */ - failNextHistory(): void { - failNextHistory = true - }, - /** Log append plus follow-stream delivery (the normal live path). */ - appendUser(id: string, msg: string): void { - append(sid(id), { type: 'user/message', surfaceOp: 'append', data: userMessage(text(msg)) }) - }, - /** Append a later durable title revision through the normal raw-event + control-frame path. */ - appendTitle(id: string, title: string): void { - const log = logOf(sid(id)) - const messageSeqs = log.filter(event => event.type === 'user/message').map(event => event.seq) - append(sid(id), { type: 'session/title', data: { title, messageSeqs, source: { kind: 'provider', provider: 'fixture' } } }) - }, - /** Disarm the single active goal without a durable phase change. */ - disarmOnlyGoal(): void { - const active = [...logs.entries()].filter(([, log]) => { - const current = backscanGoal(log) - return current?.goal.phase === 'active' - }) - if (active.length !== 1) { - throw new Error(`fixture: expected one active goal, found ${String(active.length)}`) - } - const [session] = active - if (session === undefined) return - setGoalActivation(session[0], 'disarmed') - }, - /** Start an externally paced reasoning stream for the opt-in browser stress lane. */ - startReasoningChunkStorm( - id: string, - chunkCount: number, - chunksPerInterval: number, - intervalMs: number, - ): string { - if (!Number.isSafeInteger(chunkCount) || chunkCount < 1) { - throw new Error('fixture: reasoning chunk count must be a positive safe integer') - } - if (!Number.isSafeInteger(chunksPerInterval) || chunksPerInterval < 1) { - throw new Error('fixture: reasoning chunks per interval must be a positive safe integer') - } - if (!Number.isSafeInteger(intervalMs) || intervalMs < 1) { - throw new Error('fixture: reasoning interval must be a positive safe integer') - } - if (activeReasoningChunkStorm?.emitting === true) { - throw new Error('fixture: reasoning chunk storm already running') - } - - const sessionId = sid(id) - const log = logOf(sessionId) - let turn = nextTurn.get(sessionId) ?? 0 - for (const event of log) { - const candidate = (event as unknown as { data?: { turn?: unknown } }).data?.turn - if (typeof candidate === 'number') turn = Math.max(turn, candidate + 1) - } - nextTurn.set(sessionId, turn + 1) - const marker = `REASONING_STRESS_COMPLETE:${String(turn)}:${String(chunkCount)}` - const state: ReasoningChunkStormState = { - sessionId: id, - chunkCount, - chunksPerInterval, - intervalMs, - emitted: 0, - marker, - emitting: true, - } - activeReasoningChunkStorm = state - - setRunning(sessionId, true) - append(sessionId, { type: 'turn/start', data: { turn, trigger: { kind: 'message', source: { kind: 'user' } } } }) - append(sessionId, { - type: 'user/message', surfaceOp: 'append', - data: userMessage(text(`Reasoning chunk stress: ${String(chunkCount)} chunks.`)), - }) - append(sessionId, { type: 'step/start', data: { turn, step: 0 } }) - beginAssistant(sessionId, turn, 0) - pushAssistant(sessionId, { type: 'block-start', index: 0, blockType: 'reasoning' }) - - const startedAt = Date.now() - const pump = (): void => { - const elapsedIntervals = Math.floor((Date.now() - startedAt) / intervalMs) + 1 - const due = Math.max(state.emitted + chunksPerInterval, elapsedIntervals * chunksPerInterval) - const end = Math.min(due, chunkCount) - for (let index = state.emitted; index < end; index++) { - const chunkText = index === chunkCount - 1 - ? `\n${marker}` - : index % 64 === 63 ? '推理\n' : '推理' - pushAssistant(sessionId, { type: 'reasoning-delta', index: 0, text: chunkText }) - } - state.emitted = end - if (end < chunkCount) { - setTimeout(pump, intervalMs) - } else { - state.emitting = false - } - } - setTimeout(pump, 0) - return marker - }, - /** Return a copy so browser probes cannot mutate the active producer. */ - reasoningChunkStormState(): ReasoningChunkStormState | null { - return activeReasoningChunkStorm === null ? null : { ...activeReasoningChunkStorm } - }, - /** Open one failed model step whose partial remains visible until llm/retry arrives. */ - beginModelRetry(id: string): void { - const sessionId = sid(id) - const turn = nextTurn.get(sessionId) ?? 0 - nextTurn.set(sessionId, turn + 1) - retryScenarios.set(sessionId, { turn, stepStarted: true }) - setRunning(sessionId, true) - append(sessionId, { type: 'turn/start', data: { turn } }) - append(sessionId, { type: 'user/message', surfaceOp: 'append', data: { content: text('请重试这个请求'), source: { kind: 'user' } } }) - append(sessionId, { type: 'step/start', data: { turn, step: 1 } }) - beginAssistant(sessionId, turn, 1) - pushAssistant(sessionId, { type: 'block-start', index: 0, blockType: 'text' }) - pushAssistant(sessionId, { type: 'text-delta', index: 0, text: '应撤回的半截回复' }) - }, - /** Record one retry decision; the next attempt remains in the same step. */ - scheduleModelRetry(id: string, retry = 1, delayMs = 450): void { - const sessionId = sid(id) - const scenario = retryScenarios.get(sessionId) - if (scenario === undefined) throw new Error(`fixture: no model retry scenario for ${id}`) - if (!scenario.stepStarted) { - beginAssistant(sessionId, scenario.turn, 1) - pushAssistant(sessionId, { type: 'block-start', index: 0, blockType: 'text' }) - pushAssistant(sessionId, { type: 'text-delta', index: 0, text: `第 ${String(retry)} 次应撤回的回复` }) - scenario.stepStarted = true - } - const failure = { code: 'TRANSPORT', message: '连接被重置' } - pushAssistant(sessionId, { type: 'finish', reason: { kind: 'error', failure } }) - const attempt = activeAttempts.get(sessionId) as FixtureAttemptState - const attemptEvent = append(sessionId, { - type: 'assistant/attempt', data: { turn: scenario.turn, step: 1, stream: attempt.stream.snapshot() }, - }) - commitAssistant(sessionId, attemptEvent) - append(sessionId, { - type: 'llm/retry', - data: { - turn: scenario.turn, step: 1, - provider: 'fixture', mode: 'normal', policyKey: 'fixture-normal', - retry, maxRetries: 2, delayMs, failure, - }, - }) - scenario.stepStarted = false - }, - /** Record one retry decision, then cancel its source turn before the retry starts. */ - cancelModelRetryDuringBackoff(id: string, delayMs = 450): void { - const sessionId = sid(id) - const scenario = retryScenarios.get(sessionId) - if (scenario === undefined) throw new Error(`fixture: no model retry scenario for ${id}`) - const failure = { code: 'TRANSPORT', message: '连接被重置' } - const active = activeAttempts.get(sessionId) - if (active !== undefined) { - pushAssistant(sessionId, { type: 'finish', reason: { kind: 'error', failure } }) - const attemptEvent = append(sessionId, { - type: 'assistant/attempt', data: { turn: scenario.turn, step: 1, stream: active.stream.snapshot() }, - }) - commitAssistant(sessionId, attemptEvent) - } - append(sessionId, { - type: 'llm/retry', - data: { - turn: scenario.turn, step: 1, - provider: 'fixture', mode: 'normal', policyKey: 'fixture-normal', - retry: 1, maxRetries: 2, delayMs, failure, - }, - }) - append(sessionId, { type: 'step/end', data: { turn: scenario.turn, step: 1 } }) - append(sessionId, { type: 'turn/end', data: { turn: scenario.turn, reason: { kind: 'aborted', reason: { kind: 'user' } }, - } }) - retryScenarios.delete(sessionId) - setRunning(sessionId, false) - }, - /** Finish the timing-hook retry with a finalized response in the open step. */ - completeModelRetry(id: string): void { - const sessionId = sid(id) - const scenario = retryScenarios.get(sessionId) - if (scenario === undefined) throw new Error(`fixture: no model retry scenario for ${id}`) - retryScenarios.delete(sessionId) - const completed = '重试后的完整回复' - beginAssistant(sessionId, scenario.turn, 1) - pushAssistant(sessionId, { type: 'block-start', index: 0, blockType: 'text' }) - pushAssistant(sessionId, { type: 'text-delta', index: 0, text: completed }) - pushAssistant(sessionId, { type: 'block-end', index: 0, block: { type: 'text', text: completed } }) - pushAssistant(sessionId, { type: 'finish', reason: { kind: 'stop' } }) - const attempt = activeAttempts.get(sessionId) as FixtureAttemptState - const message = append(sessionId, { - type: 'assistant/message', - surfaceOp: 'append', - data: { - turn: scenario.turn, - step: 1, - message: assistantMessage(text(completed)), - stream: attempt.stream.snapshot(), - }, - }) - commitAssistant(sessionId, message) - append(sessionId, { type: 'step/end', data: { turn: scenario.turn, step: 1 } }) - append(sessionId, { type: 'turn/end', data: { turn: scenario.turn, reason: { kind: 'completed' } } }) - setRunning(sessionId, false) - }, - /** Log append without follow delivery: a frame lost in transit that page repair must recover. */ - appendSilent(id: string, msg: string): void { - const log = logOf(sid(id)) - log.push({ type: 'user/message', surfaceOp: 'append', seq: SessionSeq(log.length), time: Date.now(), data: userMessage(text(msg)) } as unknown as SessionEvent) - }, - /** End every open stream generator (client sees both streams close -> reconnect + resync path). */ - breakStreams(): void { - for (const breakNow of [...streamBreakers]) breakNow() - }, - } - ;(globalThis as Record).__fxTiming = timingHooks - - /** Prompt replay: chunk typewriter (80ms/frame) -> assistant/message finalize -> turn/end + running flip. */ - const startReply = (id: SessionId, turn: number, replyText: string): void => { - const step = 0 - append(id, { type: 'step/start', data: { turn, step } }) - beginAssistant(id, turn, step) - pushAssistant(id, { type: 'block-start', index: 0, blockType: 'text' }) - /* v8 ignore next -- the ?? arm needs a null match, but every fixture reply is non-empty. */ - const pieces = replyText.match(/[\s\S]{1,6}/gu) ?? [replyText] - let i = 0 - const finish = (aborted: boolean): void => { - replays.delete(id) - const done = pieces.slice(0, i).join('') - pushAssistant(id, { type: 'block-end', index: 0, block: { type: 'text', text: done } }) - pushAssistant(id, { type: 'usage', usage: fixtureUsage(turn, step) }) - if (!aborted) pushAssistant(id, { type: 'finish', reason: { kind: 'stop' } }) - const attempt = activeAttempts.get(id) as FixtureAttemptState - const message = append(id, { - type: 'assistant/message', - surfaceOp: 'append', - data: { - turn, - step, - message: assistantMessage(text(done)), - stream: attempt.stream.snapshot(), - usage: fixtureUsage(turn, step), - ...(aborted ? { interrupted: true } : {}), - }, - }) - commitAssistant(id, message) - append(id, { type: 'step/end', data: { turn, step } }) - append(id, { type: 'turn/end', data: { - turn, - reason: aborted ? { kind: 'aborted', reason: { kind: 'user' } } : { kind: 'completed' }, - } }) - setRunning(id, false) - } - const tick = (): void => { - const piece = pieces[i] - if (piece === undefined) { - finish(false) - return - } - i++ - pushAssistant(id, { type: 'text-delta', index: 0, text: piece }) - replays.set(id, { timer: setTimeout(tick, 80), finish }) - } - replays.set(id, { timer: setTimeout(tick, 80), finish }) - } - - const sessionApi: FixtureSessionApi = { - list: _request => sessionOk({ items: [...sessions].sort((a, b) => b.updatedAt - a.updatedAt) }), - search: (request, signal) => { - if (signal.aborted) { - return sessionErr({ - code: 'gateway/cancelled', - message: 'fixture session search was aborted', - details: {}, - }) - } - const query = searchTokenSpans(request.query).tokens.map(token => token.value) - const matches = sessions.flatMap((summary) => { - const log = logs.get(summary.sessionId) ?? [] - const current = new Set(foldSurface(log).nodes) - const best = log.flatMap((event): FixtureSearchCandidate[] => { - if (!current.has(event.seq)) return [] - const eventText = searchEventText(event) - const document = searchTokenSpans(eventText) - const match = phraseMatch(document.tokens, query) - if (match.count === 0) return [] - return [{ - sessionId: summary.sessionId, - seq: event.seq, - time: event.time, - text: document.text, - matchCount: match.count, - matchStart: match.start, - matchEnd: match.end, - documentLength: Array.from(eventText).length, - }] - }).sort(compareSearchCandidates)[0] - return best === undefined ? [] : [best] - }).sort(compareSearchCandidates) - return sessionOk({ - items: matches.slice(0, FIXTURE_SESSION_SEARCH_RESULT_LIMIT).map(match => ({ - sessionId: match.sessionId, - snippet: searchSnippet(match.text, match.matchStart, match.matchEnd), - })), - hasMore: matches.length > FIXTURE_SESSION_SEARCH_RESULT_LIMIT, - }) - }, - create: async (request) => { - const workspace = request.workspaceId === undefined - ? undefined - : workspaces.find(w => w.workspaceId === request.workspaceId) - if (request.workspaceId !== undefined && workspace === undefined) { - return sessionErr({ - code: 'workspace/not-found', - message: `no workspace ${request.workspaceId}`, - details: { workspaceId: request.workspaceId }, - }) - } - const cwd = workspace?.path ?? request.cwd ?? '/tmp/fixture' - const requestedId = request.sessionId - const attachWorkspace = (sessionId: SessionId): void => { - /* v8 ignore next -- callers enter only when a target Workspace exists. */ - if (workspace === undefined || workspace.sessionIds.includes(sessionId)) return - workspace.sessionIds = [sessionId, ...workspace.sessionIds] - workspace.updatedAt = new Date().toISOString() - emitWorkspace({ type: 'upsert', workspace: workspaceSnapshot(workspace) }) - } - const attachFailure = ( - sessionId: SessionId, - workspaceId: WorkspaceId, - ): Promise> => sessionErr({ - code: 'session/workspace-attach-failed' as const, - message: `fixture rejected Workspace attachment for ${sessionId}`, - details: { sessionId, workspaceId }, - }) - if (requestedId !== undefined) { - const existing = summaryOf(requestedId) - if (existing !== undefined) { - if (existing.cwd !== cwd) { - return sessionErr({ - code: 'session/conflict', - message: `session ${requestedId} already uses ${existing.cwd ?? 'no cwd'}`, - details: { sessionId: requestedId, requestedCwd: cwd, ...existing.cwd === undefined ? {} : { existingCwd: existing.cwd } }, - }) - } - if (workspace !== undefined && !workspace.sessionIds.includes(requestedId)) { - if (options.failWorkspaceAttach) return attachFailure(requestedId, workspace.workspaceId) - attachWorkspace(requestedId) - } - return sessionOk({ sessionId: requestedId }) - } - } - const created: FixtureSessionSummary = { - sessionId: requestedId ?? sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, blank: true, cwd, - } - sessions.push(created) - modelSelections.set(created.sessionId, { provider: 'deepseek-official', model: 'deepseek-v4-flash' }) - const emitSession = (): void => { - emitRemote('api-session/added', [created]) - } - if (workspace !== undefined && options.failWorkspaceAttach) { - emitSession() - return attachFailure(created.sessionId, workspace.workspaceId) - } - if (workspace !== undefined && options.createFrameOrder === 'workspace-first') { - attachWorkspace(created.sessionId) - emitSession() - } else { - emitSession() - if (workspace !== undefined) attachWorkspace(created.sessionId) - } - if (options.dropSessionCreateResponse) throw new Error('fixture: dropped session.create response after publication') - return sessionOk({ sessionId: created.sessionId }) - }, - rename: (request) => { - const missing = requireRemoteSession(request) - if (missing !== undefined) return missing - const { sessionId, title } = request - const normalized = title.trim().replace(/\s+/g, ' ') - if (normalized.length === 0) { - return sessionErr({ - code: 'session/title-invalid', - message: 'session title must contain visible characters', - details: { sessionId }, - }) - } - // The append emits the durable event and its control projection frame; - // the unary response settles the caller first. - append(sessionId, { - type: 'session/title', - data: { title: normalized, messageSeqs: [], source: { kind: 'user' } }, - }) - const appended = logOf(sessionId).at(-1) as SessionEvent - return sessionOk({ title: normalized, seq: appended.seq }) - }, - fork: (request) => { - const { sessionId, atSeq } = request - const source = summaryOf(sessionId) - if (source === undefined) { - return sessionErr({ - code: 'session/not-found', - message: `no session ${sessionId}`, - details: { sessionId }, - }) - } - const log = logs.get(sessionId) ?? [] - const lastSeq = log.at(-1)?.seq ?? -1 - const anchoredBoundary = atSeq === undefined - ? undefined - : log.find(e => e.type === 'turn/end' && e.seq >= atSeq) - const boundary = anchoredBoundary - ?? (atSeq === undefined || atSeq > lastSeq - ? log.findLast(e => e.type === 'turn/end') - : undefined) - if (boundary === undefined) { - return sessionErr({ - code: 'session/fork-unavailable', - message: atSeq !== undefined && atSeq <= lastSeq - ? `session ${sessionId} has not completed the turn containing event ${String(atSeq)}` - : `session ${sessionId} has no completed turn`, - details: { sessionId }, - }) - } - const cut = boundary.seq + 1 - const child: FixtureSessionSummary = { - sessionId: sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, blank: false, - parentSessionId: sessionId, - ...source.cwd === undefined ? {} : { cwd: source.cwd }, - } - logs.set(child.sessionId, log.slice(0, cut)) - sessions.push(child) - emitRemote('api-session/added', [child]) - const workspace = workspaces.find(w => w.sessionIds.includes(sessionId)) - if (workspace !== undefined) { - workspace.sessionIds = [child.sessionId, ...workspace.sessionIds] - workspace.updatedAt = new Date().toISOString() - emitWorkspace({ type: 'upsert', workspace: workspaceSnapshot(workspace) }) - } - return sessionOk({ sessionId: child.sessionId }) - }, - history: async (request) => { - const log = logs.get(request.sessionId) ?? [] - const throughSeq = request.throughSeq ?? log.length - 1 - const boundedLog = log.slice(0, throughSeq + 1) - // Snapshot at request time, then deliver after the transit delay. - const page = pageOf(boundedLog, request.beforeSeq, request.maxMessages ?? 50) - const doomed = failNextHistory - failNextHistory = false - const delay = historyDelayMs - if (delay > 0) await new Promise(resolve => setTimeout(resolve, delay)) - if (doomed) throw new Error('fixture: simulated history transport failure') - return sessionOk(page) - }, - selectModel: (request) => { - const selected: ModelSelection = { - provider: request.provider, - model: request.model, - ...request.reasoningEffort === undefined - ? {} - : { reasoningEffort: request.reasoningEffort }, - } - append(request.sessionId, { type: 'model/selection', data: selected }) - modelSelections.set(request.sessionId, selected) - return sessionOk({ selected }) - }, - prompt: (request) => { - const { sessionId: id, mode, content } = request - const summary = summaryOf(id) - if (summary === undefined) { - return sessionErr({ code: 'session/not-found', message: `no session ${id}`, details: { sessionId: id } }) - } - if (options.rejectPrompt) { - if (content.some(block => block.type === 'image')) { - return sessionErr({ - code: 'session/attachment-invalid', - message: 'fixture: image side exceeds the deployment limit', - details: { reason: 'IMAGE_DIMENSION_TOO_LARGE' }, - }) - } - return sessionErr({ - code: 'session/agent-busy', - message: 'fixture: prompt rejected before acceptance', - details: { reason: 'fixture-prompt-rejection' }, - }) - } - summary.updatedAt = Date.now() - // First accepted prompt appends events: the summary stops being blank. - summary.blank = false - const userText = content.map(b => (b.type === 'text' ? b.text : '')).join('') - const durable: ContentBlock[] = content.map((block) => { - if (block.type === 'text') return block - const attachment: ImageAttachmentRef = { - attachmentId: `fixture:${randomUuid()}` as AttachmentIdType, - mediaType: block.mediaType, - bytes: Math.max( - 1, - Math.floor(block.data.length * 3 / 4) - - (block.data.endsWith('==') ? 2 : block.data.endsWith('=') ? 1 : 0), - ), - width: 160, - height: 90, - ...block.name === undefined ? {} : { name: block.name }, - } - attachments.set(String(attachment.attachmentId), { attachment, data: block.data }) - return { type: 'image', attachment } - }) - // The host echoes the prompt's requestId as the user source's rpcId; - // the Session object retires its local submission echo on it. The - // user-rpc source member is declared by dsh-api-session-controller, - // which this standalone fixture does not import — hence the assertion. - const promptSource = { kind: 'user', rpcId: request.requestId } as MessageSource - if (mode === 'steer' && replays.has(id)) { - // Steering: the durable user/message lands inside the current turn; the replay continues. - append(id, { type: 'user/message', surfaceOp: 'append', data: userMessage(durable, promptSource) }) - return sessionOk({ accepted: true as const }) - } - const turn = nextTurn.get(id) ?? 0 - nextTurn.set(id, turn + 1) - setRunning(id, true) - append(id, { type: 'turn/start', data: { turn } }) - // Boundary flush parallel (the host's step/start observer): an outstanding - // /plan selection commits as plan/mode inside the opened turn. - const plan = foldPlan(logOf(id)) - if (plan.wanted !== null && plan.wanted !== plan.active) { - append(id, { type: 'plan/mode', data: { active: plan.wanted } }) - } - append(id, { type: 'user/message', surfaceOp: 'append', data: userMessage(durable, promptSource) }) - // Capacity parallel of the host token-meter's request/context record: - // log-only, appended inside the open turn, and deduplicated against the - // route already recorded (the fixture never varies contextWindow). - const selection = modelSelections.get(id) ?? { provider: 'deepseek', model: 'deepseek-v4-flash' } - const previousHeader = logOf(id).findLast(event => event.type === 'request/header') - const previousSelection = previousHeader?.type === 'request/header' - ? { - provider: previousHeader.data.header.config.provider, - model: previousHeader.data.header.config.model, - ...(previousHeader.data.header.config.reasoningEffort === undefined - ? {} - : { reasoningEffort: previousHeader.data.header.config.reasoningEffort }), - } - : null - if (!sameModelSelection(previousSelection, selection)) { - append(id, { - type: 'request/header', - data: { - header: { config: selection }, - reason: previousHeader === undefined ? 'initial' : 'change', - }, - }) - } - if (lastRequestContext(logOf(id))?.model !== selection.model) { - append(id, { - type: 'request/context', - data: { provider: selection.provider, model: selection.model, contextWindow: 128_000 }, - }) - } - startReply( - id, - turn, - userText === 'render markdown' - ? MARKDOWN_FIXTURE - : userText === 'report model' - ? (() => { - const selection = modelSelections.get(id) - return `当前模型:${selection?.provider ?? 'unknown'}/${selection?.model ?? 'unknown'}` - + (selection?.reasoningEffort === undefined ? '' : ` · 推理等级:${selection.reasoningEffort}`) - })() - : `回声:${userText}。这是 fixture 的流式回复,用于验证打字机增长与定稿切换。`, - ) - return sessionOk({ accepted: true as const }) - }, - attachment: (request) => { - const stored = attachments.get(String(request.attachmentId)) - if (stored === undefined) { - return sessionErr({ - code: 'session/attachment-invalid', - message: 'fixture attachment missing', - details: { reason: 'ATTACHMENT_NOT_FOUND' }, - }) - } - if (!logReferencesAttachment( - logs.get(request.sessionId) ?? [], - String(request.attachmentId), - )) { - return sessionErr({ - code: 'session/attachment-invalid', - message: 'fixture attachment is not referenced by this session', - details: { reason: 'ATTACHMENT_NOT_REFERENCED' }, - }) - } - return sessionOk(stored) - }, - updateQueue: request => sessionErr({ - code: 'session/queue-item-not-found', - message: 'fixture has no pending queue item', - details: { itemId: request.itemId }, - }), - cancel: (request) => { - const replay = replays.get(request.sessionId) - if (replay !== undefined) { - clearTimeout(replay.timer) - replay.finish(true) - } else { - setRunning(request.sessionId, false) - } - return sessionOk({ accepted: true as const }) - }, - } - - const controlBaseline = (): Extract => { - const queues: Record = {} - const jobs: Record = {} - const projections: Record = {} - for (const summary of sessions) { - queues[summary.sessionId] = [] - jobs[summary.sessionId] = [] - const log = logs.get(summary.sessionId) ?? [] - projections[summary.sessionId] = { - asOfSeq: log.length - 1, - values: projectionValuesOf(log), - } - } - return { - type: 'baseline', - value: { - queues, - jobs, - approvals: [], - questions: [], - projections, - }, - } - } - - const approvalInvocation = (): FixtureRemoteEventInvocationFrame => ({ - type: 'waterfall', - event: 'approval/request', - eventId: pendingApprovalEventId, - agentId: sid('fx-alpha'), - request: { - toolName: 'dangerous_tool', - reason: 'fixture 常驻审批(可答:批准/拒绝后消失)', - }, - }) - - const questionInvocation = (): FixtureRemoteEventInvocationFrame => ({ - type: 'waterfall', - event: 'user-questions/request', - eventId: pendingQuestionEventId, - agentId: sid('fx-alpha'), - request: { - questions: fixtureQuestions, - }, - }) - - async function* openControl(signal: AbortSignal): AsyncGenerator { - signal.throwIfAborted() - const conn = new FxInbox() - controlConns.add(conn) - const breakNow = (): void => { conn.breakNow() } - streamBreakers.add(breakNow) - try { - yield controlBaseline() - yield* conn.drain(signal) - } finally { - streamBreakers.delete(breakNow) - controlConns.delete(conn) - } - } - - async function* openWorkspace(signal: AbortSignal): AsyncGenerator { - signal.throwIfAborted() - const conn = new FxInbox() - workspaceConns.add(conn) - const breakNow = (): void => { conn.breakNow() } - streamBreakers.add(breakNow) - try { - yield workspaceBaseline() - yield* conn.drain(signal) - } finally { - streamBreakers.delete(breakNow) - workspaceConns.delete(conn) - } - } - - async function* openWorkspaceFileChanges(signal: AbortSignal): AsyncGenerator { - signal.throwIfAborted() - const conn = new FxInbox() - const breakNow = (): void => { conn.breakNow() } - streamBreakers.add(breakNow) - // Opt-in only: an unprompted frame would age every preview of demo.txt - // into its changed state on a timer the assembled snapshots cannot see. - const announce = options.fileChanges - ? setTimeout(() => { - conn.push({ kind: 'change', change: { absolutePath: `${WORKSPACE_FILES_ROOT}/notes/demo.txt`, version: 'fx-demo-v2' } }) - }, 1000) - : undefined - try { - yield { kind: 'ready' } - yield* conn.drain(signal) - } finally { - if (announce !== undefined) clearTimeout(announce) - streamBreakers.delete(breakNow) - } - } - - async function* openRemoteEvents( - signal: AbortSignal, - ): AsyncGenerator { - signal.throwIfAborted() - const clientId = randomUuid() - const conn = new FxInbox() - remoteEventConns.set(clientId, conn) - // Periodic material for the RPC-panel acceptance: flip fx-gamma every 5s. - // fx-gamma only; the conversation replay owns fx-alpha's running state. - const timer = setInterval(() => { - const gamma = summaryOf(sid('fx-gamma')) - /* v8 ignore next -- the fixture never removes fx-gamma. */ - if (gamma !== undefined) setRunning(gamma.sessionId, !gamma.running) - }, 5000) - try { - yield { type: 'ready', clientId, host: { home: FIXTURE_HOME } } - if (approvalPending) yield approvalInvocation() - if (questionPending) yield questionInvocation() - yield* conn.drain(signal) - } finally { - clearInterval(timer) - remoteEventConns.delete(clientId) - } - } - - async function* openFollow( - request: FixtureFollowRequest, - signal: AbortSignal, - ): AsyncGenerator { - signal.throwIfAborted() - const sessionId = request.address.kind === 'session' - ? request.address.sessionId - : request.address.childSessionId - if (summaryOf(sessionId) === undefined) throw new Error(`fixture: no session ${sessionId}`) - const conn = new FxInbox() - let conns = followConns.get(sessionId) - if (conns === undefined) { - conns = new Set() - followConns.set(sessionId, conns) - } - conns.add(conn) - const breakNow = (): void => { conn.breakNow() } - streamBreakers.add(breakNow) - const snapshot = [...logOf(sessionId)] - const cursor = snapshot.at(-1)?.seq ?? -1 - const summary = summaryOf(sessionId) - /* v8 ignore next -- existence was checked before the stream registered. */ - if (summary === undefined) throw new Error(`fixture: no session ${sessionId}`) - const initial = pageOf(snapshot, undefined, request.maxMessages ?? 50) - let nextSeq = cursor + 1 - try { - yield { - type: 'snapshot', - header: { - version: SESSION_FORMAT_VERSION, - id: sessionId, - createdAt: summary.updatedAt, - ...(summary.cwd === undefined ? {} : { cwd: summary.cwd }), - ...(summary.parentSessionId === undefined ? {} : { parentSession: summary.parentSessionId }), - isSeeded: summary.parentSessionId !== undefined, - ...(summary.origin === undefined ? {} : { origin: summary.origin }), - ...(summary.agentPreset === undefined ? {} : { agentPreset: summary.agentPreset }), - }, - cursor, - records: initial.records, - hasMore: initial.hasMore, - projections: { asOfSeq: cursor, values: projectionValuesOf(snapshot) }, - ...(request.assistantStream === true ? { - assistantStream: { - revision: assistantRevisions.get(sessionId) ?? 0, - ...(activeAttempts.get(sessionId) === undefined - ? {} - : { activeAttempt: { - attemptId: (activeAttempts.get(sessionId) as FixtureAttemptState).attemptId, - startedAfterSeq: (activeAttempts.get(sessionId) as FixtureAttemptState).startedAfterSeq, - turn: (activeAttempts.get(sessionId) as FixtureAttemptState).turn, - step: (activeAttempts.get(sessionId) as FixtureAttemptState).step, - nextIndex: (activeAttempts.get(sessionId) as FixtureAttemptState).index, - stream: (activeAttempts.get(sessionId) as FixtureAttemptState).stream.snapshot(), - } }), - }, - } : {}), - } - for await (const frame of conn.drain(signal)) { - if (frame.type === 'assistant-stream') { - yield frame - continue - } - if (frame.event.seq < nextSeq) continue - if (frame.event.seq !== nextSeq) { - throw new Error(`fixture: session event stream skipped seq ${String(nextSeq)}`) - } - nextSeq++ - yield frame - } - } finally { - streamBreakers.delete(breakNow) - conns.delete(conn) - if (conns.size === 0) followConns.delete(sessionId) - } - } - - const answerRemoteEvent = (result: FixtureRemoteEventResult): ConnectionRpcResult => { - if (!remoteEventConns.has(result.clientId)) { - return { - ok: false, - error: { - code: 'gateway/invocation-unavailable', - message: 'fixture Remote event result identifies no active event stream', - details: {}, - }, - } - } - if (result.eventId === pendingApprovalEventId) { - if (!approvalPending) return { ok: true, value: undefined } - approvalPending = false - } else if (result.eventId === pendingQuestionEventId) { - if (!questionPending) return { ok: true, value: undefined } - questionPending = false - } else { - return { ok: true, value: undefined } - } - emitRemoteFrame({ type: 'cancel', eventId: result.eventId }) - return { ok: true, value: undefined } - } - - const workspaceApi: FixtureWorkspaceApi = { - create: (request) => { - const existing = workspaces.find(workspace => workspace.path === request.path) - if (existing !== undefined) { - return sessionOk({ workspace: workspaceSnapshot(existing), created: false }) - } - const now = new Date().toISOString() - const created: FixtureWorkspace = { - workspaceId: wid(`fx-ws-${nextWorkspace++}`), - path: request.path, - title: request.path.split('/').filter(Boolean).at(-1) ?? request.path, - sessionIds: [], - createdAt: now, - updatedAt: now, - } - workspaces.unshift(created) - const workspace = workspaceSnapshot(created) - emitWorkspace({ type: 'upsert', workspace }) - return sessionOk({ workspace, created: true }) - }, - rename: (request) => { - const workspace = workspaces.find(candidate => candidate.workspaceId === request.workspaceId) - if (workspace === undefined) { - return sessionErr({ - code: 'workspace/not-found', - message: `no workspace ${request.workspaceId}`, - details: { workspaceId: request.workspaceId }, - }) - } - const title = request.title.trim() - if (title === '') { - return sessionErr({ - code: 'gateway/bad-request', - message: 'Workspace rename requires a non-blank title', - details: {}, - }) - } - if (title !== workspace.title) { - if (workspaces.some(candidate => candidate.workspaceId !== request.workspaceId && candidate.title === title)) { - return sessionErr({ - code: 'workspace/name-conflict', - message: `workspace name '${title}' is already in use`, - details: { name: title }, - }) - } - workspace.title = title - workspace.updatedAt = new Date().toISOString() - emitWorkspace({ type: 'upsert', workspace: workspaceSnapshot(workspace) }) - } - return sessionOk({ workspace: workspaceSnapshot(workspace) }) - }, - delete: (request) => { - const index = workspaces.findIndex(workspace => workspace.workspaceId === request.workspaceId) - if (index === -1) { - return sessionErr({ - code: 'workspace/not-found', - message: `no workspace ${request.workspaceId}`, - details: { workspaceId: request.workspaceId }, - }) - } - workspaces.splice(index, 1) - emitWorkspace({ type: 'remove', workspaceId: request.workspaceId }) - return sessionOk({ deleted: true }) - }, - insertBefore: (request) => { - const source = workspaces.findIndex(workspace => workspace.workspaceId === request.workspaceId) - const anchor = request.beforeWorkspaceId === undefined - ? workspaces.length - : workspaces.findIndex(workspace => workspace.workspaceId === request.beforeWorkspaceId) - const missing = source === -1 - ? request.workspaceId - : anchor === -1 - ? request.beforeWorkspaceId - : undefined - if (missing !== undefined) { - return sessionErr({ - code: 'workspace/not-found', - message: `no workspace ${missing}`, - details: { workspaceId: missing }, - }) - } - if (request.beforeWorkspaceId !== request.workspaceId) { - const previousOrder = workspaces.map(workspace => workspace.workspaceId) - const [workspace] = workspaces.splice(source, 1) - /* v8 ignore next -- source was resolved from the same array immediately above. */ - if (workspace === undefined) throw new Error(`fixture lost workspace ${request.workspaceId}`) - const at = request.beforeWorkspaceId === undefined - ? workspaces.length - : workspaces.findIndex(candidate => candidate.workspaceId === request.beforeWorkspaceId) - workspaces.splice(at, 0, workspace) - if (workspaces.some((candidate, index) => candidate.workspaceId !== previousOrder[index])) { - emitWorkspace({ - type: 'order', - workspaceIds: workspaces.map(candidate => candidate.workspaceId), - }) - } - } - return sessionOk({ workspaceIds: workspaces.map(candidate => candidate.workspaceId) }) - }, - insertSessionBefore: (request) => { - const workspace = workspaces.find(candidate => candidate.workspaceId === request.workspaceId) - if (workspace === undefined) { - return sessionErr({ - code: 'workspace/not-found', - message: `no workspace ${request.workspaceId}`, - details: { workspaceId: request.workspaceId }, - }) - } - if (!workspace.sessionIds.includes(request.sessionId) - || (request.beforeSessionId !== undefined && !workspace.sessionIds.includes(request.beforeSessionId))) { - return sessionErr({ - code: 'workspace/move-invalid', - message: `session or anchor is not accounted by workspace ${request.workspaceId}`, - details: { - workspaceId: request.workspaceId, - sessionId: request.sessionId, - ...request.beforeSessionId === undefined ? {} : { beforeSessionId: request.beforeSessionId }, - }, - }) - } - const without = workspace.sessionIds.filter(id => id !== request.sessionId) - const at = request.beforeSessionId === undefined ? without.length : without.indexOf(request.beforeSessionId) - const sessionIds = [...without.slice(0, at), request.sessionId, ...without.slice(at)] - if (!sessionIds.every((id, index) => id === workspace.sessionIds[index])) { - workspace.sessionIds = sessionIds - workspace.updatedAt = new Date().toISOString() - emitWorkspace({ type: 'upsert', workspace: workspaceSnapshot(workspace) }) - } - return sessionOk({ workspace: workspaceSnapshot(workspace) }) - }, - archiveSession: (request) => { - if (summaryOf(request.sessionId) === undefined) { - return sessionErr({ - code: 'session/not-found', - message: `no session ${request.sessionId}`, - details: { sessionId: request.sessionId }, - }) - } - if (!archivedSessionIds.includes(request.sessionId)) { - archivedSessionIds.push(request.sessionId) - emitWorkspace({ type: 'archived', archivedSessionIds: [...archivedSessionIds] }) - } - return sessionOk({ archivedSessionIds: [...archivedSessionIds] }) - }, - unarchiveSession: (request) => { - if (archivedSessionIds.includes(request.sessionId)) { - archivedSessionIds.splice(archivedSessionIds.indexOf(request.sessionId), 1) - emitWorkspace({ type: 'archived', archivedSessionIds: [...archivedSessionIds] }) - } - return sessionOk({ archivedSessionIds: [...archivedSessionIds] }) - }, - } - - const rpc: ClientConnectionRpc = { - call(channel, endpoint, payload, signal) { - if (channel !== '/api') { - return Promise.reject(new Error(`fixture connection RPC channel ${JSON.stringify(channel)} is unavailable`)) - } - const args = (payload as { - args: Readonly<{ - agentId: SessionId - line?: string - query?: string - path?: string - range?: { offset?: number; limit?: number } - name?: string - images?: readonly unknown[] - // A goal ref and a credential reference name share this wire field name. - ref?: string | { id: string; revision: number } - refs?: readonly string[] - value?: string - ns?: string - settingsNs?: string - agentPreset?: string - from?: string - id?: string - request?: unknown - _request?: unknown - }> - }).args - const sessionId = args.agentId - const callSignal = signal ?? new AbortController().signal - const request = args.request - switch (endpoint) { - case 'commands/list': return Promise.resolve(commandRemotes.list(sessionId)) - case 'commands/execute': return Promise.resolve(commandRemotes.execute(sessionId, args.line as string, args.images ?? [])) - case 'fileReferences/list': return Promise.resolve(referenceRemotes.files(sessionId, args.query ?? '')) - case 'sessionReferenceResolver/candidates': return Promise.resolve(referenceRemotes.sessions(sessionId, args.query ?? '')) - case 'directoryPicker/pick': return Promise.resolve(directoryPickerRemotes.pick()) - case 'directoryPicker/list': return Promise.resolve(directoryPickerRemotes.list(args.path)) - case 'directoryPicker/createDirectory': - return Promise.resolve(directoryPickerRemotes.createDirectory(args.path ?? '', args.name ?? '')) - case 'goals/get': return Promise.resolve(goalRemotes.get(sessionId)) - case 'goals/create': return Promise.resolve(goalRemotes.create(sessionId, { - objective: (request as { objective?: string } | undefined)?.objective as string, - ...(request as { maxGoalRounds?: number } | undefined)?.maxGoalRounds === undefined - ? {} - : { maxGoalRounds: (request as { maxGoalRounds: number }).maxGoalRounds }, - })) - case 'goals/edit': return Promise.resolve(goalRemotes.edit( - sessionId, - args.ref as FxGoalRef, - request as { objective?: string; maxGoalRounds?: number }, - )) - case 'goals/pause': return Promise.resolve(goalRemotes.pause(sessionId, args.ref as FxGoalRef)) - case 'goals/resume': return Promise.resolve(goalRemotes.resume(sessionId, args.ref as FxGoalRef)) - case 'goals/complete': return Promise.resolve(goalRemotes.complete(sessionId, args.ref as FxGoalRef)) - case 'goals/clear': return Promise.resolve(goalRemotes.clear(sessionId, args.ref as FxGoalRef)) - case 'agentPresets/list': return Promise.resolve(presetRemotes.list()) - case 'agentPresets/select': return Promise.resolve(presetRemotes.select(sessionId, args.agentPreset as string)) - case 'agentPresets/read': return Promise.resolve(presetRemotes.read(args.agentPreset as string)) - case 'agentPresets/copy': return Promise.resolve(presetRemotes.copy(args.from as string, args.id as string)) - case 'agentPresets/deletePreset': return Promise.resolve(presetRemotes.deletePreset(args.id as string)) - case 'subagents/list': return Promise.resolve({ - ok: true, - value: { entries: [], parentAvailable: true }, - }) - case 'subagents/prompt': return Promise.resolve({ - ok: true, - value: { - messageId: `fixture-message-${(request as { childSessionId: SessionId }).childSessionId}`, - }, - }) - case 'subagents/interruptByParent': return Promise.resolve({ ok: true, value: { accepted: true } }) - case 'credentials/describe': return Promise.resolve(credentialRemotes.describe(args.refs ?? [])) - case 'credentials/set': return Promise.resolve(credentialRemotes.set(args.ref as string)) - case 'credentials/unset': return Promise.resolve(credentialRemotes.unset(args.ref as string)) - case 'settings/describe': return Promise.resolve(settingsRemotes.describe()) - case 'settings/canOpenAgentPresetDirectory': return Promise.resolve({ ok: true, value: true }) - case 'settings/openSettingsDocument': return Promise.resolve(settingsRemotes.openSettingsDocument()) - case 'settings/openAgentPresetDirectory': return Promise.resolve( - settingsRemotes.openAgentPresetDirectory(args.agentPreset as string), - ) - case 'skills/list': { - const skillRequest = request as { readonly sessionId: SessionId } - const missing = requireRemoteSession(skillRequest) - if (missing !== undefined) return missing - return sessionOk({ - skills: [ - { name: 'fixture-demo', description: 'fixture 技能样本', whenToUse: '仅供 UI 目录渲染验收', modelInvocable: true }, - { name: 'fixture-user-only', description: 'fixture 仅用户技能样本', modelInvocable: false }, - ], - }) - } - case 'session/openWorkspacePath': { - return sessionOk({ opened: true as const }) - } - case 'workspaceFiles/read': { - return Promise.resolve(workspaceFileRemotes.read(args.path ?? '', args.range ?? {})) - } - case 'workspaceFiles/stat': { - return Promise.resolve(workspaceFileRemotes.stat(args.path ?? '')) - } - case 'workspaceFiles/list': { - return Promise.resolve(workspaceFileRemotes.list(args.path ?? '')) - } - case 'session/canOpenWorkspacePath': return Promise.resolve({ ok: true, value: true }) - case 'session/modelCatalog': return Promise.resolve({ - ok: true, - value: { - default: { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, - routableProviders: ['deepseek-official', 'openai', 'acme-gateway'], - groups: fixtureModelGroups(), - failures: [], - }, - }) - case 'llm/listProviders': return Promise.resolve({ - ok: true, - value: [ - { id: 'deepseek-official', name: 'DeepSeek' }, - { id: 'openai', name: 'openai' }, - { id: 'acme-gateway', name: 'Acme Gateway' }, - ], - }) - case 'llm/listConfigurableProviders': return Promise.resolve({ - ok: true, - value: [ - { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] }, - { provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], declared: false }, - { provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], declared: false }, - { provider: 'acme-gateway', displayName: 'Acme Gateway', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'acme-gateway'], declared: true }, - ], - }) - // The fixture endpoint is imaginary, so interrogation answers the - // catalog it already serves without a network request. - case 'llm/discoverModels': return Promise.resolve({ - ok: true, - value: fixtureModelGroups().flatMap(group => group.models.map(model => ({ id: model.id, name: model.name }))), - }) - case 'settings/update': return Promise.resolve(settingsRemotes.update(args.ns as string)) - case 'settings/replace': return Promise.resolve(settingsRemotes.replace(args.ns as string)) - case 'settings/mutate': return Promise.resolve(settingsRemotes.mutate(args.ns as string)) - case 'session/list': return sessionApi.list( - args._request as Parameters[0], - ) - case 'session/search': return sessionApi.search( - request as Parameters[0], - callSignal, - ) - case 'session/create': return sessionApi.create( - request as Parameters[0], - ) - case 'session/selectModel': return sessionApi.selectModel( - request as Parameters[0], - ) - case 'session/rename': return sessionApi.rename( - request as Parameters[0], - ) - case 'session/fork': return sessionApi.fork( - request as Parameters[0], - ) - case 'session/prompt': return sessionApi.prompt( - request as Parameters[0], - ) - case 'session/attachment': return sessionApi.attachment( - request as Parameters[0], - ) - case 'session/updateQueue': return sessionApi.updateQueue( - request as Parameters[0], - ) - case 'session/cancel': return sessionApi.cancel( - request as Parameters[0], - ) - case 'session/page': { - const page = request as FixturePageRequest - const pageSessionId = page.address.kind === 'session' - ? page.address.sessionId - : page.address.childSessionId - return sessionApi.history({ - sessionId: pageSessionId, - throughSeq: page.throughSeq, - ...page.beforeSeq === undefined ? {} : { beforeSeq: page.beforeSeq }, - ...page.maxMessages === undefined ? {} : { maxMessages: page.maxMessages }, - }) - } - case '$events/result': return Promise.resolve(answerRemoteEvent(args as unknown as FixtureRemoteEventResult)) - case 'workspace/create': return workspaceApi.create(request as WorkspaceCreateRequest) - case 'workspace/rename': return workspaceApi.rename(request as WorkspaceRenameRequest) - case 'workspace/delete': return workspaceApi.delete(request as WorkspaceDeleteRequest) - case 'workspace/insertBefore': return workspaceApi.insertBefore(request as WorkspaceInsertBeforeRequest) - case 'workspace/insertSessionBefore': return workspaceApi.insertSessionBefore( - request as WorkspaceInsertSessionBeforeRequest, - ) - case 'workspace/archiveSession': return workspaceApi.archiveSession(request as WorkspaceArchiveSessionRequest) - case 'workspace/unarchiveSession': return workspaceApi.unarchiveSession( - request as WorkspaceUnarchiveSessionRequest, - ) - default: - return Promise.reject(new Error(`fixture connection RPC endpoint ${JSON.stringify(endpoint)} is unavailable`)) - } - }, - open(channel, endpoint, payload, signal) { - if (channel !== '/api') { - throw new Error(`fixture connection RPC channel ${JSON.stringify(channel)} is unavailable`) - } - const args = (payload as { args: Readonly<{ request?: unknown }> }).args - switch (endpoint) { - case '$events': return openRemoteEvents(signal) - case 'session/control': return openControl(signal) - case 'session/follow': return openFollow(args.request as FixtureFollowRequest, signal) - case 'workspace/follow': return openWorkspace(signal) - case 'workspaceFiles/changes': return openWorkspaceFileChanges(signal) - default: - throw new Error(`fixture connection stream endpoint ${JSON.stringify(endpoint)} is unavailable`) - } - }, - } - return { rpc } -} - -/** - * Build the browser fixture transport from the current page's query switches. - * @returns an in-memory Connection RPC transport. - */ -export function createFixtureConnectionRpc(): ClientConnectionRpc { - return createFixtureWorld(fixtureOptionsFromLocation()).rpc -} - -/** Browser query mapping; direct unit callers pass FixtureOptions explicitly. */ -function fixtureOptionsFromLocation(): FixtureOptions { - if (typeof location === 'undefined') return {} - const query = new URLSearchParams(location.search) - return { - empty: query.get('fixture') === 'empty', - rejectPrompt: query.get('fixturePrompt') === 'reject', - failWorkspaceAttach: query.get('fixtureAttach') === 'fail', - dropSessionCreateResponse: query.get('fixtureSessionCreate') === 'drop-response', - createFrameOrder: query.get('fixtureFrames') === 'workspace-first' ? 'workspace-first' : 'session-first', - fileChanges: query.get('fixtureFileChanges') === 'demo', - } -} diff --git a/packages/client/connection/src/client/index.ts b/packages/client/connection/src/client/index.ts index 590004ef46..4497ca05de 100644 --- a/packages/client/connection/src/client/index.ts +++ b/packages/client/connection/src/client/index.ts @@ -8,7 +8,6 @@ import { type ConnectionSinks, type ConnectionState, } from './connection.ts' -import { createFixtureConnectionRpc } from './fixture.ts' import { createWebConnectionRpc, type RpcFetch, type RpcStreamOpen } from './rpc.ts' import { isLoopbackHostname } from '../loopback-hostname.ts' import type { ClientConnectionRpc } from '../rpc.ts' @@ -72,10 +71,10 @@ export interface ConnectionStateSource { export const inject: string[] = [] /** - * Carrier override installed on the page global before plugin boot. The served - * web app leaves it unset and gets HTTP + WebSocket; a shell that owns a - * different physical transport (the worker preview's postMessage tunnel) - * provides both halves here instead of forking this plugin. + * Physical carrier selected when the Connection service is installed. The + * served web app omits it and gets HTTP + WebSocket; a shell that owns a + * different transport (the worker preview's postMessage tunnel) provides both + * halves instead of forking this plugin. */ export interface ClientTransportHooks { /** @@ -111,6 +110,21 @@ interface ClientTransportGlobal { __DSH_CONNECTION_RECOVERY__?: unknown } +/** Browser location fields used to classify loopback authority. */ +export interface ConnectionLocation { + readonly hostname: string +} + +/** Instance-local inputs for installing a Connection service. */ +export interface ConnectionInstallOptions { + /** Explicit physical carrier; omit for the browser HTTP + WebSocket carrier. */ + readonly transport?: ClientTransportHooks + /** Reconnect timing overrides; omitted fields use controller defaults. */ + readonly recovery?: ConnectionRecoveryConfig + /** Page location; omit for a non-browser composition. */ + readonly location?: ConnectionLocation +} + /** * The ctx.connection service API. API Gateway supplies generation readiness * and reset callbacks; Connection stays independent of downstream domain state. @@ -182,16 +196,15 @@ function watchBrowserNetwork(controller: ConnectionController): () => void { } /** - * Client plugin body: pick physical carriers by page mode and provide ctx.connection. - * @param ctx - client cordis context. + * Install one Context-owned Connection service from explicit composition inputs. + * @param ctx - client Cordis context. + * @param options - physical carrier, reconnect timing, and page location. */ -export function apply(ctx: Context): void { - const pageLocation = typeof location === 'undefined' ? undefined : location - const fixture = pageLocation !== undefined && new URLSearchParams(pageLocation.search).has('fixture') - const fixtureRpc = fixture ? createFixtureConnectionRpc() : undefined - const transport = (globalThis as ClientTransportGlobal).__DSH_TRANSPORT__ - const recovery = resolveConnectionConfig((globalThis as ClientTransportGlobal).__DSH_CONNECTION_RECOVERY__) - const rpc = fixtureRpc ?? transport?.rpc ?? createWebConnectionRpc(transport?.fetch, transport?.openStream) +export function installConnection(ctx: Context, options: ConnectionInstallOptions = {}): void { + const pageLocation = options.location + const transport = options.transport + const recovery = options.recovery ?? {} + const rpc = transport?.rpc ?? createWebConnectionRpc(transport?.fetch, transport?.openStream) let generationSource: ConnectionGenerationSource | undefined let owner: ConnectionOwner | undefined let generationId = 0 @@ -294,3 +307,18 @@ export function apply(ctx: Context): void { } ctx.provide('connection', handle) } + +/** + * Client plugin body: read the page composition and install its Connection service. + * @param ctx - client Cordis context. + */ +export function apply(ctx: Context): void { + const globals = globalThis as ClientTransportGlobal + const pageLocation = typeof location === 'undefined' ? undefined : location + const transport = globals.__DSH_TRANSPORT__ + installConnection(ctx, { + ...(transport === undefined ? {} : { transport }), + recovery: resolveConnectionConfig(globals.__DSH_CONNECTION_RECOVERY__), + ...(pageLocation === undefined ? {} : { location: pageLocation }), + }) +} diff --git a/packages/client/connection/tests/client-apply.client.spec.ts b/packages/client/connection/tests/client-apply.client.spec.ts index 3025767fd5..084f037135 100644 --- a/packages/client/connection/tests/client-apply.client.spec.ts +++ b/packages/client/connection/tests/client-apply.client.spec.ts @@ -1,6 +1,6 @@ /** - * Connection plugin browser-half apply: ctx.connection handle mounting, mode - * selection off the page URL, and single-consumer connection-loop ownership. + * Connection plugin browser-half apply: ctx.connection handle mounting, + * explicit carrier selection, and single-consumer connection-loop ownership. */ import { Context } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -15,7 +15,7 @@ import { } from '../src/client/index.ts' type Win = { - location?: { hostname: string; search: string; origin?: string } + location?: { hostname: string; origin?: string } __DSH_TRANSPORT__?: ClientTransportHooks } @@ -124,25 +124,18 @@ describe('connection client apply', () => { }) it('mounts ctx.connection and identifies a loopback page', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() expect(handle.isLoopback).toBe(true) }) - it('selects the fixture RPC transport under ?fixture', async () => { - ;(globalThis as Win).location = { hostname: '127.0.0.1', search: '?fixture' } - const handle = await mount() - await expect(handle.rpc.call('/api', 'settings/describe', { args: {} })) - .resolves.toMatchObject({ ok: true }) - }) - it('reports non-loopback page authority through the connection handle', async () => { - ;(globalThis as Win).location = { hostname: '192.0.2.20', search: '' } + ;(globalThis as Win).location = { hostname: '192.0.2.20' } expect((await mount()).isLoopback).toBe(false) }) it('requires one generation source and ignores a stale source disposer', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() const first = new GenerationProbe() const second = new GenerationProbe() @@ -165,7 +158,7 @@ describe('connection client apply', () => { }) it('start() hands out one loop, rejects a second consumer, and stop() aborts the generation', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() installGeneration(handle) const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) @@ -192,7 +185,7 @@ describe('connection client apply', () => { }) it('does not notify state subscribers when a pre-ready loop stops', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() handle.registerGenerationSource(signal => new Promise((resolve) => { signal.addEventListener('abort', () => { resolve() }, { once: true }) @@ -209,7 +202,7 @@ describe('connection client apply', () => { }) it('allows a replacement owner and ignores the previous owner handle', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() const generation = installGeneration(handle) @@ -232,7 +225,7 @@ describe('connection client apply', () => { }) it('lets the connection service force only its current owner to reconnect', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() installGeneration(handle) const requested = vi.fn() @@ -259,7 +252,7 @@ describe('connection client apply', () => { it('ignores a non-browser window shim without navigator state', async () => { vi.stubGlobal('window', new EventTarget()) - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() installGeneration(handle) const loop = handle.start({}) @@ -276,7 +269,7 @@ describe('connection client apply', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) const browser = new BrowserNetworkProbe() vi.stubGlobal('window', browser) - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() let calls = 0 const source: ConnectionGenerationSource = (signal, ready) => new Promise((resolve) => { @@ -320,7 +313,7 @@ describe('connection client apply', () => { }) it('does not announce a generation synchronously stopped by a generation subscriber', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() installGeneration(handle) const owner: { loop?: ReturnType } = {} @@ -344,7 +337,7 @@ describe('connection client apply', () => { }) it('retracts the generation while connecting and publishes the next generation', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() const generation = installGeneration(handle) const generations: Array = [] @@ -377,7 +370,7 @@ describe('connection client apply', () => { }) it('publishes connection state directly on the service and isolates subscribers', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() const generation = installGeneration(handle) const snapshots: Array = [] @@ -415,7 +408,7 @@ describe('connection client apply', () => { }) it('does not announce disconnection after a generation subscriber stops the loop', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() const generation = installGeneration(handle) const owner: { loop?: ReturnType } = {} @@ -448,7 +441,7 @@ describe('connection client apply', () => { }) it('carries RPC calls without requiring secure-context randomUUID', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '' } + ;(globalThis as Win).location = { hostname: 'localhost' } vi.stubGlobal('crypto', { getRandomValues(bytes: Uint8Array) { return bytes.fill(0) @@ -486,7 +479,7 @@ describe('connection client apply', () => { }) it('uses an already decoded rpc carrier from the transport hooks instead of the HTTP caller', async () => { - ;(globalThis as Win).location = { hostname: 'preview.example', search: '' } + ;(globalThis as Win).location = { hostname: 'preview.example' } const rpc: ClientConnectionRpc = { call: vi.fn(async (_channel: string, endpoint: string, payload: unknown) => ({ ok: true as const, value: { endpoint, payload } })), open: vi.fn((_channel: string, endpoint: string) => (async function *(): AsyncGenerator { yield endpoint })()), @@ -500,7 +493,7 @@ describe('connection client apply', () => { }) it('exposes a worker-local Gateway stream through connection.rpc.open', async () => { - ;(globalThis as Win).location = { hostname: 'preview.example', search: '' } + ;(globalThis as Win).location = { hostname: 'preview.example' } const openStream = vi.fn>( (endpoint, payload, signal) => (async function *(): AsyncGenerator { signal.throwIfAborted() @@ -538,7 +531,7 @@ describe('connection client apply', () => { it('validates generic RPC transport failures, correlation, and targets', async () => { ;(globalThis as Win).location = { - hostname: 'harness.example', search: '', origin: 'https://harness.example', + hostname: 'harness.example', origin: 'https://harness.example', } const handle = await mount() const original = globalThis.fetch @@ -552,7 +545,7 @@ describe('connection client apply', () => { expect.objectContaining({ signal: abort.signal }), ) - ;(globalThis as Win).location = { hostname: 'localhost', search: '', origin: 'null' } + ;(globalThis as Win).location = { hostname: 'localhost', origin: 'null' } globalThis.fetch = vi.fn().mockResolvedValue(Response.json({ type: 'server-response', rpcId: 'different-rpc', @@ -624,37 +617,4 @@ describe('connection client apply', () => { } }) - it('carries Goal Remotes over the client-only fixture state', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } - const handle = await mount() - const created = await handle.rpc.call('/api', 'goals/create', { - args: { agentId: 'fx-alpha', request: { objective: 'fixture remote' } }, - }) - expect(created).toMatchObject({ ok: true, value: { ref: { revision: 1 } } }) - if (!created.ok) throw new Error('fixture Goal create failed') - const ref = (created.value as { ref: { id: string; revision: number } }).ref - const edited = await handle.rpc.call('/api', 'goals/edit', { - args: { agentId: 'fx-alpha', ref, request: { objective: 'edited fixture remote' } }, - }) - expect(edited).toMatchObject({ ok: true, value: { objective: 'edited fixture remote', revision: 2 } }) - const editedRef = { id: ref.id, revision: 2 } - const paused = await handle.rpc.call('/api', 'goals/pause', { - args: { agentId: 'fx-alpha', ref: editedRef }, - }) - expect(paused).toMatchObject({ ok: true, value: { phase: 'paused', activation: 'disarmed', revision: 3 } }) - const resumed = await handle.rpc.call('/api', 'goals/resume', { - args: { agentId: 'fx-alpha', ref: { id: ref.id, revision: 3 } }, - }) - expect(resumed).toMatchObject({ ok: true, value: { phase: 'active', activation: 'armed', revision: 4 } }) - const completed = await handle.rpc.call('/api', 'goals/complete', { - args: { agentId: 'fx-alpha', ref: { id: ref.id, revision: 4 } }, - }) - expect(completed).toMatchObject({ ok: true, value: { phase: 'complete', activation: 'disarmed', revision: 5 } }) - await expect(handle.rpc.call('/api', 'goals/clear', { - args: { agentId: 'fx-alpha', ref: { id: ref.id, revision: 5 } }, - })).resolves.toEqual({ ok: true, value: { id: ref.id, revision: 6 } }) - await expect(handle.rpc.call('/other', 'goals/create', {})).rejects.toThrow(/channel.*unavailable/) - await expect(handle.rpc.call('/api', 'unknown/read', { args: { agentId: 'fx-alpha' } })) - .rejects.toThrow(/endpoint.*unavailable/) - }) }) diff --git a/packages/client/connection/tests/fixture-commands.client.spec.ts b/packages/client/connection/tests/fixture-commands.client.spec.ts deleted file mode 100644 index 103a9d9266..0000000000 --- a/packages/client/connection/tests/fixture-commands.client.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Fixture commands/skills domains: session-addressed catalogs, execute - * parse/dispatch and its logged lifecycle pair, and skills/list Session resolution. - */ -import { describe, expect, it } from 'vitest' -import type { SessionId } from '../src/client/api.ts' -import { createFixtureFaces } from '../src/client/fixture.ts' - -/** Drive one commands Remote endpoint against the fixture state graph. */ -async function callRemote( - rpc: ReturnType['rpc'], - endpoint: string, - args: Record, -): Promise { - const result = await rpc.call('/api', endpoint, { args }) - if (!result.ok) throw new Error(`${endpoint} failed: ${result.error.code}`) - return result.value as T -} - -const sid = (id: string): SessionId => id as SessionId - -describe('createFixtureApi commands/skills', () => { - it('serves the addressed session catalog', async () => { - const { rpc } = createFixtureFaces() - const commands = await callRemote<{ name: string; input?: { hint: string; attachments?: boolean } }[]>( - rpc, 'commands/list', { agentId: sid('fx-alpha') }) - expect(commands.map(c => c.name)).toEqual(['compact', 'echo', 'goal', 'permission', 'plan']) - // input hint rides only the commands declaring it. - const echo = commands.find(c => c.name === 'echo') - expect(echo?.input?.hint).toBeTruthy() - expect(commands.find(c => c.name === 'compact')?.input).toBeUndefined() - // Image acceptance is declared per descriptor; only goal and plan carry it. - expect(commands.filter(c => c.input?.attachments === true).map(c => c.name)).toEqual(['goal', 'plan']) - }) - - it('rejects a catalog request for an unknown session', async () => { - const { rpc } = createFixtureFaces() - const result = await rpc.call('/api', 'commands/list', { args: { agentId: sid('fx-nope') } }) - expect(result).toMatchObject({ ok: false, error: { code: 'session/not-found' } }) - }) - - it('executes a known command line: pure admission plus a followed lifecycle pair', async () => { - const { rpc } = createFixtureFaces() - const frames: unknown[] = [] - const abort = new AbortController() - const stream = rpc.open?.('/api', 'session/follow', { - args: { request: { address: { kind: 'session', sessionId: sid('fx-alpha') } } }, - }, abort.signal) - if (stream === undefined) throw new Error('fixture session follow stream is unavailable') - const pump = (async () => { - for await (const frame of stream) { - frames.push(frame) - if (frames.filter(f => (f as { type: string }).type === 'event').length >= 2) abort.abort() - } - })() - const execution = await callRemote<{ commandId: string } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/echo hello world' }) - expect(execution?.commandId).toBeTruthy() - await pump - const events = frames - .filter((f): f is { type: string; event: { type: string; data: Record } } => (f as { type: string }).type === 'event') - .map(f => f.event) - expect(events).toMatchObject([ - { type: 'command/run', data: { name: 'echo', args: ' hello world', source: { kind: 'user' } } }, - { type: 'command/done', data: { kind: 'success', text: 'hello world' } }, - ]) - expect(events[0]?.data.commandId).toBe(events[1]?.data.commandId) - }) - - it('addresses execute to the session; an unknown session errs', async () => { - const { rpc } = createFixtureFaces() - const hit = await callRemote<{ commandId: string } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/goal ship' }) - expect(hit?.commandId).toBeTruthy() - - const missing = await rpc.call('/api', 'commands/execute', { - args: { agentId: sid('fx-nope'), line: '/goal ship' }, - }) - expect(missing).toMatchObject({ ok: false, error: { code: 'session/not-found' } }) - }) - - it('refuses an image-carrying execute for a non-declaring command with a logged error pair', async () => { - const { rpc } = createFixtureFaces() - const frames: unknown[] = [] - const abort = new AbortController() - const stream = rpc.open?.('/api', 'session/follow', { - args: { request: { address: { kind: 'session', sessionId: sid('fx-alpha') } } }, - }, abort.signal) - if (stream === undefined) throw new Error('fixture session follow stream is unavailable') - const pump = (async () => { - for await (const frame of stream) { - frames.push(frame) - if (frames.filter(f => (f as { type: string }).type === 'event').length >= 2) abort.abort() - } - })() - const png = { mediaType: 'image/png', data: 'AA==' } - const refused = await callRemote<{ commandId: string; result: { kind: string; text?: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/echo hi', images: [png] }) - expect(refused?.commandId).toBeTruthy() - expect(refused?.result).toEqual({ kind: 'error', text: '/echo does not accept attachments' }) - await pump - const events = frames - .filter((f): f is { type: string; event: { type: string; data: Record } } => (f as { type: string }).type === 'event') - .map(f => f.event) - expect(events).toMatchObject([ - { type: 'command/run', data: { name: 'echo', args: ' hi', source: { kind: 'user' } } }, - { type: 'command/done', data: { kind: 'error', text: '/echo does not accept attachments' } }, - ]) - }) - - it('a declaring command accepts an image-carrying execute', async () => { - const { rpc } = createFixtureFaces() - const png = { mediaType: 'image/png', data: 'AA==' } - const accepted = await callRemote<{ result: { kind: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/goal ship it', images: [png] }) - expect(accepted?.result.kind).toBe('success') - const planMessage = await callRemote<{ result: { kind: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/plan sketch the layout', images: [png] }) - expect(planMessage?.result.kind).toBe('success') - const imageOnlyPlan = await callRemote<{ result: { kind: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/plan', images: [png] }) - expect(imageOnlyPlan?.result.kind).toBe('success') - }) - - it('mirrors the producer grammar rejections for control-only declaring lines', async () => { - const { rpc } = createFixtureFaces() - const png = { mediaType: 'image/png', data: 'AA==' } - const bareGoal = await callRemote<{ result: { kind: string; text?: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/goal', images: [png] }) - expect(bareGoal?.result).toEqual({ - kind: 'error', - text: 'Attachments only accompany a goal objective: /goal or /goal edit .', - }) - const refused = await callRemote<{ result: { kind: string; text?: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/plan off', images: [png] }) - expect(refused?.result).toEqual({ - kind: 'error', - text: 'Attachments cannot accompany /plan off.', - }) - }) - - it('answers no execution for an unknown name even when images accompany it', async () => { - const { rpc } = createFixtureFaces() - const png = { mediaType: 'image/png', data: 'AA==' } - expect(await callRemote(rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/nope', images: [png] })) - .toBeUndefined() - }) - - it('answers no execution for unknown names and non-command lines', async () => { - const { rpc } = createFixtureFaces() - for (const line of ['/nope', 'plain text', '/']) { - // Absence is the whole answer: nothing matched, so no lifecycle id exists. - expect(await callRemote(rpc, 'commands/execute', { agentId: sid('fx-alpha'), line })) - .toBeUndefined() - } - }) - - it('serves the skill catalog for the addressed session and rejects unknown sessions', async () => { - const { rpc } = createFixtureFaces() - const skills = await callRemote<{ skills: Array<{ name: string }> }>( - rpc, 'skills/list', { request: { sessionId: sid('fx-alpha') } }, - ) - expect(skills.skills[0]?.name).toBe('fixture-demo') - - const missingSession = await rpc.call('/api', 'skills/list', { - args: { request: { sessionId: sid('fx-nope') } }, - }) - expect(missingSession).toMatchObject({ ok: false, error: { code: 'session/not-found' } }) - }) -}) - -describe('fixture Connection command/skill dispatch', () => { - it('routes the Remote command and skill rows through one state graph', async () => { - const { rpc } = createFixtureFaces() - const commands = await callRemote<{ name: string }[]>(rpc, 'commands/list', { agentId: sid('fx-alpha') }) - expect(commands.length).toBeGreaterThan(0) - const executed = await callRemote<{ commandId: string } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/compact' }) - expect(executed?.commandId).toBeTruthy() - const skills = await callRemote<{ skills: unknown[] }>( - rpc, 'skills/list', { request: { sessionId: sid('fx-alpha') } }, - ) - expect(skills.skills.length).toBeGreaterThan(0) - }) -}) diff --git a/packages/client/connection/tests/fixture.client.spec.ts b/packages/client/connection/tests/fixture.client.spec.ts deleted file mode 100644 index efaa35cc16..0000000000 --- a/packages/client/connection/tests/fixture.client.spec.ts +++ /dev/null @@ -1,1825 +0,0 @@ -import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest' -import type { - RpcRequest, - RpcResponse, - RpcResult, - SessionEvent, - SessionId, - StreamChunk, -} from '../src/client/api.ts' -import { RpcId } from '../src/client/api.ts' -import { - createFixtureConnectionRpc, - createFixtureFaces, - type FixtureAssistantStreamFrame, - type FixtureOptions, -} from '../src/client/fixture.ts' -import type { - ClientConnectionRpc, ConnectionRpcResult, -} from '../src/rpc.ts' -import type { DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types' -import type { - ModelCatalog, ModelSelection, SessionAssistantStreamFrame, -} from '@deepseek-ai/dsh-api-session-controller/types' - -const sid = (id: string): SessionId => id as SessionId -type WorkspaceId = string & { readonly __fixtureWorkspaceId: 'WorkspaceId' } -const req =

(payload: P): RpcRequest

=> ({ rpcId: RpcId(`t-${Math.abs(Math.sin(reqCount++)).toString(36).slice(2, 10)}`), payload }) -let reqCount = 0 - -interface FixtureSessionSummary { - sessionId: SessionId - updatedAt: number - running: boolean - blank: boolean - parentSessionId?: SessionId - origin?: 'subagent' - cwd?: string - agentPreset?: string -} - -interface FixtureHistoryEntry { - readonly type: 'event' - readonly event: SessionEvent -} - -type FixtureHistoryRecord = FixtureHistoryEntry - -interface FixturePage { - readonly records: readonly FixtureHistoryRecord[] - readonly hasMore: boolean -} - -function historyEvents(records: readonly FixtureHistoryRecord[]): SessionEvent[] { - return records.map(record => record.event) -} - -function isReasoningDeltaChunk( - value: unknown, -): value is Extract { - if (typeof value !== 'object' || value === null) return false - const record = value as { readonly type?: unknown; readonly text?: unknown } - return record.type === 'reasoning-delta' && typeof record.text === 'string' -} - -type FixtureFollowFrame = - | { - readonly type: 'snapshot' - readonly cursor: number - readonly records: readonly FixtureHistoryRecord[] - readonly hasMore: boolean - readonly projections: { - readonly asOfSeq: number - readonly values: Readonly> - } - readonly assistantStream?: { - readonly revision: number - readonly activeAttempt?: { - readonly attemptId: string - readonly startedAfterSeq: number - readonly turn: number - readonly step: number - readonly nextIndex: number - readonly stream: readonly unknown[] - } - } - } - | FixtureHistoryEntry - | { readonly type: 'assistant-stream'; readonly frame: FixtureAssistantStreamFrame } - -type FixtureControlFrame = - | { - readonly type: 'baseline' - readonly value: { - readonly queues: Readonly> - readonly jobs: Readonly> - readonly approvals: readonly unknown[] - readonly questions: readonly unknown[] - readonly projections: Readonly> - }>> - } - } - | { - readonly type: 'projection' - readonly sessionId: SessionId - readonly key: string - readonly value: unknown - readonly seq: number - } - -interface FixtureSessionRequests { - list: { readonly cursor?: string } - search: { readonly query: string } - create: { - readonly workspaceId?: WorkspaceId - readonly cwd?: string - readonly sessionId?: SessionId - readonly agentPreset?: string - } - history: { - readonly sessionId: SessionId - readonly beforeSeq?: number - readonly maxMessages?: number - } - selectModel: { - readonly sessionId: SessionId - readonly provider: string - readonly model: string - readonly reasoningEffort?: string - } - prompt: { - readonly sessionId: SessionId - readonly mode: 'queue' | 'steer' - readonly content: readonly ({ readonly type: 'text'; readonly text: string } | { - readonly type: 'image' - readonly mediaType: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif' - readonly data: string - readonly name?: string - })[] - } - cancel: { readonly sessionId: SessionId } - rename: { readonly sessionId: SessionId; readonly title: string } -} - -interface FixtureSessionValues { - list: { readonly items: FixtureSessionSummary[] } - search: { readonly items: readonly { readonly sessionId: SessionId; readonly snippet: string }[]; readonly hasMore: boolean } - create: { readonly sessionId: SessionId } - history: FixturePage - selectModel: { readonly selected: ModelSelection } - prompt: { readonly accepted: true } - cancel: Record - rename: { readonly title: string; readonly seq: number } -} - -type FixtureSessionApi = { - [K in keyof FixtureSessionRequests]: ( - request: RpcRequest, - signal?: AbortSignal, - ) => Promise> -} - -type FixtureSessionClient = { - [K in keyof FixtureSessionRequests]: ( - request: FixtureSessionRequests[K], - signal?: AbortSignal, - ) => Promise> -} - -interface FixtureSessionRemote { - modelCatalog(): Promise> - follow(sessionId: SessionId, signal: AbortSignal): AsyncIterable - control(signal: AbortSignal): AsyncIterable -} - -interface FixtureWorkspaceView { - readonly workspaceId: WorkspaceId - readonly path: string - readonly title: string - readonly sessionIds: readonly SessionId[] - readonly createdAt: string - readonly updatedAt: string -} - -interface FixtureWorkspaceRequests { - create: { readonly path: string } - rename: { readonly workspaceId: WorkspaceId; readonly title: string } - delete: { readonly workspaceId: WorkspaceId } - insertBefore: { readonly workspaceId: WorkspaceId; readonly beforeWorkspaceId?: WorkspaceId } - insertSessionBefore: { - readonly workspaceId: WorkspaceId - readonly sessionId: SessionId - readonly beforeSessionId?: SessionId - } - archiveSession: { readonly sessionId: SessionId } - unarchiveSession: { readonly sessionId: SessionId } -} - -interface FixtureWorkspaceValues { - create: { readonly workspace: FixtureWorkspaceView; readonly created: boolean } - rename: { readonly workspace: FixtureWorkspaceView } - delete: { readonly deleted: true } - insertBefore: { readonly workspaceIds: readonly WorkspaceId[] } - insertSessionBefore: { readonly workspace: FixtureWorkspaceView } - archiveSession: { readonly archivedSessionIds: readonly SessionId[] } - unarchiveSession: { readonly archivedSessionIds: readonly SessionId[] } -} - -type FixtureWorkspaceApi = { - [K in keyof FixtureWorkspaceRequests]: ( - request: RpcRequest, - signal?: AbortSignal, - ) => Promise> -} - -type FixtureWorkspaceClient = { - [K in keyof FixtureWorkspaceRequests]: ( - request: FixtureWorkspaceRequests[K], - signal?: AbortSignal, - ) => Promise> -} - -type FixtureWorkspaceFrame = - | { - readonly type: 'baseline' - readonly value: { - readonly items: readonly FixtureWorkspaceView[] - readonly archivedSessionIds: readonly SessionId[] - } - } - | { readonly type: 'upsert'; readonly workspace: FixtureWorkspaceView } - | { readonly type: 'remove'; readonly workspaceId: WorkspaceId } - | { readonly type: 'order'; readonly workspaceIds: readonly WorkspaceId[] } - | { readonly type: 'archived'; readonly archivedSessionIds: readonly SessionId[] } - -interface FixtureWorkspaceRemote { - follow(signal: AbortSignal): AsyncIterable -} - -interface FixtureRemoteEventNotificationFrame { - readonly type: 'emit' - readonly event: string - readonly args: readonly unknown[] -} - -interface FixtureRemoteEventRequestFrame { - readonly type: 'waterfall' - readonly event: string - readonly eventId: string - readonly agentId: SessionId - readonly request: Readonly> -} - -interface FixtureRemoteEventCancellationFrame { - readonly type: 'cancel' - readonly eventId: string -} - -type FixtureRemoteEventFrame = - | FixtureRemoteEventNotificationFrame - | FixtureRemoteEventRequestFrame - | FixtureRemoteEventCancellationFrame - -interface FixtureRemoteEventResult { - readonly clientId: string - readonly eventId: string - readonly outcome: - | { readonly kind: 'next' } - | { readonly kind: 'result'; readonly value?: unknown } - | { - readonly kind: 'rejected' - readonly error: { - readonly name: string - readonly message: string - readonly code?: string - readonly details?: unknown - } - } -} - -interface FixtureRemoteEventStream extends AsyncIterable { - readonly clientId: Promise -} - -type FixtureTestApi = { - /** The directory-picking Remote namespace as the fixture serves it. */ - readonly directoryPickerRemote: { - pick: () => Promise> - list: (path?: string) => Promise> - createDirectory: (path: string, name: string) => Promise> - } - readonly sessions: FixtureSessionApi - readonly sessionRemote: FixtureSessionRemote - readonly workspace: FixtureWorkspaceApi - readonly workspaceRemote: FixtureWorkspaceRemote - readonly credentialRemote: FixtureCredentialRemote - readonly settingsRemote: FixtureSettingsRemote - readonly remoteEvents: (signal: AbortSignal) => FixtureRemoteEventStream - readonly answerRemoteEvent: (result: FixtureRemoteEventResult) => Promise -} - -/** Keep existing fixture assertions compact while driving only the new Session Remote endpoints. */ -function createFixtureApi(options: FixtureOptions = {}): FixtureTestApi { - const { rpc } = createFixtureFaces(options) - return { - directoryPickerRemote: { - pick: () => rpc.call('/api', 'directoryPicker/pick', { args: {} }) as - Promise>, - list: (path?: string) => rpc.call('/api', 'directoryPicker/list', { args: { path } }) as - Promise>, - createDirectory: (path: string, name: string) => - rpc.call('/api', 'directoryPicker/createDirectory', { args: { path, name } }) as - Promise>, - }, - sessions: createSessionApi(rpc), - sessionRemote: createSessionRemote(rpc), - workspace: createWorkspaceApi(rpc), - workspaceRemote: createWorkspaceRemote(rpc), - credentialRemote: createCredentialRemote(rpc), - settingsRemote: createSettingsRemote(rpc), - remoteEvents: (signal: AbortSignal) => openFixtureRemoteEvents(rpc, signal), - answerRemoteEvent: (result: FixtureRemoteEventResult) => - rpc.call('/api', '$events/result', { args: result }), - } -} - -/** The fixture's Credentials Remote endpoints over the shared RPC carrier. */ -interface FixtureCredentialRemote { - describe(refs: readonly string[]): Promise> - set(ref: string, value: string): Promise> - unset(ref: string): Promise> -} - -/** The settings Remote reads the fixture serves, addressed like the credential half. */ -interface FixtureSettingsRemote { - describe(): Promise> - update(ns: string, patch: unknown, expectedRevision?: number): Promise> - replace(ns: string, section: unknown, expectedRevision?: number): Promise> -} - -function createSettingsRemote(rpc: ClientConnectionRpc): FixtureSettingsRemote { - return { - describe: () => rpc.call('/api', 'settings/describe', { args: {} }), - update: (ns, patch, expectedRevision) => rpc.call('/api', 'settings/update', { - args: { ns, patch, expectedRevision }, - }), - replace: (ns, section, expectedRevision) => rpc.call('/api', 'settings/replace', { - args: { ns, section, expectedRevision }, - }), - } -} - -function createCredentialRemote(rpc: ClientConnectionRpc): FixtureCredentialRemote { - return { - describe: refs => rpc.call('/api', 'credentials/describe', { args: { refs } }), - set: (ref, value) => rpc.call('/api', 'credentials/set', { args: { ref, value } }), - unset: ref => rpc.call('/api', 'credentials/unset', { args: { ref } }), - } -} - -function openFixtureRemoteEvents( - rpc: ClientConnectionRpc, - signal: AbortSignal, -): FixtureRemoteEventStream { - const ready = Promise.withResolvers() - const source = (async function* (): AsyncGenerator { - const stream = rpc.open?.('/api', '$events', { args: {} }, signal) - if (stream === undefined) throw new Error('fixture forwarded-event stream is unavailable') - let opened = false - for await (const value of stream) { - if (!opened) { - expect(value).toMatchObject({ type: 'ready' }) - const clientId: unknown = Reflect.get(value as object, 'clientId') - if (typeof clientId !== 'string') throw new Error('fixture forwarded-event stream omitted its Client id') - ready.resolve(clientId) - opened = true - continue - } - yield value as FixtureRemoteEventFrame - } - })() - return Object.assign(source, { clientId: ready.promise }) -} - -function createSessionApi(rpc: ClientConnectionRpc): FixtureSessionApi { - const call = async ( - endpoint: K, - request: RpcRequest, - signal?: AbortSignal, - ): Promise> => { - const page = endpoint === 'history' - ? request.payload as FixtureSessionRequests['history'] - : undefined - const args = endpoint === 'list' - ? { _request: request.payload } - : endpoint === 'history' - ? { - request: { - address: { kind: 'session', sessionId: page?.sessionId }, - ...page?.beforeSeq === undefined ? {} : { beforeSeq: page.beforeSeq }, - ...page?.maxMessages === undefined ? {} : { maxMessages: page.maxMessages }, - }, - } - : { request: request.payload } - const remoteEndpoint = endpoint === 'history' ? 'page' : endpoint - const result = await rpc.call('/api', `session/${remoteEndpoint}`, { args }, signal) - return { - rpcId: request.rpcId, - result: result as unknown as RpcResult, - } - } - return { - list: (request, signal) => call('list', request, signal), - search: (request, signal) => call('search', request, signal), - create: (request, signal) => call('create', request, signal), - history: (request, signal) => call('history', request, signal), - selectModel: (request, signal) => call('selectModel', request, signal), - prompt: (request, signal) => call('prompt', request, signal), - cancel: (request, signal) => call('cancel', request, signal), - rename: (request, signal) => call('rename', request, signal), - } -} - -function createSessionClient(rpc: ClientConnectionRpc): FixtureSessionClient { - const api = createSessionApi(rpc) - return { - list: (request, signal) => api.list(req(request), signal), - search: (request, signal) => api.search(req(request), signal), - create: (request, signal) => api.create(req(request), signal), - history: (request, signal) => api.history(req(request), signal), - selectModel: (request, signal) => api.selectModel(req(request), signal), - prompt: (request, signal) => api.prompt(req(request), signal), - cancel: (request, signal) => api.cancel(req(request), signal), - rename: (request, signal) => api.rename(req(request), signal), - } -} - -function createSessionRemote(rpc: ClientConnectionRpc): FixtureSessionRemote { - const open = (endpoint: string, args: object, signal: AbortSignal): AsyncIterable => { - const stream = rpc.open?.('/api', endpoint, { args }, signal) - if (stream === undefined) throw new Error(`fixture ${endpoint} stream is unavailable`) - return stream as AsyncIterable - } - return { - modelCatalog: () => rpc.call('/api', 'session/modelCatalog', { args: {} }) as - Promise>, - follow: (sessionId, signal) => open('session/follow', { - request: { address: { kind: 'session', sessionId }, assistantStream: true }, - }, signal), - control: signal => open('session/control', {}, signal), - } -} - -function createWorkspaceApi(rpc: ClientConnectionRpc): FixtureWorkspaceApi { - const call = async ( - endpoint: K, - request: RpcRequest, - signal?: AbortSignal, - ): Promise> => { - const result = await rpc.call('/api', `workspace/${endpoint}`, { - args: { request: request.payload }, - }, signal) - return { - rpcId: request.rpcId, - result: result as unknown as RpcResult, - } - } - return { - create: (request, signal) => call('create', request, signal), - rename: (request, signal) => call('rename', request, signal), - delete: (request, signal) => call('delete', request, signal), - insertBefore: (request, signal) => call('insertBefore', request, signal), - insertSessionBefore: (request, signal) => call('insertSessionBefore', request, signal), - archiveSession: (request, signal) => call('archiveSession', request, signal), - unarchiveSession: (request, signal) => call('unarchiveSession', request, signal), - } -} - -function createWorkspaceClient(rpc: ClientConnectionRpc): FixtureWorkspaceClient { - const api = createWorkspaceApi(rpc) - return { - create: (request, signal) => api.create(req(request), signal), - rename: (request, signal) => api.rename(req(request), signal), - delete: (request, signal) => api.delete(req(request), signal), - insertBefore: (request, signal) => api.insertBefore(req(request), signal), - insertSessionBefore: (request, signal) => api.insertSessionBefore(req(request), signal), - archiveSession: (request, signal) => api.archiveSession(req(request), signal), - unarchiveSession: (request, signal) => api.unarchiveSession(req(request), signal), - } -} - -function createWorkspaceRemote(rpc: ClientConnectionRpc): FixtureWorkspaceRemote { - return { - follow(signal) { - const stream = rpc.open?.('/api', 'workspace/follow', { args: {} }, signal) - if (stream === undefined) throw new Error('fixture workspace/follow stream is unavailable') - return stream as AsyncIterable - }, - } -} - -interface TimingHooks { - setHistoryDelay(ms: number): void - failNextHistory(): void - appendUser(id: string, msg: string): void - appendTitle(id: string, title: string): void - disarmOnlyGoal(): void - startReasoningChunkStorm(id: string, chunkCount: number, chunksPerInterval: number, intervalMs: number): string - reasoningChunkStormState(): { - sessionId: string - chunkCount: number - chunksPerInterval: number - intervalMs: number - emitted: number - marker: string - emitting: boolean - } | null - beginModelRetry(id: string): void - scheduleModelRetry(id: string, retry?: number, delayMs?: number): void - cancelModelRetryDuringBackoff(id: string, delayMs?: number): void - completeModelRetry(id: string): void - appendSilent(id: string, msg: string): void - breakStreams(): void -} -const timing = (): TimingHooks => (globalThis as Record).__fxTiming as TimingHooks - -/** Collect value-stream frames until the predicate or a soft cap; abort ends the stream. */ -async function collectValues(stream: AsyncIterable, abort: AbortController, done: (frames: F[]) => boolean): Promise { - const frames: F[] = [] - for await (const frame of stream) { - frames.push(frame) - if (done(frames) || frames.length > 500) { - abort.abort() - break - } - } - return frames -} - -async function readControlBaseline(remote: FixtureSessionRemote): Promise> { - const abort = new AbortController() - for await (const frame of remote.control(abort.signal)) { - if (frame.type !== 'baseline') continue - abort.abort() - return frame - } - throw new Error('fixture control baseline missing') -} - -function isRemoteEventRequest(frame: FixtureRemoteEventFrame): frame is FixtureRemoteEventRequestFrame { - return frame.type === 'waterfall' -} - -function isRemoteEventCancellation(frame: FixtureRemoteEventFrame): frame is FixtureRemoteEventCancellationFrame { - return frame.type === 'cancel' -} - -async function readResidentRemoteEvents( - api: FixtureTestApi, - count: number, -): Promise { - const abort = new AbortController() - const frames = await collectValues( - api.remoteEvents(abort.signal), - abort, - seen => seen.filter(isRemoteEventRequest).length >= count, - ) - return frames.filter(isRemoteEventRequest) -} - -async function nextRemoteEvent( - iterator: AsyncIterator, - predicate: (frame: FixtureRemoteEventFrame) => boolean, -): Promise { - for (;;) { - const item = await iterator.next() - if (item.done) throw new Error('fixture Remote Event stream ended before the expected frame') - if (predicate(item.value)) return item.value - } -} - -async function readOpeningCursor(remote: FixtureSessionRemote, sessionId: SessionId): Promise { - const abort = new AbortController() - for await (const frame of remote.follow(sessionId, abort.signal)) { - if (frame.type !== 'snapshot') continue - abort.abort() - return frame.cursor - } - throw new Error('fixture follow opening cursor missing') -} - -async function readWorkspaceBaseline( - remote: FixtureWorkspaceRemote, -): Promise['value']> { - const abort = new AbortController() - for await (const frame of remote.follow(abort.signal)) { - if (frame.type !== 'baseline') continue - abort.abort() - return frame.value - } - throw new Error('fixture Workspace baseline missing') -} - -describe('createFixtureApi', () => { - it('keeps its Assistant frame vocabulary identical to the controller wire', () => { - expectTypeOf().toEqualTypeOf() - }) - - it('serves the session list sorted by updatedAt desc and echoes rpcIds on every unary', async () => { - const api = createFixtureApi() - const request = req({}) - const response = await api.sessions.list(request) - expect(response.rpcId).toBe(request.rpcId) - if (!response.result.ok) throw new Error('list failed') - expect(response.result.value.items.map(s => s.sessionId)).toEqual(['fx-alpha', 'fx-beta', 'fx-gamma']) - expect(response.result.value.items[1]?.parentSessionId).toBe('fx-alpha') // lineage material - }) - - it('returns an empty Assistant baseline when Session follow opts in', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const iterator = api.sessionRemote.follow(sid('fx-alpha'), abort.signal)[Symbol.asyncIterator]() - try { - const opening = await iterator.next() - if (opening.done || opening.value.type !== 'snapshot') throw new Error('follow opening snapshot missing') - expect(opening.value.assistantStream).toEqual({ revision: 0 }) - } finally { - abort.abort() - await iterator.return?.() - } - }) - - it('searches current message text with literal unicode61-style token phrases', async () => { - const api = createFixtureApi() - const signal = new AbortController().signal - const phrase = await api.sessions.search(req({ query: 'FIXTURE 历史消息' }), signal) - expect(phrase.result).toMatchObject({ - ok: true, - value: { - items: [{ sessionId: 'fx-alpha' }], - hasMore: false, - }, - }) - if (!phrase.result.ok) throw new Error('search failed') - expect(phrase.result.value.items[0]?.snippet).toContain('fixture 历史消息') - - timing().appendUser( - 'fx-alpha', - `${'leading context '.repeat(20)}late café token${' trailing context'.repeat(20)}`, - ) - const late = await api.sessions.search(req({ query: 'LATE CAFE TOKEN' }), signal) - if (!late.result.ok) throw new Error('late search failed') - const lateSnippet = late.result.value.items[0]?.snippet ?? '' - expect(lateSnippet).toContain('late café token') - expect(lateSnippet.startsWith('…')).toBe(true) - expect(lateSnippet.endsWith('…')).toBe(true) - expect(Array.from(lateSnippet).length).toBeLessThanOrEqual(120) - - timing().appendUser('fx-alpha', 'Greek final sigma: ος') - const finalSigma = await api.sessions.search(req({ query: 'ΟΣ' }), signal) - if (!finalSigma.result.ok) throw new Error('final sigma search failed') - expect(finalSigma.result.value.items[0]?.snippet).toContain('ος') - - const substring = await api.sessions.search(req({ query: 'ixtur' }), signal) - expect(substring.result).toEqual({ - ok: true, - value: { items: [], hasMore: false }, - }) - const punctuationOnly = await api.sessions.search(req({ query: '*' }), signal) - expect(punctuationOnly.result).toEqual({ - ok: true, - value: { items: [], hasMore: false }, - }) - const reasoningOnly = await api.sessions.search(req({ query: '思考过程' }), signal) - expect(reasoningOnly.result).toEqual({ - ok: true, - value: { items: [], hasMore: false }, - }) - - const aborted = new AbortController() - aborted.abort() - await expect(api.sessions.search(req({ query: 'fixture' }), aborted.signal)) - .resolves.toMatchObject({ result: { ok: false, error: { code: 'gateway/cancelled' } } }) - }) - - it('pages history backwards on message-boundary cuts with seq-contiguous stitching', async () => { - const api = createFixtureApi() - const tail = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 10 })) - if (!tail.result.ok) throw new Error('history failed') - const tailPage = tail.result.value - expect(tailPage.hasMore).toBe(true) - const tailEvents = historyEvents(tailPage.records) - expect(tailEvents[0]?.type).toBe('turn/start') // cut lands on a turn boundary - const boundary = tailEvents[0]?.seq ?? 0 - expect(boundary).toBeGreaterThan(0) - const older = await api.sessions.history(req({ sessionId: sid('fx-alpha'), beforeSeq: boundary, maxMessages: 10 })) - if (!older.result.ok) throw new Error('older failed') - const olderTail = historyEvents(older.result.value.records).at(-1) - expect((olderTail?.seq ?? -1) + 1).toBe(boundary) // pages stitch with no hole/overlap - // Out-of-range beforeSeq clamps instead of exploding. - const clamped = await api.sessions.history(req({ sessionId: sid('fx-alpha'), beforeSeq: -5, maxMessages: 10 })) - if (!clamped.result.ok) throw new Error('clamped failed') - expect(clamped.result.value.records).toEqual([]) - // Unknown session: empty page, not an error (history of a bare id). - const empty = await api.sessions.history(req({ sessionId: sid('no-such'), maxMessages: 10 })) - if (!empty.result.ok) throw new Error('empty failed') - expect(empty.result.value).toEqual({ records: [], hasMore: false }) - }) - - it('serves raw history entries with replayable tool-result metadata', async () => { - const api = createFixtureApi() - const response = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 200 })) - if (!response.result.ok) throw new Error('history failed') - - const records = response.result.value.records - const results = historyEvents(records) - .filter(event => event.type === 'tool/result') - - expect(results.find(event => event.data.turn === 64)).toMatchObject({ - data: { - meta: { - diffs: [ - { path: 'src/config.ts', oldText: 'const timeout = 30', newText: 'const timeout = 60' }, - { path: 'src/config.ts', oldText: 'retries: 1', newText: 'retries: 3' }, - ], - }, - }, - }) - expect(results.find(event => event.data.turn === 67)).toMatchObject({ - data: { meta: { shape: 'matches', truncated: true, total: 42 } }, - }) - expect(results.find(event => event.data.turn === 69)).toMatchObject({ - data: { meta: { path: 'packages/client/ui-primitives/src/ReadBlock.tsx', offset: 41, totalLines: 180 } }, - }) - const webSearch = results.find(event => event.data.turn === 70) - expect(webSearch).toHaveProperty('data.meta.truncated', true) - expect(webSearch).toHaveProperty('data.meta.sources', expect.arrayContaining([ - expect.objectContaining({ url: 'https://github.com/deepseek-ai/deepseek-harness' }), - ])) - expect(results.find(event => event.data.turn === 71)).toMatchObject({ - data: { meta: { url: 'https://www.deepseek.com/blog/harness-architecture', statusCode: 200 } }, - }) - const terminal = results.find(event => event.data.turn === 66) - expect(terminal).toHaveProperty('data.message.content.0.content.0.type', 'text') - expect(terminal).toHaveProperty( - 'data.message.content.0.content.0.text', - expect.stringContaining('\n[exit code: 1]'), - ) - }) - - it('serves grouped models and keeps a selection for later history and fixture requests', async () => { - const api = createFixtureApi() - const sessionId = sid('fx-alpha') - const catalog = await api.sessionRemote.modelCatalog() - if (!catalog.ok) throw new Error('models failed') - expect(catalog.value.groups.map(group => group.name)).toEqual(['DeepSeek', 'OpenAI']) - expect(catalog.value.groups[0]?.models.map(model => model.id)) - .toEqual(['deepseek-v4-flash', 'deepseek-v4-pro']) - - const selected = await api.sessions.selectModel(req({ - sessionId, - provider: 'openai', - model: 'gpt-5', - })) - if (!selected.result.ok) throw new Error('selection failed') - expect(selected.result.value.selected).toEqual({ provider: 'openai', model: 'gpt-5' }) - const history = await api.sessions.history(req({ sessionId })) - if (!history.result.ok) throw new Error('history failed') - - const prompt = await api.sessions.prompt(req({ - sessionId, - mode: 'queue', - content: [{ type: 'text', text: 'report model' }], - })) - expect(prompt.result.ok).toBe(true) - await new Promise(resolve => setTimeout(resolve, 600)) - const after = await api.sessions.history(req({ sessionId })) - if (!after.result.ok) throw new Error('history failed') - expect(JSON.stringify(after.result.value.records)).toContain('openai/gpt-5') - }) - - it('serves configured DeepSeek readiness and keeps credential values write-only', async () => { - const api = createFixtureApi() - const settings = await api.settingsRemote.describe() - if (!settings.ok) throw new Error('settings describe failed') - expect((settings.value as { namespaces: unknown[] }).namespaces).toMatchObject([{ - ns: 'llm-deepseek', - value: { apiKeyEnv: 'DEEPSEEK_API_KEY' }, - secrets: [{ path: ['apiKey'], set: false }], - }]) - for (const result of [ - await api.settingsRemote.update('llm-deepseek', {}, undefined), - await api.settingsRemote.replace('llm-deepseek', {}, undefined), - ]) { - expect(result).toMatchObject({ - ok: false, - error: { code: 'settings/rejected', message: 'fixture: the minimal readiness settings descriptor is read-only' }, - }) - } - - const describe = async (refs: readonly string[]): Promise> => { - const result = await api.credentialRemote.describe(refs) - if (!result.ok) throw new Error('credential describe failed') - return result.value as Record - } - expect(await describe(['DEEPSEEK_API_KEY', 'TEST_API_KEY'])).toEqual({ - DEEPSEEK_API_KEY: { configured: true, source: 'file', writable: true }, - TEST_API_KEY: { configured: false, writable: true }, - }) - await api.credentialRemote.set('TEST_API_KEY', 'write-only-fixture-secret') - expect((await describe(['TEST_API_KEY'])).TEST_API_KEY).toEqual({ - configured: true, - source: 'file', - writable: true, - }) - await api.credentialRemote.unset('TEST_API_KEY') - expect((await describe(['TEST_API_KEY'])).TEST_API_KEY).toEqual({ configured: false, writable: true }) - }) - - it('emits the todo/write snapshot at the real tool boundary: between tool/call and tool/result, timestamps monotonic', async () => { - const api = createFixtureApi() - const tail = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 10 })) - if (!tail.result.ok) throw new Error('history failed') - const events = historyEvents(tail.result.value.records) - const todoAt = events.findIndex(e => e.type === 'todo/write') - expect(todoAt).toBeGreaterThan(0) - // Production ordering (the tool appends mid-execution): call → snapshot → result. - expect(events[todoAt - 1]?.type).toBe('tool/call') - expect(events[todoAt + 1]?.type).toBe('tool/result') - const times = events.slice(todoAt - 1, todoAt + 2).map(e => e.time) - expect(times[0]).toBeLessThanOrEqual(times[1] ?? 0) - expect(times[1]).toBeLessThanOrEqual(times[2] ?? 0) - // The sample is a parallel plan: this fixture chooses the parallel policy, - // so the surfaces fed from here face more than one active item. - const snapshot = events[todoAt] as { data: { todos: { status: string }[] } } - expect(snapshot.data.todos.filter(t => t.status === 'in_progress')).toHaveLength(2) - }) - - it('create adds a session and announces it through the Host Remote event stream', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const seen: FixtureRemoteEventNotificationFrame[] = [] - const consuming = (async () => { - for await (const frame of api.remoteEvents(abort.signal)) { - if (frame.type !== 'emit' || frame.event !== 'api-session/added') continue - seen.push(frame) - abort.abort() - break - } - })() - await new Promise(resolve => setTimeout(resolve, 10)) // let the stream register - const created = await api.sessions.create(req({})) - if (!created.result.ok) throw new Error('create failed') - await consuming - if (!created.result.ok) throw new Error('create failed') - const createdId = created.result.value.sessionId - expect(seen).toHaveLength(1) - const added = seen[0] - expect(added).toMatchObject({ - event: 'api-session/added', - args: [{ sessionId: createdId, blank: true, cwd: '/tmp/fixture' }], - }) - const list = await api.sessions.list(req({})) - if (!list.result.ok) throw new Error('list failed') - expect(list.result.value.items.some(s => s.sessionId === createdId)).toBe(true) - }) - - it('prompt replays a full streamed turn and cancel mid-replay freezes with (已中断)', async () => { - const api = createFixtureApi() - const created = await api.sessions.create(req({})) - if (!created.result.ok) throw new Error('create failed') - const id = created.result.value.sessionId - const followAbort = new AbortController() - const controlAbort = new AbortController() - const controlFrames: FixtureControlFrame[] = [] - const followPromise = collectValues( - api.sessionRemote.follow(id, followAbort.signal), - followAbort, - frames => frames.some(frame => frame.type === 'event' && frame.event.type === 'turn/end'), - ) - const controlPromise = (async () => { - for await (const frame of api.sessionRemote.control(controlAbort.signal)) controlFrames.push(frame) - })() - await new Promise(resolve => setTimeout(resolve, 10)) - // Unknown session → session/not-found with the id echoed in details. - const missing = await api.sessions.prompt(req({ sessionId: sid('ghost'), mode: 'queue' as const, content: [{ type: 'text' as const, text: 'x' }] })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'session/not-found', details: { sessionId: 'ghost' } } }) - // Real prompt: replay starts (running flips true), cancel freezes it. - const accepted = await api.sessions.prompt(req({ sessionId: id, mode: 'queue' as const, content: [{ type: 'text' as const, text: 'render markdown' }] })) - expect(accepted.result).toMatchObject({ ok: true, value: { accepted: true } }) - await new Promise(resolve => setTimeout(resolve, 120)) // a couple of typewriter ticks - await api.sessions.cancel(req({ sessionId: id })) - const frames = await followPromise - const types = frames.flatMap(frame => frame.type === 'event' ? [frame.event.type] : []) - const assistantFrames = frames.flatMap(frame => frame.type === 'assistant-stream' ? [frame.frame] : []) - expect(types).toContain('turn/start') - expect(types).toContain('user/message') - expect(types).toContain('assistant/message') - expect(assistantFrames.some(frame => frame.type === 'chunk')).toBe(true) - const committed = assistantFrames.find(frame => frame.type === 'end' - && frame.outcome.kind === 'committed' - && frame.outcome.eventType === 'assistant/message') - expect(committed).toMatchObject({ - type: 'end', - index: assistantFrames.filter(frame => frame.type === 'chunk' - && frame.attemptId === committed?.attemptId).length, - }) - expect(types.at(-1)).toBe('turn/end') - // Capacity is durable log state, not a transient frame: the prompt path - // records request/context and the projection carries it to the client. - expect(types).toContain('request/context') - await vi.waitFor(() => { - expect(controlFrames.some(frame => - frame.type === 'projection' - && frame.key === 'contextBreakdown' - && (frame.value as { messageTokens?: number }).messageTokens! > 0)).toBe(true) - }) - expect(controlFrames.some(frame => - frame.type === 'projection' - && frame.key === 'tokenUsage' - && (frame.value as { outputTokens?: number }).outputTokens === 8)).toBe(true) - expect(controlFrames.some(frame => - frame.type === 'projection' - && frame.key === 'contextPressure' - && (frame.value as { contextWindow?: number }).contextWindow === 128_000)).toBe(true) - const finalize = frames.find(frame => frame.type === 'event' && frame.event.type === 'assistant/message') - if (finalize?.type !== 'event') throw new Error('assistant final event missing') - if (finalize.event.type !== 'assistant/message') throw new Error('assistant final event has wrong type') - expect(finalize.event.data.interrupted).toBe(true) - expect(finalize.event.data.stream.length).toBeGreaterThan(0) - controlAbort.abort() - await controlPromise - // Idle cancel: no replay in flight, must not explode; running flips false. - const idleCancel = await api.sessions.cancel(req({ sessionId: id })) - expect(idleCancel.result).toMatchObject({ ok: true }) - }) - - it('steer during a replay lands a user/message inside the current turn and the replay continues', async () => { - const api = createFixtureApi() - const created = await api.sessions.create(req({})) - if (!created.result.ok) throw new Error('create failed') - const id = created.result.value.sessionId - const abort = new AbortController() - const framesPromise = collectValues(api.sessionRemote.follow(id, abort.signal), abort, - frames => frames.some(frame => frame.type === 'event' && frame.event.type === 'turn/end')) - await new Promise(resolve => setTimeout(resolve, 10)) - await api.sessions.prompt(req({ sessionId: id, mode: 'queue' as const, content: [{ type: 'text' as const, text: '短' }] })) - await api.sessions.prompt(req({ sessionId: id, mode: 'steer' as const, content: [{ type: 'text' as const, text: '插话' }] })) - const frames = await framesPromise - const types = frames.flatMap(frame => frame.type === 'event' ? [frame.event.type] : []) - expect(JSON.stringify(frames)).toContain('插话') - expect(types.at(-1)).toBe('turn/end') // steer did not restart the turn - }) - - it('control replays projections while resident Remote Events retain ids across reconnects', async () => { - const api = createFixtureApi() - const first = await readControlBaseline(api.sessionRemote) - const second = await readControlBaseline(api.sessionRemote) - expect(first.value.approvals).toEqual([]) - expect(first.value.questions).toEqual([]) - const alpha = first.value.projections['fx-alpha'] - expect(alpha?.asOfSeq).toBeGreaterThan(0) - expect(alpha?.values).toMatchObject({ - title: 'Fixture 历史会话', - plan: { active: false, pending: false }, - goal: null, - imageLimits: { maxImagesPerMessage: 20, maxImageBytes: 5 * 1024 * 1024 }, - }) - const breakdown = alpha?.values['contextBreakdown'] as { systemTokens: number; messageTokens: number } - expect(breakdown.messageTokens).toBeGreaterThan(0) - // The seeded system/message at surface node 0 prices the system figure. - expect(breakdown.systemTokens).toBeGreaterThan(0) - expect((alpha?.values['sessionStats'] as { steps: number }).steps).toBeGreaterThan(0) - expect(second.value.projections['fx-alpha']).toEqual(alpha) - - const firstEvents = await readResidentRemoteEvents(api, 2) - const secondEvents = await readResidentRemoteEvents(api, 2) - const firstApproval = firstEvents.find(frame => frame.event === 'approval/request') - const firstQuestion = firstEvents.find(frame => frame.event === 'user-questions/request') - const secondApproval = secondEvents.find(frame => frame.event === 'approval/request') - const secondQuestion = secondEvents.find(frame => frame.event === 'user-questions/request') - expect(firstApproval).toMatchObject({ - type: 'waterfall', - request: { toolName: 'dangerous_tool' }, - agentId: 'fx-alpha', - }) - expect(firstQuestion).toMatchObject({ - type: 'waterfall', - agentId: 'fx-alpha', - }) - expect(Array.isArray(firstQuestion?.request.questions)).toBe(true) - expect(secondApproval?.eventId).toBe(firstApproval?.eventId) - expect(secondQuestion?.eventId).toBe(firstQuestion?.eventId) - expect(await readOpeningCursor(api.sessionRemote, sid('fx-alpha'))).toBeGreaterThan(0) - }) - - it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => { - const api = createFixtureApi() - const created = await api.sessions.create(req({})) - if (!created.result.ok) throw new Error('create failed') - const abort = new AbortController() - const framesPromise = collectValues( - api.sessionRemote.follow(created.result.value.sessionId, abort.signal), - abort, - frames => frames.some(frame => frame.type === 'event' && frame.event.type === 'turn/end'), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - // steer while idle + a non-text content block (covers the '' arm of the text join). - await api.sessions.prompt(req({ - sessionId: created.result.value.sessionId, mode: 'steer' as const, - content: [{ type: 'text' as const, text: '短' }, { type: 'image', data: 'x' } as never], - })) - const frames = await framesPromise - const types = frames.flatMap(frame => frame.type === 'event' ? [frame.event.type] : []) - expect(types[0]).toBe('turn/start') // idle steer degraded to a queued turn, not an in-turn insert - }) - - it('gamma interval flip emits a Remote status event and its empty follow source opens at -1', async () => { - vi.useFakeTimers() - try { - const api = createFixtureApi() - const abort = new AbortController() - const hostSeen: FixtureRemoteEventFrame[] = [] - const consuming = (async () => { - for await (const frame of api.remoteEvents(abort.signal)) hostSeen.push(frame) - })() - await vi.advanceTimersByTimeAsync(5001) // interval fires: fx-gamma flips running=true (no log exists) - expect(hostSeen).toContainEqual({ - type: 'emit', - event: 'api-session/status', - args: [sid('fx-gamma'), true], - }) - expect(await readOpeningCursor(api.sessionRemote, sid('fx-gamma'))).toBe(-1) - abort.abort() - await vi.advanceTimersByTimeAsync(10) - await consuming - } finally { - vi.useRealTimers() - } - }) - - it('answers a resident question through its Remote Event id and stops replaying it', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const stream = api.remoteEvents(abort.signal) - const iterator = stream[Symbol.asyncIterator]() - const question = await nextRemoteEvent( - iterator, - frame => isRemoteEventRequest(frame) && frame.event === 'user-questions/request', - ) - if (!isRemoteEventRequest(question)) throw new Error('fixture question Remote Event missing') - const clientId = await stream.clientId - await expect(api.answerRemoteEvent({ - clientId, - eventId: 'unrelated', - outcome: { kind: 'result', value: {} }, - })).resolves.toEqual({ ok: true, value: undefined }) - await expect(api.answerRemoteEvent({ - clientId, - eventId: question.eventId, - outcome: { kind: 'result', value: { answers: {} } }, - })).resolves.toEqual({ ok: true, value: undefined }) - const cancelled = await nextRemoteEvent( - iterator, - frame => isRemoteEventCancellation(frame) && frame.eventId === question.eventId, - ) - expect(cancelled).toEqual({ type: 'cancel', eventId: question.eventId }) - abort.abort() - await iterator.return?.() - - await expect(api.answerRemoteEvent({ - clientId, - eventId: question.eventId, - outcome: { kind: 'result', value: { answers: {} } }, - })).resolves.toMatchObject({ ok: false, error: { code: 'gateway/invocation-unavailable' } }) - const remaining = await readResidentRemoteEvents(api, 1) - expect(remaining.map(frame => frame.event)).toEqual(['approval/request']) - - const cancelledApi = createFixtureApi() - const cancelAbort = new AbortController() - const cancelStream = cancelledApi.remoteEvents(cancelAbort.signal) - const cancelIterator = cancelStream[Symbol.asyncIterator]() - const cancelQuestion = await nextRemoteEvent( - cancelIterator, - frame => isRemoteEventRequest(frame) && frame.event === 'user-questions/request', - ) - if (!isRemoteEventRequest(cancelQuestion)) throw new Error('fixture cancellation question missing') - await expect(cancelledApi.answerRemoteEvent({ - clientId: await cancelStream.clientId, - eventId: cancelQuestion.eventId, - outcome: { - kind: 'rejected', - error: { name: 'UserQuestionError', message: 'skip', code: 'ASK_CANCELLED' }, - }, - })).resolves.toEqual({ ok: true, value: undefined }) - cancelAbort.abort() - await cancelIterator.return?.() - const afterCancellation = await readResidentRemoteEvents(cancelledApi, 1) - expect(afterCancellation.map(frame => frame.event)).toEqual(['approval/request']) - }) - - it('answers a resident approval and broadcasts cancellation to its active delivery', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const stream = api.remoteEvents(abort.signal) - const iterator = stream[Symbol.asyncIterator]() - const approval = await nextRemoteEvent( - iterator, - frame => isRemoteEventRequest(frame) && frame.event === 'approval/request', - ) - if (!isRemoteEventRequest(approval)) throw new Error('fixture approval Remote Event missing') - - await expect(api.answerRemoteEvent({ - clientId: await stream.clientId, - eventId: approval.eventId, - outcome: { kind: 'result', value: 'allowed-once' }, - })).resolves.toEqual({ ok: true, value: undefined }) - const cancelled = await nextRemoteEvent( - iterator, - frame => isRemoteEventCancellation(frame) && frame.eventId === approval.eventId, - ) - expect(cancelled).toEqual({ type: 'cancel', eventId: approval.eventId }) - abort.abort() - await iterator.return?.() - - await expect(api.answerRemoteEvent({ - clientId: await stream.clientId, - eventId: approval.eventId, - outcome: { kind: 'next' }, - })).resolves.toMatchObject({ ok: false, error: { code: 'gateway/invocation-unavailable' } }) - const remaining = await readResidentRemoteEvents(api, 1) - expect(remaining.map(frame => frame.event)).toEqual(['user-questions/request']) - }) - - it('createDirectory under the root mints /name whose listing and crumbs share the identity', async () => { - const api = createFixtureApi() - const created = await api.directoryPickerRemote.createDirectory('/', 'srv') - if (!created.ok) throw new Error('create failed') - expect(created.value).toBe('/srv') - const listed = await api.directoryPickerRemote.list('/srv') - if (!listed.ok) throw new Error('list failed') - expect(listed.value.crumbs).toEqual([ - { name: '/', path: '/', hidden: false }, - { name: 'srv', path: '/srv', hidden: false }, - ]) - const root = await api.directoryPickerRemote.list('/') - if (!root.ok) throw new Error('root list failed') - expect(root.value.entries).toContainEqual({ name: 'srv', path: '/srv', hidden: false }) - }) - - it('workspace/follow serves the resident baseline and create reuses on path collision', async () => { - const api = createFixtureApi() - const baseline = await readWorkspaceBaseline(api.workspaceRemote) - expect(baseline.items).toEqual([ - expect.objectContaining({ - workspaceId: 'fx-ws-fixture', path: '/tmp/fixture', title: 'fixture', - sessionIds: ['fx-alpha', 'fx-beta', 'fx-gamma'], - }), - expect.objectContaining({ - workspaceId: 'fx-ws-home', path: '/home/fixture/Documents/project', title: 'project', - sessionIds: [], - }), - ]) - // path collision → the existing entity comes back, created:false, no frame. - const reused = await api.workspace.create(req({ path: '/tmp/fixture' })) - if (!reused.result.ok) throw new Error('reuse failed') - expect(reused.result.value).toMatchObject({ created: false, workspace: { workspaceId: 'fx-ws-fixture' } }) - }) - - it('workspace.create on a fresh path mints a new entity and pushes an upsert', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const consuming = collectValues( - api.workspaceRemote.follow(abort.signal), - abort, - frames => frames.some(frame => frame.type === 'upsert' - && frame.workspace.path === '/tmp/fixture-workspaces/nova'), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const created = await api.workspace.create(req({ path: '/tmp/fixture-workspaces/nova' })) - if (!created.result.ok) throw new Error('create failed') - expect(created.result.value.created).toBe(true) - expect(created.result.value.workspace).toMatchObject({ - path: '/tmp/fixture-workspaces/nova', title: 'nova', sessionIds: [], - }) - const frames = await consuming - expect(frames.at(-1)).toEqual({ type: 'upsert', workspace: created.result.value.workspace }) - // A basename-less path serves as its own title. - const rootPath = await api.workspace.create(req({ path: '/' })) - if (!rootPath.result.ok) throw new Error('rootPath failed') - expect(rootPath.result.value.workspace.title).toBe('/') - }) - - it('workspace.rename covers not-found, conflict, no-op, and the changed frame', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const consuming = collectValues( - api.workspaceRemote.follow(abort.signal), - abort, - frames => frames.filter(frame => frame.type === 'upsert').length >= 2, - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const wsid = 'fx-ws-fixture' as WorkspaceId - const missing = await api.workspace.rename(req({ workspaceId: 'fx-ws-void' as WorkspaceId, title: 'x' })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'workspace/not-found', details: { workspaceId: 'fx-ws-void' } } }) - - await api.workspace.create(req({ path: '/tmp/fixture-workspaces/occupied' })) - const conflict = await api.workspace.rename(req({ workspaceId: wsid, title: ' occupied ' })) - expect(conflict.result).toMatchObject({ ok: false, error: { code: 'workspace/name-conflict', details: { name: 'occupied' } } }) - - const noop = await api.workspace.rename(req({ workspaceId: wsid, title: ' fixture ' })) - if (!noop.result.ok) throw new Error('no-op rename failed') - expect(noop.result.value.workspace.title).toBe('fixture') - - const renamed = await api.workspace.rename(req({ workspaceId: wsid, title: 'renamed' })) - if (!renamed.result.ok) throw new Error('rename failed') - expect(renamed.result.value.workspace.title).toBe('renamed') - const frames = await consuming - // Only the create and the effective rename emit frames; the no-op stays silent. - const upserts = frames.filter(frame => frame.type === 'upsert') - expect(upserts).toHaveLength(2) - expect(upserts[1]).toMatchObject({ workspace: { workspaceId: wsid, title: 'renamed' } }) - }) - - it('session.rename covers not-found, blank title, and the accepted append + title frame', async () => { - const api = createFixtureApi() - const followAbort = new AbortController() - const controlAbort = new AbortController() - const followPromise = collectValues( - api.sessionRemote.follow(sid('fx-alpha'), followAbort.signal), - followAbort, - frames => frames.some(frame => frame.type === 'event' - && (frame.event as { type: string }).type === 'session/title'), - ) - const controlPromise = collectValues( - api.sessionRemote.control(controlAbort.signal), - controlAbort, - frames => frames.some(frame => frame.type === 'projection' && frame.key === 'title' && frame.value === '重命名'), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - - const missing = await api.sessions.rename(req({ sessionId: sid('fx-void'), title: 'x' })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'session/not-found', details: { sessionId: 'fx-void' } } }) - - const blank = await api.sessions.rename(req({ sessionId: sid('fx-alpha'), title: ' ' })) - expect(blank.result).toMatchObject({ ok: false, error: { code: 'session/title-invalid', details: { sessionId: 'fx-alpha' } } }) - - const renamed = await api.sessions.rename(req({ sessionId: sid('fx-alpha'), title: ' 重命名 ' })) - if (!renamed.result.ok) throw new Error('rename failed') - expect(renamed.result.value.title).toBe('重命名') - const acceptedSeq = renamed.result.value.seq - // The response seq addresses the appended title event (the client plane - // has no session/title in its event union — titles ride the projection — - // so the event is located by seq and its payload checked structurally). - const history = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 100 })) - if (!history.result.ok) throw new Error('history failed') - const appended = historyEvents(history.result.value.records).find(event => event.seq === acceptedSeq) - expect(appended).toMatchObject({ - type: 'session/title', - data: { title: '重命名', messageSeqs: [], source: { kind: 'user' } }, - }) - const followed = await followPromise - expect(followed.some(frame => frame.type === 'event' - && frame.event.seq === acceptedSeq - && (frame.event as { readonly type: string }).type === 'session/title')).toBe(true) - const frames = await controlPromise - const titleFrames = frames.filter(frame => - frame.type === 'projection' - && frame.key === 'title' - && frame.sessionId === sid('fx-alpha') - && frame.value === '重命名') - expect(titleFrames).toHaveLength(1) - expect(titleFrames[0]).toMatchObject({ seq: acceptedSeq }) - }) - - it('workspace.insertSessionBefore moves, appends, no-ops, and rejects invalid ids', async () => { - const api = createFixtureApi() - const wsid = 'fx-ws-fixture' as WorkspaceId - const missing = await api.workspace.insertSessionBefore(req({ workspaceId: 'fx-ws-void' as WorkspaceId, sessionId: sid('fx-alpha') })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'workspace/not-found' } }) - const ghost = await api.workspace.insertSessionBefore(req({ workspaceId: wsid, sessionId: sid('fx-ghost') })) - expect(ghost.result).toMatchObject({ ok: false, error: { code: 'workspace/move-invalid', details: { sessionId: 'fx-ghost' } } }) - const badAnchor = await api.workspace.insertSessionBefore(req({ workspaceId: wsid, sessionId: sid('fx-alpha'), beforeSessionId: sid('fx-ghost') })) - expect(badAnchor.result).toMatchObject({ ok: false, error: { code: 'workspace/move-invalid', details: { beforeSessionId: 'fx-ghost' } } }) - - const moved = await api.workspace.insertSessionBefore(req({ workspaceId: wsid, sessionId: sid('fx-gamma'), beforeSessionId: sid('fx-beta') })) - if (!moved.result.ok) throw new Error('move failed') - expect(moved.result.value.workspace.sessionIds).toEqual(['fx-alpha', 'fx-gamma', 'fx-beta']) - const appended = await api.workspace.insertSessionBefore(req({ workspaceId: wsid, sessionId: sid('fx-alpha') })) - if (!appended.result.ok) throw new Error('append failed') - expect(appended.result.value.workspace.sessionIds).toEqual(['fx-gamma', 'fx-beta', 'fx-alpha']) - const before = appended.result.value.workspace.updatedAt - const noop = await api.workspace.insertSessionBefore(req({ workspaceId: wsid, sessionId: sid('fx-alpha') })) - if (!noop.result.ok) throw new Error('no-op move failed') - expect(noop.result.value.workspace.sessionIds).toEqual(['fx-gamma', 'fx-beta', 'fx-alpha']) - expect(noop.result.value.workspace.updatedAt).toBe(before) - }) - - it('workspace.delete removes only the Workspace row and emits the removal frame', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const consuming = collectValues( - api.workspaceRemote.follow(abort.signal), - abort, - frames => frames.some(frame => frame.type === 'remove'), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const missing = await api.workspace.delete(req({ workspaceId: 'fx-ws-void' as WorkspaceId })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'workspace/not-found' } }) - const deleted = await api.workspace.delete(req({ workspaceId: 'fx-ws-fixture' as WorkspaceId })) - expect(deleted.result).toEqual({ ok: true, value: { deleted: true } }) - const frames = await consuming - expect(frames.at(-1)).toEqual({ type: 'remove', workspaceId: 'fx-ws-fixture' }) - const baseline = await readWorkspaceBaseline(api.workspaceRemote) - expect(baseline.items.some(workspace => workspace.workspaceId === 'fx-ws-fixture')).toBe(false) - const sessions = await api.sessions.list(req({})) - if (!sessions.result.ok) throw new Error('session list failed') - expect(sessions.result.value.items.map(session => session.sessionId)).toContain('fx-alpha') - }) - - it('session.create({workspaceId}) lands on the account and unknown ids error', async () => { - const api = createFixtureApi() - const hostAbort = new AbortController() - const workspaceAbort = new AbortController() - const seen: FixtureRemoteEventNotificationFrame[] = [] - const consuming = (async () => { - for await (const frame of api.remoteEvents(hostAbort.signal)) { - if (frame.type !== 'emit' || frame.event !== 'api-session/added') continue - seen.push(frame) - hostAbort.abort() - break - } - })() - const workspaceFrames = collectValues( - api.workspaceRemote.follow(workspaceAbort.signal), - workspaceAbort, - frames => frames.some(frame => frame.type === 'upsert' - && frame.workspace.sessionIds.length === 4), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const missing = await api.sessions.create(req({ workspaceId: 'fx-ws-void' as WorkspaceId })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'workspace/not-found', details: { workspaceId: 'fx-ws-void' } } }) - const created = await api.sessions.create(req({ workspaceId: 'fx-ws-fixture' as WorkspaceId })) - if (!created.result.ok) throw new Error('create failed') - const id = created.result.value.sessionId - await consuming - const added = seen[0] - expect(added).toMatchObject({ - event: 'api-session/added', - args: [{ sessionId: id, blank: true, cwd: '/tmp/fixture' }], - }) - expect((await workspaceFrames).at(-1)).toMatchObject({ - type: 'upsert', - workspace: { - workspaceId: 'fx-ws-fixture', - sessionIds: [id, 'fx-alpha', 'fx-beta', 'fx-gamma'], - }, - }) - }) - - it('supports an empty baseline, preallocated ids, independent streams, and idempotent retry', async () => { - const api = createFixtureApi({ empty: true, createFrameOrder: 'workspace-first' }) - const initialSessions = await api.sessions.list(req({})) - expect(initialSessions.result).toMatchObject({ ok: true, value: { items: [] } }) - expect(await readWorkspaceBaseline(api.workspaceRemote)).toEqual({ - items: [], - archivedSessionIds: [], - }) - - const made = await api.workspace.create(req({ path: '/tmp/fixture-workspaces/nova' })) - if (!made.result.ok) throw new Error('workspace create failed') - const hostAbort = new AbortController() - const workspaceAbort = new AbortController() - const hostFrames = collectValues( - api.remoteEvents(hostAbort.signal), - hostAbort, - frames => frames.length === 1, - ) - const workspaceFrames = collectValues( - api.workspaceRemote.follow(workspaceAbort.signal), - workspaceAbort, - frames => frames.some(frame => frame.type === 'upsert' - && frame.workspace.sessionIds.includes(sid('fx-preallocated'))), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const preallocated = sid('fx-preallocated') - const created = await api.sessions.create(req({ - workspaceId: made.result.value.workspace.workspaceId, - sessionId: preallocated, - })) - expect(created.result).toEqual({ ok: true, value: { sessionId: preallocated } }) - expect((await workspaceFrames).at(-1)).toMatchObject({ - type: 'upsert', workspace: { sessionIds: [preallocated] }, - }) - const added = (await hostFrames)[0] - expect(added).toMatchObject({ - event: 'api-session/added', - args: [{ - sessionId: preallocated, - blank: true, - cwd: made.result.value.workspace.path, - }], - }) - - const retried = await api.sessions.create(req({ - workspaceId: made.result.value.workspace.workspaceId, - sessionId: preallocated, - })) - expect(retried.result).toEqual({ ok: true, value: { sessionId: preallocated } }) - const listed = await api.sessions.list(req({})) - if (!listed.result.ok) throw new Error('session list failed') - expect(listed.result.value.items.filter(item => item.sessionId === preallocated)).toHaveLength(1) - - const conflict = await api.sessions.create(req({ sessionId: preallocated, cwd: '/elsewhere' })) - expect(conflict.result).toMatchObject({ - ok: false, - error: { code: 'session/conflict', details: { sessionId: preallocated, requestedCwd: '/elsewhere' } }, - }) - }) - - it('attaches an existing ungrouped Session to a matching Workspace', async () => { - const api = createFixtureApi() - const sessionId = sid('fx-existing-ungrouped') - await expect(api.sessions.create(req({ sessionId, cwd: '/tmp/fixture' }))).resolves.toMatchObject({ - result: { ok: true, value: { sessionId } }, - }) - - await expect(api.sessions.create(req({ - sessionId, - workspaceId: 'fx-ws-fixture' as WorkspaceId, - }))).resolves.toMatchObject({ result: { ok: true, value: { sessionId } } }) - - const workspaces = await readWorkspaceBaseline(api.workspaceRemote) - expect(workspaces.items[0]?.sessionIds).toContain(sessionId) - }) - - it('reports a conflict without an existing cwd detail for an unrecorded cwd', async () => { - const api = createFixtureApi() - const listed = await api.sessions.list(req({})) - if (!listed.result.ok) throw new Error('session list failed') - const existing = listed.result.value.items.find(item => item.sessionId === sid('fx-alpha')) - if (existing === undefined) throw new Error('fixture Session missing') - delete existing.cwd - - const conflict = await api.sessions.create(req({ sessionId: existing.sessionId })) - expect(conflict.result).toEqual({ - ok: false, - error: { - code: 'session/conflict', - message: `session ${existing.sessionId} already uses no cwd`, - details: { sessionId: existing.sessionId, requestedCwd: '/tmp/fixture' }, - }, - }) - }) - - it('publishes an ungrouped Session when Workspace attachment fails', async () => { - const api = createFixtureApi({ failWorkspaceAttach: true }) - const sessionId = sid('fx-partial') - const created = await api.sessions.create(req({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId, - })) - expect(created.result).toMatchObject({ - ok: false, - error: { code: 'session/workspace-attach-failed', details: { sessionId, workspaceId: 'fx-ws-fixture' } }, - }) - const listed = await api.sessions.list(req({})) - const workspaces = await readWorkspaceBaseline(api.workspaceRemote) - if (!listed.result.ok) throw new Error('list failed') - expect(listed.result.value.items.filter(item => item.sessionId === sessionId)).toHaveLength(1) - expect(workspaces.items[0]?.sessionIds).not.toContain(sessionId) - - const retried = await api.sessions.create(req({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId, - })) - expect(retried.result).toMatchObject({ ok: false, error: { code: 'session/workspace-attach-failed' } }) - const afterRetry = await api.sessions.list(req({})) - if (!afterRetry.result.ok) throw new Error('list failed') - expect(afterRetry.result.value.items.filter(item => item.sessionId === sessionId)).toHaveLength(1) - }) - - it('reconciles a dropped create response and can reject a prompt before acceptance', async () => { - const sessionId = sid('fx-lost-response') - const dropped = createFixtureApi({ dropSessionCreateResponse: true }) - await expect(Promise.resolve().then(() => dropped.sessions.create(req({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId, - })))).rejects.toThrow(/dropped session\.create response/) - const listed = await dropped.sessions.list(req({})) - const workspaces = await readWorkspaceBaseline(dropped.workspaceRemote) - if (!listed.result.ok) throw new Error('list failed') - expect(listed.result.value.items.some(item => item.sessionId === sessionId)).toBe(true) - expect(workspaces.items[0]?.sessionIds).toContain(sessionId) - await expect(dropped.sessions.create(req({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId, - }))).resolves.toMatchObject({ result: { ok: true, value: { sessionId } } }) - - const rejecting = createFixtureApi({ empty: true, rejectPrompt: true }) - const real = await rejecting.sessions.create(req({ sessionId: sid('fx-rejected') })) - if (!real.result.ok) throw new Error('session create failed') - const prompt = await rejecting.sessions.prompt(req({ - sessionId: real.result.value.sessionId, - mode: 'queue' as const, - content: [{ type: 'text' as const, text: 'keep me' }], - })) - expect(prompt.result).toMatchObject({ ok: false, error: { code: 'session/agent-busy' } }) - const imagePrompt = await rejecting.sessions.prompt(req({ - sessionId: real.result.value.sessionId, - mode: 'queue' as const, - content: [{ type: 'image' as const, mediaType: 'image/png' as const, data: 'iVBORw0KGgo=' }], - })) - expect(imagePrompt.result).toMatchObject({ - ok: false, - error: { code: 'session/attachment-invalid', details: { reason: 'IMAGE_DIMENSION_TOO_LARGE' } }, - }) - }) - - it('timing hooks: history delay + one-shot failure, silent append, and breakStreams end open generators', async () => { - const api = createFixtureApi() - const hooks = timing() - // One-shot transport failure after transit delay. - hooks.setHistoryDelay(5) - hooks.failNextHistory() - await expect(api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 5 }))).rejects.toThrow(/simulated history transport failure/) - hooks.setHistoryDelay(0) - // The failure was one-shot: the next call succeeds. - const ok = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 5 })) - expect(ok.result.ok).toBe(true) - // A durable append without a live frame creates a detectable seq gap. - const gapAbort = new AbortController() - const gapIterator = api.sessionRemote.follow(sid('fx-alpha'), gapAbort.signal)[Symbol.asyncIterator]() - const opening = await gapIterator.next() - if (opening.done || opening.value.type !== 'snapshot') throw new Error('follow opening snapshot missing') - hooks.appendSilent('fx-alpha', '静默丢帧') - hooks.appendUser('fx-alpha', '正常直播') - await expect(gapIterator.next()).rejects.toThrow(/stream skipped seq/) - - // Reopening replaces the window with a complete snapshot containing both durable events. - const followAbort = new AbortController() - const controlAbort = new AbortController() - const followed: FixtureFollowFrame[] = [] - const controlled: FixtureControlFrame[] = [] - const following = (async () => { - for await (const frame of api.sessionRemote.follow(sid('fx-alpha'), followAbort.signal)) { - followed.push(frame) - } - })() - const controlling = (async () => { - for await (const frame of api.sessionRemote.control(controlAbort.signal)) controlled.push(frame) - })() - await new Promise(resolve => setTimeout(resolve, 10)) - await vi.waitFor(() => { - const snapshot = followed.find(frame => frame.type === 'snapshot') - const events = snapshot === undefined ? [] : historyEvents(snapshot.records) - expect(events.some(event => JSON.stringify(event.data).includes('静默丢帧'))).toBe(true) - expect(events.some(event => JSON.stringify(event.data).includes('正常直播'))).toBe(true) - }) - hooks.appendTitle('fx-alpha', 'Fixture 修订标题') - hooks.beginModelRetry('fx-alpha') - hooks.scheduleModelRetry('fx-alpha') - hooks.completeModelRetry('fx-alpha') - hooks.beginModelRetry('fx-alpha') - hooks.cancelModelRetryDuringBackoff('fx-alpha') - await vi.waitFor(() => { - expect(followed.some(frame => frame.type === 'event' && (frame.event as { type: string }).type === 'llm/retry')).toBe(true) - expect(followed.some(frame => frame.type === 'event' && JSON.stringify(frame.event.data).includes('重试后的完整回复'))).toBe(true) - expect(followed.some(frame => frame.type === 'event' - && frame.event.type === 'turn/end' - && frame.event.data.reason.kind === 'aborted')).toBe(true) - expect(controlled.some(frame => frame.type === 'projection' - && frame.key === 'title' - && frame.value === 'Fixture 修订标题')).toBe(true) - }) - expect(followed.some(frame => frame.type === 'event' && (frame.event as { type: string }).type === 'session/title')).toBe(true) - // Paging and resumed follow agree on the recovered durable event. - const repull = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 5 })) - if (!repull.result.ok) throw new Error('repull failed') - expect(JSON.stringify(repull.result.value.records)).toContain('静默丢帧') - // breakStreams force-ends follow and control without client aborts. - await new Promise(resolve => setTimeout(resolve, 10)) - hooks.breakStreams() - await following - await controlling - expect(followAbort.signal.aborted).toBe(false) - expect(controlAbort.signal.aborted).toBe(false) - }) - - it('paces the opt-in reasoning stress hook from an external interval', async () => { - vi.useFakeTimers() - vi.setSystemTime(0) - const api = createFixtureApi() - const hooks = timing() - expect(hooks.reasoningChunkStormState()).toBeNull() - expect(() => hooks.startReasoningChunkStorm('fx-alpha', 0, 1, 16)).toThrow(/chunk count/) - expect(() => hooks.startReasoningChunkStorm('fx-alpha', 1, 0, 16)).toThrow(/chunks per interval/) - expect(() => hooks.startReasoningChunkStorm('fx-alpha', 1, 1, 0)).toThrow(/reasoning interval/) - const abort = new AbortController() - try { - const streamed = collectValues(api.sessionRemote.follow(sid('fx-alpha'), abort.signal), abort, frames => frames.some(frame => ( - frame.type === 'assistant-stream' - && frame.frame.type === 'chunk' - && isReasoningDeltaChunk(frame.frame.chunk) - && frame.frame.chunk.text.includes('REASONING_STRESS_COMPLETE') - ))) - const marker = hooks.startReasoningChunkStorm('fx-alpha', 3, 2, 16) - expect(() => hooks.startReasoningChunkStorm('fx-alpha', 1, 1, 16)).toThrow(/already running/) - expect(hooks.reasoningChunkStormState()).toMatchObject({ emitted: 0, emitting: true, marker }) - - await vi.advanceTimersByTimeAsync(0) - expect(hooks.reasoningChunkStormState()).toMatchObject({ emitted: 2, emitting: true }) - await vi.advanceTimersByTimeAsync(16) - expect(hooks.reasoningChunkStormState()).toEqual({ - sessionId: 'fx-alpha', chunkCount: 3, chunksPerInterval: 2, intervalMs: 16, - emitted: 3, marker, emitting: false, - }) - - const frames = await streamed - const deltas = frames.flatMap(frame => ( - frame.type === 'assistant-stream' - && frame.frame.type === 'chunk' - && isReasoningDeltaChunk(frame.frame.chunk) - ? [frame.frame.chunk.text] - : [] - )) - expect(deltas).toEqual(['推理', '推理', `\n${marker}`]) - } finally { - abort.abort() - vi.useRealTimers() - } - }) -}) - -describe('fixture Connection RPC', () => { - afterEach(() => { - vi.restoreAllMocks() - vi.unstubAllGlobals() - }) - - it.each(['turn-end', 'omitted', 'past-end'] as const)('forks through the completed turn for a %s anchor', async (anchor) => { - vi.stubGlobal('__fxTiming', (globalThis as Record).__fxTiming) - const { rpc } = createFixtureFaces() - const sessions = createSessionClient(rpc) - const sourceId = sid('fx-alpha') - const initial = await sessions.history({ sessionId: sourceId, maxMessages: 1000 }) - if (!initial.result.ok) throw new Error('source history failed') - const events = historyEvents(initial.result.value.records) - const boundary = events.findLast(event => event.type === 'turn/end') - if (boundary === undefined) throw new Error('source has no completed turn') - expect(initial.result.value.hasMore).toBe(false) - expect((await sessions.rename({ sessionId: sourceId, title: 'Title after the selected turn' })).result.ok).toBe(true) - const source = await sessions.history({ sessionId: sourceId, maxMessages: 1000 }) - if (!source.result.ok) throw new Error('source history after rename failed') - const fork = await rpc.call('/api', 'session/fork', { - args: { request: { - sessionId: sourceId, - ...(anchor === 'omitted' ? {} : { atSeq: anchor === 'turn-end' ? boundary.seq : events.length + 100 }), - } }, - }) - if (!fork.ok) throw new Error('fork failed') - const childId = (fork.value as { sessionId: SessionId }).sessionId - const child = await sessions.history({ sessionId: childId, maxMessages: 1000 }) - if (!child.result.ok) throw new Error('child history failed') - expect(child.result.value.hasMore).toBe(false) - expect(historyEvents(child.result.value.records)).toEqual(events.slice(0, boundary.seq + 1)) - expect(await sessions.history({ sessionId: sourceId, maxMessages: 1000 })).toMatchObject({ result: source.result }) - }) - - it('covers the migrated Remote dispatch table', async () => { - const rpc = createFixtureConnectionRpc() - const sessions = createSessionClient(rpc) - const workspaces = createWorkspaceClient(rpc) - expect((await sessions.search( - { query: 'fixture' }, - new AbortController().signal, - )).result.ok).toBe(true) - const created = await sessions.create({}) - if (!created.result.ok) throw new Error('create failed') - const id = created.result.value.sessionId - expect((await sessions.history({ sessionId: id })).result.ok).toBe(true) - expect((await sessions.prompt({ sessionId: id, mode: 'queue', content: [{ type: 'text', text: '嗨' }] })).result.ok).toBe(true) - expect((await sessions.cancel({ sessionId: id })).result.ok).toBe(true) - expect((await readWorkspaceBaseline(createWorkspaceRemote(rpc))).items).not.toHaveLength(0) - const workspace = await workspaces.create({ path: '/tmp/fixture-workspaces/via-client' }) - if (!workspace.result.ok) throw new Error('workspace create failed') - expect(workspace.result.value.workspace.title).toBe('via-client') - const wsid = workspace.result.value.workspace.workspaceId - const renamed = await workspaces.rename({ workspaceId: wsid, title: 'via-client-2' }) - if (!renamed.result.ok) throw new Error('workspace rename failed') - expect(renamed.result.value.workspace.title).toBe('via-client-2') - const attached = await sessions.create({ workspaceId: wsid }) - if (!attached.result.ok) throw new Error('attached create failed') - const moved = await workspaces.insertSessionBefore({ workspaceId: wsid, sessionId: attached.result.value.sessionId }) - if (!moved.result.ok) throw new Error('workspace move failed') - expect(moved.result.value.workspace.sessionIds).toEqual([attached.result.value.sessionId]) - const archived = await workspaces.archiveSession({ sessionId: attached.result.value.sessionId }) - if (!archived.result.ok) throw new Error('workspace archive failed') - expect(archived.result.value.archivedSessionIds).toEqual([attached.result.value.sessionId]) - const unarchived = await workspaces.unarchiveSession({ sessionId: attached.result.value.sessionId }) - if (!unarchived.result.ok) throw new Error('workspace unarchive failed') - expect(unarchived.result.value.archivedSessionIds).toEqual([]) - // A second unarchive of the same id leaves the set unchanged. - const repeated = await workspaces.unarchiveSession({ sessionId: attached.result.value.sessionId }) - if (!repeated.result.ok) throw new Error('repeated workspace unarchive failed') - expect(repeated.result.value.archivedSessionIds).toEqual([]) - }) - - it('folds the goal lifecycle over the Goal Remotes', async () => { - const rpc = createFixtureConnectionRpc() - const sessions = createSessionClient(rpc) - const created = await sessions.create({}) - if (!created.result.ok) throw new Error('create failed') - const id = created.result.value.sessionId - const goal = (endpoint: string, args: Record) => - rpc.call('/api', endpoint, { args: { agentId: id, ...args } }) - - // create → edit → pause → resume → complete → clear; each mutation advances the CAS - // revision by one (state rides the projection frames). - const goalCreated = await goal('goals/create', { request: { objective: 'ship it' } }) - if (!goalCreated.ok) throw new Error('goal create failed') - const { id: goalId, revision } = (goalCreated.value as { ref: { id: string; revision: number } }).ref - expect(revision).toBe(1) - const ref = (at: number) => ({ id: goalId, revision: at }) - expect((await goal('goals/edit', { ref: ref(1), request: { objective: 'ship it v2' } })).ok).toBe(true) - expect(await goal('goals/get', {})).toMatchObject({ - ok: true, value: { objective: 'ship it v2', revision: 2, activation: 'armed' }, - }) - timing().disarmOnlyGoal() - expect(await goal('goals/get', {})).toMatchObject({ - ok: true, value: { objective: 'ship it v2', revision: 2, activation: 'disarmed' }, - }) - expect((await goal('goals/pause', { ref: ref(2) })).ok).toBe(true) - expect((await goal('goals/resume', { ref: ref(3) })).ok).toBe(true) - // A stale ref loses the CAS check. - expect((await goal('goals/pause', { ref: ref(1) })).ok).toBe(false) - expect((await goal('goals/complete', { ref: ref(4) })).ok).toBe(true) - // complete → complete is an invalid transition. - expect((await goal('goals/complete', { ref: ref(5) })).ok).toBe(false) - expect(await goal('goals/clear', { ref: ref(5) })).toEqual({ ok: true, value: ref(6) }) - - const goalHistory = await sessions.history({ sessionId: id }) - if (!goalHistory.result.ok) throw new Error('goal history failed') - const goalEvents = historyEvents(goalHistory.result.value.records).map(event => event as unknown as { - type: string - data: { - operation?: string - source?: { kind?: string; round?: number } - } - }) - const goalChanges = goalEvents.filter(event => event.type === 'goal/change') - expect(goalChanges.map(event => event.data.operation)) - .toEqual(['create', 'edit', 'pause', 'resume', 'complete', 'clear']) - expect(goalEvents.some(event => event.type === 'user/message' - && event.data.source?.kind === 'goal' && event.data.source.round === 0)).toBe(false) - }) - - it('maps empty, prompt-reject, and workspace-first query scenarios', async () => { - vi.stubGlobal('location', { - search: '?fixture=empty&fixturePrompt=reject&fixtureFrames=workspace-first', - }) - const rpc = createFixtureConnectionRpc() - const sessions = createSessionClient(rpc) - const workspaces = createWorkspaceClient(rpc) - const workspaceRemote = createWorkspaceRemote(rpc) - await expect(sessions.list({})).resolves.toMatchObject({ result: { ok: true, value: { items: [] } } }) - const made = await workspaces.create({ path: '/tmp/fixture-workspaces/query-workspace' }) - if (!made.result.ok) throw new Error('workspace create failed') - const hostAbort = new AbortController() - const workspaceAbort = new AbortController() - const hostFrames = collectValues( - openFixtureRemoteEvents(rpc, hostAbort.signal), - hostAbort, - frames => frames.length === 1, - ) - const workspaceFrames = collectValues( - workspaceRemote.follow(workspaceAbort.signal), - workspaceAbort, - frames => frames.some(frame => frame.type === 'upsert' - && frame.workspace.sessionIds.includes(sid('fx-query-session'))), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const sessionId = sid('fx-query-session') - const created = await sessions.create({ - workspaceId: made.result.value.workspace.workspaceId, - sessionId, - }) - expect(created.result).toMatchObject({ ok: true, value: { sessionId } }) - expect((await workspaceFrames).at(-1)).toMatchObject({ - type: 'upsert', - workspace: { sessionIds: [sessionId] }, - }) - expect((await hostFrames)[0]).toMatchObject({ - event: 'api-session/added', - }) - const rejected = await sessions.prompt({ - sessionId, - mode: 'queue', - content: [{ type: 'text', text: 'retain' }], - }) - expect(rejected.result).toMatchObject({ ok: false, error: { code: 'session/agent-busy' } }) - }) - - it('maps attach-failure and dropped-response query scenarios', async () => { - vi.stubGlobal('location', { search: '?fixture&fixtureAttach=fail' }) - const partial = createFixtureConnectionRpc() - const partialResult = await createSessionClient(partial).create({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId: sid('fx-query-partial'), - }) - expect(partialResult.result).toMatchObject({ - ok: false, - error: { code: 'session/workspace-attach-failed', details: { sessionId: 'fx-query-partial' } }, - }) - - vi.stubGlobal('location', { search: '?fixture&fixtureSessionCreate=drop-response' }) - const dropped = createFixtureConnectionRpc() - await expect(createSessionClient(dropped).create({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId: sid('fx-query-dropped'), - })).rejects.toThrow(/dropped session\.create response/) - }) - -}) diff --git a/packages/client/connection/tests/generation.client.spec.ts b/packages/client/connection/tests/generation.client.spec.ts index d2b365de4b..48a6dc5e7b 100644 --- a/packages/client/connection/tests/generation.client.spec.ts +++ b/packages/client/connection/tests/generation.client.spec.ts @@ -7,7 +7,7 @@ import { } from '../src/client/index.ts' type BrowserGlobal = { - location?: { hostname: string; search: string } + location?: { hostname: string } } const contexts = new Set() @@ -20,12 +20,12 @@ afterEach(async () => { }) async function mount(): Promise { - ;(globalThis as BrowserGlobal).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as BrowserGlobal).location = { hostname: 'localhost' } const ctx = new Context() contexts.add(ctx) await ctx.plugin({ apply, inject: [] }) const connection = ctx.get('connection') as ConnectionHandle | undefined - if (connection === undefined) throw new Error('fixture did not provide Connection') + if (connection === undefined) throw new Error('plugin did not provide Connection') return connection } diff --git a/packages/client/connection/tsconfig.client.json b/packages/client/connection/tsconfig.client.json index 4266a1defc..ffc18472c2 100644 --- a/packages/client/connection/tsconfig.client.json +++ b/packages/client/connection/tsconfig.client.json @@ -9,7 +9,6 @@ "src/api-path.ts", "src/client/api.ts", "src/client/connection.ts", - "src/client/fixture.ts", "src/client/index.ts", "src/client/random-uuid.ts", "src/client/rpc.ts", @@ -30,24 +29,9 @@ { "path": "../../core/session" }, - { - "path": "../../todo/tool-todo" - }, - { - "path": "../../core/tools" - }, { "path": "../../credentials/credentials" }, - { - "path": "../../settings/settings" - }, - { - "path": "../../host/directory-picker" - }, - { - "path": "../../interaction/commands" - }, { "path": "../../llm/llm" }, diff --git a/packages/client/file-upload/README.i18n.yaml b/packages/client/file-upload/README.i18n.yaml index e6b4cb5fcb..4422bb94d6 100644 --- a/packages/client/file-upload/README.i18n.yaml +++ b/packages/client/file-upload/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/file-upload/README.md -README.md: 2c19e1a564d7b58226e6ccd3cb5b7c81e1b1c14e -README.zh.md: d1553d40d953dd6378a954c43a9b7a36b7334953 +README.md: e0c0d7637da0b426ebb5b4a0c6cb47d12784e0ed +README.zh.md: abdf965694f7ae62dd05a0192ffbc16e8678ec7d diff --git a/packages/client/file-upload/README.md b/packages/client/file-upload/README.md index 2c19e1a564..e0c0d7637d 100644 --- a/packages/client/file-upload/README.md +++ b/packages/client/file-upload/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -This package lets browser features store a `Blob`, exact bytes, or a `ReadableStream` for one Session and receive an opaque receipt for a later prompt. Served pages send Blob and stream bodies without aggregating their bytes on the page thread; pages whose Host runs in another execution context supply a Fetch-shaped carrier before Cordis boots. Callers can observe consumed bytes and cancel an active operation. A stream body is consumed once and transfers ownership when it crosses a Worker boundary. The standalone `?fixture` page uses the generated Remote for replayable Blob and exact-byte inputs. +This package lets browser features store a `Blob`, exact bytes, or a `ReadableStream` for one Session and receive an opaque receipt for a later prompt. Served pages send Blob and stream bodies without aggregating their bytes on the page thread; pages whose Host runs in another execution context supply a Fetch-shaped carrier before Cordis boots. Callers can observe consumed bytes and cancel an active operation. A stream body is consumed once and transfers ownership when it crosses a Worker boundary. Exact bytes use the generated Remote. ## Table of Contents @@ -32,7 +32,7 @@ Mount the package before a consumer that injects `fileUpload`, then call `ctx.fi name: '@deepseek-ai/dsh-client-file-upload' ``` -The package has no Cordis configuration fields. A `Blob` uses XMLHttpRequest inside a dedicated Worker so the service can report browser upload progress, including the total when the browser provides it. A `ReadableStream` transfers to that Worker and feeds Fetch incrementally; progress reports consumed bytes without a total. An `AbortSignal` terminates the dedicated Worker or reaches a page-owned carrier. Exact bytes and fixture Blob inputs use the generated Remote. +The package has no Cordis configuration fields. A `Blob` uses XMLHttpRequest inside a dedicated Worker so the service can report browser upload progress, including the total when the browser provides it. A `ReadableStream` transfers to that Worker and feeds Fetch incrementally; progress reports consumed bytes without a total. An `AbortSignal` terminates the dedicated Worker or reaches a page-owned carrier. Exact bytes use the generated Remote. ----- @@ -42,7 +42,7 @@ The package has no Cordis configuration fields. A `Blob` uses XMLHttpRequest ins

Implementation internals — click to expand -The Client plugin provides `ctx.fileUpload`. Its `upload()` method receives the owning Session identity, assembles the raw route request, and invokes the generated Remote fallback for replayable inputs. The provider reads the optional pre-Cordis `__DSH_FILE_UPLOAD__` hook once. Without a hook, each non-fixture raw request owns a short-lived Worker and releases it after completion, failure, or cancellation. With the hook, the service sends the body through the page-owned Fetch carrier; the Web Worker runtime transfers stream bodies through its request frame and exposes them to the Host HTTP bridge as backpressured chunks. +The Client plugin provides `ctx.fileUpload`. Its `upload()` method receives the owning Session identity, assembles the raw route request, and invokes the generated Remote for exact bytes. The provider reads the optional pre-Cordis `__DSH_FILE_UPLOAD__` hook once. Without a hook, each Blob or stream request owns a short-lived Worker and releases it after completion, failure, or cancellation. With the hook, the service sends the body through the page-owned Fetch carrier; the Web Worker runtime transfers stream bodies through its request frame and exposes them to the Host HTTP bridge as backpressured chunks. The Host plugin provides `ctx.fileUploads`. It owns the authenticated streaming route, encoded Remote fallback, command receipt resolver, and staged-receipt lifecycle; encoded admission, attachment-error recognition, and byte storage stay behind `ctx.attachments`. Receipt tables use the receiving Agent's Session object as their key. The Session Controller registers the resolver that can resume a cold ordinary Agent and consumes receipts during prompt admission. Prompt delivery holds each receipt binding in a disposable transaction: disposal restores the previous binding until successful delivery commits it, and queue or history observation then retires the committed receipt. diff --git a/packages/client/file-upload/README.zh.md b/packages/client/file-upload/README.zh.md index d1553d40d9..abdf965694 100644 --- a/packages/client/file-upload/README.zh.md +++ b/packages/client/file-upload/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -本包让浏览器功能为一个 Session 存储 `Blob`、精确字节或 `ReadableStream`,并取得供后续 prompt 使用的不透明凭证。普通服务页面发送 Blob 和 stream 请求体时,不会在页面线程聚合全部字节;Host 位于其他执行上下文中的页面会在 Cordis 启动前提供 Fetch 形式的载体。调用方可以观察已消费字节并取消活动操作。stream 请求体只能消费一次,跨 Worker 边界时会转移所有权。独立的 `?fixture` 页面通过生成的 Remote 处理可重放的 Blob 与精确字节输入。 +本包让浏览器功能为一个 Session 存储 `Blob`、精确字节或 `ReadableStream`,并取得供后续 prompt 使用的不透明凭证。普通服务页面发送 Blob 和 stream 请求体时,不会在页面线程聚合全部字节;Host 位于其他执行上下文中的页面会在 Cordis 启动前提供 Fetch 形式的载体。调用方可以观察已消费字节并取消活动操作。stream 请求体只能消费一次,跨 Worker 边界时会转移所有权。精确字节使用生成的 Remote。 ## 目录 @@ -32,7 +32,7 @@ kind: "package-reference" name: '@deepseek-ai/dsh-client-file-upload' ``` -本包没有 Cordis 配置字段。`Blob` 在专用 Worker 内通过 XMLHttpRequest 发送,因此服务可以报告浏览器上传进度,并在浏览器提供总量时一并报告。`ReadableStream` 会转移给该 Worker,再增量传入 Fetch;进度只报告已消费字节,不包含总量。`AbortSignal` 会终止专用 Worker,或传递给页面自己提供的载体。精确字节与 fixture Blob 输入使用生成的 Remote。 +本包没有 Cordis 配置字段。`Blob` 在专用 Worker 内通过 XMLHttpRequest 发送,因此服务可以报告浏览器上传进度,并在浏览器提供总量时一并报告。`ReadableStream` 会转移给该 Worker,再增量传入 Fetch;进度只报告已消费字节,不包含总量。`AbortSignal` 会终止专用 Worker,或传递给页面自己提供的载体。精确字节使用生成的 Remote。 ----- @@ -42,7 +42,7 @@ kind: "package-reference"
实现细节——点击展开 -Client 插件提供 `ctx.fileUpload`。其 `upload()` 方法接收所属 Session 标识,组装原始路由请求,并为可重放输入调用生成的 Remote 兜底。提供方只读取一次可选的 Cordis 启动前 `__DSH_FILE_UPLOAD__` 钩子。没有该钩子时,每个非 fixture 原始请求拥有一个短期 Worker,并在完成、失败或取消后释放。存在该钩子时,服务通过页面自己提供的 Fetch 载体发送请求体;Web Worker runtime 会通过请求帧转移 stream 请求体,再以带背压的分片形式交给 Host HTTP bridge。 +Client 插件提供 `ctx.fileUpload`。其 `upload()` 方法接收所属 Session 标识,组装原始路由请求,并为精确字节调用生成的 Remote。提供方只读取一次可选的 Cordis 启动前 `__DSH_FILE_UPLOAD__` 钩子。没有该钩子时,每个 Blob 或 stream 请求拥有一个短期 Worker,并在完成、失败或取消后释放。存在该钩子时,服务通过页面自己提供的 Fetch 载体发送请求体;Web Worker runtime 会通过请求帧转移 stream 请求体,再以带背压的分片形式交给 Host HTTP bridge。 Host 插件提供 `ctx.fileUploads`。它拥有经过认证的流式路由、编码 Remote 兜底、命令凭证解析器与暂存凭证生命周期;编码准入、附件错误识别与字节存储仍由 `ctx.attachments` 提供。凭证表以接收方 Agent 的 Session 对象为键。Session Controller 注册可恢复休眠普通 Agent 的解析器,并在 prompt 准入时消费凭证。Prompt 投递通过可释放事务持有每个凭证绑定。成功投递提交事务前,释放会恢复原绑定;提交后,队列或历史观察会退休该凭证。 diff --git a/packages/client/file-upload/src/client/contract.ts b/packages/client/file-upload/src/client/contract.ts index 998a71b2c3..8bf1145193 100644 --- a/packages/client/file-upload/src/client/contract.ts +++ b/packages/client/file-upload/src/client/contract.ts @@ -13,11 +13,9 @@ export interface FileUploadProgress { /** Browser upload service addressed by one Session identity. */ export interface FileUploadService { - /** Whether this page has a Host-backed background upload carrier. */ - readonly available: boolean /** * Store one file for a Session. Blob and stream bodies use - * the background carrier; exact bytes and fixture fallbacks use Remote. + * the background carrier; exact bytes use Remote. * @param sessionId - Session that owns the staged receipt. * @param data - browser Blob, exact bytes, or a one-shot byte stream. * @param name - optional display name. diff --git a/packages/client/file-upload/src/client/runtime.ts b/packages/client/file-upload/src/client/runtime.ts index a54e31121c..6601da4878 100644 --- a/packages/client/file-upload/src/client/runtime.ts +++ b/packages/client/file-upload/src/client/runtime.ts @@ -160,14 +160,12 @@ interface FileUploadTransport { /** Cordis service that owns one background carrier per upload operation. */ export class FileUploadRuntime extends Service implements FileUploadService { - readonly available: boolean private readonly transport: FileUploadTransport /** @param ctx - providing Client context. */ constructor(ctx: Context) { super(ctx, 'fileUpload') const hook = (globalThis as ClientFileUploadGlobal).__DSH_FILE_UPLOAD__ - this.available = hook !== undefined || !isFixturePage() this.transport = hook === undefined ? workerTransport() : customTransport(hook.fetch) } @@ -177,7 +175,6 @@ export class FileUploadRuntime extends Service implements FileUploadService { * @returns the response status and text body. */ post(request: FileUploadRequest): Promise { - if (!this.available) return Promise.reject(new Error('background upload is unavailable in fixture mode')) return this.transport.post(request) } @@ -197,7 +194,7 @@ export class FileUploadRuntime extends Service implements FileUploadService { signal?: AbortSignal, onProgress?: (progress: { readonly loaded: number; readonly total?: number }) => void, ): Promise> { - if (!(data instanceof Uint8Array) && this.available) { + if (!(data instanceof Uint8Array)) { const query = new URLSearchParams({ sessionId }) if (name !== undefined) query.set('name', name) const response = await this.post({ @@ -212,14 +209,10 @@ export class FileUploadRuntime extends Service implements FileUploadService { } return parseFileUploadResult(response.body) } - if (!(data instanceof Uint8Array) && !(data instanceof Blob)) { - throw new Error('stream file upload requires a background carrier') - } - const bytes = data instanceof Uint8Array ? data : new Uint8Array(await data.arrayBuffer()) return (this.ctx as FileUploadRemoteContext).remote.fileUploads.upload( sessionId, { - data: bytesToBase64(bytes), + data: bytesToBase64(data), ...(name === undefined ? {} : { name }), }, signal, @@ -311,13 +304,6 @@ function resolveUrl(path: string): URL { return new URL(path, origin === undefined || origin === 'null' ? 'http://dsh.internal' : origin) } -function isFixturePage(): boolean { - const pageLocation = Reflect.get(globalThis, 'location') as unknown - return typeof pageLocation === 'object' && pageLocation !== null - && 'search' in pageLocation && typeof pageLocation.search === 'string' - && new URLSearchParams(pageLocation.search).has('fixture') -} - function parseFileUploadResult(body: string): RemoteResult { const value = JSON.parse(body) as unknown if (!isRecord(value) || typeof value.ok !== 'boolean') { diff --git a/packages/client/file-upload/tests/file-upload.client.spec.ts b/packages/client/file-upload/tests/file-upload.client.spec.ts index 66b27b102a..ac9e63f499 100644 --- a/packages/client/file-upload/tests/file-upload.client.spec.ts +++ b/packages/client/file-upload/tests/file-upload.client.spec.ts @@ -252,17 +252,6 @@ describe('file upload service', () => { await fiber.dispose() }) - it('leaves the fixture on its generated Remote fallback', async () => { - vi.stubGlobal('location', { origin: 'https://fixture.test', search: '?fixture' }) - const ctx = new Context() - const fiber = ctx.plugin(FileUploadRuntime) - await fiber - expect(ctx.fileUpload.available).toBe(false) - await expect((ctx.fileUpload as FileUploadRuntime).post({ path: '/upload', body: new Blob() })) - .rejects.toThrow('background upload is unavailable in fixture mode') - await fiber.dispose() - }) - it('fails loud when a served browser has no Worker implementation', async () => { vi.stubGlobal('Worker', undefined) const ctx = new Context() @@ -438,8 +427,7 @@ describe('Session-addressed file upload', () => { await fiber.dispose() }) - it('uses the direct Remote fallback for exact bytes and fixture Blob bodies', async () => { - vi.stubGlobal('location', { origin: 'https://fixture.test', search: '?fixture' }) + it('uses the direct Remote fallback for exact bytes', async () => { const remote = vi.fn(() => Promise.resolve({ ok: true, value: { @@ -450,7 +438,7 @@ describe('Session-addressed file upload', () => { const { fiber, service } = await scopedService({ remote }) await expect(service.upload(SESSION_ID, Uint8Array.of(0, 0, 0), 'bytes.bin')) .resolves.toMatchObject({ ok: true }) - await expect(service.upload(SESSION_ID, new Blob([Uint8Array.of(1)]))) + await expect(service.upload(SESSION_ID, Uint8Array.of(1))) .resolves.toMatchObject({ ok: true }) expect(remote.mock.calls).toEqual([ [SESSION_ID, { data: 'AAAA', name: 'bytes.bin' }, undefined], @@ -459,14 +447,7 @@ describe('Session-addressed file upload', () => { await fiber.dispose() }) - it('rejects an unavailable stream and malformed background results', async () => { - vi.stubGlobal('location', { origin: 'https://fixture.test', search: '?fixture' }) - const fixture = await scopedService() - const stream = new ReadableStream({ start(controller) { controller.close() } }) - await expect(fixture.service.upload(SESSION_ID, stream)) - .rejects.toThrow('stream file upload requires a background carrier') - await fixture.fiber.dispose() - + it('rejects malformed background results', async () => { vi.stubGlobal('location', { origin: 'https://preview.test' }) ;(globalThis as UploadGlobal).__DSH_FILE_UPLOAD__ = { fetch: () => Promise.resolve(new Response(null, { status: 413 })), diff --git a/packages/client/locale/src/locales/en.ts b/packages/client/locale/src/locales/en.ts index 70ce31598c..d16b4913ae 100644 --- a/packages/client/locale/src/locales/en.ts +++ b/packages/client/locale/src/locales/en.ts @@ -34,8 +34,6 @@ export const en = { 'unknown': 'Unknown', 'none': 'None', 'truncated': 'Truncated', - 'json.collapseNode': 'Collapse JSON node', - 'json.expandNode': 'Expand JSON node', 'json.label': 'JSON', 'markdown.footnotes': 'Footnotes', 'markdown.truncatedCharacters': '… truncated at {total} characters', diff --git a/packages/client/locale/src/locales/zh.ts b/packages/client/locale/src/locales/zh.ts index 30cee308d5..2e932cef41 100644 --- a/packages/client/locale/src/locales/zh.ts +++ b/packages/client/locale/src/locales/zh.ts @@ -32,8 +32,6 @@ export const zh = { 'unknown': '未知', 'none': '无', 'truncated': '已截断', - 'json.collapseNode': '收起 JSON 节点', - 'json.expandNode': '展开 JSON 节点', 'json.label': 'JSON', 'markdown.footnotes': '脚注', 'markdown.truncatedCharacters': '… 已截断,共 {total} 字符', diff --git a/packages/client/modules/README.i18n.yaml b/packages/client/modules/README.i18n.yaml index 0cb60fa5d4..d163a406e8 100644 --- a/packages/client/modules/README.i18n.yaml +++ b/packages/client/modules/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/modules/README.md -README.md: 4c9dc4a3a13cb6f24e03c927d0277e137ab97b9f -README.zh.md: bc5b78d258270962661ab27ca7eb1a58cc61f01c +README.md: c83796fc8d6c364b9fad6d44e25316e2c2f7e7af +README.zh.md: 9ffb68ebc07b5970edc45a54c4cfde7cdf4ef7d8 diff --git a/packages/client/modules/README.md b/packages/client/modules/README.md index 4c9dc4a3a1..c83796fc8d 100644 --- a/packages/client/modules/README.md +++ b/packages/client/modules/README.md @@ -73,7 +73,7 @@ The Node half snapshots each client bundle and available source map before publi The bundle route follows the injected `webServer` lifetime: it registers when the service is ready and is removed and re-registered when that service is replaced. Module composition and `fetchBundle()` remain available without a Web server. -The host contributes structured index rows that inject, into ``: the `window.__ModuleLoader__` queue facade, advisory preloads for every application combo, the parser-blocking bootstrap combo scripts, then the boot graph before the shell reads it. A Web carrier renders those rows into its index response; a shell-owned carrier can render the same rows without a Web server. The facade's `create()` materializes the modules bundle, delegates construction to its `createClientModuleSystem` export, and leaves the same facade in live-registration mode. +The host contributes structured index rows that inject, into ``: the `window.__ModuleLoader__` queue facade, advisory preloads for every application combo, the parser-blocking bootstrap combo scripts, then the boot graph before the shell reads it. A Web carrier renders those rows into its index response; a shell-owned carrier can render the same rows without a Web server. The facade's `create()` materializes the modules bundle, delegates construction to its `createClientModuleSystem` export, and leaves the same facade in live-registration mode. The shell installs that returned system as its Loader's `internal`; the modules plugin publishes that instance as `ctx.modules`, so separate Cordis trees never select an instance through module-global state. ### Source map diff --git a/packages/client/modules/README.zh.md b/packages/client/modules/README.zh.md index bc5b78d258..9ffb68ebc0 100644 --- a/packages/client/modules/README.zh.md +++ b/packages/client/modules/README.zh.md @@ -73,7 +73,7 @@ Node 半侧会在发布前快照每个客户端 bundle 及其现有 source map bundle 路由随注入的 `webServer` 生命周期注册:服务就绪时注册,服务被替换时移除并重新注册。模块组合与 `fetchBundle()` 在没有 Web server 时仍可用。 -宿主贡献结构化 index 行,并向 `` 注入:`window.__ModuleLoader__` queue facade、每个 application combo 的提示性 preload、阻塞 parser 的 bootstrap combo 脚本,然后才是外壳读取前的启动图。Web 载体把这些行渲染进 index 响应;由 shell 持有的载体则可以在没有 Web server 时渲染同一批行。facade 的 `create()` 物化 modules bundle、把构造委托给其 `createClientModuleSystem` 导出,并让同一 facade 进入 live registration 模式。 +宿主贡献结构化 index 行,并向 `` 注入:`window.__ModuleLoader__` queue facade、每个 application combo 的提示性 preload、阻塞 parser 的 bootstrap combo 脚本,然后才是外壳读取前的启动图。Web 载体把这些行渲染进 index 响应;由 shell 持有的载体则可以在没有 Web server 时渲染同一批行。facade 的 `create()` 物化 modules bundle、把构造委托给其 `createClientModuleSystem` 导出,并让同一 facade 进入 live registration 模式。外壳把返回的系统装成自身 Loader 的 `internal`;modules 插件将该实例发布为 `ctx.modules`,因此不同 Cordis 树不会通过模块级全局状态选择实例。 ### 源码索引 diff --git a/packages/client/modules/src/client/index.ts b/packages/client/modules/src/client/index.ts index d7bf39e518..f99ee7caff 100644 --- a/packages/client/modules/src/client/index.ts +++ b/packages/client/modules/src/client/index.ts @@ -6,15 +6,16 @@ * parser-preloads this ordinary client bundle into the pending registration * queue. The HTML-installed loader facade materializes this bundle and calls * its bootstrap export, which constructs the system and retains the same - * exports for this package's graph row. The plugin face only enrolls that - * pre-existing instance by providing it as `ctx.modules`. + * exports for this package's graph row. The plugin face enrolls the module + * system attached to its own Loader as `ctx.modules`. * @module @deepseek-ai/dsh-client-modules/client */ import type { Context } from '@deepseek-ai/cordis' +import type { Loader } from '@deepseek-ai/cordis-plugin-loader' import { ClientModuleSystem } from './system.ts' import { parseBootManifest } from './manifest.ts' import type { - ClientBootstrapModule, ClientModuleCreateOptions, ClientModuleLoaderTarget, + ClientBootstrapModule, ClientModuleCreateOptions, ClientModuleLoader, ClientModuleLoaderTarget, } from './manifest.ts' export { ClientModuleSystem } @@ -26,37 +27,39 @@ export type { WebBootEntry, WebBootGraph, } from './manifest.ts' -let moduleSystem: ClientModuleSystem | undefined - /** * Build the live module system from the HTML facade's materialized modules bundle. * @param target - Stable registration facade whose pending queue becomes the live sink. * @param bootstrapModule - This bundle's id and already-materialized exports. * @param options - Raw boot graph, platform seed, and optional bundle transport. - * @returns The created module system, also published for this package's Cordis plugin face. + * @returns The created module system. */ export function createClientModuleSystem( target: ClientModuleLoaderTarget, bootstrapModule: ClientBootstrapModule, options: ClientModuleCreateOptions, ): ClientModuleSystem { - moduleSystem = new ClientModuleSystem({ + return new ClientModuleSystem({ manifest: parseBootManifest(options.boot), staticModules: options.staticModules, registrationTarget: target, bootstrapModule, ...(options.loadBundle === undefined ? {} : { loadBundle: options.loadBundle }), }) - return moduleSystem } +/** Required service: the Loader whose internal module system this plugin publishes. */ +export const inject = ['loader'] + /** * Enroll the kernel-built module system as `ctx.modules`. * @param ctx - client root context. */ export function apply(ctx: Context): void { - if (moduleSystem === undefined) { - throw new Error('client-modules: createClientModuleSystem must run before plugin boot') + const loader: Loader = ctx.loader + const modules = loader.internal as unknown as ClientModuleLoader | undefined + if (modules?.version !== 'client') { + throw new Error('client-modules: the Loader has no client module system') } - ctx.reflect.provide('modules', moduleSystem) + ctx.reflect.provide('modules', modules) } diff --git a/packages/client/modules/tests/loader.client.spec.ts b/packages/client/modules/tests/loader.client.spec.ts index 8de76e8cce..71ae8c4973 100644 --- a/packages/client/modules/tests/loader.client.spec.ts +++ b/packages/client/modules/tests/loader.client.spec.ts @@ -123,8 +123,12 @@ function bench( } describe('Cordis plugin face', () => { - it('rejects activation before the HTML facade creates the module system', () => { - expect(() => { apply(new Context()) }).toThrow('createClientModuleSystem must run before plugin boot') + it('rejects a Loader whose internal is absent or not a client module system', () => { + for (const internal of [undefined, { version: 'worker' }]) { + const ctx = new Context() + ctx.provide('loader', { internal } as never) + expect(() => { apply(ctx) }).toThrow('the Loader has no client module system') + } }) }) @@ -293,11 +297,19 @@ describe('bootstrap module', () => { expect(b.fetched).toEqual([APPLICATION_URL]) }) - it('publishes the same closed-over system when the modules Cordis plugin activates', () => { + it('publishes the module system attached to its own Loader', () => { + const a = bench([]) const b = bench([]) - const ctx = new Context() - apply(ctx) - expect(ctx.modules).toBe(b.loader) + const ctxA = new Context() + const ctxB = new Context() + ctxA.reflect.provide('loader', { internal: a.loader }) + ctxB.reflect.provide('loader', { internal: b.loader }) + + apply(ctxA) + apply(ctxB) + + expect(ctxA.modules).toBe(a.loader) + expect(ctxB.modules).toBe(b.loader) }) it('rejects a second queued registration for the bootstrap id', () => { diff --git a/packages/client/tsdown.client.ts b/packages/client/tsdown.client.ts index b946373430..4ca225d5ac 100644 --- a/packages/client/tsdown.client.ts +++ b/packages/client/tsdown.client.ts @@ -12,7 +12,7 @@ */ import { readFile } from 'node:fs/promises' import { existsSync, globSync, readFileSync } from 'node:fs' -import { isBuiltin } from 'node:module' +import { createRequire, isBuiltin } from 'node:module' import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from 'node:path' import { fileURLToPath } from 'node:url' import type { TsdownPlugin, UserConfig } from 'tsdown' @@ -681,6 +681,7 @@ const SOURCEMAP_COMMENT = /\n\/\/# sourceMappingURL=.*\s*$/ /** Resolve an emitted JS asset import against its source-tree counterpart. */ function sourceAssetPath(source: string, importer: string): string { + if (!source.startsWith('.') && !isAbsolute(source)) return createRequire(importer).resolve(source) const emitted = resolvePath(dirname(importer), source) if (existsSync(emitted)) return emitted const boundary = emitted.indexOf(TYPES_MARKER) diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 2c5c3fda05..c65f99e1d5 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md -README.md: 2333739f2827eb673525b9f00547d12e8d343888 -README.zh.md: 0de859abfcfac1679d605cf4447a2f4ac2312f5d +README.md: 254c511a023aa8217b73cc0fe3802f13f2a63d80 +README.zh.md: d79f8b5060bd6a452ddaf018074adaea88b85604 diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index 2333739f28..254c511a02 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -41,6 +41,8 @@ Each nonempty appended `system/message` owns a collapsed prompt row, including a A completed Turn shows an expandable usage row only when the loaded window includes `turn/start` and every started model attempt reports safe, exact usage. The row omits unavailable optional buckets. Incomplete or contradictory accounting hides the complete disclosure instead of presenting a partial total. +Reply timing survives reopening history: recorded token timestamps supply first-token latency and generation duration, including the Step's first token from an earlier retry attempt. Missing Step starts or token timestamps leave the affected metrics unavailable. + ## Completed-turn footer diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 0de859abfc..d79f8b5060 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -43,6 +43,8 @@ kind: "package-reference" 只有当已加载窗口包含 `turn/start`,且每次已启动的模型尝试都报告安全、精确的用量时,已完成轮次才显示可展开的用量行。该行会省略不可用的可选用量桶。记账不完整或相互矛盾时,整个详情都不显示,避免把部分总量冒充完整结果。 +重新打开历史后仍保留回复计时:记录中的 token 时间戳提供首 token 延迟和生成耗时,包括步骤中更早重试尝试的首个 token。缺少步骤起点或 token 时间戳时,受影响的指标保持不可用。 + ## 已完成轮次的页脚 diff --git a/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx b/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx index 3212801e00..51300f6d02 100644 --- a/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx +++ b/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx @@ -58,6 +58,8 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({ return { resolve: value => localPathMediaUrl(protocol, origin, value) } }, []) const last = blocks.length - 1 + const hasBodyOrToolCall = blocks.some(block => block.kind === 'tool-call' + || (block.kind === 'text' && block.text.trim() !== '')) // Tool-call heads render as tool rows in the chat view's grouping pass, so // a node that is only those heads (or empty) would paint an empty root // between tool groups — skip the shell unless something visible remains. @@ -89,7 +91,12 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({ hidden={reasoningHidden} reveal={revealProcess} > - + , ) break diff --git a/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx b/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx index bc7859b364..bb22eea778 100644 --- a/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx +++ b/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx @@ -22,11 +22,18 @@ function latestLine(text: string): string { * the complete text. * @param props.text - complete or streaming reasoning text. * @param props.running - whether this block is the streaming tail. + * @param props.defaultExpanded - expanded state until the reader toggles it. * @param props.t - conversation locale seat for the running status. * @returns the reasoning disclosure. */ -export function ReasoningRow({ text, running, t }: { text: string; running: boolean; t: ChatViewSlotProps['t'] }) { - const [expanded, setExpanded] = useState(false) +export function ReasoningRow({ text, running, defaultExpanded, t }: { + text: string + running: boolean + defaultExpanded: boolean + t: ChatViewSlotProps['t'] +}) { + const [expandedOverride, setExpandedOverride] = useState() + const expanded = expandedOverride ?? defaultExpanded const summary = (running ? latestLine(text) : firstLine(text)).replaceAll('**', '') return ( @@ -47,7 +54,7 @@ export function ReasoningRow({ text, running, t }: { text: string; running: bool open={expanded} expandable expandOnRowClick - onToggle={() => { setExpanded(value => !value) }} + onToggle={() => { setExpandedOverride(!expanded) }} collapsedContent={( <> diff --git a/packages/client/ui-chat/src/client/conversation-nodes/assistant.ts b/packages/client/ui-chat/src/client/conversation-nodes/assistant.ts index 41093124a8..aecfa43c5e 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/assistant.ts +++ b/packages/client/ui-chat/src/client/conversation-nodes/assistant.ts @@ -4,6 +4,7 @@ import type { ConversationNodeContext, ConversationNodeDefinition, } from '@deepseek-ai/dsh-client-ui-conversation/client' import type { StreamChunk } from '@deepseek-ai/dsh-llm' +import { assistantStreamFirstTokenTime } from '@deepseek-ai/dsh-llm/assistant-stream' import type {} from '@deepseek-ai/dsh-llm-retry/types' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' import type { AssistantChatData } from '../contract/chat-nodes.ts' @@ -161,6 +162,17 @@ function updateChunk( } } +/** The Step retains its first token across live chunks and settled retry attempts. */ +function settleTiming( + state: AssistantState, + event: SessionEvent<'assistant/message' | 'assistant/attempt'>, +): AssistantState { + return { + ...state, + firstTokenTime: state.firstTokenTime ?? assistantStreamFirstTokenTime(event.data.stream), + } +} + function settleMessage( state: AssistantState, match: ConversationMatch, @@ -168,7 +180,7 @@ function settleMessage( ): AssistantState { const blocks = toAssistantBlocks(event.data.message.content) return { - ...state, + ...settleTiming(state, event), blocks, visibleBlocks: countVisibleBlocks(blocks), hidden: false, @@ -236,6 +248,11 @@ function fallbackState(context: ConversationNodeContext): Assist state = updateChunk(state, match.event.data.chunk, match.event.seq, match.event.time) continue } + if (match.event.type === 'assistant/attempt') { + state ??= initialState(match.event.data.turn, match.event.data.step) + state = settleTiming(state, match.event) + continue + } if (match.event.type === 'assistant/message') { state ??= initialState(match.event.data.turn, match.event.data.step) state = settleMessage(state, match, match.event) @@ -296,6 +313,7 @@ export const assistantDefinition: ConversationNodeDefinition = { match: (event) => { if (event.type === 'step/start') return { id: `${event.data.turn}:${event.data.step}`, role: 'start' } if (event.type === 'assistant/live-chunk' + || event.type === 'assistant/attempt' || (event.type === 'assistant/message' && event.surfaceOp === 'append')) { return { id: `${event.data.turn}:${event.data.step}`, role: 'update' } } @@ -313,13 +331,14 @@ export const assistantDefinition: ConversationNodeDefinition = { return updateChunk(context.state, match.event.data.chunk, match.event.seq, match.event.time) } if (match.event.type === 'assistant/message') return settleMessage(context.state, match, match.event) + if (match.event.type === 'assistant/attempt') return settleTiming(context.state, match.event) if (match.event.type === 'llm/retry') { return resetForRetry(context.state) } return context.state }, publication: (match) => { - if (match.event.type === 'step/start') return 'none' + if (match.event.type === 'step/start' || match.event.type === 'assistant/attempt') return 'none' if (match.event.type !== 'assistant/live-chunk') return 'immediate' const type = match.event.data.chunk.type return type === 'usage' || type === 'finish' ? 'none' : 'animation-frame' diff --git a/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts b/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts index 3e8983fc46..8fa1b90d0a 100644 --- a/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts +++ b/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts @@ -1056,9 +1056,16 @@ describe('built-in conversation node Definitions', () => { const finalNode = (node(finalizedPacked, 'assistant-step')?.data as AssistantChatData).finalNode expect(finalNode).toMatchObject({ blocks: [{ kind: 'text', text: 'done' }], - timing: { firstTokenTime: null }, + timing: { firstTokenTime: 1_999 }, }) + const windowed = assembler(finalizedInputs.slice(2), true) + const timing = () => (node(snapshot(windowed), 'assistant-step')?.data as AssistantChatData).finalNode?.timing + expect(timing()).toMatchObject({ stepStartTime: null, firstTokenTime: 1_999 }) + windowed.prepend(finalizedInputs.slice(0, 2), false) + windowed.flush() + expect(timing()).toEqual(finalNode?.timing) + const namedToolHistory = [ at(40, 'turn/start', { turn: 3 }), at(41, 'step/start', { turn: 3, step: 1 }), @@ -1083,7 +1090,7 @@ describe('built-in conversation node Definitions', () => { const namedTool = (node(namedToolPacked, 'assistant-step')?.data as AssistantChatData).finalNode expect(namedTool).toMatchObject({ blocks: [{ kind: 'tool-call', callId: 'call-2', name: 'read', argsRaw: '' }], - timing: { firstTokenTime: null }, + timing: { firstTokenTime: 4_000 }, }) }) diff --git a/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx b/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx index 439b79bc7a..0711d89770 100644 --- a/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx +++ b/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx @@ -14,6 +14,26 @@ const t = makeTranslate(zh, commonZh) const renderMessageImages: AssistantMarkdownProps['renderMessageImages'] = () => null describe('ReasoningRow', () => { + it('collapses when body text arrives and preserves a later manual expansion', () => { + const reasoning = { kind: 'reasoning' as const, text: 'Inspect the session\nCheck persistence' } + const view = render( + , + ) + expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('true') + view.rerender( + , + ) + expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('false') + fireEvent.click(view.getByText('思考')) + view.rerender( + , + ) + expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('true') + expect(view.getByText(/Check persistence/)).toBeTruthy() + }) + it('follows the latest streaming line, then restores the settled first line', () => { const view = render( { />, ) expect(view.getByText('运行中')).toBeTruthy() + expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('true') + fireEvent.click(view.getByText('思考')) expect(view.getByText('Newest reasoning tokens').parentElement?.getAttribute('data-follow-end')) .toBe('true') @@ -62,6 +84,7 @@ describe('ReasoningRow', () => { ) const row = view.getByRole('button') + fireEvent.click(view.getByText('思考')) fireEvent.click(view.getByText('Inspect the session')) expect(row.getAttribute('aria-expanded')).toBe('true') expect(view.getByText(/Check persistence/)).toBeTruthy() @@ -91,6 +114,7 @@ describe('ReasoningRow', () => { />, ) + fireEvent.click(view.getByText('思考')) expect(view.getByText('Comparing checkout and merge bases')).toBeTruthy() expect(view.queryByText('**Comparing checkout and merge bases**')).toBeNull() @@ -98,7 +122,7 @@ describe('ReasoningRow', () => { expect(view.container.querySelector('[class*="thinkBody"]')?.textContent).toBe(text) }) - it('expanded Think drops the inline summary and renders plain prose, no IN card', () => { + it('opens reasoning-only replies as plain prose and allows manual collapse', () => { const view = render( { renderMessageImages={renderMessageImages} />, ) - fireEvent.click(view.getByText('思考')) + expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('true') expect(view.getAllByText(/Inspect the session/)).toHaveLength(1) expect(view.queryByText('IN')).toBeNull() expect(view.container.querySelector('[class*="ioCard"]')).toBeNull() expect(view.container.querySelector('[class*="thinkBody"]')).not.toBeNull() + fireEvent.click(view.getByText('思考')) + expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('false') + expect(view.queryByText(/Check persistence/)).toBeNull() }) }) diff --git a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx index a35bf956d0..678985ba88 100644 --- a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx @@ -42,7 +42,6 @@ async function bench() { }, })) const uploads = new Map Promise>([[ROOT, rootUpload]]) - runtime.fileUpload.available = true runtime.fileUpload.upload = (sessionId: SessionId, ...args: unknown[]) => { const upload = uploads.get(sessionId) if (upload === undefined) throw new Error('test file upload has no Session fixture') diff --git a/packages/client/ui-conversation/tests/service-orchestration.client.spec.ts b/packages/client/ui-conversation/tests/service-orchestration.client.spec.ts index 5e0521f0da..1c30f577a1 100644 --- a/packages/client/ui-conversation/tests/service-orchestration.client.spec.ts +++ b/packages/client/ui-conversation/tests/service-orchestration.client.spec.ts @@ -17,7 +17,6 @@ import { zh } from '../src/client/locales.ts' async function bench(maxConcurrentFileUploads = 2) { const runtime = await SlotTestRuntime.create() - runtime.fileUpload.available = true runtime.fileUpload.upload = (sessionId: SessionId, ...args: unknown[]) => { const session = runtime.sessions.behavior(sessionId) as { uploadFile?: (...input: unknown[]) => Promise diff --git a/packages/client/ui-message-feedback/package.json b/packages/client/ui-message-feedback/package.json index 051353d43f..08b42a5819 100644 --- a/packages/client/ui-message-feedback/package.json +++ b/packages/client/ui-message-feedback/package.json @@ -55,6 +55,7 @@ "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-message-feedback": "workspace:^", + "@deepseek-ai/dsh-remote-mock": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^", diff --git a/packages/client/ui-message-feedback/src/client/controller.ts b/packages/client/ui-message-feedback/src/client/controller.ts index 9a47b63e73..f50b248d99 100644 --- a/packages/client/ui-message-feedback/src/client/controller.ts +++ b/packages/client/ui-message-feedback/src/client/controller.ts @@ -7,9 +7,8 @@ * @module @deepseek-ai/dsh-client-ui-message-feedback/client/controller */ -import type { Context as ClientContext } from '@deepseek-ai/cordis' import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots' -import type { MessageId } from '@deepseek-ai/dsh-api-remotes/client' +import type { ClientRemote, MessageId } from '@deepseek-ai/dsh-api-remotes/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { FeedbackRecord } from '@deepseek-ai/dsh-command-feedback/types' import type { @@ -95,11 +94,11 @@ export class MessageFeedbackController implements HostObservable { - const carried = await this.ctx.remote.messageFeedback.put({ + const carried = await this.remote.put({ sessionId: this.sessionId, messageId, rating, @@ -217,7 +216,7 @@ export class MessageFeedbackController implements HostObservable { - const carried = await this.ctx.remote.messageFeedback.delete({ + const carried = await this.remote.delete({ sessionId: this.sessionId, messageId, ifVersion: observed.version, @@ -240,7 +239,7 @@ export class MessageFeedbackController implements HostObservable { - const carried = await this.ctx.remote.messageFeedback.list({ sessionId: this.sessionId }) + const carried = await this.remote.list({ sessionId: this.sessionId }) if (this.disposed) return OK if (!carried.ok) { this.publish({ status: 'error', items: this.view.items, error: carried.error.message }) diff --git a/packages/client/ui-message-feedback/src/client/surface.ts b/packages/client/ui-message-feedback/src/client/surface.ts index b54d04d659..3006350ce9 100644 --- a/packages/client/ui-message-feedback/src/client/surface.ts +++ b/packages/client/ui-message-feedback/src/client/surface.ts @@ -24,7 +24,7 @@ export class FeedbackSurface { * @param sessionId - Session owning the transcript and the remark. */ constructor(private readonly ctx: ClientContext, private readonly sessionId: SessionId) { - this.feedback = new MessageFeedbackController(ctx, sessionId) + this.feedback = new MessageFeedbackController(ctx.remote.messageFeedback, sessionId) this.dialog = new FeedbackDialogController((target, entry) => target.kind === 'message' ? this.feedback.rate(target.messageId, target.rating, entry) : this.recordSession(entry)) diff --git a/packages/client/ui-message-feedback/tests/controller.client.spec.ts b/packages/client/ui-message-feedback/tests/controller.client.spec.ts index 5fe01b2f58..54ed84bd8e 100644 --- a/packages/client/ui-message-feedback/tests/controller.client.spec.ts +++ b/packages/client/ui-message-feedback/tests/controller.client.spec.ts @@ -5,8 +5,8 @@ * reconciles from the authoritative item carried by the reply, mutations * serialize per Session, and a disposed controller stops publishing. */ -import { describe, expect, it, vi } from 'vitest' -import type { Context as ClientContext } from '@deepseek-ai/cordis' +import { RemoteMock, ok } from '@deepseek-ai/dsh-remote-mock' +import { beforeEach, describe, expect, it, vi } from 'vitest' import type { MessageId, SessionId } from '@deepseek-ai/dsh-api-remotes/client' import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' import type { @@ -31,49 +31,20 @@ function item(overrides: Partial = {}): MessageFeedbackItem } } -/** A recording fake Remote whose per-method answers are scripted per call. */ -type Script = { - list?: (request: unknown) => Promise - put?: (request: unknown) => Promise - delete?: (request: unknown) => Promise -} +let mock: RemoteMock -/** - * A recording fake context. Scripts return the *business* result; this wraps it - * in the carrier envelope the generated face uses, so specs stay readable. A - * script may also return an already-enveloped `{ok:false,error:RemoteError}` to - * exercise a carrier failure. - */ -function fakeRemote(script: Script = {}) { - const calls: { method: string; request: unknown }[] = [] - const isCarrier = (v: unknown): boolean => - typeof v === 'object' && v !== null && 'ok' in v && v.ok === false - && 'error' in v && 'details' in ((v as { error: object }).error ?? {}) - const record = (method: 'list' | 'put' | 'delete', real: Script[keyof Script], fallback: unknown) => - (request: never): Promise => { - calls.push({ method, request }) - const business = real === undefined ? Promise.resolve(fallback) : real(request) - return business.then(v => (isCarrier(v) ? v : { ok: true, value: v })) as Promise - } - const ctx = { - remote: { - messageFeedback: { - list: record('list', script.list, { ok: true, value: { items: [] } }), - put: record('put', script.put, { ok: true, value: item() }), - delete: record('delete', script.delete, { ok: true, value: { absent: true } }), - }, - }, - } as unknown as ClientContext - return { ctx, calls } -} +beforeEach(() => { + mock = RemoteMock.create() + mock.remote.messageFeedback.list.mockResolvedValue(ok({ ok: true, value: { items: [] } })) + mock.remote.messageFeedback.put.mockResolvedValue(ok({ ok: true, value: item() })) + mock.remote.messageFeedback.delete.mockResolvedValue(ok({ ok: true, value: { absent: true } })) +}) describe('MessageFeedbackController', () => { it('seeds the view from one list read and keys items by message id', async () => { const seeded = item({ note: 'good' }) - const { ctx, calls } = fakeRemote({ - list: () => Promise.resolve({ ok: true, value: { items: [seeded] } }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ ok: true, value: { items: [seeded] } })) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) expect(controller.getSnapshot().status).toBe('cold') expect(await controller.ensure()).toEqual({ ok: true }) @@ -81,73 +52,68 @@ describe('MessageFeedbackController', () => { const view = controller.getSnapshot() expect(view.status).toBe('ready') expect(view.items.get(MSG)).toEqual(seeded) - expect(calls).toEqual([{ method: 'list', request: { sessionId: SESSION } }]) + expect(mock.remote.messageFeedback.list).toHaveBeenCalledExactlyOnceWith({ sessionId: SESSION }) }) it('collapses concurrent loads onto one in-flight read', async () => { - const { ctx, calls } = fakeRemote() - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await Promise.all([controller.ensure(), controller.ensure(), controller.refresh()]) - expect(calls.filter(call => call.method === 'list')).toHaveLength(1) + expect(mock.remote.messageFeedback.list).toHaveBeenCalledTimes(1) }) it('sends ifVersion null for a first rating and the observed version afterwards', async () => { const first = item({ version: version('v1') }) const second = item({ version: version('v2'), rating: 'negative' }) - const { ctx, calls } = fakeRemote({ - put: request => Promise.resolve({ - ok: true, - value: (request as { rating: string }).rating === 'positive' ? first : second, - }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.put.mockImplementation(request => Promise.resolve(ok({ + ok: true, + value: request.rating === 'positive' ? first : second, + }))) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) expect(await controller.rate(MSG, 'positive')).toEqual({ ok: true }) expect(await controller.rate(MSG, 'negative')).toEqual({ ok: true }) - const puts = calls.filter(call => call.method === 'put').map(call => call.request) + const puts = mock.remote.messageFeedback.put.mock.calls.map(([request]) => request) expect(puts[0]).toMatchObject({ messageId: MSG, rating: 'positive', ifVersion: null }) expect(puts[1]).toMatchObject({ messageId: MSG, rating: 'negative', ifVersion: version('v1') }) expect(controller.getSnapshot().items.get(MSG)).toEqual(second) }) it('forwards the entry as note and category and omits absent members', async () => { - const { ctx, calls } = fakeRemote() - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.rate(MSG, 'negative', { text: 'helpful', category: 'task-result' }) await controller.rate(OTHER, 'negative') - const puts = calls.filter(call => call.method === 'put').map(call => call.request as Record) + const puts = mock.remote.messageFeedback.put.mock.calls.map(([request]) => request) expect(puts[0]).toMatchObject({ note: 'helpful', category: 'task-result' }) expect(puts[1]).not.toHaveProperty('note') expect(puts[1]).not.toHaveProperty('category') }) it('retract deletes a matching rating and ignores an opposite judgment', async () => { - const { ctx, calls } = fakeRemote({ - list: () => Promise.resolve({ ok: true, value: { items: [item({ note: 'stored', category: 'other' })] } }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ + ok: true, value: { items: [item({ note: 'stored', category: 'other' })] }, + })) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) expect(await controller.retract(MSG, 'negative')).toEqual({ ok: true }) expect(await controller.retract(MSG, 'positive')).toEqual({ ok: true }) - expect(calls.filter(call => call.method === 'put')).toHaveLength(0) - expect(calls.filter(call => call.method === 'delete')[0]?.request) - .toEqual({ sessionId: SESSION, messageId: MSG, ifVersion: version('v1') }) + expect(mock.remote.messageFeedback.put).not.toHaveBeenCalled() + expect(mock.remote.messageFeedback.delete).toHaveBeenCalledExactlyOnceWith({ + sessionId: SESSION, messageId: MSG, ifVersion: version('v1'), + }) }) it('never turns a queued stale retraction into a bare rating put', async () => { const existing = item({ rating: 'positive', version: version('v1') }) const replacement = item({ rating: 'negative', version: version('v2') }) - const { ctx, calls } = fakeRemote({ - list: () => Promise.resolve({ ok: true, value: { items: [existing] } }), - put: () => Promise.resolve({ ok: true, value: replacement }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ ok: true, value: { items: [existing] } })) + mock.remote.messageFeedback.put.mockResolvedValueOnce(ok({ ok: true, value: replacement })) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.ensure() const replaced = controller.rate(MSG, 'negative') @@ -155,20 +121,18 @@ describe('MessageFeedbackController', () => { await expect(replaced).resolves.toEqual({ ok: true }) await expect(staleRetraction).resolves.toEqual({ ok: true }) - expect(calls.filter(call => call.method === 'put')).toHaveLength(1) - expect(calls.filter(call => call.method === 'delete')).toHaveLength(0) + expect(mock.remote.messageFeedback.put).toHaveBeenCalledTimes(1) + expect(mock.remote.messageFeedback.delete).not.toHaveBeenCalled() expect(controller.getSnapshot().items.get(MSG)).toEqual(replacement) }) it('reconciles a version conflict from the authoritative item without refetching', async () => { const authoritative = item({ version: version('v9'), rating: 'negative', note: 'changed elsewhere' }) - const { ctx, calls } = fakeRemote({ - put: () => Promise.resolve({ - ok: false, - error: { code: 'version-conflict', current: authoritative }, - }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.put.mockResolvedValueOnce(ok({ + ok: false, + error: { code: 'version-conflict', current: authoritative }, + })) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) expect(await controller.rate(MSG, 'positive')).toEqual({ ok: false, @@ -176,18 +140,16 @@ describe('MessageFeedbackController', () => { }) expect(controller.getSnapshot().items.get(MSG)).toEqual(authoritative) - expect(calls.filter(call => call.method === 'list')).toHaveLength(1) + expect(mock.remote.messageFeedback.list).toHaveBeenCalledTimes(1) }) it('drops the local item when a conflict reports the feedback is gone', async () => { - const { ctx } = fakeRemote({ - list: () => Promise.resolve({ ok: true, value: { items: [item()] } }), - delete: () => Promise.resolve({ - ok: false, - error: { code: 'version-conflict', current: null }, - }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ ok: true, value: { items: [item()] } })) + mock.remote.messageFeedback.delete.mockResolvedValueOnce(ok({ + ok: false, + error: { code: 'version-conflict', current: null }, + })) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.ensure() expect(await controller.retract(MSG, 'positive')).toMatchObject({ ok: false, error: { code: 'version-conflict' } }) @@ -195,16 +157,17 @@ describe('MessageFeedbackController', () => { }) it('deletes with the observed version and removes the item on success', async () => { - const { ctx, calls } = fakeRemote({ - list: () => Promise.resolve({ ok: true, value: { items: [item({ version: version('v7') })] } }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ + ok: true, value: { items: [item({ version: version('v7') })] }, + })) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.ensure() expect(await controller.retract(MSG, 'positive')).toEqual({ ok: true }) - expect(calls.filter(call => call.method === 'delete')[0]?.request) - .toEqual({ sessionId: SESSION, messageId: MSG, ifVersion: version('v7') }) + expect(mock.remote.messageFeedback.delete).toHaveBeenCalledExactlyOnceWith({ + sessionId: SESSION, messageId: MSG, ifVersion: version('v7'), + }) expect(controller.getSnapshot().items.has(MSG)).toBe(false) }) @@ -213,32 +176,30 @@ describe('MessageFeedbackController', () => { let overlapped = false const versions = [version('v1'), version('v2')] let index = 0 - const { ctx, calls } = fakeRemote({ - put: async () => { - inFlight += 1 - if (inFlight > 1) overlapped = true - await Promise.resolve() - inFlight -= 1 - const next = versions[index] ?? version('vN') - index += 1 - return { ok: true, value: item({ version: next }) } - }, + mock.remote.messageFeedback.put.mockImplementation(async () => { + inFlight += 1 + if (inFlight > 1) overlapped = true + await Promise.resolve() + inFlight -= 1 + const next = versions[index] ?? version('vN') + index += 1 + return ok({ ok: true, value: item({ version: next }) }) }) - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await Promise.all([controller.rate(MSG, 'positive'), controller.rate(MSG, 'negative')]) expect(overlapped).toBe(false) - const puts = calls.filter(call => call.method === 'put').map(call => call.request as Record) + const puts = mock.remote.messageFeedback.put.mock.calls.map(([request]) => request) expect(puts[0]?.ifVersion).toBeNull() expect(puts[1]?.ifVersion).toBe(version('v1')) }) it('publishes an error status when the list read is rejected by the Host', async () => { - const { ctx } = fakeRemote({ - list: () => Promise.resolve({ ok: false, error: { code: 'session-not-found', sessionId: SESSION } }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ + ok: false, error: { code: 'session-not-found', sessionId: SESSION }, + })) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) expect(await controller.ensure()).toMatchObject({ ok: false, error: { code: 'session-not-found' } }) expect(controller.getSnapshot()).toMatchObject({ @@ -248,8 +209,7 @@ describe('MessageFeedbackController', () => { }) it('notifies subscribers on publication and stops after unsubscribe', async () => { - const { ctx } = fakeRemote() - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) const listener = vi.fn() const unsubscribe = controller.subscribe(listener) @@ -263,8 +223,7 @@ describe('MessageFeedbackController', () => { }) it('contains a throwing subscriber at the observable boundary', async () => { - const { ctx } = fakeRemote() - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) controller.subscribe(() => { throw new Error('subscriber exploded') }) const healthy = vi.fn() @@ -278,17 +237,16 @@ describe('MessageFeedbackController', () => { }) it('refuses mutations and stops publishing once disposed', async () => { - const { ctx, calls } = fakeRemote() - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.ensure() const listener = vi.fn() controller.subscribe(listener) controller.dispose() - const before = calls.length + const puts = mock.remote.messageFeedback.put.mock.calls.length expect(await controller.rate(MSG, 'positive')).toMatchObject({ ok: false, error: { code: 'disposed' } }) - expect(calls).toHaveLength(before) + expect(mock.remote.messageFeedback.put).toHaveBeenCalledTimes(puts) expect(listener).not.toHaveBeenCalled() }) @@ -300,20 +258,20 @@ describe('MessageFeedbackController', () => { ['note-too-large', 'the note is too long'], ] as const for (const [code, message] of codes) { - const { ctx } = fakeRemote({ - list: () => Promise.resolve({ ok: false, error: { code, sessionId: SESSION } } as never), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ + ok: false, error: { code, sessionId: SESSION }, + } as never)) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) expect(await controller.ensure()).toMatchObject({ ok: false, error: { code } }) expect(controller.getSnapshot().error).toBe(message) } }) it('falls back to the raw code for an unrecognized failure', async () => { - const { ctx } = fakeRemote({ - list: () => Promise.resolve({ ok: false, error: { code: 'brand-new-code' } } as never), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ + ok: false, error: { code: 'brand-new-code' }, + } as never)) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) expect(await controller.ensure()).toMatchObject({ ok: false, error: { code: 'brand-new-code' } }) expect(controller.getSnapshot().error).toBe('brand-new-code') @@ -322,13 +280,11 @@ describe('MessageFeedbackController', () => { it('publishes nothing when the list settles after disposal', async () => { let release = (): void => {} const gate = new Promise((resolve) => { release = resolve }) - const { ctx } = fakeRemote({ - list: async () => { - await gate - return { ok: true, value: { items: [item()] } } - }, + mock.remote.messageFeedback.list.mockImplementationOnce(async () => { + await gate + return ok({ ok: true, value: { items: [item()] } }) }) - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) const pending = controller.ensure() const listener = vi.fn() controller.subscribe(listener) @@ -342,30 +298,28 @@ describe('MessageFeedbackController', () => { }) it('propagates a failed load to a queued mutation without calling the wire', async () => { - const { ctx, calls } = fakeRemote({ - list: () => Promise.resolve({ ok: false, error: { code: 'session-not-found', sessionId: SESSION } }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ + ok: false, error: { code: 'session-not-found', sessionId: SESSION }, + })) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) expect(await controller.rate(MSG, 'positive')).toMatchObject({ ok: false, error: { code: 'session-not-found' }, }) - expect(calls.filter(call => call.method === 'put')).toHaveLength(0) + expect(mock.remote.messageFeedback.put).not.toHaveBeenCalled() }) it('keeps a later mutation running after an earlier one settles as a failure', async () => { let first = true - const { ctx } = fakeRemote({ - put: () => { - if (first) { - first = false - return Promise.resolve({ ok: false, error: new RemoteError('gateway/internal', 'first blew up', {}) }) - } - return Promise.resolve({ ok: true, value: item({ rating: 'negative' }) }) - }, + mock.remote.messageFeedback.put.mockImplementation(() => { + if (first) { + first = false + return Promise.resolve({ ok: false as const, error: new RemoteError('gateway/internal', 'first blew up', {}) }) + } + return Promise.resolve(ok({ ok: true, value: item({ rating: 'negative' }) })) }) - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) const [a, b] = await Promise.all([ controller.rate(MSG, 'positive'), @@ -383,14 +337,17 @@ describe('MessageFeedbackController', () => { // fiber unloads, and its authoritative item must not be published. let release = (): void => {} const gate = new Promise((resolve) => { release = resolve }) - const { ctx } = fakeRemote({ - list: () => Promise.resolve({ ok: true, value: { items: [item({ version: version('v1') })] } }), - put: async () => { - await gate - return { ok: false, error: { code: 'version-conflict', current: item({ version: version('v2'), rating: 'negative' }) } } - }, + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ + ok: true, value: { items: [item({ version: version('v1') })] }, + })) + mock.remote.messageFeedback.put.mockImplementationOnce(async () => { + await gate + return ok({ + ok: false, + error: { code: 'version-conflict', current: item({ version: version('v2'), rating: 'negative' }) }, + }) }) - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.ensure() const listener = vi.fn() controller.subscribe(listener) @@ -407,14 +364,12 @@ describe('MessageFeedbackController', () => { it('drops a delete conflict reconciliation once disposed mid-flight', async () => { let release = (): void => {} const gate = new Promise((resolve) => { release = resolve }) - const { ctx } = fakeRemote({ - list: () => Promise.resolve({ ok: true, value: { items: [item()] } }), - delete: async () => { - await gate - return { ok: false, error: { code: 'version-conflict', current: null } } - }, + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ ok: true, value: { items: [item()] } })) + mock.remote.messageFeedback.delete.mockImplementationOnce(async () => { + await gate + return ok({ ok: false, error: { code: 'version-conflict', current: null } }) }) - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.ensure() const pending = controller.retract(MSG, 'positive') @@ -430,11 +385,11 @@ describe('MessageFeedbackController', () => { it('leaves the local item untouched when a rating fails for a non-conflict reason', async () => { const existing = item({ version: version('v3'), rating: 'positive' }) - const { ctx } = fakeRemote({ - list: () => Promise.resolve({ ok: true, value: { items: [existing] } }), - put: () => Promise.resolve({ ok: false, error: { code: 'note-too-large', maxBytes: 8, actualBytes: 9 } }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ ok: true, value: { items: [existing] } })) + mock.remote.messageFeedback.put.mockResolvedValueOnce(ok({ + ok: false, error: { code: 'note-too-large', maxBytes: 8, actualBytes: 9 }, + })) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.ensure() expect(await controller.rate(MSG, 'negative', { text: 'far too long' })).toMatchObject({ @@ -446,11 +401,11 @@ describe('MessageFeedbackController', () => { it('leaves the local item untouched when a delete fails for a non-conflict reason', async () => { const existing = item({ version: version('v4') }) - const { ctx } = fakeRemote({ - list: () => Promise.resolve({ ok: true, value: { items: [existing] } }), - delete: () => Promise.resolve({ ok: false, error: { code: 'session-not-found', sessionId: SESSION } }), - }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ ok: true, value: { items: [existing] } })) + mock.remote.messageFeedback.delete.mockResolvedValueOnce(ok({ + ok: false, error: { code: 'session-not-found', sessionId: SESSION }, + })) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.ensure() expect(await controller.retract(MSG, 'positive')).toMatchObject({ @@ -466,19 +421,17 @@ describe('MessageFeedbackController', () => { const order: string[] = [] let releasePut = (): void => {} const putGate = new Promise((r) => { releasePut = r }) - const { ctx } = fakeRemote({ - list: () => { - order.push('list') - return Promise.resolve({ ok: true, value: { items: [item({ version: version('v1') })] } }) - }, - put: async () => { - order.push('put:start') - await putGate - order.push('put:end') - return { ok: true, value: item({ version: version('v9'), rating: 'negative' }) } - }, + mock.remote.messageFeedback.list.mockImplementation(() => { + order.push('list') + return Promise.resolve(ok({ ok: true, value: { items: [item({ version: version('v1') })] } })) }) - const controller = new MessageFeedbackController(ctx, SESSION) + mock.remote.messageFeedback.put.mockImplementationOnce(async () => { + order.push('put:start') + await putGate + order.push('put:end') + return ok({ ok: true, value: item({ version: version('v9'), rating: 'negative' }) }) + }) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.ensure() const rating = controller.rate(MSG, 'negative') @@ -498,14 +451,12 @@ describe('MessageFeedbackController', () => { const gate = new Promise((r) => { release = r }) let started = (): void => {} const listStarted = new Promise((r) => { started = r }) - const { ctx, calls } = fakeRemote({ - list: async () => { - started() - await gate - return { ok: true, value: { items: [] } } - }, + mock.remote.messageFeedback.list.mockImplementationOnce(async () => { + started() + await gate + return ok({ ok: true, value: { items: [] } }) }) - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) const pending = controller.rate(MSG, 'positive') await listStarted @@ -513,19 +464,17 @@ describe('MessageFeedbackController', () => { release() expect(await pending).toMatchObject({ ok: false, error: { code: 'disposed' } }) - expect(calls.filter(c => c.method === 'put')).toHaveLength(0) + expect(mock.remote.messageFeedback.put).not.toHaveBeenCalled() }) it('renders a carrier failure from the Remote envelope', async () => { // The generated face folds transport faults into ok:false with a // RemoteFailure, so the controller reads them as values, not rejections. - const { ctx } = fakeRemote({ - list: () => Promise.resolve({ - ok: false, - error: new RemoteError('gateway/internal', 'socket closed', {}), - }), + mock.remote.messageFeedback.list.mockResolvedValueOnce({ + ok: false, + error: new RemoteError('gateway/internal', 'socket closed', {}), }) - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) expect(await controller.ensure()).toEqual({ ok: false, @@ -535,13 +484,11 @@ describe('MessageFeedbackController', () => { }) it('renders a carrier failure on a mutation without touching the view', async () => { - const { ctx } = fakeRemote({ - put: () => Promise.resolve({ - ok: false, - error: new RemoteError('gateway/internal', 'socket closed', {}), - }), + mock.remote.messageFeedback.put.mockResolvedValueOnce({ + ok: false, + error: new RemoteError('gateway/internal', 'socket closed', {}), }) - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) expect(await controller.rate(MSG, 'positive')).toEqual({ ok: false, @@ -551,14 +498,12 @@ describe('MessageFeedbackController', () => { }) it('renders a carrier failure on a delete', async () => { - const { ctx } = fakeRemote({ - list: () => Promise.resolve({ ok: true, value: { items: [item()] } }), - delete: () => Promise.resolve({ - ok: false, - error: new RemoteError('gateway/internal', 'socket closed', {}), - }), + mock.remote.messageFeedback.list.mockResolvedValueOnce(ok({ ok: true, value: { items: [item()] } })) + mock.remote.messageFeedback.delete.mockResolvedValueOnce({ + ok: false, + error: new RemoteError('gateway/internal', 'socket closed', {}), }) - const controller = new MessageFeedbackController(ctx, SESSION) + const controller = new MessageFeedbackController(mock.remote.messageFeedback, SESSION) await controller.ensure() expect(await controller.retract(MSG, 'positive')).toMatchObject({ ok: false, error: { code: 'gateway/internal' } }) diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 2e24bbc618..ff5374fea9 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: d22fa2eed35457c0599fd41473102b1aebe4bd2e -README.zh.md: b53cf76dc8b8a253da83d1e94b6f9f1b5abf19cc +README.md: 07e61be1bb993716c85273aac8ee6e650d221365 +README.zh.md: 194d8e994d82ae167ce2c7af06d9b199ef066b27 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index d22fa2eed3..07e61be1bb 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -52,7 +52,7 @@ Check this table before writing a control in a feature package. A plugin cannot | `HoverCard` | Hover preview the pointer can rest on and select from; optional copy button. | | `Toast` | Transient top-center banner held for the owner's `holdMs`. | | `JsonTree`, `JsonBlock` | Read-only JSON inspection. | -| `MarkdownText`, `CodeBlock` | Untrusted GFM with TeX math, and highlighted code. `CodeBlock` accepts opt-in `lineNumbers`; copied source excludes the gutter, and `contentRef` exposes its stable source wrapper to an owner that uses it as a scrollport. | +| `MarkdownText`, `CodeBlock` | Untrusted GFM with TeX math, and highlighted code. `CodeBlock` accepts opt-in `lineNumbers`; copied source excludes the gutter, and `contentRef` exposes its stable source wrapper to an owner that uses it as a scrollport. Set `showHeader={false}` when the owner supplies its own language and copy toolbar. | | `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, `WebBlock` | The agent-output card matching each tool-result intent. | | `icons/*`, `FishLogo`, `BrandWordmark`, `ReferenceIcon`, `LinkIcon` | Glyphs and brand marks. Use `LinkIcon` for 14px clickable-link categories. | | `FileTypeIcon`, `classifyFileType`, `fileExtension` | A category-colored 28px file or folder glyph and the shared case-insensitive filename mapping behind it. Code and configuration files use detailed full-color technology glyphs; use `LinkIcon` for link-leading glyphs and image previews for image content. | @@ -74,6 +74,8 @@ The catalog above lists what each export is for; this section covers the behavio `MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `projectUserText` projects sent user text into inline plain runs and reference chips for the message bubble and queue rows. When supplied with `UserTextReferences`, file and skill references become keyboard-accessible preview buttons using the same hover and focus styling as prose file links; the first pointer click can open a preview, while subsequent clicks and existing text selections retain native selection handling. Keyboard activation opens previews even when text is selected. +`JsonTree` clamps collapsed strings to `collapsedStringLines` (three by default). Expanded strings show raw text, retain sibling commas, and fit within the window and outer scrolling containers. Resize and ancestor-scroll events update that limit. Row copy feedback updates independently of JSON value rendering; pending clipboard writes cannot update a different row or an unmounted tree. + ### Localizing copy The atoms cannot read the application locale, so every piece of user-facing copy arrives through required label props. `HoverCard`, `TerminalBlock`, `JsonTree`, `CodeBlock`, `MarkdownText`, `JsonBlock`, `ConnectionIndicator`, `Modal`, `DiffBlock`, `ReadBlock`, `SearchBlock`, and `WebBlock` accept complete localized labels. The package owns no language fallback; omission fails typechecking, and each feature maps its typed `t` seat into the primitive's label interface. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index b53cf76dc8..194d8e994d 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -52,7 +52,7 @@ kind: "package-library" | `HoverCard` | 指针可停留、可选中的悬停预览;可选带复制按钮。 | | `Toast` | 顶部居中的瞬时横幅,保持时长由所有者的 `holdMs` 决定。 | | `JsonTree`、`JsonBlock` | 只读 JSON 查看。 | -| `MarkdownText`、`CodeBlock` | 不可信 GFM 与 TeX 数学,以及高亮代码。`CodeBlock` 可通过 `lineNumbers` 开启行号;复制的源码不含行号栏,`contentRef` 则向需要把稳定源码包装节点用作滚动区的 owner 提供该节点。 | +| `MarkdownText`、`CodeBlock` | 不可信 GFM 与 TeX 数学,以及高亮代码。`CodeBlock` 可通过 `lineNumbers` 开启行号;复制的源码不含行号栏,`contentRef` 则向需要把稳定源码包装节点用作滚动区的 owner 提供该节点。调用方提供自己的语言与复制工具栏时,设置 `showHeader={false}`。 | | `TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock`、`WebBlock` | 与各类工具结果意图对应的 agent 输出卡片。 | | `icons/*`、`FishLogo`、`BrandWordmark`、`ReferenceIcon`、`LinkIcon` | 字形与品牌标识。`LinkIcon` 用于 14px 的可点击链接分类。 | | `FileTypeIcon`、`classifyFileType`、`fileExtension` | 按类别着色的 28px 文件或文件夹图形,以及它背后共享的不区分大小写文件名映射。代码与配置文件使用细分的全彩技术图形;链接前置图形使用 `LinkIcon`,图片内容使用图片预览。 | @@ -74,6 +74,8 @@ kind: "package-library" `MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`projectUserText` 把已发送的用户文本投影为行内普通文本段与引用 chip,供消息气泡和排队行使用。 传入 `UserTextReferences` 时,文件和 skill 引用成为支持键盘操作的预览按钮,复用正文文件链接的悬停和聚焦样式;第一次指针点击可以打开预览,后续点击和已有选区保留原生选择行为。键盘激活在存在选区时仍可打开预览。 +`JsonTree` 把折叠字符串限制为 `collapsedStringLines` 行(默认三行)。展开后显示原始文本、保留同级逗号,并限制在窗口与外层滚动容器内;尺寸变化和祖先滚动事件会更新此限制。行复制反馈独立于 JSON 值渲染更新;尚未完成的剪贴板写入不会更新另一行或已卸载的树。 + ### 本地化文案 这些原子组件无法读取应用 locale,因此每段面向用户的文案都必须通过 label prop 提供。`HoverCard`、`TerminalBlock`、`JsonTree`、`CodeBlock`、`MarkdownText`、`JsonBlock`、`ConnectionIndicator`、`Modal`、`DiffBlock`、`ReadBlock`、`SearchBlock` 与 `WebBlock` 接收完整的本地化 label。本包不拥有语言回退;遗漏会导致类型检查失败,各功能会把带类型的 `t` 席位映射到 primitive 的 label 接口。 diff --git a/packages/client/ui-primitives/src/JsonTree.module.css b/packages/client/ui-primitives/src/JsonTree.module.css index 33ec3f30d3..9abbc23bdc 100644 --- a/packages/client/ui-primitives/src/JsonTree.module.css +++ b/packages/client/ui-primitives/src/JsonTree.module.css @@ -29,17 +29,17 @@ .container { box-sizing: border-box; - width: max-content; - min-width: 100%; + width: 100%; + min-width: 0; margin: 0; - padding: 6px 8px 8px; + padding: 6px 8px 8px 8px; white-space: pre; } .expandedTopLevel { box-sizing: border-box; - width: max-content; - min-width: 100%; + width: 100%; + min-width: 0; padding: 6px 8px 8px 14px; } @@ -66,11 +66,13 @@ .row { position: relative; box-sizing: border-box; - min-width: 100%; + min-width: 0; min-height: 16px; margin: 0; padding: 0 0 0 10px; list-style: none; + overflow-wrap: anywhere; + white-space: pre-wrap; } .row:not(.topLevelBracket):hover:not(:has(.row:hover))::after, @@ -81,13 +83,13 @@ top: 0; right: 0; left: 0; - height: 16px; + height: var(--json-tree-row-height, 16px); background: var(--json-tree-hover); content: ''; pointer-events: none; } -.row > span:not(.expander) { +.row > span:not(.expander, .copySlot) { position: relative; z-index: 1; } @@ -106,6 +108,100 @@ color: var(--json-tree-string); } +.stringField { + position: relative; + z-index: 1; + display: flow-root; +} + +.stringText { + display: block; + max-height: calc(var(--json-tree-collapsed-lines) * 16px); + padding-left: 12px; + overflow: hidden; + text-indent: -12px; + word-break: break-all; +} + +.stringRaw { + margin: 4px 0 0 12px; + overflow: auto; + color: var(--dsw-alias-label-primary); + font: inherit; + overflow-wrap: normal; + white-space: pre; + word-break: normal; +} + +.stringActions { + position: absolute; + top: 1px; + right: 4px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + width: fit-content; + height: 16px; +} + +.stringRaw[data-wrap='true'] { + overflow-wrap: anywhere; + white-space: pre-wrap; + word-break: break-all; +} + +.stringActions > .copySlot { + opacity: 1; + pointer-events: auto; +} + +.stringToggleSlot { + display: flex; + float: right; + align-items: flex-end; + height: calc(var(--json-tree-collapsed-lines) * 16px); + margin-left: 2px; + shape-outside: inset(calc((var(--json-tree-collapsed-lines) - 1) * 16px) 0 0); + pointer-events: none; +} + +.stringToggle { + display: inline-flex; + align-items: baseline; + gap: 2px; + margin: 0; + padding: 0; + border: 0; + color: var(--dsw-alias-label-secondary); + background: transparent; + font: inherit; + text-indent: 0; + white-space: nowrap; + cursor: pointer; + pointer-events: auto; +} + +.stringToggle:hover { + color: var(--dsw-alias-label-primary); +} + +.stringToggle:focus-visible { + outline: 1px solid var(--dsw-alias-state-business-primary); + outline-offset: 1px; +} + +.row:has(> .stringField) { + --json-tree-row-height: 100%; +} + +.summary { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .numberValue { color: var(--json-tree-number); } @@ -134,13 +230,38 @@ color: var(--dsw-alias-label-tertiary); } -.copyAnchor { - position: fixed; - z-index: 3; +.copySlot { display: inline-flex; + width: 20px; + height: 16px; + opacity: 0; + pointer-events: none; } -.copyButton { +.row > .copySlot { + position: absolute; + z-index: 2; + top: 0; + right: 0; +} + +.row:has(> .stringField) > .copySlot { + top: auto; + bottom: 0; +} + +.row:has(> .copySlot) > .stringField .stringToggleSlot { + padding-right: 24px; +} + +.row:hover:not(:has(.row:hover)) > .copySlot, +.row[data-json-copy-active] > .copySlot, +.copySlot:focus-within { + opacity: 1; + pointer-events: auto; +} + +.actionButton { box-sizing: border-box; display: inline-flex; align-items: center; @@ -152,22 +273,24 @@ border: 0; border-radius: 3px; color: var(--dsw-alias-label-secondary); - background: var(--dsw-alias-bg-layer-1); - box-shadow: -5px 0 5px var(--dsw-alias-bg-layer-1); + background: transparent; cursor: pointer; } -.copyButton:hover { +.actionButton:hover { color: var(--dsw-alias-label-primary); - background: var(--dsw-alias-interactive-bg-hover); } -.copyButton:focus-visible { +.actionButton[aria-pressed='true'] { + color: var(--dsw-alias-state-business-primary); +} + +.actionButton:focus-visible { outline: 1px solid var(--dsw-alias-state-business-primary); outline-offset: -1px; } -.copyButton[data-state='failed'] { +.actionButton[data-state='failed'] { color: var(--dsw-alias-state-error-primary); } diff --git a/packages/client/ui-primitives/src/JsonTree.tsx b/packages/client/ui-primitives/src/JsonTree.tsx index 8399f3681e..7a3f2e8b7f 100644 --- a/packages/client/ui-primitives/src/JsonTree.tsx +++ b/packages/client/ui-primitives/src/JsonTree.tsx @@ -1,12 +1,12 @@ import clsx from 'clsx' -import { useEffect, useId, useRef, useState } from 'react' +import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react' import type { + CSSProperties, KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent, ReactNode, - UIEvent as ReactUIEvent, } from 'react' -import { IconCheckOutline16, IconCopyOutline16 } from './icons/index.tsx' +import { IconCheckOutline16, IconCopyOutline16, IconWrapLinesOutline16 } from './icons/index.tsx' import { Menu } from './Menu.tsx' import type { MenuEntry } from './Menu.tsx' import css from './JsonTree.module.css' @@ -65,10 +65,103 @@ interface RowTarget { value: unknown } -interface CopyTarget extends RowTarget { - left: number - side: 'bottom' | 'top' - top: number +type CopyMode = 'json' | 'path' | 'prettyJson' | 'value' + +interface CopySnapshot { + id: string + target: RowTarget + state: 'idle' | 'copied' | 'failed' + menuOpen: boolean +} + +/** Notify only the old and new row actions; JSON values do not subscribe to hover state. */ +function createCopyStore() { + let current: CopySnapshot | undefined + const listeners = new Map void>>() + return { + get: () => current, + set(next: CopySnapshot | undefined) { + const previous = current?.id + current = next + for (const id of new Set([previous, next?.id])) { + if (id === undefined) continue + for (const listener of listeners.get(id) ?? []) listener() + } + }, + subscribe(id: string, listener: () => void) { + let row = listeners.get(id) + if (row === undefined) listeners.set(id, row = new Set()) + row.add(listener) + return () => { + row.delete(listener) + if (row.size === 0) listeners.delete(id) + } + }, + } +} + +function JsonCopyAction({ store, target, persistent, labels, onCopy, onClose }: { + store: ReturnType + target: RowTarget + persistent: boolean + labels: JsonTreeLabels + onCopy: (target: RowTarget, mode: CopyMode) => Promise + onClose: () => void +}) { + const id = pathId(target.path) + const subscribe = useCallback((listener: () => void) => store.subscribe(id, listener), [id, store]) + const getSnapshot = () => { + const current = store.get() + return current?.id === id ? current : undefined + } + const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot) + const buttonRef = useRef(null) + const state = snapshot?.state ?? 'idle' + const object = typeof target.value === 'object' && target.value !== null + const copyTitle = state === 'copied' + ? labels.copied + : state === 'failed' + ? labels.copyFailed + : object ? labels.copyPrettyJson : labels.copyValue + return ( + + {(persistent || snapshot !== undefined) && ( + void onCopy(target, object ? 'prettyJson' : 'value')} + onContextMenu={(event) => { + event.preventDefault() + event.stopPropagation() + store.set({ id, target, state, menuOpen: true }) + }} + > + {state === 'copied' + ? + : } + + )} + items={object ? objectCopyMenuItems(labels) : valueCopyMenuItems(labels)} + onSelect={(mode) => { + void onCopy(target, mode as CopyMode) + }} + onClose={onClose} + getAnchorRect={() => (buttonRef.current as HTMLButtonElement).getBoundingClientRect()} + /> + )} + + ) } function isExpandableValue(value: unknown): value is object | unknown[] { @@ -224,6 +317,8 @@ function NodeField({ } interface JsonTreeNodeProps { + collapsedStringLines: number + stringWrapping: JsonTreeProps['stringWrapping'] field?: string initialExpanded: boolean labels: JsonTreeLabels @@ -231,11 +326,180 @@ interface JsonTreeNodeProps { onClaimTabStop: (id: string) => void onRowHover: (row: HTMLElement, target: RowTarget) => void path: JsonPath + renderCopy: ((target: RowTarget, persistent?: boolean) => ReactNode) | undefined tabStopId: string | null value: unknown } +function JsonString({ + collapsedStringLines, + stringWrapping, + field, + labels, + lastElement, + renderCopy, + value, +}: { + collapsedStringLines: number + stringWrapping: JsonTreeProps['stringWrapping'] + field: string | undefined + labels: JsonTreeLabels + lastElement: boolean + renderCopy: ((persistent?: boolean) => ReactNode) | undefined + value: string +}) { + const contentsId = useId() + const contentRef = useRef(null) + const rawRef = useRef(null) + const [expanded, setExpanded] = useState(false) + const [wrapped, setWrapped] = useState(false) + const [truncated, setTruncated] = useState(false) + + useLayoutEffect(() => { + if (expanded) return + const content = contentRef.current as HTMLSpanElement + const measure = () => { + const lineHeight = Number.parseFloat(getComputedStyle(content).lineHeight) + setTruncated(content.scrollHeight > lineHeight * collapsedStringLines) + } + measure() + if (typeof ResizeObserver === 'undefined') return + const observer = new ResizeObserver(measure) + observer.observe(content) + return () => { observer.disconnect() } + }, [collapsedStringLines, expanded, field, lastElement, value]) + + useLayoutEffect(() => { + if (!expanded) return + const raw = rawRef.current as HTMLPreElement + // Keep raw text within the window and clipping ancestors outside the tree. + // Capture scrolling because an ancestor can move the string without resizing it. + const clips: HTMLElement[] = [] + const tree = raw.closest(`.${css.root}`) as HTMLElement + for (let parent = tree.parentElement; parent !== null; parent = parent.parentElement) { + if (/auto|scroll|hidden|clip/.test(getComputedStyle(parent).overflowY)) clips.push(parent) + } + const measure = () => { + let top = 0 + let bottom = window.innerHeight + for (const clip of clips) { + const rect = clip.getBoundingClientRect() + const style = getComputedStyle(clip) + top = Math.max(top, rect.top + clip.clientTop) + bottom = Math.min(bottom, rect.top + clip.clientTop + clip.clientHeight + - Number.parseFloat(style.paddingBottom)) + } + const available = bottom - Math.max(top, raw.getBoundingClientRect().top) + raw.style.maxHeight = `${Math.max(16, available - 4)}px` + } + measure() + const observer = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(measure) + observer?.observe(raw) + for (const clip of clips) observer?.observe(clip) + window.addEventListener('resize', measure) + window.addEventListener('scroll', measure, true) + return () => { + observer?.disconnect() + window.removeEventListener('resize', measure) + window.removeEventListener('scroll', measure, true) + } + }, [expanded, value]) + + if (expanded) { + const fieldId = `${contentsId}-field` + return ( +
+ {field !== undefined && {fieldText(field)}:} +
+          {value}
+        
+ {!lastElement && ,} +
+ {stringWrapping !== undefined && ( + + )} + + {renderCopy?.(true)} +
+
+ ) + } + + return ( + <> + {renderCopy?.()} + + + {truncated && ( + + + + )} + {field !== undefined && {fieldText(field)}:} + {primitiveValue(value)} + {!lastElement && ,} + + + + ) +} + function JsonTreeNode({ + collapsedStringLines, + stringWrapping, field, initialExpanded, labels, @@ -243,6 +507,7 @@ function JsonTreeNode({ onClaimTabStop, onRowHover, path, + renderCopy, tabStopId, value, }: JsonTreeNodeProps) { @@ -281,10 +546,25 @@ function JsonTreeNode({ onRowHover(event.currentTarget, { path, value }) }} > + {typeof value !== 'string' && renderCopy?.({ path, value })} {children} ) + if (typeof value === 'string') { + return row( + renderCopy({ path, value }, persistent)} + />, + ) + } + if (!container) { return row(( <> @@ -322,14 +602,18 @@ function JsonTreeNode({ onClick={toggle} onKeyDown={onExpanderKeyDown} /> - - {previewValue(value, 0)} - {!lastElement && ,} + + + {previewValue(value, 0)} + {!lastElement && ,} + {expanded && (
    {entries.map(([key, item], index) => ( ))}
@@ -356,7 +641,7 @@ function formattedPath(path: JsonPath): string { }, '$') } -function copyText(target: CopyTarget, mode: 'json' | 'path' | 'prettyJson' | 'value'): string { +function copyText(target: RowTarget, mode: CopyMode): string { if (mode === 'path') return formattedPath(target.path) if (mode === 'prettyJson') return JSON.stringify(target.value, null, 2) if (mode === 'json') return JSON.stringify(target.value) @@ -376,6 +661,17 @@ export interface JsonTreeProps { label: string /** Optional positioning class owned by the caller. */ className?: string | undefined + /** Maximum visible lines per collapsed string; defaults to 3. */ + collapsedStringLines?: number + /** Optional wrap toggle; each expansion reads the shared default without changing other open strings. */ + stringWrapping?: { + /** Localized label for the wrapping toggle. */ + label: string + /** Read the wrapping preference when a string is expanded. @returns Whether to wrap long lines. */ + getDefault: () => boolean + /** Remember a user toggle for future expansions. @param wrapped - Whether to wrap long lines. */ + setDefault: (wrapped: boolean) => void + } | undefined /** Whether JSON rows expose copy actions. */ copyable?: boolean /** Whether the top-level object or array is always expanded. */ @@ -393,6 +689,8 @@ export function JsonTree({ data, label, className, + collapsedStringLines = 3, + stringWrapping, copyable = true, expandTopLevel = true, labels, @@ -407,14 +705,10 @@ export function JsonTree({ ? null : pathId([Array.isArray(data) ? firstExpandableIndex : firstExpandableEntry[0]]) : isExpandableValue(data) && rootEntries.length > 0 ? pathId([]) : null - const rootRef = useRef(null) const activeRowRef = useRef() - const copyButtonRef = useRef(null) - const copyMenuOpenRef = useRef(false) const resetTimer = useRef>() - const [copyTarget, setCopyTarget] = useState() - const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>('idle') - const [copyMenuOpen, setCopyMenuOpen] = useState(false) + const copySequence = useRef(0) + const [copyStore] = useState(createCopyStore) const [tabStopId, setTabStopId] = useState(initialTabStopId) const setActiveRow = (row: HTMLElement | undefined) => { @@ -424,121 +718,74 @@ export function JsonTree({ } const clearCopyTarget = () => { + copySequence.current += 1 + if (resetTimer.current !== undefined) clearTimeout(resetTimer.current) setActiveRow(undefined) - setCopyTarget(undefined) - setCopyState('idle') - copyMenuOpenRef.current = false - setCopyMenuOpen(false) - } - - const copyPosition = (row: HTMLElement): Pick => { - const root = rootRef.current - /* v8 ignore next -- row events and viewport listeners run only after the root ref mounts. */ - if (root === null) throw new Error('JsonTree root is not mounted') - const rootRect = root.getBoundingClientRect() - const rowRect = row.getBoundingClientRect() - return { - left: rootRect.left + root.clientWidth - 26, - side: rowRect.top - rootRect.top > root.clientHeight / 2 ? 'top' : 'bottom', - top: rowRect.top, - } - } - - const positionCopyButton = (row: HTMLElement, target: RowTarget) => { - const position = copyPosition(row) - setCopyTarget({ ...target, ...position }) - } - - const repositionCopyButton = (row: HTMLElement) => { - const position = copyPosition(row) - setCopyTarget((current) => { - /* v8 ignore next -- an active row and its copy target are installed together. */ - if (current === undefined) return current - return { ...current, ...position } - }) + copyStore.set(undefined) } useEffect(() => () => { + copySequence.current += 1 if (resetTimer.current !== undefined) clearTimeout(resetTimer.current) activeRowRef.current?.removeAttribute('data-json-copy-active') }, []) useEffect(() => { - activeRowRef.current?.removeAttribute('data-json-copy-active') - activeRowRef.current = undefined - copyMenuOpenRef.current = false - setCopyTarget(undefined) - setCopyState('idle') - setCopyMenuOpen(false) + clearCopyTarget() setTabStopId(initialTabStopId) }, [data, expandTopLevel, initialTabStopId]) - useEffect(() => { - const reposition = () => { - const row = activeRowRef.current - if (row !== undefined) repositionCopyButton(row) - } - window.addEventListener('scroll', reposition, true) - window.addEventListener('resize', reposition) - return () => { - window.removeEventListener('scroll', reposition, true) - window.removeEventListener('resize', reposition) - } - }, []) - const handleRowHover = (row: HTMLElement, target: RowTarget) => { - if (!copyable || copyMenuOpenRef.current) return + if (!copyable || copyStore.get()?.menuOpen) return if (activeRowRef.current === row) return setActiveRow(row) - setCopyState('idle') - copyMenuOpenRef.current = false - setCopyMenuOpen(false) - positionCopyButton(row, target) + copyStore.set({ id: pathId(target.path), target, state: 'idle', menuOpen: false }) } const handleRootMouseOver = (event: ReactMouseEvent) => { - if (!copyable || copyMenuOpenRef.current) return + if (!copyable || copyStore.get()?.menuOpen) return /* v8 ignore next -- browser mouse events delivered through React target an Element. */ if (!(event.target instanceof Element)) return if (event.target.closest('[data-json-copy-button]') === null) clearCopyTarget() } - const handleScroll = (_event: ReactUIEvent) => { - const row = activeRowRef.current - if (row !== undefined) repositionCopyButton(row) - } - - const copy = async (mode: 'json' | 'path' | 'prettyJson' | 'value') => { - /* v8 ignore next -- copy controls only render while their target exists. */ - if (copyTarget === undefined) return - try { - await navigator.clipboard.writeText(copyText(copyTarget, mode)) - setCopyState('copied') - } catch { - setCopyState('failed') + const copy = async (target: RowTarget, mode: CopyMode) => { + const sequence = ++copySequence.current + const snapshot: CopySnapshot = { + id: pathId(target.path), target, state: 'idle', menuOpen: false, } + copyStore.set(snapshot) + let state: CopySnapshot['state'] + try { + await navigator.clipboard.writeText(copyText(target, mode)) + state = 'copied' + } catch { + state = 'failed' + } + const current = copyStore.get() + if (sequence !== copySequence.current || current?.target !== target) return + copyStore.set({ ...current, state }) if (resetTimer.current !== undefined) clearTimeout(resetTimer.current) - resetTimer.current = setTimeout(() => { setCopyState('idle') }, 1_500) + resetTimer.current = setTimeout(() => { + const current = copyStore.get() + if (current?.target === target) copyStore.set({ ...current, state: 'idle' }) + }, 1_500) } const [rootOpen, rootClose] = bracketOf(data) - const copyTargetIsObject = typeof copyTarget?.value === 'object' && copyTarget.value !== null - const defaultCopyMode = copyTargetIsObject ? 'prettyJson' : 'value' - const copyTitle = copyState === 'copied' - ? labels.copied - : copyState === 'failed' - ? labels.copyFailed - : copyTargetIsObject ? labels.copyPrettyJson : labels.copyValue + const renderCopy = copyable ? (target: RowTarget, persistent = false) => ( + + ) : undefined return (
{ - if (!copyMenuOpenRef.current) clearCopyTarget() + if (!copyStore.get()?.menuOpen) clearCopyTarget() }} - onScroll={handleScroll} > {expandTopLevel ? ( @@ -551,6 +798,7 @@ export function JsonTree({ handleRowHover(event.currentTarget, { path: [], value: data }) }} > + {renderCopy?.({ path: [], value: data })} {rootOpen}
( ))}
@@ -581,6 +832,8 @@ export function JsonTree({ : (
)} - {copyTarget !== undefined && ( - - void copy(defaultCopyMode)} - onContextMenu={(event) => { - event.preventDefault() - event.stopPropagation() - copyMenuOpenRef.current = true - setCopyMenuOpen(true) - }} - > - {copyState === 'copied' - ? - : } - - )} - items={copyTargetIsObject ? objectCopyMenuItems(labels) : valueCopyMenuItems(labels)} - onSelect={(id) => { - void copy(id as 'json' | 'path' | 'prettyJson' | 'value') - copyMenuOpenRef.current = false - setCopyMenuOpen(false) - }} - onClose={clearCopyTarget} - getAnchorRect={() => ( - copyButtonRef.current as HTMLButtonElement - ).getBoundingClientRect()} - /> - - )} ) } diff --git a/packages/client/ui-primitives/src/icons/index.tsx b/packages/client/ui-primitives/src/icons/index.tsx index 7f1a3de926..a7bf608e47 100644 --- a/packages/client/ui-primitives/src/icons/index.tsx +++ b/packages/client/ui-primitives/src/icons/index.tsx @@ -938,6 +938,14 @@ export const IconArchiveOutline20 = ({ size = 20, className }: IconProps) => ( ) +/** Line-wrapping toggle glyph. */ +export const IconWrapLinesOutline16 = ({ size = 16, className }: IconProps) => ( + +) + /** * Plan row glyph of the composer menu (#3567): a document with three ruled * lines and a pencil over its corner. Drawn on a 14 grid whose content spans diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx index 4bd573d57e..14bfd6290b 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx @@ -29,6 +29,8 @@ export interface CodeBlockProps { contentRef?: Ref | undefined /** Show a numbered gutter without adding numbers to copied source. Defaults to false. */ lineNumbers?: boolean | undefined + /** Show the language and copy header; false when the caller supplies a toolbar. Defaults to true. */ + showHeader?: boolean | undefined /** Copy-button idle label; the owner passes localized copy (this package is cordis-free, so copy arrives via props). */ copyLabel: string /** Copy-button label during the post-copy confirmation window. */ @@ -61,7 +63,9 @@ function renderLine(line: readonly HighlightSpan[], index: number): ReactNode { ) } -export function CodeBlock({ code, lang, streaming, className, contentRef, lineNumbers = false, copyLabel, copiedLabel }: CodeBlockProps) { +export function CodeBlock({ + code, lang, streaming, className, contentRef, lineNumbers = false, showHeader = true, copyLabel, copiedLabel, +}: CodeBlockProps) { const trimmed = code.endsWith('\n') ? code.slice(0, -1) : code const sourceLines = lineNumbers ? trimmed.split('\n') : undefined const rootRef = useRef(null) @@ -180,7 +184,7 @@ export function CodeBlock({ code, lang, streaming, className, contentRef, lineNu '--dsl-code-block-line-number-width': `${Math.max(2, String(sourceLines.length).length)}ch`, } as CSSProperties}> {/* These paired attributes are stable semantic hooks for owner styling and DOM tests. */} -
+ {showHeader &&
{lang ?? ''}
@@ -189,7 +193,7 @@ export function CodeBlock({ code, lang, streaming, className, contentRef, lineNu
-
+
}
{body}
) diff --git a/packages/client/ui-primitives/src/markdown/MarkdownText.module.css b/packages/client/ui-primitives/src/markdown/MarkdownText.module.css index d29c50c99a..652affcf85 100644 --- a/packages/client/ui-primitives/src/markdown/MarkdownText.module.css +++ b/packages/client/ui-primitives/src/markdown/MarkdownText.module.css @@ -198,10 +198,11 @@ scrollbar STYLES (neither hover-conditioned `::-webkit-scrollbar*` rules nor a :hover `scrollbar-color` change reaches the painted bar), so the toggle is `overflow-x` itself — a layout change repaints reliably. The - resting padding matches the themed bar's height, so on an overflowing - table the appearing bar exactly replaces it and nothing below shifts. + resting padding matches the themed bar's height on the WebKit scrollbar + path. There, `scroll` reserves that height even when the table fits. + Other engines and overlay scrollbars may reserve a different height. Wheel and trackpad scrolling need the pointer over the table, which is - already the hover that re-enables `auto`. */ + already the hover that re-enables scrolling. */ .tableScroll:global(.md-table-wide) { overflow-x: hidden; padding-bottom: var(--dsh-scrollbar-width, 8px); @@ -209,7 +210,7 @@ .tableScroll:global(.md-table-wide):hover, .tableScroll:global(.md-table-wide):focus-visible { - overflow-x: auto; + overflow-x: scroll; padding-bottom: 0; } diff --git a/packages/client/ui-primitives/tests/icons.client.spec.tsx b/packages/client/ui-primitives/tests/icons.client.spec.tsx index d8b566704a..98f56c5d8f 100644 --- a/packages/client/ui-primitives/tests/icons.client.spec.tsx +++ b/packages/client/ui-primitives/tests/icons.client.spec.tsx @@ -17,8 +17,8 @@ const icons = Object.fromEntries( const iconNames = Object.keys(icons) describe('ic_ds_ icon set', () => { - it('exports the full icon set (46 deepsuite + 21 figma extracts + eleven product glyphs outside those sets)', () => { - expect(iconNames.length).toBe(78) + it('exports the full icon set (46 deepsuite + 21 figma extracts + twelve product glyphs outside those sets)', () => { + expect(iconNames.length).toBe(79) // The composer menu's own glyphs, pinned by name. expect(iconNames).toEqual(expect.arrayContaining(['IconPlanOutline14', 'IconCompactOutline16', 'IconShieldOutline16'])) }) diff --git a/packages/client/ui-primitives/tests/json-tree.client.spec.tsx b/packages/client/ui-primitives/tests/json-tree.client.spec.tsx index 691ba88652..7a7c335ac0 100644 --- a/packages/client/ui-primitives/tests/json-tree.client.spec.tsx +++ b/packages/client/ui-primitives/tests/json-tree.client.spec.tsx @@ -12,10 +12,12 @@ function JsonTree(props: Omit, 'label' return } -let writeText: ReturnType +let writeText: ReturnType> +let originalClipboard: PropertyDescriptor | undefined beforeEach(() => { - writeText = vi.fn().mockResolvedValue(undefined) + originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard') + writeText = vi.fn().mockResolvedValue(undefined) Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText }, @@ -25,9 +27,162 @@ beforeEach(() => { afterEach(() => { cleanup() vi.useRealTimers() + vi.restoreAllMocks() + vi.unstubAllGlobals() + if (originalClipboard === undefined) Reflect.deleteProperty(navigator, 'clipboard') + else Object.defineProperty(navigator, 'clipboard', originalClipboard) }) +function stubStringLayout(scrollHeight = 200): void { + vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(scrollHeight) + const computedStyle = window.getComputedStyle.bind(window) + vi.spyOn(window, 'getComputedStyle').mockImplementation((element) => { + const style = computedStyle(element) + style.lineHeight = '16px' + if (style.paddingBottom === '') style.paddingBottom = '0px' + return style + }) +} + describe('JsonTree', () => { + it('ignores clipboard settlement after the row changes or the tree unmounts', async () => { + vi.useFakeTimers() + const pending: (() => void)[] = [] + writeText.mockImplementation(() => new Promise((resolve) => { pending.push(resolve) })) + const view = render() + const rows = screen.getAllByRole('treeitem') + fireEvent.mouseOver(rows[0] as HTMLElement) + fireEvent.click(screen.getByRole('button', { name: 'Copy value' })) + fireEvent.mouseOver(rows[1] as HTMLElement) + await act(async () => { pending[0]!() }) + expect(screen.queryByRole('button', { name: 'Copied' })).toBeNull() + fireEvent.click(screen.getByRole('button', { name: 'Copy value' })) + view.unmount() + const timerCount = vi.getTimerCount() + await act(async () => { pending[1]!() }) + expect(vi.getTimerCount()).toBe(timerCount) + }) + + it('updates copy actions without rereading JSON properties on hover', () => { + const readValue = vi.fn(() => 'payload '.repeat(100)) + const data = Object.fromEntries(Array.from({ length: 500 }, (_, index) => [ + `field${index}`, + { get value() { return readValue() } }, + ])) + render() + const rows = within(screen.getByRole('tree')).getAllByRole('treeitem') + readValue.mockClear() + + fireEvent.mouseOver(rows[0] as HTMLElement) + expect(within(rows[0] as HTMLElement).getByRole('button', { name: 'Copy pretty JSON' })).toBeTruthy() + fireEvent.mouseOver(rows[1] as HTMLElement) + expect(within(rows[0] as HTMLElement).queryByRole('button', { name: 'Copy pretty JSON' })).toBeNull() + expect(within(rows[1] as HTMLElement).getByRole('button', { name: 'Copy pretty JSON' })).toBeTruthy() + expect(readValue).not.toHaveBeenCalled() + }) + + it('expands raw strings without ResizeObserver and keeps the visible viewport limit', () => { + stubStringLayout() + vi.stubGlobal('ResizeObserver', undefined) + let rawTop = 150 + const view = render( +
+ +
, + ) + const clip = view.container.firstElementChild as HTMLElement + vi.spyOn(clip, 'clientTop', 'get').mockReturnValue(2) + vi.spyOn(clip, 'clientHeight', 'get').mockReturnValue(140) + vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) { + return new DOMRect(0, this === clip ? 100 : rawTop, 200, 140) + }) + const rows = within(screen.getByRole('tree')).getAllByRole('treeitem') + fireEvent.click(within(rows[0] as HTMLElement).getByRole('button', { name: 'Expand JSON node' })) + const raw = rows[0]?.querySelector('pre') as HTMLPreElement + expect(raw.textContent).toBe('raw\ntext') + expect(raw.style.maxHeight).toBe('78px') + expect(raw.nextElementSibling?.textContent).toBe(',') + + rawTop = 80 + fireEvent.scroll(clip) + expect(raw.style.maxHeight).toBe('126px') + rawTop = 300 + fireEvent.resize(window) + expect(raw.style.maxHeight).toBe('16px') + fireEvent.click(within(rows[0] as HTMLElement).getByRole('button', { name: 'Collapse JSON node' })) + expect(raw.isConnected).toBe(false) + rawTop = 100 + fireEvent.scroll(clip) + expect(raw.style.maxHeight).toBe('16px') + + fireEvent.click(within(rows[1] as HTMLElement).getByRole('button', { name: 'Expand JSON node' })) + expect(rows[1]?.querySelector('pre')?.nextElementSibling?.textContent).not.toBe(',') + }) + + it('shows the string expander only beyond the configured collapsed line count', () => { + stubStringLayout(48) + let resize: (() => void) | undefined + const disconnect = vi.fn() + vi.stubGlobal('ResizeObserver', class { + constructor(callback: () => void) { resize = callback } + observe() {} + disconnect = disconnect + }) + const data = { text: 'three lines of text' } + const view = render() + expect(screen.queryByRole('button', { name: 'Expand JSON node' })).toBeNull() + view.rerender() + expect(screen.getByRole('button', { name: 'Expand JSON node' })).toBeTruthy() + vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(16) + act(() => { resize?.() }) + expect(screen.queryByRole('button', { name: 'Expand JSON node' })).toBeNull() + view.unmount() + expect(disconnect).toHaveBeenCalledTimes(2) + }) + + it('keeps raw strings intact and samples the wrapping preference on every expansion', async () => { + stubStringLayout() + vi.stubGlobal('ResizeObserver', class { + observe() {} + disconnect() {} + }) + let wrapped = false + const stringWrapping = { + label: 'Wrap lines', + getDefault: () => wrapped, + setDefault: (value: boolean) => { wrapped = value }, + } + const original = ` leading spaces\n\t"quoted" \\${'long'.repeat(100)}\nlast line\n` + render() + const [first, second] = within(screen.getByRole('tree')).getAllByRole('treeitem') + const a = within(first as HTMLElement) + const b = within(second as HTMLElement) + fireEvent.click(a.getByRole('button', { name: 'Expand JSON node' })) + fireEvent.click(b.getByRole('button', { name: 'Expand JSON node' })) + expect(a.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('false') + + fireEvent.click(a.getByRole('button', { name: 'Wrap lines' })) + expect(wrapped).toBe(true) + expect(a.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('true') + expect(b.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('false') + + fireEvent.click(b.getByRole('button', { name: 'Collapse JSON node' })) + fireEvent.click(b.getByRole('button', { name: 'Expand JSON node' })) + expect(b.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('true') + fireEvent.click(b.getByRole('button', { name: 'Wrap lines' })) + expect(wrapped).toBe(false) + expect(a.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('true') + + fireEvent.click(a.getByRole('button', { name: 'Collapse JSON node' })) + fireEvent.click(a.getByRole('button', { name: 'Expand JSON node' })) + const toggle = a.getByRole('button', { name: 'Wrap lines' }) + expect(toggle.getAttribute('aria-pressed')).toBe('false') + const contents = document.getElementById(toggle.getAttribute('aria-controls') as string) + expect(contents?.textContent).toBe(original) + fireEvent.click(a.getByRole('button', { name: 'Copy value' })) + await waitFor(() => { expect(writeText).toHaveBeenCalledWith(original) }) + }) + it('keeps the top level open and renders expandable value previews', () => { render( { view.unmount() }) - it('keeps copy placement synchronized and clears stale targets', () => { + it('keeps the copy action on its hovered row and clears stale targets', () => { const view = render() const root = view.container.firstElementChild as HTMLElement const tree = screen.getByRole('tree') const firstRow = within(tree).getAllByRole('treeitem')[0] as HTMLElement const secondRow = within(tree).getAllByRole('treeitem')[1] as HTMLElement - Object.defineProperty(root, 'clientHeight', { configurable: true, value: 100 }) - Object.defineProperty(root, 'clientWidth', { configurable: true, value: 300 }) - vi.spyOn(root, 'getBoundingClientRect').mockReturnValue({ - bottom: 100, - height: 100, - left: 10, - right: 310, - top: 0, - width: 300, - x: 10, - y: 0, - toJSON: () => ({}), - }) - vi.spyOn(firstRow, 'getBoundingClientRect').mockReturnValue({ - bottom: 91, - height: 16, - left: 10, - right: 200, - top: 75, - width: 190, - x: 10, - y: 75, - toJSON: () => ({}), - }) - fireEvent.mouseOver(firstRow) const copyButton = screen.getByRole('button', { name: 'Copy pretty JSON' }) - expect((copyButton.closest('span')?.parentElement as HTMLElement).style.left).toBe('284px') + expect(firstRow.contains(copyButton)).toBe(true) fireEvent.mouseOver(copyButton) expect(screen.getByRole('button', { name: 'Copy pretty JSON' })).toBeDefined() fireEvent.mouseOver(firstRow) fireEvent.scroll(root) - fireEvent.scroll(window) - fireEvent.resize(window) fireEvent.contextMenu(copyButton) fireEvent.mouseOver(secondRow) diff --git a/packages/client/ui-sidebar-right/README.i18n.yaml b/packages/client/ui-sidebar-right/README.i18n.yaml index b8e427d0f9..3384df0f37 100644 --- a/packages/client/ui-sidebar-right/README.i18n.yaml +++ b/packages/client/ui-sidebar-right/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-sidebar-right/README.md -README.md: c947b5411f51aff54f480949af203d192662adbf -README.zh.md: 641a8fab400bbfadaf9a1b4b96ab8369a8121edf +README.md: 75c600edf014564894c9299422c840bce15eb8e8 +README.zh.md: 203bc30d0550234e9ed97c1ccac70bed433d0605 diff --git a/packages/client/ui-sidebar-right/README.md b/packages/client/ui-sidebar-right/README.md index c947b5411f..75c600edf0 100644 --- a/packages/client/ui-sidebar-right/README.md +++ b/packages/client/ui-sidebar-right/README.md @@ -67,7 +67,7 @@ After every action the kit's settle planner keeps the expanded surface populated The docked surface's last tab carries one more rule, decided in the store's `closeTab` and mirrored to the kit through `canCloseTab`: the guide standing as the only docked tab draws no close control and no menu close item — its chip sits quiet, and with no extension item contributed a secondary press opens no menu — and a programmatic close of it records nothing; any other tab standing alone closes together with the column in one entry — the layout stays empty until the next expansion seeds its current default page. Floating panels take no part in the rule: they render whether or not the column is expanded, and their tabs close freely. -State is memory-only. A reload returns every session to the collapsed default; switching sessions keeps each surface where it was. +Layout, open tabs, navigation parameters and selection are memory-only. A reload constructs the collapsed default; content plugins can then reopen their own live content, as [terminal recovery](../ui-sidebar-terminal/README.md#use-this-package) does. Switching sessions keeps each surface where it was. ## Extension seats @@ -84,7 +84,7 @@ Two more seats extend what is already there: `sidebar.right.tab.guide` (chain) r ## `ctx.sidebarRight` -`openResource(address, options?)` and `openTab(kind, options?)` are the navigation controller, and every way into the column calls one of them: the conversation's file links and a tool row's line reference (`openResource(fileAddress, { params: { line } })`), the strip's add control and a guide entry box (`openTab`), a file tree's rows (`tab.actions.openResource`). A resource address is a `dsh-resource:///…` URI; without `options.kind` the registry claims it (globs and `canOpen`, best band wins), with it that kind's type in force opens it. A page is named by kind; the tab is recorded under an address this package composes and nobody else spells (`contract/seed.ts`). Both run the same steps as one history entry: a resource tab already showing the same (kind, contentId) is focused wherever it sits unless `revealIfOpened: false`; page tabs always deduplicate within the target pane, regardless of that option; otherwise a new tab lands in `options.replaceTab`'s pane and slot (closing that tab), else `options.paneId`, else the active docked pane; the panel expands, because content the user cannot see is not opened. Then the Tab domain records the navigation — `params` reach the body as `navigation.params`, with `revision` stepped — outside the layout history. `params` is typed by what is opened: a viewer for a resource type merges its entry into `SidebarRightResourceParamsMap` (the text preview declares `{ line?: number }`); a page type that takes parameters merges into `SidebarRightTabParamsMap` under its kind; values are JSON-shaped by convention, unchecked at run time. An address outside `dsh-resource://`, one no type claims, or a kind nothing registered throws: that is a wiring mistake, not a user error. +`openResource(address, options?)` and `openTab(kind, options?)` are the navigation controller, and every way into the column calls one of them: the conversation's file links and a tool row's line reference (`openResource(fileAddress, { params: { line } })`), the strip's add control and a guide entry box (`openTab`), a file tree's rows (`tab.actions.openResource`). A resource address is a `dsh-resource:///…` URI; without `options.kind` the registry claims it (globs and `canOpen`, best band wins), with it that kind's type in force opens it. A page is named by kind; the tab is recorded under an address this package composes and nobody else spells (`contract/seed.ts`). Both run the same steps as one history entry: a resource tab already showing the same (kind, contentId) is focused wherever it sits unless `revealIfOpened: false`; ordinary page tabs always deduplicate within the target pane, regardless of that option; otherwise a new tab lands in `options.replaceTab`'s pane and slot (closing that tab), else `options.paneId`, else the active docked pane; the panel expands, because content the user cannot see is not opened. Then the Tab domain records the navigation — `params` reach the body as `navigation.params`, with `revision` stepped — outside the layout history. `params` is typed by what is opened: a viewer for a resource type merges its entry into `SidebarRightResourceParamsMap` (the text preview declares `{ line?: number }`); a page type that takes parameters merges into `SidebarRightTabParamsMap` under its kind; values are JSON-shaped by convention, unchecked at run time. An address outside `dsh-resource://`, one no type claims, or a kind nothing registered throws: that is a wiring mistake, not a user error. `close(tabId)` closes a tab; `active()` reads the active tab. `isExpanded()` and `toggleExpanded()` read and drive the column's expansion; the presentation switch is the panel's own control and not part of this face. Layout operations, for callers that arrange the column programmatically, each recorded like the gesture it stands in for: `focus(tabId)` focuses a tab and its pane; `split(paneId?)` splits a docked pane (the active one by default) under the same pane budget and room rule as the strip's control and returns the new pane's id, or `undefined` — recording nothing — when it cannot; `float(tabId, rect?)` takes a docked tab out into a panel; `dock(paneId)` returns a floating panel to the active docked pane. A tab or pane that does not exist, or already is where the call would put it, is left alone. The face exposes operations only: no layout snapshot, no operation log, no lookup by address. `_undo()` / `_redo()` step the mounted surface's history; they are `@internal` — the sequence has no user-facing control, and these exist for tests. Commands need a mounted session surface; with none, they throw rather than write into a surface nobody draws. @@ -93,10 +93,14 @@ Two more seats extend what is already there: `sidebar.right.tab.guide` (chain) r The Tab domain retains navigation, an abort signal, and bound actions per (Session, tab id). A private assembly callback adopts each Session's store and reconciles records on its commits. Only record removal or plugin unload aborts the signal; closing the sidebar and switching Sessions retain records, while undo restores a new occurrence. `useTabInfo()` composes framework-bound store and navigation hooks without manual component subscriptions or render-time record creation. `tab.actions` always target their own Session; `tab.visible` distinguishes bodies from titles, and floating tabs remain visible when the sidebar closes. `adopt` is absent from the public controller. +Tab owners register `registerCloseHandler(kind, handler)` through an effect. A handler synchronously retains any background cleanup before allowing explicit close or replacement. The resource owner tracks completion and retry; the sidebar does not wait. A thrown handler preserves the tab. Collapse, presentation changes and plugin disposal do not invoke close handlers; the tab abort signal identifies occurrence disposal, not an explicit close. + ## The guide -Default pages depend on the number of registered guide entries, not the number of tab types or open tabs. Exactly one entry opens its page directly (Files in the shipped composition); zero or multiple entries open the guide. Explicitly adding a guide still opens the guide, even with one entry. The sole docked guide is the only tab that cannot close; closing any other sole tab also collapses the column. The chip, context menu, and `close` API apply the same rule. +A tab type can declare `multiple: true` for independent instances such as terminals. Each open receives a distinct content address, so placement and docking preserve those instances; ordinary pages retain per-pane uniqueness. + +Default pages depend on the number of registered guide entries, not the number of tab types or open tabs. Exactly one entry opens its page directly; zero or multiple entries open the guide. Explicitly adding a guide still opens the guide, even with one entry. The sole docked guide is the only tab that cannot close; closing any other sole tab also collapses the column. The chip, context menu, and `close` API apply the same rule. The guide tab is a muted compass over one entry capsule per `guide` entry the registered types contributed, in `order`, centred in the body; the guide has no words of its own. A capsule shows the entry's glyph — or the guide's quieter cube placeholder when the entry registered none — and its title; while at most four entries are listed, an entry that registered a `description` shows it under the title, and a longer list drops every description. Picking a capsule calls `tab.actions.openTab(entry.kind, { replaceTab: true })`, so the guide gives way to the page it opened. A pane holds at most one guide tab. The strip's add control is drawn only while its pane holds none and opens one there with `openTab('guide', { paneId, revealIfOpened: false })`, so a guide in another pane does not capture the click; opening the guide into a pane that already has one focuses it instead; a guide dragged, dropped, or docked into such a pane merges into it — the arriving guide closes and the pane's own is focused; `duplicateTab` on the guide records nothing. A split, an expanded empty root pane, and the pane a sole tab vacates by dropping on its own edge use the same default-page rule, one tab per new pane; the self-edge drop leaves the dragged tab focused. A plain `openTab('guide')` opens or focuses the guide only within the active or named pane. Splitting an empty pane does nothing and returns no new pane. The product allows two horizontal panes, initially equal, with divider ratios limited to 20%–80%. Insufficient width blocks a new split; with two panes already present, a body drop moves the tab between panes instead of creating a third. At the two-pane limit, split controls are hidden; closing back to one pane restores them. @@ -118,7 +122,7 @@ None; this package neither assembles nor sends a provider request. -- **Memory-only.** Nothing is persisted; a reload starts every session collapsed. +- **Memory-only layout.** Sidebar state is not persisted; content-specific recovery cannot reproduce the previous layout or selection. - **No surface without a session.** State is keyed by session id, so the hero screen shows nothing on the right. - **Hard-coded stacking.** The panel and the float host use fixed z-index values because the client has no z-index token layer yet. - **Undo is not exposed.** The recorded sequence is stepped only through the `@internal` service methods; product controls are deliberately absent. diff --git a/packages/client/ui-sidebar-right/README.zh.md b/packages/client/ui-sidebar-right/README.zh.md index 641a8fab40..203bc30d05 100644 --- a/packages/client/ui-sidebar-right/README.zh.md +++ b/packages/client/ui-sidebar-right/README.zh.md @@ -67,7 +67,7 @@ kind: "package-reference" 停靠面的最后一个 tab 还多带一条规则,由 store 的 `closeTab` 决定并经 `canCloseTab` 镜像给套件:作为唯一停靠 tab 的引导页不画关闭控件也不画菜单里的关闭项——它的 chip 呈安静样式,在没有扩展条目时次键按下也不弹出菜单——对它的编程式关闭什么都不记录;任何其它 tab 独自留下时,点击关闭会连同整列一起收起,记为一条历史,布局保持为空,直到下次展开时创建当时的默认页。浮动面板不参与这条规则:它们无论列是否展开都会渲染,其 tab 照常关闭。 -状态只在内存中。刷新会让每个会话回到折叠的默认态;切换会话则让每个停靠面留在原处。 +布局、打开的标签页、导航参数和选中项只在内存中。刷新会构造折叠的默认态;内容插件随后可重新打开自己仍存活的内容,例如[终端恢复](../ui-sidebar-terminal/README.zh.md#use-this-package)。切换会话则让每个停靠面留在原处。 ## 扩展席位 @@ -93,10 +93,14 @@ tab 类型分两阶段注册,随包发布的引导类型走的正是别的包 Tab域按(Session,Tab id)保留导航、中止信号与绑定动作;私有装配回调收养各会话的store,并在每次提交时对齐记录。记录消失或插件卸载才中止signal,收起和切会话不销毁记录;undo恢复的是新occurrence。`useTabInfo()` 组合框架绑定的store与导航hook,不在组件中手写订阅或在渲染时创建记录。`tab.actions` 始终作用于自己的会话;`tab.visible` 区分正文与标题,浮窗不受整栏收起影响。`adopt` 不在公开控制器上。 +标签页所有者通过 effect 注册 `registerCloseHandler(kind, handler)`。handler 在允许显式关闭或替换前同步保存后台清理任务。资源所有者跟踪完成和重试,侧栏不等待清理。handler 抛错时保留标签页。折叠、展示方式改变和插件卸载不调用关闭 handler;tab abort signal 标识 occurrence 卸载,不代表显式关闭。 + ## 引导页 -默认页取决于已注册的引导入口数,不取决于 tab 类型数或已打开的 tab 数。恰好一个入口时直接打开对应页面(随包组合中为 Files);没有入口或有多个入口时打开引导页。即使只有一个入口,显式添加引导页仍会打开引导页。只有作为唯一停靠 tab 的引导页不可关闭;关闭其它任何唯一 tab 时会同时收起整列。chip、上下文菜单与 `close` API 使用同一规则。 +终端等需要独立实例的 tab 类型可声明 `multiple: true`。每次打开都会获得独立内容地址,因此移动和停靠会保留这些实例;普通页面仍在每个格内去重。 + +默认页取决于已注册的引导入口数,不取决于 tab 类型数或已打开的 tab 数。恰好一个入口时直接打开对应页面;没有入口或有多个入口时打开引导页。即使只有一个入口,显式添加引导页仍会打开引导页。只有作为唯一停靠 tab 的引导页不可关闭;关闭其它任何唯一 tab 时会同时收起整列。chip、上下文菜单与 `close` API 使用同一规则。 引导 tab 是一枚弱化的罗盘,下方是各已注册类型贡献的每个 `guide` 条目一个入口胶囊,按 `order` 排列并在正文中居中;引导页自己没有文字。胶囊显示条目的图标——条目没注册图标时用引导页自己更浅的立方体占位符——和标题;列出的条目不超过四个时,注册了 `description` 的条目在标题下显示它,更长的列表则去掉所有描述。选中一个胶囊会调用 `tab.actions.openTab(entry.kind, { replaceTab: true })`,于是引导页让位给它打开的页。一个格最多持有一个引导 tab。tab 条的添加控件只在该格没有引导 tab 时绘制,并以 `openTab('guide', { paneId, revealIfOpened: false })` 在该格打开一个,这样别的格里的引导页不会截走这次点击;把引导页开进已有引导页的格则改为聚焦它;把引导页拖入、放入或收回到这样的格会合并进去——来者关闭,该格自己的被聚焦;对引导页 `duplicateTab` 不记录任何东西。分栏、展开且为空的根格、以及唯一 tab 拖到本格边缘分屏后腾出的格使用相同的默认页规则,每个新格一个 tab;这种本格边缘拖放让被拖的 tab 保持聚焦。普通的 `openTab('guide')` 只在活跃或指定分栏内打开或聚焦引导页。对空分栏执行 split 不产生变化,也不返回新分栏。产品最多保留左右两格,默认均分,分隔条限定在 20%~80%。宽度不足以容纳两格时不允许新分栏;已有两格时,正文拖放用于跨格移动,不再创建第三格。达到两格上限时隐藏分栏控件;关闭回单格后恢复。 @@ -118,7 +122,7 @@ Tab域按(Session,Tab id)保留导航、中止信号与绑定动作;私 -- **只在内存中。** 不持久化任何东西;刷新让每个会话从折叠态开始。 +- **布局只在内存中。** 侧栏状态不持久化;内容自身的恢复无法还原此前的布局或选中项。 - **没有会话就没有停靠面。** 状态按会话 id 键控,因此 hero 画面右侧什么都不显示。 - **硬编码的层叠。** 面板与浮窗宿主使用固定的 z-index 值,因为客户端还没有 z-index token 层。 - **未暴露撤销。** 记录的序列只能通过 `@internal` 服务方法步进;产品控件是有意缺席的。 diff --git a/packages/client/ui-sidebar-right/package.json b/packages/client/ui-sidebar-right/package.json index 05c813fcc2..5a1495b09a 100644 --- a/packages/client/ui-sidebar-right/package.json +++ b/packages/client/ui-sidebar-right/package.json @@ -67,7 +67,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "picomatch": "^4.0.4", - "clsx": "^2.0.0" + "clsx": "^2.0.0", + "@deepseek-ai/dsh-util-crypto": "workspace:^" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-sidebar-right/src/client/index.ts b/packages/client/ui-sidebar-right/src/client/index.ts index 6e2df4fe40..5ffcbd34e1 100644 --- a/packages/client/ui-sidebar-right/src/client/index.ts +++ b/packages/client/ui-sidebar-right/src/client/index.ts @@ -49,7 +49,7 @@ export type { ExpandButtonProps } from './shell/ExpandButton.tsx' export type { SidebarRightState, SurfaceState } from './stores.ts' export type { ISidebarRight, SidebarRightBinding, SidebarRightOpenResourceOptions, SidebarRightOpenTabOptions, - SidebarRightPlacement, SurfaceActions, + SidebarRightPlacement, SidebarRightCloseHandler, SurfaceActions, } from './service.ts' export type { SidebarRightGuideBox, SidebarRightGuideEntry, SidebarRightTabClaim, SidebarRightTabDefinition, @@ -135,7 +135,7 @@ export function apply(ctx: ClientContext): void { }, } const layout: ILayout = ctx.layout - const injected: Omit = { + const injected: Omit = { syncPresentation({ shown, track, fullscreen }) { if (shown) layout.openRightbar(track, fullscreen) else layout.closeRightbar() @@ -162,6 +162,10 @@ export function apply(ctx: ClientContext): void { store, inject: (sessionId): SidebarRightInjected => ({ ...injected, + closeTab: (tabId) => { + try { controller.closeIn(sessionId, tabId) } + catch (error) { console.error('Sidebar tab close failed:', error) } + }, keyedHooks: { tabNavigation: key => controller.tabDomain.occurrence(sessionId, { id: key as TabId }).navigation }, occurrence: tab => controller.tabDomain.occurrence(sessionId, tab), }), diff --git a/packages/client/ui-sidebar-right/src/client/service.ts b/packages/client/ui-sidebar-right/src/client/service.ts index e80a291a4f..def6c222cc 100644 --- a/packages/client/ui-sidebar-right/src/client/service.ts +++ b/packages/client/ui-sidebar-right/src/client/service.ts @@ -27,14 +27,15 @@ * The registration adopts Session stores and injects the mounted seat binding; * callers use the service's navigation methods. */ +import { randomUUID } from '@deepseek-ai/dsh-util-crypto' import type { FloatRect, PaneId, TabId, TabRecord } from '@deepseek-ai/dsh-client-ui-dockkit' -import { activeDockPaneId, canSplit, dockPaneIds, findTabPane, getPane } from '@deepseek-ai/dsh-client-ui-dockkit' +import { activeDockPaneId, canSplit, findContentTab, dockPaneIds, findTabPane, getPane } from '@deepseek-ai/dsh-client-ui-dockkit' import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { SidebarRightNavigationParams, SidebarRightResourceParams, SidebarRightTabParamsFor } from './contract/params.ts' import { pageAddress } from './contract/seed.ts' import type { SidebarRightTabClaim, SidebarRightTabRegistry } from './tab-registry.ts' -import type { SidebarRightState, SurfaceState } from './stores.ts' +import { canCloseTab, type SidebarRightState, type SurfaceState } from './stores.ts' import type { createSidebarRightStore } from './stores.ts' import { TabDomain, type PinResource } from './tab-domain.ts' @@ -134,6 +135,9 @@ export interface SidebarRightOpenTabOptions extends S /** The scheme every resource address carries; anything else is not a resource this face opens. */ const RESOURCE_SCHEME = 'dsh-resource://' +/** Synchronous close/replacement hook; resource owners retain any background cleanup. */ +export type SidebarRightCloseHandler = (sessionId: SessionId, tab: TabRecord) => void + /** The outward right-Sidebar face (`ctx.sidebarRight`). */ export interface ISidebarRight { /** @@ -203,6 +207,19 @@ export interface ISidebarRight { /** Cross-plugin right-Sidebar face (ctx.sidebarRight). */ export class SidebarRightController implements ISidebarRight { private binding: SidebarRightBinding | undefined + private readonly closeHandlers = new Map() + + /** + * Register resource cleanup before explicit removal. Failure preserves the tab. + * @param kind - tab kind owned by the registering plugin. + * @param handler - saves any background cleanup before returning and allowing removal. + * @returns an effect-scoped unregister callback. + */ + registerCloseHandler(kind: string, handler: SidebarRightCloseHandler): () => void { + if (this.closeHandlers.has(kind)) throw new Error(`sidebarRight: close handler already registered for ${kind}`) + this.closeHandlers.set(kind, handler) + return () => { if (this.closeHandlers.get(kind) === handler) this.closeHandlers.delete(kind) } + } /** * The Tab domain this controller navigates into; synced from each adopted @@ -294,7 +311,16 @@ export class SidebarRightController implements ISidebarRight { */ closeIn(sessionId: SessionId, tabId: TabId): void { const actions = this.actionsFor(sessionId) - if (actions !== undefined) actions.closeTab(sessionId, tabId) + const surface = this.adopted.get(sessionId)?.store.getSnapshot().bySession[sessionId] + if (actions === undefined || surface === undefined) return + const tab = surface.layout.tabs[tabId] + if (tab === undefined || !canCloseTab(surface, tabId)) return + this.removeAfterCleanup(sessionId, tab, () => { actions.closeTab(sessionId, tabId) }) + } + + private removeAfterCleanup(sessionId: SessionId, tab: TabRecord, commit: () => void): void { + this.closeHandlers.get(tab.kind)?.(sessionId, tab) + commit() } /** Claim a resource and place it in one session; an address outside the scheme or one no type claims throws. */ @@ -319,7 +345,7 @@ export class SidebarRightController implements ISidebarRight { ): void { const definition = this.tabs.get(kind) if (definition === undefined) throw new Error(`sidebarRight: no tab type is registered as "${kind}"`) - const address = pageAddress(kind) + const address = definition.multiple === true ? `${pageAddress(kind)}/${randomUUID()}` : pageAddress(kind) this.place(sessionId, actions, { kind, contentId: address, title: definition.title(address) }, address, options, options.params) } @@ -332,14 +358,20 @@ export class SidebarRightController implements ISidebarRight { placement: SidebarRightPlacement, params: SidebarRightNavigationParams, ): void { - actions.openContent(sessionId, { + const commit = (): void => { actions.openContent(sessionId, { kind: claim.kind, contentId: claim.contentId, title: claim.title, ...placement.paneId === undefined ? {} : { paneId: placement.paneId }, ...placement.replaceTab === undefined ? {} : { replaceTab: placement.replaceTab }, ...placement.revealIfOpened === undefined ? {} : { revealIfOpened: placement.revealIfOpened }, - }, (tabId) => { this.tabDomain.navigate(sessionId, tabId, { address, params }) }) + }, (tabId) => { this.tabDomain.navigate(sessionId, tabId, { address, params }) }) } + const layout = this.adopted.get(sessionId)?.store.getSnapshot().bySession[sessionId]?.layout + const replaced = placement.replaceTab === undefined ? undefined : layout?.tabs[placement.replaceTab] + const revealed = layout === undefined || placement.revealIfOpened === false + ? undefined : findContentTab(layout, claim.contentId, claim.kind) + if (replaced === undefined || replaced.id === revealed) { commit(); return } + this.removeAfterCleanup(sessionId, replaced, commit) } /** @@ -348,6 +380,7 @@ export class SidebarRightController implements ISidebarRight { */ close(tabId: TabId): void { const { sessionId, actions } = this.require() + if (this.adopted.has(sessionId)) { this.closeIn(sessionId, tabId); return } actions.closeTab(sessionId, tabId) } diff --git a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx index 355eedc190..476134c47d 100644 --- a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx +++ b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx @@ -97,6 +97,8 @@ export interface SidebarRightInjected { * the guide opened by kind, through the same path as every other open. */ readonly openTab: (kind: string, options?: SidebarRightOpenTabOptions) => void + /** Close through the resource owner's cleanup handler. */ + readonly closeTab: (tabId: TabId) => void readonly hooks: { readonly tabTypes: HostObservable } @@ -123,6 +125,7 @@ interface PanelProps { readonly t: RightbarSeatProps['t'] readonly renderSlot: Children['renderSlot'] readonly openTab: SidebarRightInjected['openTab'] + readonly closeTab: SidebarRightInjected['closeTab'] readonly useTabTypes: RightbarSeatProps['useTabTypes'] readonly useTabNavigation: RightbarSeatProps['useTabNavigation'] readonly useStore: Store['useStore'] @@ -145,7 +148,7 @@ function guideIn(layout: LayoutState, paneId: PaneId): TabId | undefined { * @param openTab - the navigation face's `openTab`, which the strip's add control asks for a guide through. * @returns the intents the kit reports gestures to. */ -export function intentsFor(sessionId: SessionId, actions: Store['actions'], openTab: PanelProps['openTab']): DockIntents { +export function intentsFor(sessionId: SessionId, actions: Store['actions'], openTab: PanelProps['openTab'], closeTab?: PanelProps['closeTab']): DockIntents { return { focusTab: (tabId) => { actions.focusTab(sessionId, tabId) }, focusPane: (paneId) => { actions.focusPane(sessionId, paneId) }, @@ -155,7 +158,7 @@ export function intentsFor(sessionId: SessionId, actions: Store['actions'], open // guides in other panes; the store settles the open on a guide the pane // already holds, so the ask is idempotent all the same. addTab: (paneId) => { openTab(GUIDE_KIND, { paneId, revealIfOpened: false }) }, - closeTab: (tabId) => { actions.closeTab(sessionId, tabId) }, + closeTab: closeTab ?? ((tabId) => { actions.closeTab(sessionId, tabId) }), duplicateTab: (tabId) => { actions.duplicateTab(sessionId, tabId) }, floatTab: (tabId, rect?: FloatRect) => { actions.floatTab(sessionId, tabId, rect) }, unfloatPane: (paneId) => { actions.unfloatPane(sessionId, paneId) }, @@ -311,7 +314,7 @@ function SidebarPanel(panel: PanelProps & { width: number; panelRef: RefObject guideIn(surface.layout, paneId) === undefined} canCloseTab={tabId => canCloseTab(surface, tabId)} - intents={intentsFor(sessionId, actions, openTab)} + intents={intentsFor(sessionId, actions, openTab, panel.closeTab)} labels={dockLabels(t)} renderTab={bodiesFor(panel)} renderTabTitle={titlesFor(panel)} @@ -334,7 +337,7 @@ function Floats(panel: PanelProps): ReactNode { canCloseTab(surface, tabId)} - intents={intentsFor(sessionId, actions, openTab)} + intents={intentsFor(sessionId, actions, openTab, panel.closeTab)} labels={dockLabels(t)} renderTab={bodiesFor(panel)} renderTabTitle={titlesFor(panel)} @@ -351,7 +354,7 @@ function Floats(panel: PanelProps): ReactNode { * which session it is acting on, because this is the seat that knows both. */ export function RightbarSeat({ - sessionId, width, viewportWidth, canShow, useStore, actions, t, renderSlot, syncPresentation, bindService, openTab, + sessionId, width, viewportWidth, canShow, useStore, actions, t, renderSlot, syncPresentation, bindService, openTab, closeTab, useTabTypes, useTabNavigation, occurrence, }: RightbarSeatProps): ReactNode { // One store instance per session, so this map holds this session's surface. @@ -416,7 +419,7 @@ export function RightbarSeat({ if (surface === undefined) return null const panel: PanelProps = { - sessionId, actions, t, renderSlot, surface, openTab, useTabTypes, useTabNavigation, useStore, occurrence, + sessionId, actions, t, renderSlot, surface, openTab, closeTab, useTabTypes, useTabNavigation, useStore, occurrence, fullscreen, autoFullscreen, reportRoom, } return ( diff --git a/packages/client/ui-sidebar-right/src/client/tab-registry.ts b/packages/client/ui-sidebar-right/src/client/tab-registry.ts index 1b93096010..88f676a204 100644 --- a/packages/client/ui-sidebar-right/src/client/tab-registry.ts +++ b/packages/client/ui-sidebar-right/src/client/tab-registry.ts @@ -95,6 +95,8 @@ export interface SidebarRightTabDefinition { readonly id: string /** Type discriminator: what the tabs of this type are, and what `openTab` names. */ readonly kind: string + /** Each open by kind creates independent content; omission keeps one page per kind in each pane. */ + readonly multiple?: boolean /** * Resource-address globs this type recognizes; omit for a page type, which is * opened by kind and recognizes no address. diff --git a/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx b/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx index e88c5b9563..d809426429 100644 --- a/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx +++ b/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx @@ -80,7 +80,7 @@ describe('GuideBody', () => { cleanup() }) - it('picking a box opens that type in the guide\'s own place', () => { + it('picking a box without a reveal preference preserves the default opening behavior', () => { const { view, openTab } = mountGuide([box('files', 10)]) const entry = view.container.querySelector('[data-sidebar-right-guide-entry="files"]') if (entry === null) throw new Error('expected the files box') @@ -89,6 +89,7 @@ describe('GuideBody', () => { cleanup() }) + it('draws an empty guide while no type contributed an entry, and follows the registry when one does', () => { const { view, guideEntries, boxes } = mountGuide([]) expect(view.container.querySelector('[data-sidebar-right-guide]')).not.toBeNull() diff --git a/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx b/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx index ac1a0173b6..58c0368169 100644 --- a/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx +++ b/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx @@ -585,3 +585,22 @@ describe('intentsFor — the kit\'s gestures as one session\'s store actions', ( expect(openTab).toHaveBeenCalledWith('guide', { paneId: PANE_1, revealIfOpened: false }) }) }) + +it('keeps a resource tab and reports a synchronous cleanup failure from its close button', async () => { + const h = await mountSeat() + const tab = h.open('terminal') + const failure = new Error('process still running') + const release = h.controller.registerCloseHandler('text', () => { throw failure }) + const logged = vi.spyOn(console, 'error').mockImplementation(() => {}) + try { + fireEvent.click(element(h.view.container, `[data-dockkit-tab-close="${tab.id}"]`)) + await expect.poll(() => logged.mock.calls).toEqual([['Sidebar tab close failed:', failure]]) + expect(h.layout().tabs[tab.id]).toBeDefined() + release() + fireEvent.click(element(h.view.container, `[data-dockkit-tab-close="${tab.id}"]`)) + expect(h.layout().tabs[tab.id]).toBeUndefined() + } finally { + logged.mockRestore() + release() + } +}) diff --git a/packages/client/ui-sidebar-right/tests/service.client.spec.ts b/packages/client/ui-sidebar-right/tests/service.client.spec.ts index dc4b16d5c5..de35bc78f0 100644 --- a/packages/client/ui-sidebar-right/tests/service.client.spec.ts +++ b/packages/client/ui-sidebar-right/tests/service.client.spec.ts @@ -80,6 +80,25 @@ function harness() { } describe('SidebarRightController — opening', () => { + it('keeps independently opened instances distinct when they return to the same pane', () => { + const h = harness() + h.tabs.register({ id: 'test/terminal', kind: 'terminal', multiple: true, title: () => 'terminal' }) + const release = h.publish() + try { + h.controller.openTab('terminal') + h.controller.openTab('terminal') + const terminals = Object.values(h.layout().tabs).filter(tab => tab.kind === 'terminal') + expect(terminals).toHaveLength(2) + expect(terminals[0]!.contentId).not.toBe(terminals[1]!.contentId) + const pane = findTabPane(h.layout(), terminals[0]!.id).id + h.instance.actions.floatTab(SESSION, terminals[0]!.id) + const floating = findTabPane(h.layout(), terminals[0]!.id).id + h.instance.actions.unfloatPane(SESSION, floating) + expect(getPane(h.layout(), pane).tabs).toContain(terminals[0]!.id) + expect(getPane(h.layout(), pane).tabs).toContain(terminals[1]!.id) + } finally { release() } + }) + it('refuses every write while no seat is mounted', () => { const { controller } = harness() expect(() => { controller.openResource('dsh-resource://file/session/s-test/a.txt') }).toThrow('no session surface is mounted') @@ -590,3 +609,89 @@ describe('SidebarRightController — binding lifetime', () => { expect(() => { controller.toggleExpanded() }).not.toThrow() }) }) + +describe('explicit tab resource cleanup', () => { + it.each(['close', 'replace'] as const)('immediately removes the tab after a synchronous cleanup handler during %s', (operation) => { + const h = harness() + const release = h.adopt(SESSION, h.instance) + const unbind = h.publish() + h.controller.openResource('dsh-resource://file/session/s-test/terminal') + const tabId = h.tabOf('terminal') + const original = h.layout().tabs[tabId] + const handler = vi.fn(() => { expect(h.layout().tabs[tabId]).toBe(original) }) + const unregister = h.controller.registerCloseHandler('text', handler) + try { + const before = h.entries() + if (operation === 'close') h.controller.close(tabId) + else h.controller.openTab('guide', { replaceTab: tabId, revealIfOpened: false }) + expect(handler).toHaveBeenCalledExactlyOnceWith(SESSION, original) + expect(h.layout().tabs[tabId]).toBeUndefined() + expect(h.entries()).toBe(before + 1) + h.controller.closeIn(SESSION, tabId) + expect(handler).toHaveBeenCalledOnce() + } finally { + unregister() + unbind() + release() + } + }) + + it('does not invoke cleanup on collapse or when revealing the same tab', () => { + const h = harness() + h.adopt(SESSION, h.instance) + h.publish() + const address = 'dsh-resource://file/session/s-test/terminal' + h.controller.openResource(address) + h.publish() + const tabId = h.tabOf('terminal') + const handler = vi.fn() + h.controller.registerCloseHandler('text', handler) + h.controller.toggleExpanded() + h.controller.openResource(address, { replaceTab: tabId }) + expect(handler).not.toHaveBeenCalled() + expect(h.layout().tabs[tabId]).toBeDefined() + }) + + it.each(['close', 'replace'] as const)('preserves a tab when its synchronous %s handler throws', (operation) => { + const h = harness() + h.adopt(SESSION, h.instance) + h.publish() + h.controller.openResource('dsh-resource://file/session/s-test/terminal') + const tabId = h.tabOf('terminal') + const release = h.controller.registerCloseHandler('text', () => { throw new Error('cannot retain cleanup') }) + const perform = () => { + if (operation === 'close') h.controller.closeIn(SESSION, tabId) + else h.controller.openTab('guide', { replaceTab: tabId, revealIfOpened: false }) + } + expect(perform).toThrow('cannot retain cleanup') + expect(h.layout().tabs[tabId]).toBeDefined() + release() + perform() + expect(h.layout().tabs[tabId]).toBeUndefined() + }) +}) + +it('releases close handlers without a stale disposer removing a replacement', () => { + const h = harness() + h.adopt(SESSION, h.instance) + h.publish() + const first = vi.fn(() => {}) + const second = vi.fn(() => {}) + const release = h.controller.registerCloseHandler('text', first) + expect(() => h.controller.registerCloseHandler('text', second)).toThrow('already registered') + release() + const releaseSecond = h.controller.registerCloseHandler('text', second) + release() + h.controller.openResource('dsh-resource://file/session/s-test/one') + h.publish() + h.controller.closeIn(SESSION, h.tabOf('one')) + expect(first).not.toHaveBeenCalled() + expect(second).toHaveBeenCalledOnce() + releaseSecond() + h.controller.openResource('dsh-resource://file/session/s-test/two') + h.publish() + const tabId = h.tabOf('two') + h.controller.openTab('guide', { replaceTab: tabId, revealIfOpened: false }) + expect(h.layout().tabs[tabId]).toBeUndefined() + expect(second).toHaveBeenCalledOnce() +}) diff --git a/packages/client/ui-sidebar-terminal/README.i18n.yaml b/packages/client/ui-sidebar-terminal/README.i18n.yaml new file mode 100644 index 0000000000..ad7c48a193 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/client/ui-sidebar-terminal/README.md +README.md: dec4c0a88649b74e90afa23ef07d3f11a9dc67cb +README.zh.md: a10cde602ad4edfd13652a93535c4f5d7fd26b52 diff --git a/packages/client/ui-sidebar-terminal/README.md b/packages/client/ui-sidebar-terminal/README.md new file mode 100644 index 0000000000..dec4c0a886 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/README.md @@ -0,0 +1,80 @@ +--- +description: "Open, recover and control interactive shell tabs in the Web right sidebar." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-client-ui-sidebar-terminal + +English | [中文](README.zh.md) + +## Summary + +Choose an installed shell from the right sidebar's Start page to run commands in the Session workspace. Rename terminals in their tabs and recover retained processes after reloading the page. Collapse the sidebar to keep commands running; close a terminal tab to request process termination. Tab completion follows the shell configuration. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +Open the right sidebar and select **New terminal** on the Start page; first select **New tab** when another content tab is open. Choose an installed **Shell** and select **Start terminal**. The last selected shell is preselected from localStorage; first use or an unavailable saved choice selects the current default. Use the sidebar's **New tab** control to open more terminals. + +Double-click the terminal's tab title to rename it. **Take control** makes the current attachment writable when another page owns input. A failed connection offers **Reconnect**. An exited shell remains visible with its exit code and never restarts automatically. Exited terminals count toward the Session limit; close unused tabs when the limit is reached. + +Closing or replacing a terminal tab removes it immediately and ends its process in the background. A cleanup failure shows a small notification with **Retry**; retrying does not reopen the tab. Collapsing, switching tabs or Sessions, floating and fullscreen presentation preserve the process. + +Displaying a Session after a page reload reopens its retained Host terminals as new tabs. A recovery failure offers **Retry terminal recovery**. A recovered process that disappears reports an error instead of starting another shell. The [sidebar layout remains memory-only](../../client/ui-sidebar-right/README.md#state). + + +## Understand the implementation + +
+Implementation internals — click to expand + +This plugin registers the `terminal` type and body/title seats with the right sidebar. The guide uses a compact dark terminal card, while tab titles retain the line glyph. The React-free terminal model belongs to `api-terminal-controller`; keyed framework hooks expose its state. `ui-primitives` Menu and Button provide the shell picker and startup controls, including keyboard navigation and the selected-item marker. xterm.js and FitAddon render the screen and measure the viewport. The body reserves an 8px gap below the tab strip within the pane height. Input, including Tab and control characters, travels unchanged to the PTY. + +A Session header contribution queries Host terminals and opens recovery tabs. Their navigation parameters carry `terminalId` only within the current page; a recovered view cannot allocate a replacement process. The sidebar's close handler schedules cleanup through the [terminal controller](../../api/terminal-controller/README.md#understand-the-implementation) and returns synchronously. Browser component cleanup and the tab's abort signal only detach browser work. + +
+ + +## Further Exploration + +- [Subprocess](../../subprocess/subprocess/README.md) +- [Right Sidebar](../../client/ui-sidebar-right/README.md) +- [Web terminal decision](../../../.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md) + + +## Model Experience + +None, as this package handles user terminal interaction without adding model input. + +#### KV Cache effect + +None; terminal output travels only between the browser and Host. + +## Known Limitations and Deferred Work + + + +- Shell discovery or native PTY startup can fail. The tab reports the failure without launching a different shell. +- Completion menus and inline suggestions depend on shell configuration. The Web UI adds no independent completion engine. +- Terminal history is bounded. The feature does not send terminal output to the Agent, provide split terminal panes inside a tab, or restore processes after Host restart. + + +### Dev Note + +
+Working context for maintainers — click to expand + +No runtime invariant companion is published. One owner orders terminal metadata and screen updates; the provider exposes no independently observed dimensions to compare. + +
diff --git a/packages/client/ui-sidebar-terminal/README.zh.md b/packages/client/ui-sidebar-terminal/README.zh.md new file mode 100644 index 0000000000..a10cde602a --- /dev/null +++ b/packages/client/ui-sidebar-terminal/README.zh.md @@ -0,0 +1,80 @@ +--- +description: "在 Web 右侧栏打开、恢复和控制交互式 shell 标签页。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-client-ui-sidebar-terminal + +[English](README.md) | 中文 + +## 概述 + +从右侧栏开始页选择已安装的 shell,在会话工作区运行命令。在标签页上重命名终端,并在刷新页面后恢复保留的进程。折叠侧栏让命令继续运行,关闭终端标签页则请求结束进程。Tab 补全使用 shell 的配置。 + +## 目录 + +- [使用此包](#use-this-package) +- [了解实现](#understand-the-implementation) +- [延伸阅读](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与延期工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用此包 + +展开右侧栏,在开始页选择**新建终端**;已有其他标签页时,先选择**新标签页**。选择已安装的 **Shell**,再点击**启动终端**。localStorage 会记住上次选择的 shell 并预选;首次使用或已保存选项不可用时,预选当前默认项。通过侧栏的**新标签页**控件打开更多终端。 + +双击终端标签页标题即可重命名。另一页面持有输入权时,**接管输入**让当前连接可写。连接失败时可以**重新连接**。shell 退出后仍显示屏幕和退出码,不自动重启。 已退出的终端计入 Session 数量上限;达到上限时请关闭不用的标签页。 + +关闭或替换终端标签页会立即移除标签页,并在后台结束进程。清理失败时显示带**重试**操作的轻量通知;重试不会重新打开标签页。折叠、切换标签页或 Session、浮动和全屏都保留进程。 + +刷新页面后显示某个 Session,会把 Host 保留的终端重新打开为新标签页。恢复失败时可点击**重试恢复终端**。恢复目标进程消失时显示错误,不启动另一个 shell。[侧栏布局仅保存在内存中](../../client/ui-sidebar-right/README.zh.md#state)。 + + +## 了解实现 + +
+实现细节 + +插件向右侧栏注册 `terminal` 类型及正文和标题 seat。开始页入口使用紧凑的黑底终端图标,页签标题保留线条图标。无 React 依赖的终端模型属于 `api-terminal-controller`,通过框架 keyed hooks 暴露状态。`ui-primitives` 的 Menu 与 Button 提供 shell 选择和启动控件,支持键盘导航与选中标记。xterm.js 与 FitAddon 负责屏幕渲染和视口测量。正文在面板高度内为页签条下方预留 8px 间距。输入原样传到 PTY,包括 Tab 和控制字符。 + +Session header contribution 查询 Host 终端并打开恢复标签页。导航参数中的 `terminalId` 只在当前页面内保留;恢复视图不能分配替代进程。侧栏关闭 handler 通过[终端 controller](../../api/terminal-controller/README.zh.md#understand-the-implementation)安排清理并同步返回。浏览器组件清理和 tab 的 abort signal 只停止浏览器工作。 + +
+ + +## 延伸阅读 + +- [Subprocess](../../subprocess/subprocess/README.zh.md) +- [Right Sidebar](../../client/ui-sidebar-right/README.zh.md) +- [Web terminal decision](../../../.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md) + + +## 模型体验 + +无;此包只处理用户交互式终端,不向模型请求添加内容。 + +#### KV 缓存影响 + +无;终端输出只在浏览器与 Host 之间传输。 + +## 已知限制与延期工作 + + + +- 默认 shell 或原生 PTY 可能启动失败。标签页显示错误,不启动其他 shell。 +- 补全菜单和内联建议取决于 shell 配置,Web UI 不提供独立补全引擎。 +- 终端历史有上限。此功能不向 Agent 发送终端输出,不在单个标签页内拆分终端,也不在 Host 重启后恢复进程。 + + +### 开发备注 + +
+维护说明 + +不发布运行时 invariant companion。终端元数据与屏幕更新由同一对象按序写入,没有独立的进程尺寸观测可供比较。 + +
diff --git a/packages/client/ui-sidebar-terminal/package.json b/packages/client/ui-sidebar-terminal/package.json new file mode 100644 index 0000000000..0f7ebb30d0 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/package.json @@ -0,0 +1,76 @@ +{ + "name": "@deepseek-ai/dsh-client-ui-sidebar-terminal", + "description": "Interactive shell tabs for the right Sidebar", + "version": "0.1.5-rc.2", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/client/ui-sidebar-terminal" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./client": { + "types": "./lib/types/client/index.d.ts", + "default": "./lib/client.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "dsh": { + "client": { + "inject": [ + "@deepseek-ai/dsh-api-terminal-controller", + "@deepseek-ai/dsh-client-ui-sidebar-right", + "@deepseek-ai/dsh-api-remotes" + ], + "platform": "web" + } + }, + "scripts": { + "bundle": "tsdown", + "watch": "tsdown --watch" + }, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-api-remotes": "workspace:^", + "@deepseek-ai/dsh-client-locale": "workspace:^", + "@deepseek-ai/dsh-client-store": "workspace:^", + "@deepseek-ai/dsh-client-test-runtime": "workspace:^", + "@deepseek-ai/dsh-client-ui-dockkit": "workspace:^", + "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", + "@deepseek-ai/dsh-client-ui-session": "workspace:^", + "@deepseek-ai/dsh-client-ui-sidebar-right": "workspace:^", + "@deepseek-ai/dsh-client-ui-slots": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@testing-library/react": "^16.1.0", + "@types/react": "~18.3.1", + "@types/react-dom": "~18.3.0", + "react": "^18.2.0", + "clsx": "^2.0.0", + "react-dom": "^18.2.0", + "@deepseek-ai/dsh-api-terminal-controller": "workspace:^", + "@xterm/xterm": "^6.0.0", + "@xterm/addon-fit": "^0.11.0", + "@deepseek-ai/dsh-client-ui-layout": "workspace:^", + "@deepseek-ai/dsh-client-ui-conversation": "workspace:^" + }, + "files": [ + "lib/index.js", + "lib/client.js", + "lib/types/**/*.d.ts" + ] +} diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalBody.module.css b/packages/client/ui-sidebar-terminal/src/client/TerminalBody.module.css new file mode 100644 index 0000000000..80fa425b11 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalBody.module.css @@ -0,0 +1,14 @@ +.root { display: flex; flex: 1; flex-direction: column; box-sizing: border-box; height: 100%; min-height: 0; padding-top: 8px; color: var(--dsw-alias-label-primary); font-size: 13px; } +.title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.name { width: 120px; min-width: 48px; max-width: 100%; padding: 0 4px; color: inherit; font: inherit; background: var(--dsw-alias-bg-l1); border: 0.5px solid var(--dsw-alias-border-l3); border-radius: 4px; outline: none; } +.screen { flex: 1; min-height: 0; min-width: 0; overflow: hidden; padding: 8px; background: #17191d; } +.screen :global(.xterm) { height: 100%; } +.status { display: flex; align-items: center; gap: 8px; padding: 5px 12px; background: var(--dsw-alias-bg-l2); } +.status button { padding: 6px 10px; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: 6px; color: inherit; background: transparent; cursor: pointer; } +.status button:hover { background: var(--dsw-alias-bg-l2); } +.error { margin: 0; padding: 10px 12px; overflow-wrap: anywhere; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-l2); } +.launch { display: flex; align-items: end; flex-wrap: wrap; gap: 8px; padding: 12px; } +.shellField { display: flex; flex: 1; flex-direction: column; gap: 6px; min-width: 0; } +.shellMenu { width: 100%; min-width: 0; } +.shellTrigger { width: 100%; min-width: 0; justify-content: space-between; gap: 8px; } +.shellValue { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalBody.tsx b/packages/client/ui-sidebar-terminal/src/client/TerminalBody.tsx new file mode 100644 index 0000000000..d9adf4fae2 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalBody.tsx @@ -0,0 +1,159 @@ +/** Sidebar terminal screen and connection recovery. */ +import { useEffect, useId, useLayoutEffect, useRef, useState, type ReactNode } from 'react' +import { Button, Menu, IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' +import { Terminal } from '@xterm/xterm' +import { FitAddon } from '@xterm/addon-fit' +import type { TerminalViewState, TerminalView } from '@deepseek-ai/dsh-api-terminal-controller/client' +import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type { TerminalInjected } from './face.ts' +import type {} from './locales.ts' +import '@xterm/xterm/css/xterm.css' +import css from './TerminalBody.module.css' + +/** Standard sidebar owner share plus terminal model and localized copy. */ +export type TerminalBodyProps = PropsRuntime<'sidebar.right.pane.tab'> & PropsLocale<'sidebarTerminal'> & InjectFace + +/** + * Offer shell selection for a new terminal or mount a restored screen. + * @param props - sidebar occurrence, model lookup and translated copy. + * @returns the terminal screen and any pending or exceptional state. + */ +export function TerminalBody({ useTabInfo, useTerminal, view, t }: TerminalBodyProps): ReactNode { + const { tab } = useTabInfo() + const model = view(tab.id) + const state = useTerminal(tab.id) + useEffect(() => model.mount(), [model]) + if (state === undefined) return null + const error = state.issue === undefined ? state.error ?? state.info?.error : t(state.issue) + let status: string | undefined + if (state.phase === 'idle' || state.phase === 'loading') status = t('loading') + else if (state.phase === 'creating' || state.phase === 'connecting' || state.phase === 'disconnected') status = t(state.phase) + else if (state.info?.state === 'exited') status = t('exited', { code: String(state.info.exitCode ?? '—') }) + else if (state.info?.state === 'failed') status = t('unavailable') + else if (state.phase === 'closed') status = t('closed') + const retry = state.phase === 'failed' || state.phase === 'disconnected' + const readOnly = state.phase === 'connected' && state.info?.state === 'running' && !state.writable + return ( +
+ {state.phase === 'selecting' && } + {(status !== undefined || retry || readOnly) &&
+ {status} + {readOnly && <>{t('readonly')} } + {retry && (state.info === undefined + ? + : )} +
} + {state.info !== undefined && } + {error !== undefined &&

{t('failed', { message: error })}

} +
+ ) +} + +function TerminalLauncher({ state, model, t }: { + state: TerminalViewState + model: TerminalView + t: TerminalBodyProps['t'] +}): ReactNode { + const [open, setOpen] = useState(false) + const selectionId = useId() + const selected = state.shells?.find(shell => shell.path === state.selectedShell) + return
{ event.preventDefault(); void model.start() }}> +
+ {t('shell')} + ({ id: shell.path, label: `${shell.name} — ${shell.path}` })) ?? []} + selectedId={state.selectedShell} + onClose={() => { setOpen(false) }} + onSelect={(path) => { model.selectShell(path); setOpen(false) }} + anchor={} + /> +
+ +
+} + +/* oxlint-disable typescript/no-non-null-assertion -- React sets the DOM ref, then these effects initialize and use the emulator. */ +function TerminalScreen({ state, model, visible, label }: { + state: TerminalViewState + model: TerminalView + visible: boolean + label: string +}): ReactNode { + const element = useRef(null) + const terminal = useRef() + const fit = useRef() + const lastRevision = useRef(0) + const current = useRef({ state, visible }) + current.current = { state, visible } + + useLayoutEffect(() => { + const node = element.current! + const xterm = new Terminal({ cursorBlink: true, fontSize: 13, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', scrollback: current.current.state.environment?.scrollback ?? 0, theme: { background: '#17191d', foreground: '#e7e9ee' } }) + const addon = new FitAddon() + xterm.loadAddon(addon) + xterm.open(node) + xterm.textarea?.setAttribute('aria-label', label) + terminal.current = xterm + fit.current = addon + lastRevision.current = 0 + const input = xterm.onData((data) => { model.write(data) }) + const measure = (): void => { + if (!current.current.visible || !current.current.state.writable || node.clientWidth === 0 || node.clientHeight === 0) return + fitScreen(xterm, addon, current.current.state, model) + } + const observer = new ResizeObserver(measure) + observer.observe(node) + return () => { + observer.disconnect() + input.dispose() + xterm.dispose() + terminal.current = undefined + fit.current = undefined + } + }, [model]) + + useLayoutEffect(() => { + const xterm = terminal.current! + const render = state.render + if (render === undefined || render.revision <= lastRevision.current) return + lastRevision.current = render.revision + if (render.frame.type === 'snapshot') { + xterm.reset() + xterm.resize(render.frame.info.cols, render.frame.info.rows) + } + xterm.write(render.frame.type === 'snapshot' ? render.frame.screen : render.frame.data, () => { model.acknowledge(render.revision) }) + }, [state.render, model]) + + useLayoutEffect(() => { + const xterm = terminal.current! + xterm.options.disableStdin = !state.writable + if (visible && state.writable && element.current?.clientWidth && element.current.clientHeight) { + fitScreen(xterm, fit.current!, state, model) + } else if (state.info !== undefined && !state.writable) xterm.resize(state.info.cols, state.info.rows) + }, [visible, state.writable, state.info?.cols, state.info?.rows, model]) + useEffect(() => { + terminal.current!.textarea?.setAttribute('aria-label', label) + }, [label]) + useEffect(() => { if (visible && state.writable) terminal.current!.focus() }, [visible, state.writable]) + return
+} +/* oxlint-enable typescript/no-non-null-assertion */ + +function fitScreen(xterm: Terminal, fit: FitAddon, state: TerminalViewState, model: TerminalView): void { + const dimensions = fit.proposeDimensions() + const environment = state.environment + if (dimensions === undefined || environment === undefined) return + const cols = Math.min(dimensions.cols, environment.maxCols) + const rows = Math.min(dimensions.rows, environment.maxRows) + if (cols < 2 || rows < 1) return + xterm.resize(cols, rows) + model.resize(cols, rows) +} diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalCleanup.module.css b/packages/client/ui-sidebar-terminal/src/client/TerminalCleanup.module.css new file mode 100644 index 0000000000..aabc259179 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalCleanup.module.css @@ -0,0 +1,3 @@ +.stack { position: fixed; right: 20px; bottom: 20px; display: grid; gap: 8px; max-width: min(440px, calc(100vw - 40px)); pointer-events: auto; } +.notice { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: 8px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-l1); box-shadow: 0 4px 20px #0002; font-size: 13px; overflow-wrap: anywhere; } +.notice button { flex-shrink: 0; padding: 4px 8px; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: 4px; color: inherit; background: transparent; cursor: pointer; } diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalCleanup.tsx b/packages/client/ui-sidebar-terminal/src/client/TerminalCleanup.tsx new file mode 100644 index 0000000000..97783a36a6 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalCleanup.tsx @@ -0,0 +1,29 @@ +/** Failed background cleanup remains actionable after the originating tab disappears. */ +import type { ReactNode } from 'react' +import type { TerminalCloseFailure } from '@deepseek-ai/dsh-api-terminal-controller/client' +import type { WebTerminalId } from '@deepseek-ai/dsh-api-terminal-controller/types' +import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type {} from '@deepseek-ai/dsh-client-ui-layout/client' +import type {} from './locales.ts' +import css from './TerminalCleanup.module.css' + +/** The model owns cleanup; this seat only shows failures and requests retries. */ +export interface TerminalCleanupInjected { + readonly hooks: { readonly closeFailures: HostObservable } + /** @param id - failed cleanup identity. */ + readonly retryClose: (id: WebTerminalId) => void +} + +/** + * Render failed cleanup tasks without recreating or blocking any sidebar tab. + * @param props - root overlay hooks, retry command and localized copy. + * @returns a compact alert stack, empty when no close has failed. + */ +export function TerminalCleanup({ useCloseFailures, retryClose, t }: PropsRuntime<'shell.overlay'> & PropsLocale<'sidebarTerminal'> & InjectFace): ReactNode { + const failures = useCloseFailures(value => value) + if (failures.length === 0) return null + return
{failures.map(failure =>
+ {t('cleanupFailed', { title: failure.title, message: failure.message })} + +
)}
+} diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx b/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx new file mode 100644 index 0000000000..df5525a08d --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx @@ -0,0 +1,25 @@ +/** Terminal glyphs for the sidebar guide and tab title. */ +import type { ReactNode } from 'react' +import type { IconProps } from '@deepseek-ai/dsh-client-ui-primitives' + +/** + * Render the tab title's terminal prompt in the surrounding text color. + * @returns a decorative sixteen-pixel line glyph. + */ +export function TerminalIcon(): ReactNode { + return +} + +/** + * Render the guide's terminal card within the folder glyph's painted bounds. + * @param props - canvas size and layout class supplied by the guide. + * @returns a dark rounded terminal card with a white prompt. + */ +export function TerminalGuideIcon({ size = 26, className }: IconProps): ReactNode { + return +} diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalRecovery.tsx b/packages/client/ui-sidebar-terminal/src/client/TerminalRecovery.tsx new file mode 100644 index 0000000000..6557af4b3a --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalRecovery.tsx @@ -0,0 +1,29 @@ +/** A Session header lifetime restores retained Host terminals without saving sidebar layout. */ +import { useEffect, useState, type ReactNode } from 'react' +import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' +import type {} from './locales.ts' + +/** The plugin coordinates once-per-page recovery; the component only owns an error notice. */ +export interface TerminalRecoveryInjected { + /** @returns after retained terminals have been opened as sidebar tabs. */ + readonly restore: () => Promise +} + +/** + * Restore terminals when a Session is displayed, with a retry action on lookup failure. + * @param props - Session header lifetime, restoration callback and localized copy. + * @returns nothing on success, or an unobtrusive retry control. + */ +export function TerminalRecovery({ restore, t }: PropsRuntime<'conversation.session.header.actions'> & PropsLocale<'sidebarTerminal'> & TerminalRecoveryInjected): ReactNode { + const [error, setError] = useState() + const [attempt, setAttempt] = useState(0) + useEffect(() => { + let active = true + void restore().then(() => { if (active) setError(undefined) }, (reason: unknown) => { + if (active) setError(reason instanceof Error ? reason.message : String(reason)) + }) + return () => { active = false } + }, [restore, attempt]) + return error === undefined ? null : +} diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalTitle.tsx b/packages/client/ui-sidebar-terminal/src/client/TerminalTitle.tsx new file mode 100644 index 0000000000..59cb1246b2 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalTitle.tsx @@ -0,0 +1,60 @@ +/** Live terminal names in docked and floating tab chrome. */ +import { useLayoutEffect, useRef, useState, type ReactNode } from 'react' +import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import { TerminalIcon } from './TerminalIcon.tsx' +import type { TerminalInjected } from './face.ts' +import type {} from './locales.ts' +import css from './TerminalBody.module.css' + +/** + * Render the terminal name, editable in place on double-click. + * @param props - sidebar occurrence, terminal model and localized copy. + * @returns the terminal icon and current name or its editor. + */ +export function TerminalTitle({ useTabInfo, useTerminal, view, t }: PropsRuntime<'sidebar.right.pane.tab.title'> & PropsLocale<'sidebarTerminal'> & InjectFace): ReactNode { + const { tab } = useTabInfo() + const title = useTerminal(tab.id, state => state?.info?.title ?? state?.title) ?? tab.title + const [editing, setEditing] = useState(false) + const input = useRef(null) + const label = useRef(null) + const cancelled = useRef(false) + useLayoutEffect(() => { + if (editing) return + // Dockkit captures the pointer on its drag handle, which receives the double-click. + const chip = label.current?.closest('[data-dockkit-tab], [data-dockkit-float-grip]') + const rename = (event: Event): void => { + event.stopPropagation() + cancelled.current = false + setEditing(true) + } + chip?.addEventListener('dblclick', rename) + return () => { chip?.removeEventListener('dblclick', rename) } + }, [editing]) + useLayoutEffect(() => { + if (!editing) return + input.current?.focus() + input.current?.select() + }, [editing]) + return <> + + {editing ? { event.stopPropagation() }} + onClick={(event) => { event.stopPropagation() }} + onDoubleClick={(event) => { event.stopPropagation() }} + onBlur={(event) => { + setEditing(false) + const next = event.currentTarget.value.trim() + if (!cancelled.current && next !== '' && next !== title) void view(tab.id).rename(next) + }} + onKeyDown={(event) => { + event.stopPropagation() + // oxlint-disable-next-line typescript/no-deprecated -- Some IMEs report composition only through keyCode 229. + if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return + if (event.key === 'Escape') { + cancelled.current = true + event.currentTarget.blur() + } else if (event.key === 'Enter') event.currentTarget.blur() + }} /> + : {title}} + +} diff --git a/packages/client/ui-sidebar-terminal/src/client/face.ts b/packages/client/ui-sidebar-terminal/src/client/face.ts new file mode 100644 index 0000000000..95920e23b1 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/client/face.ts @@ -0,0 +1,19 @@ +/** Injected terminal commands and keyed observable state. */ +import type { WebTerminalId } from '@deepseek-ai/dsh-api-terminal-controller/types' +import type { TerminalView, TerminalViewState } from '@deepseek-ai/dsh-api-terminal-controller/client' +import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots' + +/** The terminal's React-free model is resolved by sidebar occurrence. */ +export interface TerminalInjected { + /** @param key - sidebar occurrence key. @returns its terminal commands. */ + readonly view: (key: string) => TerminalView + readonly keyedHooks: { readonly terminal: (key: string) => HostObservable } +} + + +declare module '@deepseek-ai/dsh-client-ui-sidebar-right/client' { + interface SidebarRightTabParamsMap { + /** An existing Host terminal selected from the Session terminal list. */ + terminal: { terminalId: WebTerminalId } + } +} diff --git a/packages/client/ui-sidebar-terminal/src/client/index.ts b/packages/client/ui-sidebar-terminal/src/client/index.ts new file mode 100644 index 0000000000..cf71fb2f87 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/client/index.ts @@ -0,0 +1,81 @@ +/** Register interactive terminal tabs and explicit process cleanup with the sidebar. */ +import type { Context } from '@deepseek-ai/cordis' +import type { WebTerminalId } from '@deepseek-ai/dsh-api-terminal-controller/types' +import type { TabId } from '@deepseek-ai/dsh-client-ui-sidebar-right/client' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type {} from '@deepseek-ai/dsh-api-terminal-controller/client' +import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client' +import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' +import type {} from '@deepseek-ai/dsh-client-locale/client' +import type {} from '@deepseek-ai/dsh-client-ui-session/client' +import { TerminalGuideIcon } from './TerminalIcon.tsx' +import { TerminalBody } from './TerminalBody.tsx' +import { TerminalTitle } from './TerminalTitle.tsx' +import { TerminalRecovery, type TerminalRecoveryInjected } from './TerminalRecovery.tsx' +import { TerminalCleanup, type TerminalCleanupInjected } from './TerminalCleanup.tsx' +import type { TerminalInjected } from './face.ts' +import { en, zh } from './locales.ts' + +/** Services needed by the terminal's two sidebar seats. */ +export const inject = ['slots', 'locale', 'sidebarRight', 'sidebarRightTabs', 'webTerminals'] + +/** + * Register the terminal type, observable views and background process cleanup. + * @param ctx - Client root Context with sidebar and terminal services. + */ +export function apply(ctx: Context): void { + let disposed = false + const recovered = new Map>() + ctx.effect(() => () => { disposed = true; recovered.clear() }, 'ui-sidebar-terminal.lifetime') + const terminalId = (sessionId: SessionId, key: string): WebTerminalId | undefined => { + const params = ctx.sidebarRight.tabDomain.occurrence(sessionId, { id: key as TabId }).navigation.getSnapshot().params + return (params as { terminalId?: WebTerminalId } | undefined)?.terminalId + } + const view = (sessionId: SessionId, key: string) => ctx.webTerminals.view(sessionId, key, terminalId(sessionId, key)) + const namespace = 'sidebarTerminal' + const id = '@deepseek-ai/dsh-client-ui-sidebar-terminal' + const t = ctx.locale.bind(namespace) + ctx.effect(() => ctx.locale.register(namespace, { zh, en }), 'ui-sidebar-terminal.copy') + ctx.effect(() => ctx.sidebarRightTabs.register({ + id, kind: 'terminal', multiple: true, priority: 'builtin', title: () => t('title'), + guide: [{ order: 20, title: () => t('new'), description: () => t('description'), icon: TerminalGuideIcon }], + }), 'ui-sidebar-terminal.type') + ctx.effect(() => ctx.sidebarRight.registerCloseHandler('terminal', (sessionId, tab) => { + ctx.webTerminals.close(sessionId, tab.id, terminalId(sessionId, tab.id)) + }), 'ui-sidebar-terminal.close') + const inject = (sessionId: SessionId): TerminalInjected => ({ + view: key => view(sessionId, key), + keyedHooks: { terminal: key => view(sessionId, key).state }, + }) + ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register( + { name: 'sidebar.right.pane.tab', key: id, locale: namespace, inject }, TerminalBody, + )), 'ui-sidebar-terminal.body') + ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register( + { name: 'sidebar.right.pane.tab.title', key: id, locale: namespace, inject }, TerminalTitle, + )), 'ui-sidebar-terminal.title') + ctx.effect(() => ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({ + name: 'conversation.session.header.actions', id, locale: namespace, + inject: (sessionId): TerminalRecoveryInjected => ({ + restore: () => { + let pending = recovered.get(sessionId) + if (pending === undefined) { + pending = ctx.webTerminals.recover(sessionId).then((terminals) => { + if (disposed) return + for (const info of terminals) ctx.sidebarRight.openTabIn(sessionId, 'terminal', { + params: { terminalId: info.id }, + }) + }).catch((error: unknown) => { recovered.delete(sessionId); throw error }) + recovered.set(sessionId, pending) + } + return pending + }, + }), + }, TerminalRecovery)), 'ui-sidebar-terminal.recovery') + ctx.effect(() => ctx.slots.inject('shell.overlay', () => ctx.slots.register({ + name: 'shell.overlay', id, locale: namespace, + inject: (): TerminalCleanupInjected => ({ + hooks: { closeFailures: ctx.webTerminals.closeFailures }, + retryClose: (terminalId) => { ctx.webTerminals.retryClose(terminalId) }, + }), + }, TerminalCleanup)), 'ui-sidebar-terminal.cleanup') +} diff --git a/packages/client/ui-sidebar-terminal/src/client/locales.ts b/packages/client/ui-sidebar-terminal/src/client/locales.ts new file mode 100644 index 0000000000..d6ee5a5d17 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/client/locales.ts @@ -0,0 +1,42 @@ +/** Copy owned by the sidebar terminal feature. */ +import type {} from '@deepseek-ai/dsh-client-ui-slots' + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface LocaleNamespaceMap { + sidebarTerminal: keyof typeof zh + } +} + +/** Simplified Chinese terminal copy. */ +export const zh = { + recoveryFailed: '恢复终端失败:{message}', retryRecovery: '重试恢复终端', + shell: 'Shell', start: '启动终端', description: '在会话工作区运行命令', + title: '终端', new: '新建终端', loading: '正在读取终端环境…', creating: '正在启动…', + connecting: '正在连接…', disconnected: '连接已断开。', reconnect: '重新连接', + readonly: '此页面当前只读。', control: '接管输入', + closed: '终端已关闭。', exited: '进程已退出({code})', failed: '终端错误:{message}', + rename: '终端名称', unavailable: '不可用', retry: '重试', + cleanupFailed: '终端「{title}」未能结束:{message}', + missingTerminal: '此终端已不存在,请新建终端。', + inputFull: '输入缓冲区已满,请重新连接后重试。', + attachmentEnded: '终端连接已结束,请重新连接。', + invalidOutput: '终端画面传输异常,请重新连接。', + terminalLimit: '终端数量已达上限,请关闭不用的终端后重试。已退出的终端也计入数量。', +} satisfies Record + +/** English terminal copy. */ +export const en = { + recoveryFailed: 'Terminal recovery failed: {message}', retryRecovery: 'Retry terminal recovery', + shell: 'Shell', start: 'Start terminal', description: 'Run commands in the Session workspace', + title: 'Terminal', new: 'New terminal', loading: 'Reading terminal environment…', creating: 'Starting…', + connecting: 'Connecting…', disconnected: 'Disconnected.', reconnect: 'Reconnect', + readonly: 'This view is read-only.', control: 'Take control', + closed: 'Terminal closed.', exited: 'Process exited ({code})', failed: 'Terminal error: {message}', + rename: 'Terminal name', unavailable: 'Unavailable', retry: 'Retry', + cleanupFailed: 'Terminal “{title}” could not be ended: {message}', + missingTerminal: 'This terminal no longer exists. Open a new terminal.', + inputFull: 'The input buffer is full. Reconnect and try again.', + attachmentEnded: 'The terminal connection ended. Reconnect to continue.', + invalidOutput: 'The terminal screen could not be received. Reconnect to recover it.', + terminalLimit: 'The terminal limit has been reached. Close unused terminals and try again. Exited terminals also count toward the limit.', +} satisfies Record diff --git a/packages/client/ui-sidebar-terminal/src/css-modules.d.ts b/packages/client/ui-sidebar-terminal/src/css-modules.d.ts new file mode 100644 index 0000000000..bc5e482353 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/css-modules.d.ts @@ -0,0 +1,6 @@ +declare module '*.module.css' { + const classes: Record + export default classes +} + +declare module '*.css' diff --git a/packages/client/ui-sidebar-terminal/src/index.ts b/packages/client/ui-sidebar-terminal/src/index.ts new file mode 100644 index 0000000000..d7b0a4904f --- /dev/null +++ b/packages/client/ui-sidebar-terminal/src/index.ts @@ -0,0 +1,4 @@ +/** Host companion for the interactive terminal Client plugin. */ + +/** Mount the browser-only plugin through the Client loader. */ +export function apply(): void {} diff --git a/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts b/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts new file mode 100644 index 0000000000..dbbd4fa040 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts @@ -0,0 +1,182 @@ +// @vitest-environment jsdom +/** Terminal type, copy, seats and explicit cleanup follow the plugin lifetime. */ +import { createElement } from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import { Context } from '@deepseek-ai/cordis' +import { expect, it, vi } from 'vitest' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { WebTerminalId, WebTerminalInfo } from '@deepseek-ai/dsh-api-terminal-controller/types' +import { SidebarRightTabRegistry } from '@deepseek-ai/dsh-client-ui-sidebar-right/src/client/tab-registry.ts' +import type { SidebarRightCloseHandler } from '@deepseek-ai/dsh-client-ui-sidebar-right/client' +import { apply, inject } from '../src/client/index.ts' +import { apply as hostApply } from '../src/index.ts' +import { TerminalBody } from '../src/client/TerminalBody.tsx' +import { TerminalTitle } from '../src/client/TerminalTitle.tsx' +import { TerminalRecovery, type TerminalRecoveryInjected } from '../src/client/TerminalRecovery.tsx' +import { TerminalCleanup, type TerminalCleanupInjected } from '../src/client/TerminalCleanup.tsx' +import type { TerminalInjected } from '../src/client/face.ts' +import { en, zh } from '../src/client/locales.ts' + +vi.mock('@xterm/xterm', () => ({ Terminal: vi.fn() })) + +const terminalInfo = (id: string): WebTerminalInfo => ({ id: id as WebTerminalId, title: id, shell: { path: '/bin/sh', name: 'sh', args: ['-i'] }, cwd: '/workspace', cols: 80, rows: 24, state: 'running', exitCode: null }) + +async function mountPlugin() { + const ctx = new Context() + const tabs = new SidebarRightTabRegistry(ctx) + ctx.provide('sidebarRightTabs', tabs) + const entries: { + name: string + key?: string + id?: string + locale: string + component: unknown + inject: (id: SessionId) => unknown + }[] = [] + const dictionaries = new Map() + let closeHandler: SidebarRightCloseHandler | undefined + const model = { state: {} } + const terminals = { + view: vi.fn(() => model), close: vi.fn(), closeFailures: {}, retryClose: vi.fn(), + recover: vi.fn(async (_sessionId: SessionId): Promise => []), + } + let params: { terminalId: WebTerminalId } | undefined + const occurrence = vi.fn(() => ({ navigation: { getSnapshot: () => ({ params }) } })) + const openTabIn = vi.fn() + ctx.provide('webTerminals', terminals as never) + ctx.provide('sidebarRight', { + tabDomain: { occurrence }, openTabIn, + registerCloseHandler: (kind: string, handler: SidebarRightCloseHandler) => { expect(kind).toBe('terminal'); closeHandler = handler; return () => { closeHandler = undefined } }, + } as never) + ctx.provide('slots', { + inject: (_name: string, register: () => () => void) => register(), + register: (options: Omit, component: unknown) => { const entry = { ...options, component }; entries.push(entry); return () => { entries.splice(entries.indexOf(entry), 1) } }, + } as never) + ctx.provide('locale', { + bind: () => (key: string) => key, + register: (name: string, values: unknown) => { dictionaries.set(name, values); return () => { dictionaries.delete(name) } }, + } as never) + const fiber = await ctx.plugin({ inject, apply }) + return { + tabs, entries, dictionaries, terminals, model, occurrence, openTabIn, + get closeHandler() { return closeHandler }, + setParams(next: typeof params) { params = next }, + async dispose() { await fiber.dispose(); await ctx.fiber.dispose() }, + } +} + +it('registers terminal views, recovery and cleanup, then releases every contribution on unload', async () => { + expect(hostApply).not.toThrow() + const h = await mountPlugin() + try { + const definition = h.tabs.get('terminal')! + expect(definition.title('sidebar://terminal')).toBe('title') + expect(definition.guide?.map(entry => [entry.order, entry.title(), entry.description?.()])).toEqual([[20, 'new', 'description']]) + const Icon = definition.guide?.[0]?.icon + if (Icon === undefined) throw new Error('Terminal guide icon was not registered') + expect(renderToStaticMarkup(createElement(Icon, { size: 22 }))).toContain('width="22"') + expect(renderToStaticMarkup(createElement(Icon))).toContain('width="26"') + expect(definition.multiple).toBe(true) + expect(h.dictionaries.get('sidebarTerminal')).toEqual({ en, zh }) + expect(h.entries.map(entry => [entry.name, entry.component, entry.locale])).toEqual([ + ['sidebar.right.pane.tab', TerminalBody, 'sidebarTerminal'], + ['sidebar.right.pane.tab.title', TerminalTitle, 'sidebarTerminal'], + ['conversation.session.header.actions', TerminalRecovery, 'sidebarTerminal'], + ['shell.overlay', TerminalCleanup, 'sidebarTerminal'], + ]) + const sessionId = 'session' as SessionId + const face = h.entries[0]!.inject(sessionId) as TerminalInjected + expect(face.view('tab')).toBe(h.model) + expect(h.terminals.view).toHaveBeenLastCalledWith(sessionId, 'tab', undefined) + const terminalId = 'retained' as WebTerminalId + h.setParams({ terminalId }) + expect(face.keyedHooks.terminal('tab')).toBe(h.model.state) + expect(h.terminals.view).toHaveBeenLastCalledWith(sessionId, 'tab', terminalId) + expect(h.occurrence).toHaveBeenLastCalledWith(sessionId, { id: 'tab' }) + if (h.closeHandler === undefined) throw new Error('Terminal close handler was not registered') + h.closeHandler(sessionId, { id: 'tab' } as Parameters[1]) + expect(h.terminals.close).toHaveBeenLastCalledWith(sessionId, 'tab', terminalId) + h.setParams(undefined) + h.closeHandler(sessionId, { id: 'new-tab' } as Parameters[1]) + expect(h.terminals.close).toHaveBeenLastCalledWith(sessionId, 'new-tab', undefined) + const cleanupFace = h.entries[3]!.inject(sessionId) as TerminalCleanupInjected + expect(cleanupFace.hooks.closeFailures).toBe(h.terminals.closeFailures) + cleanupFace.retryClose('terminal' as Parameters[0]) + expect(h.terminals.retryClose).toHaveBeenCalledWith('terminal') + } finally { + await h.dispose() + } + expect(h.closeHandler).toBeUndefined() + expect(h.tabs.get('terminal')).toBeUndefined() + expect(h.entries).toEqual([]) + expect(h.dictionaries.size).toBe(0) +}) + +it('shares pending and completed recovery across Session headers and opens each returned terminal once', async () => { + const h = await mountPlugin() + const pending = Promise.withResolvers() + h.terminals.recover.mockImplementationOnce(() => pending.promise) + const sessionId = 'session' as SessionId + const recovery = h.entries[2]!.inject(sessionId) as TerminalRecoveryInjected + const remounted = h.entries[2]!.inject(sessionId) as TerminalRecoveryInjected + const completion = recovery.restore() + try { + expect(remounted.restore()).toBe(completion) + expect(h.terminals.recover).toHaveBeenCalledExactlyOnceWith(sessionId) + expect(h.openTabIn).not.toHaveBeenCalled() + pending.resolve([terminalInfo('build'), terminalInfo('tests')]) + await completion + expect(h.openTabIn.mock.calls).toEqual([ + [sessionId, 'terminal', { params: { terminalId: 'build' } }], + [sessionId, 'terminal', { params: { terminalId: 'tests' } }], + ]) + await remounted.restore() + expect(h.terminals.recover).toHaveBeenCalledTimes(1) + expect(h.openTabIn).toHaveBeenCalledTimes(2) + const otherSession = 'other-session' as SessionId + await (h.entries[2]!.inject(otherSession) as TerminalRecoveryInjected).restore() + expect(h.terminals.recover).toHaveBeenLastCalledWith(otherSession) + expect(h.terminals.recover).toHaveBeenCalledTimes(2) + expect(h.openTabIn).toHaveBeenCalledTimes(2) + } finally { + pending.resolve([]) + await completion + await h.dispose() + } +}) + +it('allows a failed Host lookup to be retried without marking the Session recovered', async () => { + const h = await mountPlugin() + const unavailable = new Error('Host unavailable') + h.terminals.recover.mockRejectedValueOnce(unavailable) + const sessionId = 'session' as SessionId + const recovery = h.entries[2]!.inject(sessionId) as TerminalRecoveryInjected + try { + await expect(recovery.restore()).rejects.toBe(unavailable) + expect(h.openTabIn).not.toHaveBeenCalled() + await recovery.restore() + await recovery.restore() + expect(h.terminals.recover).toHaveBeenCalledTimes(2) + } finally { + await h.dispose() + } +}) + +it('does not open retained terminals when their lookup completes after plugin unload', async () => { + const h = await mountPlugin() + const pending = Promise.withResolvers() + h.terminals.recover.mockImplementationOnce(() => pending.promise) + const recovery = h.entries[2]!.inject('session' as SessionId) as TerminalRecoveryInjected + const completion = recovery.restore() + try { + expect(h.terminals.recover).toHaveBeenCalledOnce() + await h.dispose() + pending.resolve([terminalInfo('build')]) + await completion + expect(h.openTabIn).not.toHaveBeenCalled() + } finally { + pending.resolve([]) + await completion + await h.dispose() + } +}) diff --git a/packages/client/ui-sidebar-terminal/tests/cleanup.client.spec.tsx b/packages/client/ui-sidebar-terminal/tests/cleanup.client.spec.tsx new file mode 100644 index 0000000000..fb1f61aab4 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/tests/cleanup.client.spec.tsx @@ -0,0 +1,53 @@ +// @vitest-environment jsdom +/** Cleanup errors remain reachable after the terminal tab has disappeared. */ +import { afterEach, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render, within } from '@testing-library/react' +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import type { TerminalCloseFailure } from '@deepseek-ai/dsh-api-terminal-controller/client' +import type { WebTerminalId } from '@deepseek-ai/dsh-api-terminal-controller/types' +import { TerminalCleanup } from '../src/client/TerminalCleanup.tsx' +import { en } from '../src/client/locales.ts' + +afterEach(cleanup) + +type CleanupProps = Parameters[0] +const unusedHook = (): never => { throw new Error('Terminal cleanup does not consume global hooks') } +const standard: Omit = { + useSessions: unusedHook, useSessionPendingInteraction: unusedHook, usePanelInfo: unusedHook, + useWorkspaces: unusedHook, useResource: unusedHook, +} + +it('stays absent for ordinary closes and offers a retry only after cleanup fails', () => { + let failures: readonly TerminalCloseFailure[] = [] + const retryClose = vi.fn() + const useCloseFailures: CleanupProps['useCloseFailures'] = selector => selector(failures) + const props: CleanupProps = { ...standard, useCloseFailures, retryClose, t: makeTranslate(en) } + const view = render() + expect(view.container.childElementCount).toBe(0) + failures = [{ id: 'terminal' as WebTerminalId, title: 'Build', message: 'Host unavailable' }] + view.rerender() + expect(view.getByRole('alert').textContent).toContain('Terminal “Build” could not be ended: Host unavailable') + fireEvent.click(view.getByRole('button', { name: en.retry })) + expect(retryClose).toHaveBeenCalledWith('terminal') + failures = [] + view.rerender() + expect(view.container.childElementCount).toBe(0) +}) + +it('keeps cleanup failures separate and retries the terminal selected by the user', () => { + const failures: readonly TerminalCloseFailure[] = [ + { id: 'build' as WebTerminalId, title: 'Build', message: 'Host unavailable' }, + { id: 'tests' as WebTerminalId, title: 'Tests', message: 'Process did not exit' }, + ] + const retryClose = vi.fn() + const useCloseFailures: CleanupProps['useCloseFailures'] = selector => selector(failures) + const props: CleanupProps = { ...standard, useCloseFailures, retryClose, t: makeTranslate(en) } + const view = render() + const notices = view.getAllByRole('alert') + expect(notices.map(notice => notice.textContent)).toEqual([ + 'Terminal “Build” could not be ended: Host unavailableRetry', + 'Terminal “Tests” could not be ended: Process did not exitRetry', + ]) + fireEvent.click(within(notices[1]!).getByRole('button', { name: en.retry })) + expect(retryClose).toHaveBeenCalledExactlyOnceWith('tests') +}) diff --git a/packages/client/ui-sidebar-terminal/tests/recovery.client.spec.tsx b/packages/client/ui-sidebar-terminal/tests/recovery.client.spec.tsx new file mode 100644 index 0000000000..853ea8ff3e --- /dev/null +++ b/packages/client/ui-sidebar-terminal/tests/recovery.client.spec.tsx @@ -0,0 +1,83 @@ +// @vitest-environment jsdom +/** Session terminal recovery hides ordinary work and keeps failed lookup retryable. */ +import { afterEach, expect, it, vi } from 'vitest' +import { act, cleanup, fireEvent, render } from '@testing-library/react' +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { TerminalRecovery } from '../src/client/TerminalRecovery.tsx' +import { en } from '../src/client/locales.ts' + +afterEach(cleanup) + +function props(restore: () => Promise): Parameters[0] { + // The renderer supplies Session shares; recovery needs only the injected callback and copy. + return { restore, t: makeTranslate(en) } as Parameters[0] +} + +it('restores on mount without adding controls during a successful lookup', async () => { + const pending = Promise.withResolvers() + const restore = vi.fn(() => pending.promise) + const view = render() + try { + expect(restore).toHaveBeenCalledOnce() + expect(view.container.childElementCount).toBe(0) + await act(async () => { pending.resolve(undefined); await pending.promise }) + view.rerender() + expect(restore).toHaveBeenCalledOnce() + expect(view.container.childElementCount).toBe(0) + } finally { + await act(async () => { pending.resolve(undefined); await pending.promise }) + } +}) + +it.each([new Error('Host unavailable'), 'Host unavailable'])('offers recovery retry for a failed lookup: %s', async (reason) => { + const pending = Promise.withResolvers() + const restore = vi.fn<() => Promise>().mockRejectedValueOnce(reason).mockImplementationOnce(() => pending.promise) + const view = render() + try { + const retry = await view.findByRole('button', { name: en.retryRecovery }) + expect(retry.getAttribute('title')).toBe('Terminal recovery failed: Host unavailable') + fireEvent.click(retry) + expect(restore).toHaveBeenCalledTimes(2) + expect(view.container.childElementCount).toBe(0) + await act(async () => { pending.resolve(undefined); await pending.promise }) + expect(view.container.childElementCount).toBe(0) + } finally { + await act(async () => { pending.resolve(undefined); await pending.promise }) + } +}) + +it('keeps the current Session failure when an earlier Session lookup later succeeds', async () => { + const pending = Promise.withResolvers() + const previous = vi.fn(() => pending.promise) + const current = vi.fn(async () => { throw new Error('Current Session failed') }) + const view = render() + try { + view.rerender() + const retry = await view.findByRole('button', { name: en.retryRecovery }) + expect(retry.getAttribute('title')).toContain('Current Session failed') + await act(async () => { pending.resolve(undefined); await pending.promise }) + expect(view.getByRole('button', { name: en.retryRecovery }).getAttribute('title')).toContain('Current Session failed') + } finally { + await act(async () => { pending.resolve(undefined); await pending.promise }) + } +}) + +it('ignores an earlier Session failure after a replacement Session recovers', async () => { + const pending = Promise.withResolvers() + const previous = vi.fn(() => pending.promise) + const completed = Promise.resolve() + const current = vi.fn(() => completed) + const view = render() + try { + view.rerender() + await act(async () => { await completed }) + await act(async () => { + pending.reject(new Error('Previous Session failed')) + await expect(pending.promise).rejects.toThrow('Previous Session failed') + }) + expect(view.container.childElementCount).toBe(0) + } finally { + pending.resolve(undefined) + await Promise.allSettled([pending.promise]) + } +}) diff --git a/packages/client/ui-sidebar-terminal/tests/terminal-body.client.spec.tsx b/packages/client/ui-sidebar-terminal/tests/terminal-body.client.spec.tsx new file mode 100644 index 0000000000..7070b43df9 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/tests/terminal-body.client.spec.tsx @@ -0,0 +1,333 @@ +// @vitest-environment jsdom +/** Terminal startup, title editing and xterm's screen lifetime. */ +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/react' +import type { ReactNode } from 'react' +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import type { TerminalViewState } from '@deepseek-ai/dsh-api-terminal-controller/client' +import type { WebTerminalId } from '@deepseek-ai/dsh-api-terminal-controller/types' +import { TerminalBody, type TerminalBodyProps } from '../src/client/TerminalBody.tsx' +import { TerminalTitle } from '../src/client/TerminalTitle.tsx' +import { en, zh } from '../src/client/locales.ts' + +const fake = vi.hoisted(() => ({ + terminals: [] as FakeTerminal[], + dimensions: { cols: 120, rows: 40 } as { cols: number; rows: number } | undefined, +})) +class FakeTerminal { + options: { disableStdin?: boolean } + textarea: HTMLTextAreaElement | undefined = document.createElement('textarea') + input: ((data: string) => void) | undefined + readonly disposeInput = vi.fn() + readonly resize = vi.fn() + readonly reset = vi.fn() + readonly focus = vi.fn() + readonly dispose = vi.fn() + readonly write = vi.fn((_data: string, callback: () => void) => { callback() }) + readonly loadAddon = vi.fn() + constructor(options: object) { this.options = options; fake.terminals.push(this) } + open(node: HTMLElement) { node.appendChild(this.textarea!) } + onData(input: (data: string) => void) { this.input = input; return { dispose: this.disposeInput } } +} +vi.mock('@xterm/xterm', () => ({ Terminal: vi.fn(function (options: object) { return new FakeTerminal(options) }) })) +vi.mock('@xterm/addon-fit', () => ({ FitAddon: class { proposeDimensions() { return fake.dimensions } } })) + +let measure: (() => void) | undefined +const disconnect = vi.fn() +let boxWidth = 800 +let boxHeight = 600 +beforeEach(() => { + fake.terminals.length = 0 + fake.dimensions = { cols: 120, rows: 40 } + boxWidth = 800 + boxHeight = 600 + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => boxWidth) + vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => boxHeight) + vi.stubGlobal('ResizeObserver', class { constructor(callback: () => void) { measure = callback } observe() {} disconnect = disconnect }) +}) +afterEach(() => { cleanup(); vi.restoreAllMocks(); vi.unstubAllGlobals(); disconnect.mockClear(); measure = undefined }) + +const info = { id: 'terminal' as WebTerminalId, title: 'bash', shell: { path: '/bin/bash', name: 'bash', args: ['-i'] }, cwd: '/workspace', cols: 80, rows: 24, state: 'running' as const, exitCode: null } +const environment = { cwd: '/workspace', maxCols: 200, maxRows: 100, scrollback: 1000, maxInputBytes: 1000 } +const idle: TerminalViewState = { phase: 'idle', writable: false, environment } +const titleSurfaces = [ + { + name: 'docked tab', selector: '[data-dockkit-tab]', + wrap: (title: ReactNode) =>
{title}
, + }, + { + name: 'floating grip', selector: '[data-dockkit-float-grip]', + wrap: (title: ReactNode) =>
{title}
, + }, +] +function mount(initial: TerminalViewState | undefined = idle, dictionary = en) { + let state: TerminalViewState | undefined = initial + let visible = true + const detach = vi.fn() + const model = { + start: vi.fn(async () => {}), selectShell: vi.fn(), + mount: vi.fn(() => detach), refresh: vi.fn(async () => {}), + rename: vi.fn(async () => {}), connect: vi.fn(), write: vi.fn(), resize: vi.fn(), acknowledge: vi.fn(), + } + const openTab = vi.fn() + const tab = () => ({ tab: { id: 'tab', title: 'Terminal', visible, actions: { openTab } } }) + // The test supplies the owner and model hooks consumed here; the remaining slot props are framework-owned. + const props = { + view: () => model, + useTerminal: (_key: string, select?: (value: TerminalViewState | undefined) => unknown) => select === undefined ? state : select(state), + useTabInfo: tab, t: makeTranslate(dictionary), + } as unknown as TerminalBodyProps + const view = render() + return { + view, props, model, detach, openTab, + update(next: TerminalViewState | undefined, shown = visible) { + state = next; visible = shown; view.rerender() + }, + } +} + +it('mounts automatic startup and offers retry only when startup fails', () => { + const h = mount({ ...idle, phase: 'loading', environment: undefined }) + expect(h.model.mount).toHaveBeenCalledOnce() + expect(h.view.getByRole('status').textContent).toBe(en.loading) + expect(h.view.queryByRole('combobox')).toBeNull() + expect(h.view.queryByRole('button')).toBeNull() + h.update({ ...idle, phase: 'failed', error: 'unavailable' }) + expect(h.view.getByRole('alert').textContent).toContain('unavailable') + fireEvent.click(h.view.getByRole('button', { name: en.retry })) + expect(h.model.refresh).toHaveBeenCalledOnce() + h.update({ ...idle, phase: 'disconnected' }) + fireEvent.click(h.view.getByRole('button', { name: en.retry })) + expect(h.model.refresh).toHaveBeenCalledTimes(2) + h.update({ ...idle, phase: 'creating' }) + expect(h.view.getByRole('status').textContent).toBe(en.creating) + h.update({ ...idle, phase: 'closing' }) + expect(h.view.queryByRole('status')).toBeNull() + expect(h.view.queryByRole('button')).toBeNull() + h.update(undefined) + expect(h.view.container.childElementCount).toBe(0) + expect(h.model.mount).toHaveBeenCalledOnce() +}) + +it('shows only the terminal screen while connected and detaches on unmount', () => { + const h = mount({ ...idle, info, phase: 'connected', writable: true }) + expect(h.view.getByRole('textbox', { name: en.title })).toBeDefined() + expect(h.view.queryByRole('status')).toBeNull() + expect(h.view.queryByRole('button')).toBeNull() + expect(h.view.queryByRole('combobox')).toBeNull() + expect(h.view.queryByRole('heading')).toBeNull() + expect(h.view.queryByText('/workspace')).toBeNull() + expect(h.view.container.querySelector('header')).toBeNull() + h.view.unmount() + expect(h.detach).toHaveBeenCalledOnce() +}) + +it('keeps one emulator across rename and locale updates, applies snapshots and output once, and releases listeners', () => { + const snapshot: TerminalViewState = { ...idle, info, phase: 'connected', writable: true, render: { revision: 1, frame: { type: 'snapshot', sequence: 0, info, screen: 'restored' } } } + const h = mount(snapshot) + const terminal = fake.terminals[0]! + expect(terminal.reset).toHaveBeenCalledOnce() + expect(terminal.write).toHaveBeenCalledWith('restored', expect.any(Function)) + expect(h.model.acknowledge).toHaveBeenCalledWith(1) + expect(terminal.focus).toHaveBeenCalledOnce() + terminal.input?.('help\t') + expect(h.model.write).toHaveBeenCalledWith('help\t') + h.update({ ...snapshot, render: { revision: 2, frame: { type: 'output', sequence: 1, data: 'live' } } }) + expect(terminal.write).toHaveBeenCalledWith('live', expect.any(Function)) + h.update({ ...snapshot, render: { revision: 2, frame: { type: 'output', sequence: 1, data: 'duplicate' } } }) + expect(terminal.write).toHaveBeenCalledTimes(2) + h.update({ ...snapshot, info: { ...info, title: 'Development' } }) + h.view.rerender() + expect(fake.terminals).toHaveLength(1) + expect(terminal.textarea?.getAttribute('aria-label')).toBe(zh.title) + h.view.unmount() + expect(terminal.dispose).toHaveBeenCalledOnce() + expect(terminal.disposeInput).toHaveBeenCalledOnce() + expect(disconnect).toHaveBeenCalledOnce() +}) + +it('displays disconnect, close and exit states and offers explicit reconnection and takeover', () => { + const h = mount({ ...idle, info, phase: 'connecting' }) + expect(h.view.getByRole('status').textContent).toBe(en.connecting) + h.update({ ...idle, info, phase: 'connected' }) + expect(h.view.getByRole('status').textContent).toContain(en.readonly) + expect(fake.terminals[0]!.options.disableStdin).toBe(true) + fireEvent.click(h.view.getByRole('button', { name: en.control })) + expect(h.model.connect).toHaveBeenCalledOnce() + for (const phase of ['disconnected', 'failed'] as const) { + h.update({ ...idle, info, phase }) + fireEvent.click(h.view.getByRole('button', { name: en.reconnect })) + } + expect(h.model.connect).toHaveBeenCalledTimes(3) + h.update({ ...idle, info, phase: 'closing' }) + expect(h.view.queryByRole('status')).toBeNull() + h.update({ ...idle, info, phase: 'closed' }) + expect(h.view.getByRole('status').textContent).toBe(en.closed) + h.update({ ...idle, info: { ...info, state: 'exited', exitCode: 5 }, phase: 'connected' }) + expect(h.view.getByRole('status').textContent).toBe('Process exited (5)') + h.update({ ...idle, info: { ...info, state: 'exited', error: 'provider stopped' }, phase: 'connected' }) + expect(h.view.getByRole('status').textContent).toBe('Process exited (—)') + expect(h.view.getByRole('alert').textContent).toContain('provider stopped') + h.update({ ...idle, info: { ...info, state: 'failed', error: 'provider unreachable' }, phase: 'connected' }) + expect(h.view.getByRole('status').textContent).toBe(en.unavailable) +}) + +it('fits only visible writable terminals with measurable dimensions and clamps the provider limits', () => { + const state: TerminalViewState = { ...idle, info, phase: 'connected', writable: true } + const h = mount(state) + const terminal = fake.terminals[0]! + expect(h.model.resize).toHaveBeenCalledWith(120, 40) + h.model.resize.mockClear() + fake.dimensions = { cols: 500, rows: 300 } + measure?.() + expect(h.model.resize).toHaveBeenCalledWith(200, 100) + h.model.resize.mockClear() + h.update(state, false) + measure?.() + h.update({ ...state, writable: false }, true) + measure?.() + expect(h.model.resize).not.toHaveBeenCalled() + expect(terminal.resize).toHaveBeenLastCalledWith(80, 24) + boxWidth = 0 + h.update(state) + measure?.() + boxWidth = 800; boxHeight = 0 + measure?.() + h.update({ ...state, info: { ...info, cols: 81 } }) + expect(h.model.resize).not.toHaveBeenCalled() + boxHeight = 600 + fake.dimensions = undefined + measure?.() + h.update({ ...state, environment: undefined }) + fake.dimensions = { cols: 40, rows: 20 } + measure?.() + h.update(state) + for (const dimensions of [{ cols: 1, rows: 20 }, { cols: 40, rows: 0 }]) { fake.dimensions = dimensions; measure?.() } + expect(h.model.resize).not.toHaveBeenCalled() +}) + +it.each(titleSurfaces)('shows saved and attached terminal names in the $name', (surface) => { + const h = mount(idle) + const chip = () => surface.wrap() + const title = render(chip()) + expect(title.container.textContent).toBe('Terminal') + h.update({ ...idle, title: 'Saved terminal' }) + title.rerender(chip()) + expect(title.container.textContent).toBe('Saved terminal') + h.update({ ...idle, info }) + title.rerender(chip()) + expect(title.container.textContent).toBe('bash') + expect(title.container.querySelector('svg')?.getAttribute('aria-hidden')).toBe('true') + expect(title.container.querySelector('svg rect')).toBeNull() +}) + +it.each(titleSurfaces)('renames from the $name pointer target, preserving dragging and isolating editor shortcuts', (surface) => { + const h = mount({ ...idle, title: 'Saved terminal' }) + const outer = vi.fn() + const title = render(
+ {surface.wrap()} +
) + fireEvent.pointerDown(title.getByText('Saved terminal')) + expect(outer).toHaveBeenCalledOnce() + outer.mockClear() + fireEvent.doubleClick(title.container.querySelector(surface.selector)!) + const input = title.getByRole('textbox', { name: en.rename }) as HTMLInputElement + expect(document.activeElement).toBe(input) + expect(input.selectionStart).toBe(0) + expect(input.selectionEnd).toBe('Saved terminal'.length) + fireEvent.pointerDown(input) + fireEvent.click(input) + fireEvent.doubleClick(input) + fireEvent.keyDown(input, { key: 'ArrowLeft' }) + fireEvent.change(input, { target: { value: ' Development ' } }) + fireEvent.keyDown(input, { key: 'Enter' }) + expect(h.model.rename).toHaveBeenCalledWith('Development') + expect(h.model.rename).toHaveBeenCalledOnce() + expect(title.queryByRole('textbox', { name: en.rename })).toBeNull() + expect(outer).not.toHaveBeenCalled() +}) + +it.each(titleSurfaces)('handles Escape, unchanged names and IME composition while editing the $name', (surface) => { + const h = mount({ ...idle, info }) + const title = render(surface.wrap()) + const edit = () => { + fireEvent.doubleClick(title.container.querySelector(surface.selector)!) + return title.getByRole('textbox', { name: en.rename }) + } + let input = edit() + fireEvent.change(input, { target: { value: 'Cancelled' } }) + fireEvent.keyDown(input, { key: 'Escape' }) + expect(h.model.rename).not.toHaveBeenCalled() + input = edit() + fireEvent.blur(input) + expect(h.model.rename).not.toHaveBeenCalled() + input = edit() + fireEvent.change(input, { target: { value: ' ' } }) + fireEvent.blur(input) + expect(h.model.rename).not.toHaveBeenCalled() + input = edit() + fireEvent.change(input, { target: { value: '开发' } }) + fireEvent.keyDown(input, { key: 'Enter', isComposing: true }) + fireEvent.keyDown(input, { key: 'Enter', keyCode: 229 }) + expect(title.getByRole('textbox', { name: en.rename })).toBe(input) + expect(h.model.rename).not.toHaveBeenCalled() + fireEvent.blur(input) + expect(h.model.rename).toHaveBeenCalledWith('开发') +}) + +it.each(titleSurfaces)('removes the native $name listener when its title unmounts', (surface) => { + const h = mount({ ...idle, info }) + const outer = vi.fn() + const seat = (shown: boolean) =>
+ {surface.wrap(shown && )} +
+ const title = render(seat(false)) + const chip = title.container.querySelector(surface.selector)! + const add = vi.spyOn(chip, 'addEventListener') + const remove = vi.spyOn(chip, 'removeEventListener') + title.rerender(seat(true)) + const listener = add.mock.calls.find(([type]) => type === 'dblclick')?.[1] + expect(listener).toBeTypeOf('function') + title.rerender(seat(false)) + expect(remove).toHaveBeenCalledExactlyOnceWith('dblclick', listener) + fireEvent.doubleClick(chip) + expect(outer).toHaveBeenCalledOnce() + expect(h.model.rename).not.toHaveBeenCalled() +}) + +it('starts a recovered screen with no local history when environment discovery is unavailable', () => { + mount({ ...idle, info, environment: undefined }) + expect(fake.terminals[0]!.options).toHaveProperty('scrollback', 0) +}) + + +it.each([en, zh])('translates known terminal failures while retaining unknown Host diagnostics', (dictionary) => { + const h = mount(idle, dictionary) + for (const issue of ['missingTerminal', 'inputFull', 'attachmentEnded', 'invalidOutput', 'terminalLimit'] as const) { + h.update({ ...idle, phase: 'failed', issue, error: 'raw diagnostic' }) + expect(h.view.getByRole('alert').textContent).toContain(dictionary[issue]) + expect(h.view.getByRole('alert').textContent).not.toContain('raw diagnostic') + } + h.update({ ...idle, phase: 'failed', error: 'Host permission denied' }) + expect(h.view.getByRole('alert').textContent).toContain('Host permission denied') +}) + +it('lets the user select an installed shell and start it before rendering a screen', () => { + const other = { path: '/bin/zsh', name: 'zsh', args: ['-i'] } + const h = mount({ phase: 'selecting', writable: false, environment, shells: [info.shell, other], selectedShell: other.path }) + const selector = h.view.getByRole('button', { name: 'Shell' }) + expect(selector.textContent).toContain(other.path) + expect(fake.terminals).toHaveLength(0) + fireEvent.click(selector) + expect(h.view.getByRole('menu')).toBeTruthy() + fireEvent.keyDown(document, { key: 'Escape' }) + expect(h.view.queryByRole('menu')).toBeNull() + fireEvent.click(selector) + fireEvent.click(h.view.getByRole('menuitem', { name: `bash — ${info.shell.path}` })) + expect(h.view.queryByRole('menu')).toBeNull() + expect(h.model.selectShell).toHaveBeenCalledWith(info.shell.path) + fireEvent.click(h.view.getByRole('button', { name: 'Start terminal' })) + expect(h.model.start).toHaveBeenCalledOnce() + h.update({ phase: 'selecting', writable: false, environment }) + expect(h.view.getByRole('button', { name: 'Start terminal' })).toHaveProperty('disabled', true) +}) diff --git a/packages/client/ui-sidebar-terminal/tsconfig.json b/packages/client/ui-sidebar-terminal/tsconfig.json new file mode 100644 index 0000000000..4e229078ec --- /dev/null +++ b/packages/client/ui-sidebar-terminal/tsconfig.json @@ -0,0 +1,51 @@ +{ + "extends": "../../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../api/remotes/tsconfig.client.json" + }, + { + "path": "../../core/session" + }, + { + "path": "../locale" + }, + { + "path": "../store" + }, + { + "path": "../ui-dockkit" + }, + { + "path": "../ui-primitives" + }, + { + "path": "../ui-session" + }, + { + "path": "../ui-sidebar-right" + }, + { + "path": "../ui-slots" + }, + { + "path": "../../api/terminal-controller/tsconfig.client.json" + }, + { + "path": "../ui-layout" + }, + { + "path": "../ui-conversation" + } + ] +} diff --git a/packages/client/ui-sidebar-terminal/tsdown.config.ts b/packages/client/ui-sidebar-terminal/tsdown.config.ts new file mode 100644 index 0000000000..016643c5fb --- /dev/null +++ b/packages/client/ui-sidebar-terminal/tsdown.config.ts @@ -0,0 +1,3 @@ +import { clientBundle } from '../tsdown.client.ts' + +export default clientBundle('@deepseek-ai/dsh-client-ui-sidebar-terminal', ['lib/types/index.js']) diff --git a/packages/client/ui-trajectory/README.i18n.yaml b/packages/client/ui-trajectory/README.i18n.yaml index 9541cacecf..1dae344665 100644 --- a/packages/client/ui-trajectory/README.i18n.yaml +++ b/packages/client/ui-trajectory/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-trajectory/README.md -README.md: b3846ccbe6c6cb4ff2a68b74ddc3d4c219953c08 -README.zh.md: f4b788eacf0de69342d33e5f33bb966b4a1d8246 +README.md: 63bd664b902d8d40f8a05153770306d3b03dbe4e +README.zh.md: 89db03a99ac68065211f92ff7e47307813c1ab76 diff --git a/packages/client/ui-trajectory/README.md b/packages/client/ui-trajectory/README.md index b3846ccbe6..63bd664b90 100644 --- a/packages/client/ui-trajectory/README.md +++ b/packages/client/ui-trajectory/README.md @@ -29,10 +29,14 @@ Open the Trajectory tab in the conversation's view ring to inspect agent activit ### Inspecting records +Calls are identified by their recorded tool name. For `run_code`, the row shows the program description and the inspector opens numbered, highlighted source. The Code tab provides wrapping, a `{}` toggle for the original JSON arguments, and exact source copying, including trailing newlines. Copying the original arguments retains their recorded JSON whitespace. Each newly opened code view takes the last wrapping choice; changing it leaves other open views as they are. Output preserves the recorded text, using a tree for complete JSON objects or arrays. Highlighting uses only an unambiguous TypeScript or Python hint in the recorded tool schema; missing or conflicting hints leave plain source. See the [PTC inspection decision](../../../.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md) for replay constraints. + Selection, timeline navigation, folding, and search cover the React-visible window. Request numbers and cumulative usage cover the complete resident snapshot. Selecting a record opens a local inspector for token usage, duration, Input, Output, Timing, and durable images. Image URLs use the Conversation-owned per-session cache, so Chat and Trajectory share one authorized read per attachment. A user record shows the generic-file count beside its text, while a record without text shows its image and file counts. A standalone compaction request appears chronologically in its own `Between turns` section, while a numbered compaction remains inside its owning turn. ### The timing overview +Historical replies retain TTFT, generation duration, and throughput when their recorded streams contain token timestamps. TTFT measures from the Step start to its first token, including output from an earlier retry attempt; an unloaded Step start or a stream without tokens leaves the corresponding metric unavailable. + A fixed Overview above the ledger projects real record start/duration timing from left to right; Assistant spans divide recorded TTFT from decoding, and a 500 ms hover reveals exact clock and duration details. Dragging an interval focuses the ledger on every record active at any point in that inclusive range; wheel gestures zoom the time domain; a right-button click clears the selected interval, and a right-button drag pans an already zoomed viewport. The initial view and streaming updates stay at the tail; scrolling upward suspends following so new records do not interrupt inspection of earlier rows. ----- diff --git a/packages/client/ui-trajectory/README.zh.md b/packages/client/ui-trajectory/README.zh.md index f4b788eacf..89db03a99a 100644 --- a/packages/client/ui-trajectory/README.zh.md +++ b/packages/client/ui-trajectory/README.zh.md @@ -29,10 +29,14 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间 ### 检查记录 +调用按已记录的工具名识别。对于 `run_code`,记录行展示程序说明,检查器显示带行号和语法高亮的源码。“代码”页提供换行、查看原始 JSON 参数的 `{}` 切换按钮,以及保留末尾换行的源码原样复制。复制原始参数时保留记录中 JSON 的空白。每个新打开的代码视图采用上次选择的换行状态;切换时,其他已打开视图保持原状。输出保留记录中的文本,完整 JSON 对象或数组使用树形展示。高亮只采用已记录工具 Schema 中明确且无冲突的 TypeScript 或 Python 提示;提示缺失或冲突时显示纯文本源码。回放约束见 [PTC 检查决策](../../../.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md)。 + 选择、时间线导航、折叠与搜索只覆盖 React 可见窗口。请求编号与累计用量覆盖完整的驻留快照。选择记录会打开局部检查器,查看 token 用量、耗时、输入、输出、计时与持久保留的图片。图片 URL 使用 Conversation 拥有的逐会话缓存,因此 Chat 与 Trajectory 对每个附件共享一次已授权读取。用户记录会在文本旁显示通用文件数量;记录没有文本时,则显示图片与文件数量。独立运行的压缩请求会按时间顺序显示在自己的 `Between turns` 区段中,而带编号的压缩仍位于其所属轮次内。 ### 时间概览 +当记录的流包含 token 时间戳时,历史回复会保留 TTFT、生成耗时与吞吐量。TTFT 从步骤开始计量到首个 token,包括更早重试尝试中的输出;步骤起点尚未加载或流中没有 token 时,对应指标保持不可用。 + 固定在记录表上方的 Overview 区域从左到右投影记录的真实开始时间与耗时;助手时间条区分记录到的 TTFT 与解码时间,悬停 500 ms 后可查看精确时刻与耗时详情。拖选区间会把记录表聚焦到该闭区间内任何时刻处于活动的记录;滚轮手势用于缩放时间域;右键单击会清除所选区间,在已放大的视口上按住右键拖动则会平移。初始视图与流式更新都停留在尾部;向上滚动会暂停跟随,因此新记录不会打断对旧记录的检查。 ----- diff --git a/packages/client/ui-trajectory/src/client/TrajectoryCell.tsx b/packages/client/ui-trajectory/src/client/TrajectoryCell.tsx index a25b09833e..bb0b247979 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryCell.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryCell.tsx @@ -57,6 +57,7 @@ export function TrajectoryCell({ assistantMetrics: _assistantMetrics, result: _result, callId: _callId, + toolName: _toolName, isError: _isError, timeSeconds, startedAt: _startedAt, diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css index 35266eed00..c743e0e503 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css @@ -137,6 +137,10 @@ width: 122px; } +.eventColumn:where(:lang(zh)) { + width: 84px; +} + .contentColumn { width: auto; } @@ -448,6 +452,10 @@ width: 76px; } +.kindSlot:where(:lang(zh)) { + width: 44px; +} + .kindSlot :global([role='tooltip']) { border: 0; background: var(--dsw-alias-bg-layer-2); @@ -1038,6 +1046,7 @@ .detailBodySummary > .overview { flex: 0 1 auto; min-height: 0; + padding-bottom: 0; overflow: auto; overscroll-behavior: contain; } @@ -1176,13 +1185,13 @@ display: flex; flex: 1 1 0; max-height: max-content; - min-height: 28px; + min-height: 24px; flex-direction: column; overflow: hidden; } .overviewSection + .overviewSection { - padding-top: 8px; + padding-top: 2px; } .overviewHeading { @@ -1191,7 +1200,7 @@ align-items: flex-end; box-sizing: border-box; width: 100%; - height: 28px; + height: 24px; margin: 0; padding: 0 0 3px 14px; color: var(--dsw-alias-label-secondary); @@ -1243,13 +1252,157 @@ background: var(--dsw-alias-bg-layer-1); } +.overviewPreview[data-scroll-more] { + mask-image: linear-gradient(to bottom, #000 calc(100% - 12px), transparent); +} + .overviewPreview > :last-child { margin-bottom: 0; padding-bottom: 0; } +.programDescription { + flex: none; + margin: 0; + padding: 8px 14px 2px; + color: var(--dsw-alias-label-primary); + font: var(--dsw-font-xs-13); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.programIcon { + margin-right: 4px; + vertical-align: -2px; +} + +.programSummary { + margin-left: 5px; + font-family: var(--dsw-font-family); +} + +.detailBodyProgram, +.programPanel { + display: flex; + min-height: 0; + flex-direction: column; + overflow: hidden; +} + +.programPanel { + flex: 1; + background: var(--dsw-alias-bg-layer-1); +} + +.programActions { + display: inline-flex; + flex: none; + height: 16px; + align-items: center; + gap: 4px; + margin-left: auto; + padding-right: 14px; +} + +.programLanguage { + padding-right: 4px; + color: var(--dsw-alias-label-tertiary); + font: 11px/16px var(--ds-font-family-code); +} + +.programAction { + display: inline-flex; + box-sizing: border-box; + width: 20px; + height: 16px; + align-items: center; + justify-content: center; + padding: 0; + border: 0; + color: var(--dsw-alias-label-secondary); + background: transparent; + cursor: pointer; +} + +.programAction:hover { + color: var(--dsw-alias-label-primary); +} + +.programAction[aria-pressed='true'] { + color: var(--dsw-alias-state-business-primary); +} + +.programAction[data-state='failed'] { + color: var(--dsw-alias-state-error-primary); +} + +.programAction:focus-visible { + outline: 1px solid var(--dsw-alias-state-business-primary); + outline-offset: -1px; +} + +.programContent { + min-width: 0; + min-height: 0; +} + +.programPanel > .programContent { + flex: 1; + overflow: auto; + overscroll-behavior: contain; +} + +.programSource { + margin: 0 !important; + border-radius: 0; + background: var(--dsw-alias-bg-layer-1); + --dsl-code-block-line-white-space: pre; +} + +.programSource :where(pre) { + padding: 2px 14px 8px; + overflow: visible; + border-radius: 0; + background: var(--dsw-alias-bg-layer-1) !important; + font: 12px/16px var(--ds-font-family-code); + overflow-wrap: normal; + word-break: normal; + tab-size: 2; +} + +.programSource pre code > :global(.line) { + box-sizing: border-box; + width: max-content; + min-width: 100%; +} + +.programContent[data-wrap='true'] .programSource { + --dsl-code-block-line-white-space: pre-wrap; +} + +.programContent[data-wrap='true'] .programSource pre code > :global(.line) { + width: auto; + overflow-wrap: anywhere; + word-break: break-all; +} + +.programOutput { + margin: 0; + padding: 2px 14px 8px; + color: var(--dsw-alias-label-primary); + font: 12px/16px var(--ds-font-family-code); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.programError::first-line { + color: var(--dsw-alias-state-error-primary); + font-weight: 600; +} + .overviewPreview .jsonPreview > :first-child, .overviewPreview .schemaTree > :first-child { + padding-top: 2px; padding-bottom: 0; } @@ -1405,7 +1558,7 @@ } .markdownPreview { - padding: 6px 14px 8px; + padding: 2px 14px 8px; } .markdownPayload { @@ -1435,7 +1588,7 @@ } .schemaPreview .schemaIntro { - padding-top: 6px; + padding-top: 2px; } .schemaName { @@ -1482,7 +1635,7 @@ .payloadPreview { min-height: 0; - padding: 6px 14px 8px; + padding: 2px 14px 8px; } .sourceBlocks { @@ -1733,7 +1886,7 @@ .resultBlocksPreview { min-height: 0; - padding: 6px 14px 8px; + padding: 2px 14px 8px; gap: 6px; } @@ -1748,7 +1901,7 @@ } .overviewPreview .noPayload { - padding: 6px 14px 8px; + padding: 2px 14px 0; } .noPayload { diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index d2a952704f..6babf88c4e 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -1,18 +1,24 @@ /** Turn-aware trajectory event ledger with a local record inspector. */ -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react' import type { CSSProperties, ReactNode } from 'react' import { useVirtualizer } from '@tanstack/react-virtual' import { + CodeBlock, + IconCheckOutline16, IconChevronRightOutline14, + IconCodeOutline16, + IconWrapLinesOutline16, + IconCopyOutline16, IconSettingsOutline16, IconSparkle16, IconUserOutline16, JsonTree, MarkdownText, Tooltip, + writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives' -import type { JsonTreeLabels, MarkdownLabels } from '@deepseek-ai/dsh-client-ui-primitives' +import type { JsonTreeLabels, JsonTreeProps, MarkdownLabels } from '@deepseek-ai/dsh-client-ui-primitives' import { structuredPatch } from 'diff' import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' import type { @@ -30,6 +36,7 @@ import type { TrajectoryTurnModel } from './layout.ts' import { trajectoryPreviewText } from './trajectory-preview.ts' import type { TrajectoryKey, TrajectoryTranslate } from './locales.ts' import { COMPACTION_INTERRUPTED_ERROR } from './copy-codes.ts' +import { codeProgram, PTC_TOOL_NAME, type CodeProgram } from './code-program.ts' import css from './TrajectoryTable.module.css' const BOTTOM_FOLLOW_THRESHOLD_PX = 2 @@ -183,6 +190,7 @@ interface ParentRecords { } interface ToolCallTextParts { + program: boolean name: string args?: string } @@ -232,8 +240,8 @@ function jsonTreeLabels(t: TrajectoryTranslate): JsonTreeLabels { copyCompactJson: t('copy.compactJson'), copied: t('copied'), copyFailed: t('copy.failed'), - collapseNode: t('json.collapseNode'), - expandNode: t('json.expandNode'), + collapseNode: t('collapse'), + expandNode: t('expand'), copyButtonTitle: action => t('copy.optionsHint', { action }), } } @@ -376,6 +384,8 @@ function AssistantTimingPanel({ /** Props for the trajectory ledger. */ export interface TrajectoryTableProps { + /** Wrapping control shared by all JSON inspectors. */ + stringWrapping?: JsonTreeProps['stringWrapping'] /** Trajectory locale seat. */ t: TrajectoryTranslate /** Slot-backed durable image renderer shared with the Chat gallery. */ @@ -844,10 +854,12 @@ function RequestUsagePanel({ function RequestOptions({ options, preview = false, + stringWrapping, t, }: { options: AssistantRequestConfig | undefined preview?: boolean + stringWrapping: JsonTreeProps['stringWrapping'] t: TrajectoryTranslate }) { if (options === undefined) { @@ -856,6 +868,8 @@ function RequestOptions({ return ( {t('source.notRecorded')}

const data = typeof source === 'object' && source !== null @@ -895,6 +913,8 @@ function MessageSource({ record, t }: { record: TableRecord; t: TrajectoryTransl return ( recordDisplayText(cell, t), [ - cell.kind, cell.text, cell.previewMarkdown, + cell.kind, cell.text, cell.toolName, cell.previewMarkdown, cell.inputDetail, cell.outputDetail, cell.thinkingDetail, t, ], ) @@ -1057,7 +1084,7 @@ function RecordPresentation({ [cell.result, cell.resultPreviewMarkdown], ) const toolCallOnly = isToolCallOnly(cell, t) - const toolCallText = toolCallTextParts(cell.kind, displayText) + const toolCallText = toolCallTextParts(cell, displayText) const listDisplayText = toolCallOnly ? t('record.toolCallOnly') : toolCallText === undefined @@ -1087,10 +1114,11 @@ function RecordListText({ return ( <> + {toolCallText.program && } {toolCallText.name || '—'} {toolCallText.args !== undefined && ( - + {toolCallText.args} )} @@ -1281,8 +1309,13 @@ function ToolGlyph() { function ToolCatalog({ tools, + stringWrapping, t, -}: { tools: ConversationPromptSnapshot['tools']; t: TrajectoryTranslate }) { +}: { + tools: ConversationPromptSnapshot['tools'] + stringWrapping: JsonTreeProps['stringWrapping'] + t: TrajectoryTranslate +}) { if (tools.length === 0) return

{t('record.toolsMissing')}

return (
@@ -1300,6 +1333,7 @@ function ToolCatalog({ )} : (
{t('timing.started')}
{t('timing.duration')}
{formatElapsedSeconds(record.cell.timeSeconds, t)}
-
{t('timing.source')}
{record.cell.timeSeconds === null ? t('timing.notAvailable') : t('timing.sessionTimestamps')}
+ {!preview && ( +
{t('timing.source')}
{record.cell.timeSeconds === null ? t('timing.notAvailable') : t('timing.sessionTimestamps')}
+ )}
) } @@ -1560,14 +1600,16 @@ function RequestTiming({ assistant, anchor, request, + preview = false, t, }: { assistant: TableRecord | undefined anchor: TableRecord | undefined request: TrajectoryRequestNumber | undefined + preview?: boolean t: TrajectoryTranslate }) { - if (assistant !== undefined) return + if (assistant !== undefined) return if (request?.startedAt !== undefined) { const duration = request.completedAt === null || request.completedAt === undefined ? null @@ -1576,10 +1618,12 @@ function RequestTiming({
{t('timing.started')}
{t('timing.duration')}
{formatElapsedSeconds(duration, t)}
-
-
{t('timing.source')}
-
{duration === null ? t('timing.sessionTimestampsRunning') : t('timing.sessionTimestamps')}
-
+ {!preview && ( +
+
{t('timing.source')}
+
{duration === null ? t('timing.sessionTimestampsRunning') : t('timing.sessionTimestamps')}
+
+ )}
) } @@ -1599,12 +1643,14 @@ function RecordPayload({ direction, preview = false, renderImages, + stringWrapping, t, }: { record: TableRecord direction: 'input' | 'output' preview?: boolean renderImages: RenderMessageImages + stringWrapping: JsonTreeProps['stringWrapping'] t: TrajectoryTranslate }) { const value = direction === 'input' ? record.cell.inputDetail : record.cell.outputDetail @@ -1624,6 +1670,8 @@ function RecordPayload({ return ( {t('record.parameters')} ('idle') + useEffect(() => { + if (state === 'idle') return + const timer = setTimeout(() => { setState('idle') }, 1_500) + return () => { clearTimeout(timer) } + }, [state]) + const title = state === 'idle' ? label : t(state === 'copied' ? 'copied' : 'copy.failed') + return ( + + ) +} + +function ProgramInput({ program, initialWrapped, stringWrapping, onOpen, t }: { + program: CodeProgram + initialWrapped: boolean + stringWrapping: JsonTreeProps['stringWrapping'] + onOpen?: () => void + t: TrajectoryTranslate +}) { + const contentsId = useId() + const [wrapped, setWrapped] = useState(initialWrapped) + const [showJson, setShowJson] = useState(false) + const actions = ( + + {!showJson && program.language !== undefined && ( + {program.language} + )} + {!showJson && ( + + )} + {onOpen === undefined && ( + + )} + + + ) + const body = ( +
+ {showJson + ? + : } +
+ ) + return onOpen === undefined + ? ( +
+
+ {t('code.source')} + {actions} +
+ {body} +
+ ) + : {body} +} + +function ProgramOutput({ record, stringWrapping, onOpen, t }: { + record: TableRecord + stringWrapping: JsonTreeProps['stringWrapping'] + onOpen?: () => void + t: TrajectoryTranslate +}) { + const output = record.cell.outputDetail + const json = output === undefined || record.cell.isError === true ? undefined : parseJsonContainer(output) + const actions = output === undefined ? undefined : ( + + + + ) + const body = ( +
+ {output === undefined || output === '' + ?

{t(stateOf(record) === 'running' ? 'code.running' : 'record.noOutput')}

+ : json !== undefined + ? + :
{output}
} +
+ ) + return onOpen === undefined + ? ( +
+
+ {t('code.output')} + {actions} +
+ {body} +
+ ) + : {body} +} + function OverviewSection({ label, onOpen, + actions, children, }: { label: string onOpen: () => void + actions?: ReactNode children: ReactNode }) { + const previewRef = useRef(null) + const [hasMore, setHasMore] = useState(false) + const measureOverflow = useCallback((preview: HTMLElement) => { + setHasMore(preview.scrollHeight - preview.clientHeight - preview.scrollTop > 1) + }, []) + + useLayoutEffect(() => { + const preview = previewRef.current as HTMLDivElement + const measure = () => { measureOverflow(preview) } + measure() + if (typeof ResizeObserver === 'undefined') return + const observer = new ResizeObserver(measure) + observer.observe(preview) + for (const child of preview.children) observer.observe(child) + return () => { observer.disconnect() } + }, [children, measureOverflow]) + return (

@@ -1783,10 +1996,14 @@ function OverviewSection({ {label} + {actions}

{ measureOverflow(event.currentTarget) }} > {children}
@@ -1802,6 +2019,7 @@ function OverviewSection({ */ export function TrajectoryTable({ t, + stringWrapping, renderImages, requestNumbers: sessionRequestNumbers, turns, @@ -1828,7 +2046,8 @@ export function TrajectoryTable({ const [selectedRecordId, setSelectedRecordId] = useState(null) const [selectedRequest, setSelectedRequest] = useState(null) const [activeTab, setActiveTab] = useState('overview') - const [thinkingExpanded, setThinkingExpanded] = useState(false) + const [codeWrappingOnOpen, setCodeWrappingOnOpen] = useState(false) + const [thinkingDisclosure, setThinkingDisclosure] = useState<{ recordId: string; expanded: boolean }>() const [detailsWidth, setDetailsWidth] = useState(null) const [toolRequestOffset, setToolRequestOffset] = useState(null) const detailsResizeDrag = useRef(null) @@ -1860,6 +2079,16 @@ export function TrajectoryTable({ const selected = selectedTemplate === undefined ? undefined : currentRecord(selectedTemplate) + const selectedProgram = selected === undefined ? undefined : codeProgram(selected.cell) + const thinkingExpanded = thinkingDisclosure?.recordId === selectedRecordId + ? thinkingDisclosure.expanded + : selected?.cell.kind === 'message' + && Boolean(selected.cell.thinkingDetail?.trim()) + const setThinkingExpanded = (expanded: boolean) => { + if (selectedRecordId !== null) { + setThinkingDisclosure({ recordId: selectedRecordId, expanded }) + } + } const selectedIndex = selected?.cell.index ?? null useEffect(() => { onSelectedIndexChange?.(selectedIndex) @@ -2060,6 +2289,7 @@ export function TrajectoryTable({ } const activateTab = (tab: DetailTab) => { + setCodeWrappingOnOpen(stringWrapping?.getDefault() ?? false) tabHistory.current.delete(tab) tabHistory.current.add(tab) setActiveTab(tab) @@ -2076,6 +2306,7 @@ export function TrajectoryTable({ } const selectRecord = useCallback((index: number) => { + setCodeWrappingOnOpen(stringWrapping?.getDefault() ?? false) const record = allRecords.find(candidate => candidate.cell.index === index) onRecordSelect?.(index) setSelectedRequest(null) @@ -2085,7 +2316,7 @@ export function TrajectoryTable({ const available = new Set(tabs.map(tab => tab.id)) const recent = [...tabHistory.current].reverse().find(tab => available.has(tab)) setActiveTab(recent ?? tabs[0]?.id ?? 'overview') - }, [allRecords, onRecordSelect]) + }, [allRecords, onRecordSelect, stringWrapping]) useEffect(() => { if ( recordSelection === null @@ -2809,7 +3040,9 @@ export function TrajectoryTable({ id="trajectory-detail-panel" className={activeTab === 'overview' ? `${css.detailBody} ${css.detailBodySummary}` - : css.detailBody} + : selectedProgram !== undefined && (activeTab === 'input' || activeTab === 'output') + ? `${css.detailBody} ${css.detailBodyProgram}` + : css.detailBody} role="tabpanel" aria-labelledby={`trajectory-detail-${activeTab}`} > @@ -2916,7 +3149,7 @@ export function TrajectoryTable({
{selectedRequestOptions !== undefined && ( { activateTab('options') }}> - + )} { activateTab('usage') }}> @@ -2927,6 +3160,7 @@ export function TrajectoryTable({ assistant={selectedRequestAssistant} anchor={selectedRequestAnchor} request={selectedRequestInfo} + preview t={t} /> @@ -2934,7 +3168,7 @@ export function TrajectoryTable({ )} {selectedRequestInfo !== undefined && activeTab === 'options' && ( - + )} {selectedRequestInfo !== undefined && activeTab === 'usage' && ( + )} {!promptSelected && selected?.cell.kind === 'compacted' @@ -3020,6 +3254,9 @@ export function TrajectoryTable({ && selectedState !== undefined && activeTab === 'overview' && ( <> + {selectedProgram !== undefined && selectedProgram.description !== '' && ( +

{selectedProgram.description}

+ )}
- {isMarkdownRecord(selected) + {selectedProgram !== undefined ? ( <> - { activateTab('rendered') }}> - - + { activateTab('input') }} t={t} /> + { activateTab('output') }} t={t} /> ) - : ( - <> - {selected.cell.inputDetail && ( - { activateTab('input') }}> - + : isMarkdownRecord(selected) + ? ( + <> + { activateTab('rendered') }}> + - )} - {selected.cell.outputDetail && ( - { activateTab('output') }}> - + + ) + : ( + <> + {selected.cell.inputDetail && ( + { activateTab('input') }}> + + + )} + {selected.cell.outputDetail && ( + { activateTab('output') }}> + + + )} + { activateTab('schema') }}> + - )} - { activateTab('schema') }}> - - - - )} + + )} {selectedAssistantRequestTarget !== undefined && ( - + )} {(selected.cell.kind === 'tool' || selected.cell.kind === 'subtool') && ( { activateTab('timing') }}> - + )}
@@ -3186,16 +3433,22 @@ export function TrajectoryTable({ /> )} {!promptSelected && selected !== undefined && activeTab === 'source' && ( - + )} {!promptSelected && selected !== undefined && activeTab === 'input' && ( - + selectedProgram === undefined + ? + : )} {!promptSelected && selected !== undefined && activeTab === 'output' && ( - + selectedProgram === undefined + ? + : )} {!promptSelected && selected !== undefined && activeTab === 'schema' && ( - + )} {!promptSelected && selected !== undefined && activeTab === 'timing' && ( diff --git a/packages/client/ui-trajectory/src/client/TrajectoryView.tsx b/packages/client/ui-trajectory/src/client/TrajectoryView.tsx index 4120779d9d..6c2cd98ec8 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryView.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryView.tsx @@ -7,6 +7,7 @@ import type { } from '@deepseek-ai/dsh-client-ui-conversation/client' import type { InjectFace, PropsLocale, PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import type { SnapshotStore } from '@deepseek-ai/dsh-client-store' +import type { JsonTreeProps } from '@deepseek-ai/dsh-client-ui-primitives' import { TrajectoryTable, type TrajectoryRequestNumber, @@ -74,6 +75,8 @@ function partialStructureSignature(partial: TrajectorySnapshot['partial']): stri /** Session-bound controls not already supplied by the conversation view slot. */ export interface TrajectoryViewInjected { + /** Shared wrapping preference read by expansion handlers. */ + jsonStringWrapping?: Omit, 'label'> hooks: { duration: SnapshotStore } @@ -128,7 +131,7 @@ function addUsage( export function TrajectoryView({ useSession, useTrajectory, useDuration, loadOlder, loadImage, setActualDuration, - viewRequest, completeViewRequest, renderSlot, t, + viewRequest, completeViewRequest, renderSlot, t, jsonStringWrapping, }: ConvViewProps & PropsRenderSlots<'conversation.trajectory.images'> & InjectFace @@ -541,6 +544,10 @@ export function TrajectoryView({
+ language: 'typescript' | 'python' | undefined +} + +function record(value: unknown): Record | undefined { + return typeof value === 'object' && value !== null && !Array.isArray(value) + ? value as Record + : undefined +} + +function parseRecord(value: string | undefined): Record | undefined { + if (value === undefined) return undefined + try { + return record(JSON.parse(value)) + } catch { + return undefined + } +} + +function recordedLanguage(schemaRaw: string | undefined): CodeProgram['language'] { + const schema = parseRecord(schemaRaw) + const properties = record(record(schema?.parameters)?.properties) + const description = record(properties?.code)?.description + if (typeof description !== 'string') return undefined + const typescript = /\bTypeScript\b/i.test(description) + const python = /\bPython\b/i.test(description) + if (typescript === python) return undefined + return typescript ? 'typescript' : 'python' +} + +/** + * Resolve a PTC program without guessing its language from source or current runtime settings. + * @param cell - Recorded tool arguments and the schema visible at call time. + * @returns The program, or undefined for another tool or unsupported arguments. + */ +export function codeProgram(cell: TrajectoryCellProps): CodeProgram | undefined { + if (cell.kind !== 'tool' && cell.kind !== 'subtool') return undefined + if (cell.toolName !== PTC_TOOL_NAME || cell.inputDetail === undefined) return undefined + const args = parseRecord(cell.inputDetail) + if (typeof args?.code !== 'string') return undefined + if (args.description !== undefined && typeof args.description !== 'string') return undefined + return { + rawInput: cell.inputDetail, + source: args.code, + description: typeof args.description === 'string' && args.description.trim() !== '' + ? args.description + : args.code.split(/\r?\n/).find(line => line.trim() !== '')?.trim() ?? '', + arguments: args, + language: recordedLanguage(cell.schemaDetail), + } +} diff --git a/packages/client/ui-trajectory/src/client/index.ts b/packages/client/ui-trajectory/src/client/index.ts index 118f3b2c28..935a43ff2c 100644 --- a/packages/client/ui-trajectory/src/client/index.ts +++ b/packages/client/ui-trajectory/src/client/index.ts @@ -15,6 +15,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' import { createTrajectoryDurationStore } from './duration-store.ts' +import { createTrajectoryStringWrappingStore } from './string-wrapping-store.ts' import { en, NS, zh } from './locales.ts' import { registerTrajectoryAssistantDefinition } from './trajectory-assistant-definition.ts' import { registerTrajectoryCompactionDefinitions } from './trajectory-compaction-definition.ts' @@ -64,6 +65,7 @@ export function apply(ctx: Context): void { // re-registration. const t = ctx.locale.bind(NS) const duration = createTrajectoryDurationStore() + const stringWrapping = createTrajectoryStringWrappingStore() registerTrajectoryMessageDefinitions(ctx) registerTrajectoryRequestHeaderDefinition(ctx) registerTrajectoryAssistantDefinition(ctx) @@ -91,6 +93,10 @@ export function apply(ctx: Context): void { const trajectory = ctx.uiConversation.binding(sessionId).target('trajectory') return { hooks: { duration }, + jsonStringWrapping: { + getDefault: () => stringWrapping.getSnapshot(), + setDefault: (value) => { stringWrapping.set(value) }, + }, loadOlder: async () => { const before = trajectory.getSnapshot() await session.loadOlder() diff --git a/packages/client/ui-trajectory/src/client/layout.ts b/packages/client/ui-trajectory/src/client/layout.ts index fd361899d0..7691b7f74a 100644 --- a/packages/client/ui-trajectory/src/client/layout.ts +++ b/packages/client/ui-trajectory/src/client/layout.ts @@ -1062,8 +1062,9 @@ function expandSubCalls( function summarizeCall( name: string, argsRaw: string, -): Pick { +): Pick { return { + toolName: name, text: name, ...(argsRaw === '' ? {} : { previewMarkdown: argsRaw }), } diff --git a/packages/client/ui-trajectory/src/client/locales.ts b/packages/client/ui-trajectory/src/client/locales.ts index 6bdc621413..7e76444ca1 100644 --- a/packages/client/ui-trajectory/src/client/locales.ts +++ b/packages/client/ui-trajectory/src/client/locales.ts @@ -37,7 +37,7 @@ export const zh = { 'turn.label': '第 {turn} 轮', 'section.betweenTurns': '轮次之间', 'group.message': '消息', - 'group.step': '步骤 {step}', + 'group.step': '第 {step} 步', 'group.compaction': '压缩 {seq}', 'status.failed': '失败', 'status.pending': '等待中', @@ -114,6 +114,13 @@ export const zh = { 'record.payloadJson': '参数 JSON', 'record.outputJson': '结果 JSON', 'record.thinking': '思考', + 'record.wrapLines': '自动换行', + 'code.source': '代码', + 'code.output': '输出', + 'code.copySource': '复制代码', + 'code.copyOutput': '复制输出', + 'code.originalJson': '原始 JSON', + 'code.running': '执行中…', 'record.systemPromptMissing': '本次请求没有系统提示词', 'record.toolsMissing': '本次请求没有工具', 'record.systemPrompt': '系统提示词', @@ -307,6 +314,13 @@ export const en: Record = { 'record.payloadJson': 'Payload JSON', 'record.outputJson': 'Result JSON', 'record.thinking': 'Thinking', + 'record.wrapLines': 'Wrap lines', + 'code.source': 'Code', + 'code.output': 'Output', + 'code.copySource': 'Copy code', + 'code.copyOutput': 'Copy output', + 'code.originalJson': 'Original JSON', + 'code.running': 'Running…', 'record.systemPromptMissing': 'No system prompt in this request', 'record.toolsMissing': 'No tools in this request', 'record.systemPrompt': 'System Prompt', diff --git a/packages/client/ui-trajectory/src/client/string-wrapping-store.ts b/packages/client/ui-trajectory/src/client/string-wrapping-store.ts new file mode 100644 index 0000000000..bcd86dfccc --- /dev/null +++ b/packages/client/ui-trajectory/src/client/string-wrapping-store.ts @@ -0,0 +1,13 @@ +import { + createSnapshotStore, type SnapshotStore, +} from '@deepseek-ai/dsh-client-store' + +/** + * Create the browser-wide default for expanded JSON strings. + * @returns A persisted preference sampled only when a string is expanded. + */ +export function createTrajectoryStringWrappingStore(): SnapshotStore { + return createSnapshotStore(false, { + persist: { name: 'dsh.trajectory.jsonStringWrapping' }, + }) +} diff --git a/packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts b/packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts index 6b1d6cd004..2e885c4fdb 100644 --- a/packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts +++ b/packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts @@ -5,6 +5,7 @@ import type { PartialAssistant, RequestView, } from '@deepseek-ai/dsh-client-ui-conversation/client' import type { StreamChunk } from '@deepseek-ai/dsh-llm' +import { assistantStreamFirstTokenTime } from '@deepseek-ai/dsh-llm/assistant-stream' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' import { trajectoryNode } from './trajectory-definition-common.ts' import { @@ -191,6 +192,17 @@ function updateChunk( } } +/** The Step retains its first token across live chunks and settled retry attempts. */ +function settleTiming( + state: AssistantState, + event: SessionEvent<'assistant/message' | 'assistant/attempt'>, +): AssistantState { + return { + ...state, + firstTokenTime: state.firstTokenTime ?? assistantStreamFirstTokenTime(event.data.stream), + } +} + function settleMessage( state: AssistantState, match: ConversationMatch, @@ -198,7 +210,7 @@ function settleMessage( ): AssistantState { const blocks = toAssistantBlocks(event.data.message.content) return { - ...state, + ...settleTiming(state, event), sawChunk: false, blocks, visibleBlocks: countVisibleBlocks(blocks), @@ -226,6 +238,9 @@ function fallbackState(context: ConversationNodeContext): Assist if (event.type === 'assistant/live-chunk') { state ??= initialState(event.data.turn, event.data.step, event.seq, event.time, false) state = updateChunk(state, event.data.chunk, event.seq, event.time) + } else if (event.type === 'assistant/attempt') { + state ??= initialState(event.data.turn, event.data.step, event.seq, event.time, false) + state = settleTiming(state, event) } else if (event.type === 'assistant/message') { state ??= initialState(event.data.turn, event.data.step, event.seq, event.time, false) state = settleMessage(state, match, event) @@ -325,6 +340,7 @@ const trajectoryAssistantDefinition: ConversationNodeDefinition } if (event.type === 'assistant/live-chunk' || event.type === 'assistant/message' + || event.type === 'assistant/attempt' || event.type === 'llm/retry' || event.type === 'step/end') { return { id: `${event.data.turn}:${event.data.step}`, role: 'update' } @@ -348,6 +364,7 @@ const trajectoryAssistantDefinition: ConversationNodeDefinition return updateChunk(context.state, match.event.data.chunk, match.event.seq, match.event.time) } if (match.event.type === 'assistant/message') return settleMessage(context.state, match, match.event) + if (match.event.type === 'assistant/attempt') return settleTiming(context.state, match.event) if (match.event.type === 'step/end') return { ...context.state, stepEnd: match } if (match.event.type !== 'llm/retry') return context.state const data = match.event.data @@ -372,7 +389,7 @@ const trajectoryAssistantDefinition: ConversationNodeDefinition } }, publication: (match) => { - if (match.event.type === 'step/start') return 'none' + if (match.event.type === 'step/start' || match.event.type === 'assistant/attempt') return 'none' if (match.event.type !== 'assistant/live-chunk') return 'immediate' const type = match.event.data.chunk.type return type === 'usage' || type === 'finish' ? 'none' : 'animation-frame' diff --git a/packages/client/ui-trajectory/src/client/trajectory-record.ts b/packages/client/ui-trajectory/src/client/trajectory-record.ts index 2bcb268f22..017143c487 100644 --- a/packages/client/ui-trajectory/src/client/trajectory-record.ts +++ b/packages/client/ui-trajectory/src/client/trajectory-record.ts @@ -80,6 +80,8 @@ export interface TrajectoryCellProps extends HTMLAttributes { resultPreviewMarkdown?: string /** Tool call id used to link message source blocks to tool records. */ callId?: string + /** Recorded tool name; independent of the display summary. */ + toolName?: string /** Tool-only result failure state. */ isError?: boolean /** Own duration in seconds, or `null` when no duration is known. */ diff --git a/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx b/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx new file mode 100644 index 0000000000..fab29e0637 --- /dev/null +++ b/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx @@ -0,0 +1,116 @@ +// @vitest-environment jsdom +/** PTC source, output, and independent wrapping through the trajectory inspector. */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render, waitFor, within } from '@testing-library/react' +import { TrajectoryTable } from '../src/client/TrajectoryTable.tsx' +import type { TrajectoryCellProps } from '../src/client/trajectory-record.ts' +import { t } from './locale.client.ts' + +afterEach(() => { + cleanup() + vi.restoreAllMocks() + vi.unstubAllGlobals() +}) + +const SOURCE = 'const value = await tools.bash({ command: "pwd" })\nreturn value\n' +const ARGS = JSON.stringify({ code: SOURCE, description: 'Read the working directory' }) +const CELL: TrajectoryCellProps = { + index: 1, kind: 'tool', toolName: 'run_code', text: `run_code · ${ARGS}`, callId: 'code-1', + inputDetail: ARGS, outputDetail: 'printed line\n42', result: 'printed line', timeSeconds: 0.5, +} + +function Harness({ cell = CELL, getDefault = () => false, setDefault = () => {} }: { + cell?: TrajectoryCellProps + getDefault?: () => boolean + setDefault?: (value: boolean) => void +}) { + return null} t={t} + stringWrapping={{ label: 'Wrap lines', getDefault, setDefault }} + collapsedTurns={new Set()} onToggleTurn={() => {}} + collapsedAssistants={new Set()} onToggleAssistant={() => {}} + /> +} + +describe('PTC trajectory inspector', () => { + it('shows the description, numbered source, original arguments, and verbatim output', () => { + const view = render() + const ui = within(view.container) + fireEvent.click(ui.getByRole('row', { name: /run_code Read the working directory/ })) + expect(ui.getAllByRole('tab').map(tab => tab.textContent)).toEqual(['Summary', 'Code', 'Result', 'Schema', 'Timing']) + expect(ui.getByRole('tabpanel').textContent).toContain('Read the working directory') + expect(view.container.querySelector('[data-line-numbers] pre')?.textContent).toBe(SOURCE.trimEnd()) + expect(ui.getByRole('tabpanel').textContent).toContain('printed line\n42') + fireEvent.click(ui.getByRole('button', { name: 'Code' })) + expect(ui.getByRole('tab', { name: 'Code' }).getAttribute('aria-selected')).toBe('true') + fireEvent.click(ui.getByRole('button', { name: 'Original JSON' })) + expect(ui.getByRole('tree', { name: 'parameters JSON' }).textContent).toContain('description:') + fireEvent.click(ui.getByRole('button', { name: 'Original JSON' })) + expect(view.container.querySelector('[data-line-numbers] pre')?.textContent).toBe(SOURCE.trimEnd()) + }) + + it('copies exact source, raw JSON, and output with feedback', async () => { + const writeText = vi.fn().mockResolvedValue(undefined) + vi.stubGlobal('navigator', { clipboard: { writeText } }) + const view = render() + const ui = within(view.container) + fireEvent.click(ui.getByRole('row', { name: /run_code Read/ })) + fireEvent.click(ui.getByRole('button', { name: 'Copy code' })) + await waitFor(() => { expect(writeText).toHaveBeenLastCalledWith(SOURCE) }) + await waitFor(() => { expect(ui.getByRole('button', { name: 'Copied' })).toBeTruthy() }) + fireEvent.click(ui.getByRole('button', { name: 'Copy output' })) + await waitFor(() => { expect(writeText).toHaveBeenLastCalledWith('printed line\n42') }) + fireEvent.click(ui.getByRole('tab', { name: 'Code' })) + fireEvent.click(ui.getByRole('button', { name: 'Original JSON' })) + fireEvent.click(ui.getByRole('button', { name: 'Copy JSON' })) + await waitFor(() => { expect(writeText).toHaveBeenLastCalledWith(ARGS) }) + }) + + it('samples the last wrapping choice on open without changing another open inspector', () => { + let preference = false + const props = { getDefault: () => preference, setDefault: (value: boolean) => { preference = value } } + const first = render() + const second = render() + const a = within(first.container) + const b = within(second.container) + fireEvent.click(a.getByRole('row', { name: /run_code Read/ })) + fireEvent.click(b.getByRole('row', { name: /run_code Read/ })) + fireEvent.click(a.getByRole('button', { name: 'Wrap lines' })) + expect(preference).toBe(true) + expect(a.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('true') + expect(b.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('false') + fireEvent.click(b.getByRole('tab', { name: 'Code' })) + expect(b.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('true') + fireEvent.click(b.getByRole('button', { name: 'Wrap lines' })) + expect(preference).toBe(false) + expect(a.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('true') + fireEvent.click(a.getByRole('button', { name: 'Close details' })) + fireEvent.click(a.getByRole('row', { name: /run_code Read/ })) + expect(a.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('false') + }) + + it('shows pending execution and keeps a failed run’s captured output', () => { + const pending = { ...CELL, timeSeconds: null } + delete pending.result + delete pending.outputDetail + const view = render() + const ui = within(view.container) + fireEvent.click(ui.getByRole('row', { name: /run_code Read/ })) + expect(ui.getByRole('tabpanel').textContent).toContain('Running…') + view.rerender() + expect(ui.getByRole('tabpanel').textContent).toContain('Failed') + expect(ui.getByRole('tabpanel').textContent).toContain('Error: execution failed\nCaptured output:\nstep one') + }) + + it('uses a tree for complete JSON output and generic input for malformed arguments', () => { + const view = render() + const ui = within(view.container) + fireEvent.click(ui.getByRole('row', { name: /run_code Read/ })) + expect(ui.getByRole('tree', { name: 'Result JSON' }).textContent).toContain('passed:12') + view.rerender() + expect(ui.getByRole('tab', { name: 'Payload' })).toBeTruthy() + expect(ui.queryByRole('tab', { name: 'Code' })).toBeNull() + expect(ui.getByRole('tree', { name: 'Payload JSON' }).textContent).toContain('code:3') + }) +}) diff --git a/packages/client/ui-trajectory/tests/code-program.client.spec.ts b/packages/client/ui-trajectory/tests/code-program.client.spec.ts new file mode 100644 index 0000000000..a9bd21c807 --- /dev/null +++ b/packages/client/ui-trajectory/tests/code-program.client.spec.ts @@ -0,0 +1,70 @@ +/** Recorded PTC argument validation and historical language selection. */ +import { describe, expect, it } from 'vitest' +import { codeProgram } from '../src/client/code-program.ts' +import type { TrajectoryCellProps } from '../src/client/trajectory-record.ts' + +const CELL: TrajectoryCellProps = { + index: 1, kind: 'tool', toolName: 'run_code', text: 'run_code · {}', timeSeconds: 1, + inputDetail: JSON.stringify({ code: 'return 42\n', description: 'Compute the answer' }), +} + +describe('recorded code programs', () => { + it('keeps the program and arguments verbatim', () => { + expect(codeProgram(CELL)).toEqual({ + rawInput: CELL.inputDetail, + source: 'return 42\n', description: 'Compute the answer', + arguments: { code: 'return 42\n', description: 'Compute the answer' }, language: undefined, + }) + }) + + it.each([ + ['The program: the body of an async TypeScript function.', 'typescript'], + ['The program: the body of an async Python function.', 'python'], + ['A TypeScript or Python program.', undefined], + ['An executable program.', undefined], + ])('uses the recorded parameter description %s', (description, language) => { + const schemaDetail = JSON.stringify({ parameters: { properties: { code: { description } } } }) + expect(codeProgram({ ...CELL, schemaDetail })?.language).toBe(language) + }) + + it.each([undefined, '{', '{}', 'null', '[]'])('keeps missing or unusable schemas plain: %s', (schemaDetail) => { + expect(codeProgram({ ...CELL, ...(schemaDetail === undefined ? {} : { schemaDetail }) })?.language).toBeUndefined() + }) + + it('uses the first nonempty source line when a description is unavailable', () => { + const source = '\n const x = 1\nreturn x\n' + for (const description of [undefined, '', ' ']) { + expect(codeProgram({ ...CELL, inputDetail: JSON.stringify({ code: source, description }) })) + .toMatchObject({ source, description: 'const x = 1' }) + } + }) + + it('retains empty source and unknown argument fields for JSON inspection', () => { + expect(codeProgram({ ...CELL, inputDetail: '{"code":"","extra":true}' })) + .toMatchObject({ source: '', description: '', arguments: { code: '', extra: true } }) + }) + + it.each([undefined, '{', '[]', 'null', '42', '{}', '{"code":42}', '{"code":"x","description":[]}' ])( + 'declines incomplete or unsupported arguments: %s', (inputDetail) => { + const cell = { ...CELL } + if (inputDetail === undefined) delete cell.inputDetail + else cell.inputDetail = inputDetail + expect(codeProgram(cell)).toBeUndefined() + }, + ) + + it('uses recorded tool metadata independently of display text', () => { + expect(codeProgram({ ...CELL, text: 'Execute program' })?.source).toBe('return 42\n') + const withoutName = { ...CELL } + delete withoutName.toolName + expect(codeProgram(withoutName)).toBeUndefined() + const rawInput = ' { "code": "return 42" }\n' + expect(codeProgram({ ...CELL, inputDetail: rawInput })?.rawInput).toBe(rawInput) + }) + + it('does not specialize other tools or non-tool records', () => { + expect(codeProgram({ ...CELL, kind: 'message' })).toBeUndefined() + expect(codeProgram({ ...CELL, toolName: 'run_code_extra' })).toBeUndefined() + expect(codeProgram({ ...CELL, kind: 'subtool' })?.source).toBe('return 42\n') + }) +}) diff --git a/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts b/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts index a3a0623be2..1f15cc202a 100644 --- a/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts +++ b/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts @@ -335,13 +335,20 @@ describe('Trajectory conversation Definitions', () => { const finalizedPacked = snapshot(assembler(finalizedInputs)) expect(finalizedPacked.eventNodes.find(node => node.kind === 'assistant')).toMatchObject({ blocks: [{ kind: 'text', text: 'done' }], - timing: { firstTokenTime: null }, + timing: { firstTokenTime: 3_000 }, }) expect(finalizedPacked.requests).toMatchObject([{ purpose: 'assistant', retry: 1, }]) + const windowed = assembler(finalizedInputs.slice(2), true) + const assistant = () => snapshot(windowed).eventNodes.find(node => node.kind === 'assistant') + expect(assistant()).toMatchObject({ timing: { stepStartTime: null, firstTokenTime: 3_000 } }) + windowed.prepend(finalizedInputs.slice(0, 2), false) + windowed.flush() + expect(assistant()).toEqual(finalizedPacked.eventNodes.find(node => node.kind === 'assistant')) + const namedToolHistory = [ at(40, 'turn/start', { turn: 3 }), at(41, 'step/start', { turn: 3, step: 1 }), @@ -365,7 +372,7 @@ describe('Trajectory conversation Definitions', () => { const namedToolPacked = snapshot(assembler(namedToolInputs)) expect(namedToolPacked.eventNodes.find(node => node.kind === 'assistant')).toMatchObject({ blocks: [{ kind: 'tool-call', callId: 'call-2', name: 'read', argsRaw: '' }], - timing: { firstTokenTime: null }, + timing: { firstTokenTime: 4_000 }, }) }) diff --git a/packages/client/ui-trajectory/tests/string-wrapping-store.client.spec.ts b/packages/client/ui-trajectory/tests/string-wrapping-store.client.spec.ts new file mode 100644 index 0000000000..7ab269f2cf --- /dev/null +++ b/packages/client/ui-trajectory/tests/string-wrapping-store.client.spec.ts @@ -0,0 +1,26 @@ +// @vitest-environment jsdom +import { afterEach, beforeEach, expect, it } from 'vitest' +import { createTrajectoryStringWrappingStore } from '../src/client/string-wrapping-store.ts' + +const key = 'dsh.trajectory.jsonStringWrapping' +let original: string | null + +beforeEach(() => { + original = localStorage.getItem(key) + localStorage.removeItem(key) +}) + +afterEach(() => { + if (original === null) localStorage.removeItem(key) + else localStorage.setItem(key, original) +}) + +it('restores the last wrapping choice when the view is recreated', () => { + const first = createTrajectoryStringWrappingStore() + expect(first.getSnapshot()).toBe(false) + first.set(true) + const second = createTrajectoryStringWrappingStore() + expect(second.getSnapshot()).toBe(true) + second.set(false) + expect(createTrajectoryStringWrappingStore().getSnapshot()).toBe(false) +}) diff --git a/packages/client/ui-trajectory/tests/table.client.spec.tsx b/packages/client/ui-trajectory/tests/table.client.spec.tsx index b16475162a..5a882141bb 100644 --- a/packages/client/ui-trajectory/tests/table.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/table.client.spec.tsx @@ -231,7 +231,12 @@ describe('TrajectoryTable', () => { expect(panel.querySelector('[data-summary-scroll-region]')).toBeNull() }) - it('keeps long thinking collapsed until the user asks to render it', () => { + it.each([ + { outputDetail: undefined, toolCall: false }, + { outputDetail: 'Visible answer', toolCall: false }, + { outputDetail: undefined, toolCall: true }, + { outputDetail: 'Visible answer', toolCall: true }, + ])('opens thinking with output=$outputDetail and toolCall=$toolCall', ({ outputDetail, toolCall }) => { const thinking = 'private chain '.repeat(1_000) const turns: readonly TrajectoryTurnModel[] = [{ turn: 1, @@ -242,6 +247,10 @@ describe('TrajectoryTable', () => { kind: 'message', text: 'private chain…', thinkingDetail: thinking, + ...(outputDetail === undefined ? {} : { outputDetail }), + ...(toolCall ? { sourceBlocks: [{ + type: 'tool-call', content: '{}', callId: 'call-1', toolName: 'read', + }] } : {}), timeSeconds: 1, }], }], @@ -249,14 +258,39 @@ describe('TrajectoryTable', () => { render() fireEvent.click(screen.getByRole('row', { name: /ASSISTANT/ })) + fireEvent.click(screen.getByRole('tab', { name: 'Preview' })) const toggle = screen.getByRole('button', { name: 'Thinking' }) - expect(toggle.getAttribute('aria-expanded')).toBe('false') - expect(screen.queryByText(thinking)).toBeNull() - - fireEvent.click(toggle) - expect(screen.getByRole('button', { name: 'Thinking' })).toBe(toggle) expect(toggle.getAttribute('aria-expanded')).toBe('true') expect(toggle.parentElement?.textContent?.length).toBeGreaterThan(thinking.length) + + fireEvent.click(toggle) + expect(toggle.getAttribute('aria-expanded')).toBe('false') + expect(screen.queryByText(thinking)).toBeNull() + fireEvent.click(screen.getByRole('tab', { name: 'Summary' })) + expect(screen.getByRole('button', { name: 'Thinking' }).getAttribute('aria-expanded')).toBe('false') + fireEvent.click(screen.getByRole('tab', { name: 'Preview' })) + fireEvent.click(screen.getByRole('button', { name: 'Thinking' })) + expect(screen.getByRole('button', { name: 'Thinking' }).getAttribute('aria-expanded')).toBe('true') + }) + + it('opens thinking on another record after collapsing the selected record', () => { + const turns: readonly TrajectoryTurnModel[] = [{ + turn: 1, + groups: [{ + title: 'Step 1', + cells: [1, 2].map(index => ({ + index, kind: 'message', text: `Answer ${index}`, outputDetail: `Answer ${index}`, + thinkingDetail: `Reasoning ${index}`, timeSeconds: 1, + })), + }], + }] + render() + fireEvent.click(screen.getByRole('row', { name: /ASSISTANT, Answer 1/ })) + fireEvent.click(screen.getByRole('button', { name: 'Thinking' })) + expect(screen.getByRole('button', { name: 'Thinking' }).getAttribute('aria-expanded')).toBe('false') + fireEvent.click(screen.getByRole('row', { name: /ASSISTANT, Answer 2/ })) + expect(screen.getByRole('button', { name: 'Thinking' }).getAttribute('aria-expanded')).toBe('true') + expect(screen.getByText('Reasoning 2')).toBeTruthy() }) it('keeps raw HTML tags in a Markdown-derived context preview', () => { diff --git a/packages/client/ui-trajectory/tests/views.client.spec.tsx b/packages/client/ui-trajectory/tests/views.client.spec.tsx index 79a0e14b0e..d0a00549d4 100644 --- a/packages/client/ui-trajectory/tests/views.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/views.client.spec.tsx @@ -538,7 +538,7 @@ describe('tab switching in ConversationRoot', () => { fireEvent.keyDown(screen.getByRole('row', { name: /工具/ }), { key: 'Enter' }) expect(screen.getByRole('complementary', { name: '事件详情' })).toBeTruthy() - expect(screen.getByText('第 1 轮 · 步骤 1')).toBeTruthy() + expect(screen.getByText('第 1 轮 · 第 1 步')).toBeTruthy() expect(screen.getByText('已完成')).toBeTruthy() expect(screen.getByRole('tab', { name: '结果' })).toBeTruthy() diff --git a/packages/compaction/README.i18n.yaml b/packages/compaction/README.i18n.yaml index 2bdee04415..fe00439d62 100644 --- a/packages/compaction/README.i18n.yaml +++ b/packages/compaction/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/compaction/README.md -README.md: dbab0fa400026f965e1fcbe8af88e0873bb560e4 -README.zh.md: 579f9941012f7250173e04f314316348d8e07e8a +README.md: 46ab8fab72d55b4926c9fe582187e025ce7a67d0 +README.zh.md: cb2f3cbab8bf0aee7d3dd57d77a39131f7fe6cbc diff --git a/packages/compaction/README.md b/packages/compaction/README.md index dbab0fa400..46ab8fab72 100644 --- a/packages/compaction/README.md +++ b/packages/compaction/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -The `compaction/` group keeps long agent conversations working near the model's context limit: older history is condensed into a summary automatically as token pressure builds, on demand with `/compact`, and oversized tool outputs can be trimmed first so there is less to condense. The shipped `dsh` base enables the feature by default — mount the packages explicitly to tune when and how condensation happens. The token measurement that decides when to condense lives in a separate LLM-family service. +The `compaction/` group keeps long agent conversations working near the model's context limit: older history is condensed into a summary automatically as token pressure builds, on demand with `/compact`, oversized tool outputs can be trimmed first so there is less to condense, and images an image-capable route can no longer send are replaced with placeholders. The shipped `dsh` base enables the feature by default — mount the packages explicitly to tune when and how condensation happens. The token measurement that decides when to condense lives in a separate LLM-family service. ## Table of Contents @@ -29,6 +29,7 @@ Each package below provides one piece of the feature; open a package page for ho | [`compaction/`](compaction/README.md) | The shared condensation contract: the operations and summary format every backend and trigger use | `ctx.compaction` | | [`compaction-basic/`](compaction-basic/README.md) | Automatic condensation of older history into a summary as token pressure builds | registers `ctx.compaction` | | [`compaction-tool-result-pruner/`](compaction-tool-result-pruner/README.md) | Trims oversized tool outputs so less history needs condensing | `ctx.toolResultPruner` | +| [`compaction-image-offload/`](compaction-image-offload/README.md) | Replaces over-budget request images with placeholders when an image-capable route rejects a request | listens to `agent/request-error` | | [`command-compact/`](command-compact/README.md) | The `/compact` command to condense history on demand | registers on `ctx.commands` | ----- diff --git a/packages/compaction/README.zh.md b/packages/compaction/README.zh.md index 579f994101..cb2f3cbab8 100644 --- a/packages/compaction/README.zh.md +++ b/packages/compaction/README.zh.md @@ -9,7 +9,7 @@ kind: "package-group" ## 概述 -`compaction/` 组让长时 agent(智能体)会话在接近模型上下文上限时仍能正常工作:token 压力上升时自动把较早历史压缩为摘要,可用 `/compact` 按需压缩,超大工具输出也可以先被修剪,从而减少需要压缩的内容。随附 `dsh` 基础配置默认启用该功能——显式挂载各包即可调整压缩发生的时机与方式。决定何时压缩的 token 测量属于独立的 LLM(大语言模型)家族服务。 +`compaction/` 组让长时 agent(智能体)会话在接近模型上下文上限时仍能正常工作:token 压力上升时自动把较早历史压缩为摘要,可用 `/compact` 按需压缩,超大工具输出也可以先被修剪,从而减少需要压缩的内容,支持图片的路由发不出的图片则被替换为占位文本。随附 `dsh` 基础配置默认启用该功能。显式挂载各包即可调整压缩发生的时机与方式。决定何时压缩的 token 测量属于独立的 LLM(大语言模型)家族服务。 ## 目录 @@ -29,6 +29,7 @@ kind: "package-group" | [`compaction/`](compaction/README.zh.md) | 共享的压缩约定:所有后端与触发器使用的操作与摘要格式 | `ctx.compaction` | | [`compaction-basic/`](compaction-basic/README.zh.md) | 随 token 压力上升自动把较早历史压缩为摘要 | 注册 `ctx.compaction` | | [`compaction-tool-result-pruner/`](compaction-tool-result-pruner/README.zh.md) | 修剪超大工具输出,减少需要压缩的历史 | `ctx.toolResultPruner` | +| [`compaction-image-offload/`](compaction-image-offload/README.zh.md) | 支持图片的路由拒绝请求时,把超出预算的请求图片替换为占位文本 | 监听 `agent/request-error` | | [`command-compact/`](command-compact/README.zh.md) | 按需压缩历史的 `/compact` 命令 | 注册到 `ctx.commands` | ----- diff --git a/packages/compaction/command-compact/README.i18n.yaml b/packages/compaction/command-compact/README.i18n.yaml index 3d14a886b9..00d262d04d 100644 --- a/packages/compaction/command-compact/README.i18n.yaml +++ b/packages/compaction/command-compact/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/compaction/command-compact/README.md -README.md: b8881234a3892ac566e41f28fbccbbf9ffa7fc3d -README.zh.md: 821017a1a89bc7ffd07b91313f9bf56b42f143c2 +README.md: 4c9a550febd3cef13fea82c61f9bf96f65a17b64 +README.zh.md: ad31604e9e37c3b676da9b6497cab660e90bc0e4 diff --git a/packages/compaction/command-compact/README.md b/packages/compaction/command-compact/README.md index b8881234a3..4c9a550feb 100644 --- a/packages/compaction/command-compact/README.md +++ b/packages/compaction/command-compact/README.md @@ -42,8 +42,8 @@ The command turns each expected failure into a stable message you can show direc | Situation | Message you see | |---|---| | Compaction already running, or the agent is mid-turn | `Compaction is unavailable because this process has an active compaction, or the agent is not idle.` | -| The history changed while condensing | `The history selected for compaction changed before it could be replaced. The conversation is unchanged; the attempt is recorded in the session log.` | -| No useful summary could be produced | `Compaction could not produce a useful summary. The conversation is unchanged; the attempt is recorded in the session log.` | +| The history changed while condensing | `The history selected for compaction changed before it could be replaced. The attempt is recorded in the session log.` | +| No useful summary could be produced | `Compaction could not produce a useful summary. The attempt is recorded in the session log.` | | Condensation did not finish cleanly | `Compaction did not finish cleanly; some session history may have changed. Inspect the current session state before retrying.` | | The conversation could not be saved | `Compaction finished, but the session could not be saved.` | diff --git a/packages/compaction/command-compact/README.zh.md b/packages/compaction/command-compact/README.zh.md index 821017a1a8..ad31604e9e 100644 --- a/packages/compaction/command-compact/README.zh.md +++ b/packages/compaction/command-compact/README.zh.md @@ -42,8 +42,8 @@ kind: "package-reference" | 情形 | 你看到的消息 | |---|---| | 压缩已在运行,或 agent 正在轮次中 | `Compaction is unavailable because this process has an active compaction, or the agent is not idle.` | -| 压缩过程中历史发生了变化 | `The history selected for compaction changed before it could be replaced. The conversation is unchanged; the attempt is recorded in the session log.` | -| 无法产生有用的摘要 | `Compaction could not produce a useful summary. The conversation is unchanged; the attempt is recorded in the session log.` | +| 压缩过程中历史发生了变化 | `The history selected for compaction changed before it could be replaced. The attempt is recorded in the session log.` | +| 无法产生有用的摘要 | `Compaction could not produce a useful summary. The attempt is recorded in the session log.` | | 压缩未干净地完成 | `Compaction did not finish cleanly; some session history may have changed. Inspect the current session state before retrying.` | | 会话无法保存 | `Compaction finished, but the session could not be saved.` | diff --git a/packages/compaction/command-compact/src/index.ts b/packages/compaction/command-compact/src/index.ts index cd31b9e0a7..2e95de4ae4 100644 --- a/packages/compaction/command-compact/src/index.ts +++ b/packages/compaction/command-compact/src/index.ts @@ -33,12 +33,12 @@ function expectedFailure(error: ManualCompactionError): CommandResult { case 'changed': return { kind: 'error', - text: 'The history selected for compaction changed before it could be replaced. The conversation is unchanged; the attempt is recorded in the session log.', + text: 'The history selected for compaction changed before it could be replaced. The attempt is recorded in the session log.', } case 'summary': return { kind: 'error', - text: 'Compaction could not produce a useful summary. The conversation is unchanged; the attempt is recorded in the session log.', + text: 'Compaction could not produce a useful summary. The attempt is recorded in the session log.', } case 'commit': return { diff --git a/packages/compaction/command-compact/tests/command-compact.spec.ts b/packages/compaction/command-compact/tests/command-compact.spec.ts index cf4fc3eb3a..73f0cf9728 100644 --- a/packages/compaction/command-compact/tests/command-compact.spec.ts +++ b/packages/compaction/command-compact/tests/command-compact.spec.ts @@ -208,8 +208,8 @@ describe('/compact human command', () => { it.each([ ['busy', 'Compaction is unavailable because this process has an active compaction, or the agent is not idle.'], ['cancelled', 'Compaction cancelled.'], - ['changed', 'The history selected for compaction changed before it could be replaced. The conversation is unchanged; the attempt is recorded in the session log.'], - ['summary', 'Compaction could not produce a useful summary. The conversation is unchanged; the attempt is recorded in the session log.'], + ['changed', 'The history selected for compaction changed before it could be replaced. The attempt is recorded in the session log.'], + ['summary', 'Compaction could not produce a useful summary. The attempt is recorded in the session log.'], ['commit', 'Compaction did not finish cleanly; some session history may have changed. Inspect the current session state before retrying.'], ['persistence', 'Compaction finished, but the session could not be saved.'], ] as const)('maps expected %s failures to direct errors', async (code, text) => { diff --git a/packages/compaction/compaction-basic/README.i18n.yaml b/packages/compaction/compaction-basic/README.i18n.yaml index 12e4a62f41..fba8635a73 100644 --- a/packages/compaction/compaction-basic/README.i18n.yaml +++ b/packages/compaction/compaction-basic/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/compaction/compaction-basic/README.md -README.md: 6f2c416f8c1adc87f04bbceb6a2ea1ba81fde2aa -README.zh.md: a830a83683c9aa87f974d090355a70570d785e79 +README.md: 68d5a321e04a90bfb726392d1fb179612f6e0626 +README.zh.md: f941cac74053a680cad859b68c7836e181065bc9 diff --git a/packages/compaction/compaction-basic/README.md b/packages/compaction/compaction-basic/README.md index 6f2c416f8c..68d5a321e0 100644 --- a/packages/compaction/compaction-basic/README.md +++ b/packages/compaction/compaction-basic/README.md @@ -119,6 +119,8 @@ A direct `ctx.llm.stream()` call uses the configured provider/model pair and cap ### The region transaction +Failed summary requests dispatch synchronous `compaction/summary-error` after checking cancellation and selection stability. A recovery listener must record a durable input change before requesting retry. The backend re-derives the selected messages and refreshes their token prices and shrink baseline. The image-offload plugin owns image selection; its recorded omissions remain effective if the summary later fails or is cancelled. + The transaction validates the surface span and the durable lock, appends `compaction/start`, summarizes through the hook, revalidates stability (whole-surface for automatic calls, selected-span for manual calls), rejects a summary that does not shrink its source, appends `compaction/summary` plus the replacement `user/message`, and makes exactly one `compaction/end` attempt. A live unmatched start is the durable lock: an unmatched marker before a newer `session/end-seed` is stale evidence from a prior lifecycle and does not block; one after that boundary reports `busy`. A failed close deliberately leaves a blocking orphan. Cancellation remains authoritative after cleanup and durability. ### Config resolution diff --git a/packages/compaction/compaction-basic/README.zh.md b/packages/compaction/compaction-basic/README.zh.md index a830a83683..f941cac740 100644 --- a/packages/compaction/compaction-basic/README.zh.md +++ b/packages/compaction/compaction-basic/README.zh.md @@ -119,6 +119,8 @@ kind: "package-reference" ### 区域事务 +摘要请求失败后,后端先检查取消和选区稳定性,再触发同步的 `compaction/summary-error`。恢复监听器必须先记录持久的输入变更,才能请求重试。后端重新派生所选消息,更新 token 价格及判断摘要是否缩短输入的基准。图片省略插件负责选图;后续摘要失败或取消后,已经记录的省略仍然有效。 + 事务验证表层范围与持久锁,追加 `compaction/start`,通过钩子生成摘要,重新验证稳定性(自动调用要求整个表层、手动调用只要求所选范围),拒绝不缩小源内容的摘要,追加 `compaction/summary` 与替换 `user/message`,并恰好进行一次 `compaction/end` 尝试。活动的未匹配 start 是持久锁:位于较新 `session/end-seed` 之前的未匹配标记是先前生命周期留下的陈旧证据,不会阻塞;位于该边界之后的标记报告 `busy`。闭合失败会有意留下阻塞性的未匹配标记。完成清理与持久化后,取消仍具有最终决定权。 ### 配置解析 diff --git a/packages/compaction/compaction-basic/package.json b/packages/compaction/compaction-basic/package.json index 82f980e727..281cac91fc 100644 --- a/packages/compaction/compaction-basic/package.json +++ b/packages/compaction/compaction-basic/package.json @@ -61,6 +61,7 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", - "@deepseek-ai/dsh-tools": "workspace:^" + "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-compaction-image-offload": "workspace:^" } } diff --git a/packages/compaction/compaction-basic/src/index.ts b/packages/compaction/compaction-basic/src/index.ts index 7a6ce3daba..8be02cb64a 100644 --- a/packages/compaction/compaction-basic/src/index.ts +++ b/packages/compaction/compaction-basic/src/index.ts @@ -8,7 +8,6 @@ import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { CompactionEngine, ManualCompactionError } from '@deepseek-ai/dsh-compaction' import type { CompactionResult, CompactionTrigger } from '@deepseek-ai/dsh-compaction' -import type { TokenMeter } from '@deepseek-ai/dsh-token-meter' import type { Session, SessionSeq } from '@deepseek-ai/dsh-session' import { CONTEXT_WINDOW_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm' import type { LlmCallConfig } from '@deepseek-ai/dsh-llm' @@ -46,9 +45,6 @@ export type { ResolvedTargetPolicy, } from './types.ts' -/** The region transaction's view of this service's dynamically dispatched summarizer. */ -type RegionSummarize = (input: SummarizationInput, agent: Agent, signal?: AbortSignal) => Promise - /** Resolve the exact provider/model durably routed for the latest request. */ function routedTarget( session: Session, @@ -421,10 +417,16 @@ export class BasicCompactionEngine extends CompactionEngine { } /** Bind the effective token meter and dynamically dispatched summarizer hook. */ - private regionDependencies(): { meter: TokenMeter; summarize: RegionSummarize } { + private regionDependencies(): Parameters[0] { return { meter: this.ctx.tokenMeter, summarize: (input, owner, abort) => this.summarize(input, owner, abort), + recover: (error, agent, sourceEventSeqs, signal) => this.ctx.waterfall('compaction/summary-error', { + session: agent.session, + sourceEventSeqs, + error, + ...signal === undefined ? {} : { signal }, + }, () => false), } } } diff --git a/packages/compaction/compaction-basic/src/region.ts b/packages/compaction/compaction-basic/src/region.ts index 048c732c8d..b4ba4ae380 100644 --- a/packages/compaction/compaction-basic/src/region.ts +++ b/packages/compaction/compaction-basic/src/region.ts @@ -27,6 +27,7 @@ import type { SummarizationInput, SummaryResult } from './summarizer.ts' interface RegionDependencies { readonly meter: TokenMeter summarize(input: SummarizationInput, agent: Agent, signal?: AbortSignal): Promise + recover(error: unknown, agent: Agent, sourceEventSeqs: readonly SessionSeq[], signal?: AbortSignal): boolean } /** One validated inclusive span of current surface positions. */ @@ -226,6 +227,7 @@ export async function compactSurfaceRegion( agent, compactionId, options.sourceCommandId, + assertStable, signal, ) if (options.owner === null) signal?.throwIfAborted() @@ -389,9 +391,23 @@ async function summarizeCompaction( agent: Agent, compactionId: CompactionResult['compactionId'], sourceCommandId: CommandId | undefined, + assertStable: StabilityCheck, signal?: AbortSignal, ): Promise { - const summaryResult = await dependencies.summarize(prepared.input, agent, signal) + let summaryResult: SummaryResult + for (;;) { + signal?.throwIfAborted() + try { + summaryResult = await dependencies.summarize(prepared.input, agent, signal) + break + } catch (error: unknown) { + if (signal?.aborted === true) throw error + assertStable(dependencies, agent.session, prepared) + if (!dependencies.recover(error, agent, prepared.shadowedSeqs, signal)) throw error + prepared = prepareCompaction(dependencies, agent.session, + validateSurfaceRegion(agent.session, prepared.start, prepared.end)) + } + } const checkpointMessage = createUserMessage({ content: frameSummary(summaryResult.summary), source: compactCheckpointSource(compactionId, sourceCommandId), diff --git a/packages/compaction/compaction-basic/src/summarizer.ts b/packages/compaction/compaction-basic/src/summarizer.ts index 5fe259a756..e015bf887d 100644 --- a/packages/compaction/compaction-basic/src/summarizer.ts +++ b/packages/compaction/compaction-basic/src/summarizer.ts @@ -196,9 +196,7 @@ function finishError(finish: FinishReason): Error | undefined { switch (finish.kind) { case 'error': case 'aborted': { - const error = new Error(finish.failure.message) as Error & { code?: string } - error.code = finish.failure.code - return error + return new LlmError(finish.failure.message, finish.failure.code, finish.failure) } case 'max-tokens': { const error = new Error('summarization truncated at the token cap (incomplete checkpoint)') as Error & { code?: string } diff --git a/packages/compaction/compaction-basic/tests/manual-compaction.spec.ts b/packages/compaction/compaction-basic/tests/manual-compaction.spec.ts index 9a118cd5a9..26d957ce92 100644 --- a/packages/compaction/compaction-basic/tests/manual-compaction.spec.ts +++ b/packages/compaction/compaction-basic/tests/manual-compaction.spec.ts @@ -1,3 +1,4 @@ +import { imageOffloadProjection } from '@deepseek-ai/dsh-compaction-image-offload/projection' import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentLoop from '@deepseek-ai/dsh-agent-loop' @@ -99,6 +100,7 @@ interface LoopHarness { async function loopHarness(): Promise { const ctx = new Context() await mountAgentLoopTestDependencies(ctx) + ctx.sessions.registerMessageProjection(imageOffloadProjection) await ctx.plugin(InvariantRegistry) await ctx.plugin(SessionInvariant) await ctx.plugin(AgentInvariant) @@ -237,6 +239,28 @@ function compactEvents(session: Session): SessionEvent[] { } describe('compactNow through the real loop', () => { + it('passes logged image omissions to the summarizer without changing the original message', async () => { + const { ctx, agent, compact } = await loopHarness() + try { + agent.followup(createUserMessage({ + content: [{ type: 'text', text: PROMPT }, { + type: 'image', + attachment: { attachmentId: `sha256:${'a'.repeat(64)}` as never, mediaType: 'image/png', bytes: 1, width: 1, height: 1 }, + }], + source: { kind: 'user' }, + })) + await agent.whenIdle() + const source = agent.session.snapshotEvents().find(event => event.type === 'user/message')! + agent.session.append('image/offload', { targets: [{ seq: source.seq, imageIndexes: [0] }] }) + expect(await compact.compactNow(agent, SIGNAL)).not.toBeNull() + const image = compact.calls[0]?.messages.flatMap(message => message.content).find(block => block.type === 'image') + expect(image).toMatchObject({ offloaded: true }) + expect(JSON.stringify(source)).not.toContain('offloaded') + } finally { + await ctx.fiber.dispose() + } + }) + it('holds a prompt accepted during summarization until the standalone bracket is flushed', async () => { const harness = await loopHarness() const { agent, compact, adapter, log } = harness diff --git a/packages/compaction/compaction-image-offload/README.i18n.yaml b/packages/compaction/compaction-image-offload/README.i18n.yaml new file mode 100644 index 0000000000..c3da3ba739 --- /dev/null +++ b/packages/compaction/compaction-image-offload/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/compaction/compaction-image-offload/README.md +README.md: 8e4d812c043234e07207c7326882856b445b0950 +README.zh.md: 06ed168e750cffbaa6594454ac4459f0ceb99788 diff --git a/packages/compaction/compaction-image-offload/README.md b/packages/compaction/compaction-image-offload/README.md new file mode 100644 index 0000000000..8e4d812c04 --- /dev/null +++ b/packages/compaction/compaction-image-offload/README.md @@ -0,0 +1,108 @@ +--- +description: "The image offload executor for deployments composing compaction: what happens when an image-capable route rejects a request as over its image budget." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-compaction-image-offload + +English | [中文](README.zh.md) + +## Summary + +Image-heavy conversations continue when older images exceed a model route's budget. The plugin permanently replaces those images with text naming each attachment and its available read-only path, then retries without spending the provider retry budget. Later requests retain that choice across route changes, resume, and replay. Token accounting follows the logged selections, and provider cache reuse ends at the first changed message. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +Mount this plugin in every composition that runs the agent loop with an image-capable route. The shipped `dsh` base does. Without it, an `IMAGE_OFFLOAD_REQUIRED` failure reaches ordinary recovery and ends the turn as an error. The plugin has no configuration: the DeepSeek adapter enforces its file-mode and inline-fallback budgets, the pi-ai adapter its base64 bound, and each reports the count it needs offloaded. + +### Minimal configuration + +```yaml +- name: '@deepseek-ai/dsh-compaction-image-offload' +``` + +### What you can observe + +Each decision appends one `image/offload` event identifying the selected occurrences by current message-event sequence and depth-first image index. The message events and surface node identities remain unchanged. An agent retry follows a fresh `request/header` identifying a new message series. Summary retries remain inside the same compaction bracket. + +### Failures and recovery + +The plugin acts only on `IMAGE_OFFLOAD_REQUIRED` failures that carry `offloadImages`. It walks the surface in model request order, skips assistant nodes and images already marked, and marks that many occurrences. When nothing remains to offload it delegates on the `agent/request-error` waterfall, so downstream recovery or the ordinary turn error applies. The retry spends no provider retry budget and logs no `llm/retry` event. + +----- + + +## Understand the implementation + +
+Implementation internals — click to expand + +This package owns the event declaration, reference validation, immutable image projection, selection, and retry policy. It registers `imageOffloadProjection` with Session before accepting image-offload events. The same browser-safe definition is exported from `./projection` for detached replay; the installed format catalog assembles it for offline readers. Missing registration rejects live restore, and unloading a used definition blocks further message derivation. Token measurement folds the same selections without replacement shadow prices. + +Summary failures use the synchronous `compaction/summary-error` waterfall. The plugin selects only the supplied summary region and returns true after recording an omission. The compaction backend re-derives and re-prices that region before retrying; each retry omits additional retained occurrences, so recovery ends when none remain. Cancellation and unrelated selection changes reject the summary. Recorded omissions survive later failure or cancellation. + +No runtime invariant companion is published: the pure projection rejects invalid or repeated image references before Session commits the event, and this executor retains no separate mutable offload state. + +
+ +----- + + +## Further Exploration + +- [Dedicated image-offload events](../../../.agents/notes/implemented/architecture/2026-09-10-image-offload-events.md) — durable selections, ownership, and rejected alternatives. +- [compaction seam](../compaction/README.md) — the neighboring summary and text-pruning operations. +- [compaction-tool-result-pruner](../compaction-tool-result-pruner/README.md) — the sibling executor that trims tool outputs while preserving image selections. +- [dsh-llm](../../llm/llm/README.md) — `ImageBlock.offloaded`, `IMAGE_OFFLOAD_REQUIRED`, and the placeholder projection. +- [llm-deepseek adapter](../../llm/llm-deepseek/README.md) and [llm-pi-ai adapter](../../llm/llm-pi-ai/README.md) — the route budgets that report offload counts. + +----- + + +## Model Experience + +### Offloaded request images + +#### What the model sees + +Every selected image occurrence reaches the model as placeholder text (`offloadedImageText`) naming the attachment and its available read-only path; unselected occurrences stay images. Selections persist across requests. A new tool read may introduce a new occurrence of the same attachment without restoring the old one. + +#### Token effect + +An offloaded occurrence costs its placeholder text instead of visual tokens. The token meter applies the logged selections when pricing each node. Reference-only heuristic counts exclude offload metadata. + +#### KV Cache effect + +A selection turns earlier images into placeholder text, so provider cache reuse ends at the first changed image for that request. The selected occurrences remain omitted afterwards. + +## Known Limitations and Deferred Work + + + +- **Offloaded images never return automatically** — a larger budget, a larger route, or compaction lowering the total leaves the marks in place; recovery is the read-only path in the placeholder. +- **Every offload costs one failed attempt** — nothing plans an offload before dispatch; the route's rejection is the signal, which matches the planned move to provider-reported uncacheable images. +- **A temporary small budget offloads permanently** — a Files outage that forces the inline fallback, or a temporary switch to a small-budget route, offloads images a later route would have sent. + + +### Dev Note + +
+Working context for maintainers — click to expand + +This Dev Note is non-authoritative working context: notes for maintainers and open questions. Shipped behavior and accepted rationale live in the sections above, the package code, and the linked Agent Notes. + +- The route-local byte checks that produce `IMAGE_OFFLOAD_REQUIRED` are provisional: once the provider reports uncacheable images itself, the adapters map that report to a count and this executor stays as it is. + +
diff --git a/packages/compaction/compaction-image-offload/README.zh.md b/packages/compaction/compaction-image-offload/README.zh.md new file mode 100644 index 0000000000..06ed168e75 --- /dev/null +++ b/packages/compaction/compaction-image-offload/README.zh.md @@ -0,0 +1,108 @@ +--- +description: "面向组合 compaction 的部署的图片省略执行器说明:支持图片的路由以请求超出图片预算拒绝时会发生什么。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-compaction-image-offload + +[English](README.md) | 中文 + +## 概述 + +当较早的图片超出模型路由的预算时,图片密集的会话仍可继续。本插件永久将这些图片替换为注明附件及其可用只读路径的文本,然后重试,不消耗提供方重试预算。后续请求在切换路由、恢复和回放时都保留这一选择。token 计量随日志记录的选择更新,提供方缓存只能复用到第一条被修改的消息之前。 + +## 目录 + +- [使用本包](#use-this-package) +- [理解实现](#understand-the-implementation) +- [进一步探索](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与延期工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用本包 + +凡是运行 agent loop(智能体循环)并带有支持图片的路由的组合,都应挂载本插件,随附的 `dsh` 基础配置已经挂载。没有它,`IMAGE_OFFLOAD_REQUIRED` 失败会进入普通恢复并以错误结束该轮次。本插件没有配置:DeepSeek 适配器执行其 file 模式和内联回退预算,pi-ai 适配器执行其 base64 上限,各自上报需要省略的数量。 + +### 最小可用组合 + +```yaml +- name: '@deepseek-ai/dsh-compaction-image-offload' +``` + +### 你可以观察到什么 + +每次决定追加一条 `image/offload` 事件,通过当前消息事件的序号和消息内深度优先的图片序号指定省略位置。消息事件和表层节点标识保持不变。agent 重试前会追加新的 `request/header`,标明新的消息序列。摘要重试保留在同一对压缩标记内。 + +### 失败与恢复 + +本插件只处理带 `offloadImages` 的 `IMAGE_OFFLOAD_REQUIRED` 失败。它按模型请求顺序遍历表层,跳过 assistant 节点和已经标记的图片,给指定数量的出现位置打标记。没有可省略的图片时在 `agent/request-error` waterfall 上委派下游,由下游恢复或普通的轮次错误处理。这次重试不占提供方重试预算,也不追加 `llm/retry` 事件。 + +----- + + +## 理解实现 + +
+实现内部——点击展开 + +本包拥有事件声明、引用校验、不可变图片投影、选图和重试策略。它在接受图片省略事件前向 Session 注册 `imageOffloadProjection`。相同的浏览器安全处理器通过 `./projection` 导出,供独立回放使用,当前格式目录为离线读取器装配该处理器。缺少注册时拒绝实时恢复,卸载已经使用的处理器后拒绝继续派生消息。token 测量应用同一份选择,不依赖替换影子价格。 + +摘要失败使用同步的 `compaction/summary-error` waterfall。插件只在传入的摘要选区内选图,记录省略后返回 true。压缩后端重新派生该选区并计价,然后重试。每次重试都会继续省略保留的图片,没有可省略的位置时结束恢复。取消或无关的选区变更会使摘要失败。已经记录的省略在后续失败或取消后仍然有效。 + +本包不发布运行时 invariant 伴生插件:纯投影在 Session 提交事件前拒绝无效或重复省略的图片引用,执行器不维护独立可变的省略状态。 + +
+ +----- + + +## 进一步探索 + +- [独立的图片省略事件](../../../.agents/notes/implemented/architecture/2026-09-10-image-offload-events.zh.md),记录持久选择、职责和被否决的方案。 +- [compaction seam](../compaction/README.zh.md),相邻的摘要和文本剪枝操作。 +- [compaction-tool-result-pruner](../compaction-tool-result-pruner/README.zh.md),保留图片选择并修剪工具输出的兄弟执行器。 +- [dsh-llm](../../llm/llm/README.zh.md)——`ImageBlock.offloaded`、`IMAGE_OFFLOAD_REQUIRED` 与占位投影。 +- [llm-deepseek 适配器](../../llm/llm-deepseek/README.zh.md)与 [llm-pi-ai 适配器](../../llm/llm-pi-ai/README.zh.md)——上报省略数量的路由预算。 + +----- + + +## 模型体验 + +### 已省略的请求图片 + +#### 模型看到的内容 + +每个选中的图片出现位置以占位文本(`offloadedImageText`)到达模型,文本注明附件及其可用只读路径,未选中的位置仍是图片。选择在后续请求中持续生效。新的工具读取可以引入同一附件的新出现位置,不会恢复旧位置。 + +#### Token 影响 + +被省略的出现位置只花费占位文本,不再花费视觉 token。token meter 为每个节点定价时应用日志记录的选择。仅针对引用的启发式计数不计入省略元数据。 + +#### KV Cache 影响 + +一次选择把较早的图片换成占位文本,该请求的提供方缓存复用因此止于第一张被修改的图片。所选位置之后仍保持省略。 + +## 已知限制与延期工作 + + + +- **被省略的图片不会自动回归**——更大的预算、更大的路由或 compaction 降低总量都不会移除标记;恢复手段是占位文本中的只读路径。 +- **每次省略都要先失败一次**——发送前没有任何规划,路由的拒绝就是信号,这和计划中改由 provider 报告无法缓存图片的方向一致。 +- **临时的小预算会永久省略**——迫使内联回退的 Files 故障,或临时切到小预算路由,会省略后来的路由本可以发送的图片。 + + +### 开发备注 + +
+维护者工作背景——点击展开 + +本开发备注是非权威的工作背景:给维护者的备注和未决问题。已交付的行为与被接受的理由见上文各节、包代码和链接的 Agent Note。 + +- 产生 `IMAGE_OFFLOAD_REQUIRED` 的路由本地字节检查是过渡实现:provider 自己报告无法缓存的图片之后,adapter 把该报告换算成数量,本执行器保持不变。 + +
diff --git a/packages/compaction/compaction-image-offload/package.json b/packages/compaction/compaction-image-offload/package.json new file mode 100644 index 0000000000..a00b22b0d1 --- /dev/null +++ b/packages/compaction/compaction-image-offload/package.json @@ -0,0 +1,57 @@ +{ + "name": "@deepseek-ai/dsh-compaction-image-offload", + "description": "Durable image offload for image-capable routes: replace over-budget request images with placeholders and retry", + "version": "0.1.5-rc.2", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/compaction/compaction-image-offload" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./package.json": "./package.json", + "./projection": { + "types": "./lib/types/projection.d.ts", + "default": "./lib/types/projection.js" + } + }, + "files": [ + "lib/index.js", + "lib/types/**/*.js", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-compaction": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-loop": "workspace:^", + "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", + "@deepseek-ai/dsh-attachment": "workspace:^", + "@deepseek-ai/dsh-compaction": "workspace:^", + "@deepseek-ai/dsh-compaction-basic": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-llm-deepseek": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/dsh-token-meter": "workspace:^" + }, + "dependencies": { + "@deepseek-ai/dsh-util-values": "workspace:^" + } +} diff --git a/packages/compaction/compaction-image-offload/src/image-offload.ts b/packages/compaction/compaction-image-offload/src/image-offload.ts new file mode 100644 index 0000000000..5c686ba4c8 --- /dev/null +++ b/packages/compaction/compaction-image-offload/src/image-offload.ts @@ -0,0 +1,48 @@ +/** Select and log permanent image omissions in current model-request order. */ + +import type { ContentBlock } from '@deepseek-ai/dsh-llm' +import type { Session, SessionSeq } from '@deepseek-ai/dsh-session' +import type { ImageOffloadTarget } from './projection.ts' + +/** + * Record one decision omitting the oldest retained input-image occurrences. + * Assistant nodes carry model output and are excluded. Image indexes count + * every occurrence, including previously offloaded ones, within each message. + * @param session - session whose next request applies the decision. + * @param sourceEventSeqs - input message events in the failed request's order. + * @param count - additional retained occurrences the adapter needs omitted. + * @returns whether any occurrence remained to offload. + */ +export function offloadOldestImages(session: Session, sourceEventSeqs: readonly SessionSeq[], count: number): boolean { + const targets: ImageOffloadTarget[] = [] + for (const seq of sourceEventSeqs) { + if (count === 0) break + // Existing Session history read; migration deferred. Current surface nodes index the durable log. + // oxlint-disable-next-line typescript/no-non-null-assertion, typescript/no-deprecated + const event = session.eventAt(seq)! + if (event.type !== 'user/message' && event.type !== 'tool/result') continue + // oxlint-disable-next-line typescript/no-non-null-assertion -- both input node types produce a message + const message = session.deriveEventMessage(event)! + const imageIndexes: number[] = [] + let imageIndex = 0 + const visit = (blocks: readonly ContentBlock[]): void => { + for (const block of blocks) { + if (count === 0) break + if (block.type === 'image') { + if (block.offloaded !== true) { + imageIndexes.push(imageIndex) + count -= 1 + } + imageIndex += 1 + } else if (block.type === 'tool-result') { + visit(block.content) + } + } + } + visit(message.content) + if (imageIndexes.length > 0) targets.push({ seq, imageIndexes }) + } + if (targets.length === 0) return false + session.append('image/offload', { targets }) + return true +} diff --git a/packages/compaction/compaction-image-offload/src/index.ts b/packages/compaction/compaction-image-offload/src/index.ts new file mode 100644 index 0000000000..5415cd5c4c --- /dev/null +++ b/packages/compaction/compaction-image-offload/src/index.ts @@ -0,0 +1,41 @@ +/** + * Image offload executor for the compaction seam. When an image-capable route + * fails a request with `IMAGE_OFFLOAD_REQUIRED`, the plugin records one + * `image/offload` decision selecting the oldest retained input occurrences + * and retries through the agent or compaction summary error waterfall. Every route + * sends placeholder text for those occurrences in subsequent requests. + * + * @module @deepseek-ai/dsh-compaction-image-offload + */ + +import type { Context } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-compaction' +import type { RequestErrorAction } from '@deepseek-ai/dsh-agent' +import { IMAGE_OFFLOAD_REQUIRED_CODE, LlmError } from '@deepseek-ai/dsh-llm' +import { offloadOldestImages } from './image-offload.ts' +import { imageOffloadProjection } from './projection.ts' + +export const name = 'compaction-image-offload' +export const inject = ['agents', 'sessions'] + +/** + * Mount agent and summary recovery listeners without configuration. + * @param ctx - the plugin context. + */ +export function apply(ctx: Context): void { + ctx.sessions.registerMessageProjection(imageOffloadProjection) + ctx.on('agent/request-error', ({ agent, failure }, next): Promise => { + if (failure.code !== IMAGE_OFFLOAD_REQUIRED_CODE || failure.offloadImages === undefined) return next() + // A durable surface repair, not a provider retry: it spends no retry budget and logs no retry event. + if (!offloadOldestImages(agent.session, agent.session.surface.nodes, failure.offloadImages)) return next() + return Promise.resolve({ kind: 'retry' }) + }) + ctx.on('compaction/summary-error', ({ session, sourceEventSeqs, error, signal }, next) => { + if (!(error instanceof LlmError) + || error.code !== IMAGE_OFFLOAD_REQUIRED_CODE + || error.failure.offloadImages === undefined) return next() + signal?.throwIfAborted() + if (!offloadOldestImages(session, sourceEventSeqs, error.failure.offloadImages)) return next() + return true + }) +} diff --git a/packages/compaction/compaction-image-offload/src/project-message.ts b/packages/compaction/compaction-image-offload/src/project-message.ts new file mode 100644 index 0000000000..8d5754e2ee --- /dev/null +++ b/packages/compaction/compaction-image-offload/src/project-message.ts @@ -0,0 +1,39 @@ +/** Immutable application of the image occurrences recorded by image/offload. */ + +import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm' +import { deepFreeze } from '@deepseek-ai/dsh-util-values' + +/** + * Project selected image occurrences to immutable offloaded blocks. + * @param message - message projected before this decision. + * @param indexes - nonempty, strictly increasing depth-first image indexes. + * @returns an immutable message with the same identity and selected images marked. + * @throws when a selected occurrence is missing or already offloaded. + */ +export function offloadMessageImages(message: Message, indexes: readonly number[]): Message { + let imageIndex = 0 + let selected = 0 + const visit = (blocks: readonly ContentBlock[]): ContentBlock[] => { + let next: ContentBlock[] | undefined + for (const [index, block] of blocks.entries()) { + let projected = block + if (block.type === 'image') { + if (imageIndex === indexes[selected]) { + if (block.offloaded === true) throw new Error(`image/offload: image index ${imageIndex} is already offloaded`) + projected = { ...block, offloaded: true } + selected += 1 + } + imageIndex += 1 + } else if (block.type === 'tool-result') { + const content = visit(block.content) + if (content !== block.content) projected = { ...block, content } + } + if (projected !== block) next ??= blocks.slice(0, index) + next?.push(projected) + } + return next ?? blocks as ContentBlock[] + } + const content = visit(message.content) + if (selected !== indexes.length) throw new Error(`image/offload: image index ${indexes[selected]} does not exist`) + return deepFreeze({ ...message, content }) +} diff --git a/packages/compaction/compaction-image-offload/src/projection.ts b/packages/compaction/compaction-image-offload/src/projection.ts new file mode 100644 index 0000000000..1dd0e81bd0 --- /dev/null +++ b/packages/compaction/compaction-image-offload/src/projection.ts @@ -0,0 +1,74 @@ +/** Browser-safe durable image selection declaration and pure replay definition. */ + +import type { Message } from '@deepseek-ai/dsh-llm' +import type { SessionSeq } from '@deepseek-ai/dsh-session/types' +import type { SessionMessageProjection } from '@deepseek-ai/dsh-session/surface' +import { offloadMessageImages } from './project-message.ts' + +/** Exact input-image occurrences selected by one durable offload decision. */ +export interface ImageOffloadTarget { + /** Current message-producing event containing these occurrences. */ + seq: SessionSeq + /** Zero-based depth-first image indexes within the immutable message. */ + imageIndexes: number[] +} + +declare module '@deepseek-ai/dsh-session/types' { + interface SessionEventMap { + /** + * Permanently omit selected input-image occurrences from subsequent model requests. + * Targets name unique current user/message or tool/result nodes. Nonempty, strictly + * increasing indexes count all images in depth-first order, including nested tool + * results and already omitted images. Message nodes and identities remain unchanged. + * @messageProjection + */ + 'image/offload': { targets: ImageOffloadTarget[] } + } +} + +/** Whether a durable value is a JSON object. */ +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +/** Whether a durable occurrence index or sequence is canonical. */ +function isIndex(value: unknown): value is SessionSeq { + return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0) +} + +/** Atomic validation and reconstruction shared by live sessions and detached replay. */ +export const imageOffloadProjection: SessionMessageProjection<'image/offload'> = { + type: 'image/offload', + project(event, context) { + const data: unknown = event.data + if (!isRecord(data) || Object.keys(data).length !== 1 || !Array.isArray(data['targets']) || data['targets'].length === 0) { + throw new Error('image/offload: data must contain a nonempty targets array') + } + const messages = new Map() + const nodes = new Set(context.nodes) + for (const target of data['targets'] as unknown[]) { + if (!isRecord(target) || Object.keys(target).length !== 2 || !isIndex(target['seq']) + || !Array.isArray(target['imageIndexes']) || target['imageIndexes'].length === 0) { + throw new Error('image/offload: each target must contain a seq and nonempty imageIndexes') + } + const seq = target['seq'] + if (messages.has(seq)) throw new Error(`image/offload: duplicate target seq ${seq}`) + if (!nodes.has(seq)) throw new Error(`image/offload: target seq ${seq} is not a current surface node`) + const source = context.events[seq - context.baseSeq] + if (source?.type !== 'user/message' && source?.type !== 'tool/result') { + throw new Error(`image/offload: target seq ${seq} must be user/message or tool/result`) + } + let previous = -1 + for (const index of target['imageIndexes'] as unknown[]) { + if (!isIndex(index) || index <= previous) { + throw new Error('image/offload: imageIndexes must be strictly increasing non-negative safe integers') + } + previous = index + } + const message = context.messages.get(seq) + ?? (source.type === 'user/message' ? source.data : source.data.message) + messages.set(seq, offloadMessageImages(message, target['imageIndexes'] as number[])) + } + return messages + }, +} diff --git a/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts b/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts new file mode 100644 index 0000000000..59ecd36568 --- /dev/null +++ b/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts @@ -0,0 +1,409 @@ +/** + * Image offload recovery: an adapter's `IMAGE_OFFLOAD_REQUIRED` failure + * records exact image occurrences and retries without replacing messages. + */ + +import { afterEach, describe, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import type { Agent } from '@deepseek-ai/dsh-agent' +import BasicCompactionEngine from '@deepseek-ai/dsh-compaction-basic' +import TokenMeter from '@deepseek-ai/dsh-token-meter' +import { ImageVariantId } from '@deepseek-ai/dsh-attachment' +import { serializeRequestWithImages } from '@deepseek-ai/dsh-llm-deepseek/src/protocols/chat-completions/serialize.ts' +import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' +import { createAssistantMessage, createToolResultMessage, createUserMessage, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ToolCallId } from '@deepseek-ai/dsh-llm' +import type { ContentBlock, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' +import { isReplacementSurfaceEvent, SessionId } from '@deepseek-ai/dsh-session' +import type { Session } from '@deepseek-ai/dsh-session' +import * as offload from '../src/index.ts' + +type ScriptEntry = StreamChunk[] | (() => never) + +/** Replies one scripted stream per request and declares no retry policy. */ +class ScriptedAdapter extends LlmAdapter { + readonly requests: GenerateOptions[] = [] + serializeSummary = false + + constructor(readonly script: ScriptEntry[]) { + super() + } + + async * stream(options: GenerateOptions): AsyncIterable { + this.requests.push(options) + if (this.serializeSummary && options.purpose === 'compaction') { + await serializeRequestWithImages(options, { + representation: { kind: 'base64' }, + requestImages: new Map([image('first').attachment].map(ref => [ref.attachmentId, { + variantId: ImageVariantId(`sha256:${'b'.repeat(64)}`), attachment: ref, + data: new Uint8Array(ref.bytes), mediaType: ref.mediaType, bytes: ref.bytes, + width: ref.width, height: ref.height, depth: 'uchar', space: 'srgb', hasAlpha: false, + }])), + maxRequestImageBytes: 1, + }) + } + const entry = this.script.shift() + if (entry === undefined) throw new Error('script exhausted') + if (typeof entry === 'function') entry() + else yield * entry + } +} + +function textResponse(text: string): StreamChunk[] { + return [ + { type: 'block-start', index: 0, blockType: 'text' }, + { type: 'block-end', index: 0, block: { type: 'text', text } }, + { type: 'finish', reason: { kind: 'stop' } }, + ] +} + +function offloadRequired(offloadImages: number): () => never { + return () => { + throw new LlmError('request images exceed the route budget', IMAGE_OFFLOAD_REQUIRED_CODE, { offloadImages }) + } +} + +async function harness(adapter: ScriptedAdapter): Promise { + const ctx = new Context() + contexts.push(ctx) + await mountAgentLoopTestDependencies(ctx) + await ctx.plugin(offload) + await ctx.plugin(AgentLoop, { agents: [] }) + ctx.llm.registerAdapter(['mock'], adapter) + return ctx +} + +const contexts: Context[] = [] +afterEach(async () => { + await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) +}) + +function image(name: string): Extract { + return { + type: 'image', + attachment: { attachmentId: `sha256:${'a'.repeat(64)}` as never, name, mediaType: 'image/png', bytes: 1, width: 1, height: 1 }, + } +} + +function offloadedNames(options: GenerateOptions): string[] { + const names: string[] = [] + const visit = (blocks: readonly ContentBlock[]): void => { + for (const block of blocks) { + if (block.type === 'image' && block.offloaded === true) names.push(block.attachment.name ?? '') + if (block.type === 'tool-result') visit(block.content) + } + } + for (const message of options.messages) visit(message.content) + return names +} + +/** Surface replacements appended by the recovery, as `[original seq, replacement seq]` pairs. */ +function replacements(session: Session): [number, number][] { + return session.snapshotEvents() + .filter(isReplacementSurfaceEvent) + .map(event => [Number(event.sourceEventSeqs?.[0]), Number(event.seq)]) +} + +function decisions(session: Session) { + return session.snapshotEvents().filter(event => event.type === 'image/offload') +} + +async function summaryHarness(script: ScriptEntry[]) { + const adapter = new ScriptedAdapter(script) + const ctx = await harness(adapter) + await ctx.plugin(TokenMeter) + const compact = new BasicCompactionEngine(ctx, { auto: false, summarizationProvider: 'mock', summarizationModel: 'summary' }) + const agent = await ctx.agentLoop.create(SessionId('summary-offload'), { provider: 'mock', model: 'mock' }) + return { ctx, compact, agent, adapter } +} + +async function seedImages(agent: Agent, names: string[]) { + agent.followup(createUserMessage({ + content: [{ type: 'text', text: 'conversation details '.repeat(400) }, ...names.map(image)], + source: { kind: 'user' }, + })) + await agent.whenIdle() + const nodes = agent.session.surface.nodes + return { start: nodes.at(-2)!, end: nodes.at(-1)! } +} + +describe('summary image offload', () => { + it.each([new Error('summary failed'), new LlmError('no count', IMAGE_OFFLOAD_REQUIRED_CODE)])('delegates unhandled summary errors: %s', async (error) => { + const { ctx, agent } = await summaryHarness([]) + expect(ctx.waterfall('compaction/summary-error', { session: agent.session, sourceEventSeqs: [], error }, () => false)).toBe(false) + expect(decisions(agent.session)).toEqual([]) + }) + + it('recovers the real summary serializer with a tighter image budget and fresh pricing', async () => { + const { compact, agent, adapter } = await summaryHarness([textResponse('answer'), textResponse('checkpoint')]) + const span = await seedImages(agent, ['first', 'second']) + adapter.serializeSummary = true + const result = await compact.compactNow(agent, new AbortController().signal) + expect(result).not.toBeNull() + expect(adapter.requests.map(offloadedNames)).toEqual([[], [], ['first', 'second']]) + expect(adapter.requests.slice(1).map(request => request.model)).toEqual(['summary', 'summary']) + const events = agent.session.snapshotEvents() + const types = events.map(event => event.type) + expect(types.filter(type => type === 'compaction/start')).toHaveLength(1) + expect(types.filter(type => type === 'compaction/end')).toHaveLength(1) + const [decision] = decisions(agent.session) + expect(decision?.data.targets).toEqual([{ seq: span.start, imageIndexes: [0, 1] }]) + expect(decision!.seq).toBeLessThan(types.indexOf('compaction/summary')) + expect(events[span.start]).not.toHaveProperty('data.content.1.offloaded') + expect(types).not.toContain('compaction/prune') + expect(types).not.toContain('llm/retry') + }) + + it('offloads only the selected summary span and stops after exhausting its images', async () => { + const { compact, agent, adapter } = await summaryHarness([ + textResponse('before'), textResponse('selected'), textResponse('after'), + offloadRequired(1), offloadRequired(1), offloadRequired(1), + ]) + await seedImages(agent, ['outside-before']) + const selected = await seedImages(agent, ['first', 'second']) + await seedImages(agent, ['outside-after']) + agent.session.append('turn/start', { turn: 4 }) + await expect(compact.compactRegion(selected.start, selected.end, agent)).rejects.toMatchObject({ code: IMAGE_OFFLOAD_REQUIRED_CODE }) + expect(adapter.requests.slice(3).map(offloadedNames)).toEqual([[], ['first'], ['first', 'second']]) + expect(decisions(agent.session).map(event => event.data.targets)).toEqual([ + [{ seq: selected.start, imageIndexes: [0] }], [{ seq: selected.start, imageIndexes: [1] }], + ]) + expect(agent.session.surface.replaceGeneration).toBe(0) + const end = agent.session.snapshotEvents().at(-1) + expect(end?.type).toBe('compaction/end') + expect(end?.type === 'compaction/end' && typeof end.data.error).toBe('string') + }) + + it('preserves omission when a subsequent summary failure is terminal', async () => { + const { compact, agent, adapter } = await summaryHarness([ + textResponse('answer'), offloadRequired(1), () => { throw new LlmError('provider outage', 'SERVER') }, + ]) + await seedImages(agent, ['first']) + await expect(compact.compactNow(agent, new AbortController().signal)).rejects.toMatchObject({ code: 'summary', cause: { code: 'SERVER' } }) + expect(adapter.requests).toHaveLength(3) + expect(decisions(agent.session)).toHaveLength(1) + expect(agent.session.surface.replaceGeneration).toBe(0) + }) + + it('does not record a decision after cancellation during a failed summary', async () => { + const controller = new AbortController() + const reason = new Error('cancel summary') + const { compact, agent, adapter } = await summaryHarness([ + textResponse('answer'), () => { controller.abort(reason); return offloadRequired(1)() }, + ]) + await seedImages(agent, ['first']) + await expect(compact.compactNow(agent, controller.signal)).rejects.toBe(reason) + expect(adapter.requests).toHaveLength(2) + expect(decisions(agent.session)).toEqual([]) + }) + + it('does not retry when cancellation follows a durable omission', async () => { + const { ctx, compact, agent, adapter } = await summaryHarness([textResponse('answer'), offloadRequired(1)]) + await seedImages(agent, ['first']) + const controller = new AbortController() + const reason = new Error('cancel after offload') + ctx.on('session/event', (_session, event) => { if (event.type === 'image/offload') controller.abort(reason) }) + await expect(compact.compactNow(agent, controller.signal)).rejects.toBe(reason) + expect(adapter.requests).toHaveLength(2) + expect(decisions(agent.session)).toHaveLength(1) + }) + + it('rejects a concurrently changed selection before recording omission', async () => { + const { compact, agent, adapter } = await summaryHarness([textResponse('answer')]) + const selected = await seedImages(agent, ['first']) + adapter.script.push(() => { + agent.session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'replacement' }], source: { kind: 'user' } }), { + surfaceOp: { op: 'replace', startSeq: selected.start, endSeq: selected.end }, + sourceEventSeqs: [selected.start, selected.end], + }) + return offloadRequired(1)() + }) + await expect(compact.compactNow(agent, new AbortController().signal)).rejects.toMatchObject({ code: 'changed' }) + expect(decisions(agent.session)).toEqual([]) + }) +}) + +describe('compaction-image-offload', () => { + it('logs one exact image selection and retries without replacing the message', async () => { + const adapter = new ScriptedAdapter([offloadRequired(2), textResponse('sent')]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('offload-required'), { provider: 'mock', model: 'mock' }) + const delegated: string[] = [] + ctx.on('agent/request-error', ({ failure }, next) => { + delegated.push(failure.code) + return next() + }) + + agent.followup(createUserMessage({ + content: [image('a'), { type: 'tool-result', toolCallId: ToolCallId('shot'), content: [image('b')] }, image('c')], + source: { kind: 'user' }, + })) + await agent.whenIdle() + + expect(adapter.requests).toHaveLength(2) + expect(offloadedNames(adapter.requests[0]!)).toEqual([]) + expect(offloadedNames(adapter.requests[1]!)).toEqual(['a', 'b']) + expect(delegated).toEqual([]) + const events = agent.session.snapshotEvents() + const types = events.map(event => event.type) + expect(types.filter(type => type === 'llm/retry')).toHaveLength(0) + expect(types.filter(type => type === 'assistant/attempt')).toHaveLength(1) + expect(replacements(agent.session)).toHaveLength(0) + expect(types).not.toContain('compaction/prune') + expect(decisions(agent.session)).toHaveLength(1) + const decision = decisions(agent.session)[0]! + const original = events.find(event => event.type === 'user/message')!.seq + expect(decision.data).toEqual({ targets: [{ seq: original, imageIndexes: [0, 1] }] }) + expect(events[original]).toMatchObject({ type: 'user/message', surfaceOp: 'append' }) + expect(types.indexOf('assistant/attempt')).toBeLessThan(decision.seq) + expect(decision.seq).toBeLessThan(types.indexOf('assistant/message')) + expect(events[decision.seq + 1]).toMatchObject({ type: 'request/header', data: { reason: 'series' } }) + const durable = events[original]! + expect(durable.type === 'user/message' ? durable.data.content[0] : undefined).not.toHaveProperty('offloaded') + }) + + it('counts the adapter prefix in request order after a surface replacement', async () => { + const adapter = new ScriptedAdapter([offloadRequired(2), textResponse('sent')]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('offload-reordered'), { provider: 'mock', model: 'mock' }) + // An empty-content assistant node derives no message and carries no occurrence. + agent.session.append('assistant/message', { + turn: 0, + step: 0, + message: createAssistantMessage({ content: [], source: { provider: 'mock', model: 'mock' } }), + stream: [], + }, { surfaceOp: 'append' }) + const first = agent.session.append('user/message', createUserMessage({ + content: [image('first')], source: { kind: 'user' }, + }), { surfaceOp: 'append' }) + agent.session.append('user/message', createUserMessage({ + content: [image('second')], source: { kind: 'user' }, + }), { surfaceOp: 'append' }) + agent.session.append('user/message', createUserMessage({ + content: [image('replacement')], source: { kind: 'user' }, + }), { + surfaceOp: { op: 'replace', startSeq: first.seq, endSeq: first.seq }, + sourceEventSeqs: [first.seq], + }) + + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'send' }], source: { kind: 'user' } })) + await agent.whenIdle() + + expect(adapter.requests).toHaveLength(2) + expect(offloadedNames(adapter.requests[1]!)).toEqual(['replacement', 'second']) + expect(replacements(agent.session)).toHaveLength(1) + expect(decisions(agent.session).map(event => event.data.targets)).toEqual([[ + { seq: 3, imageIndexes: [0] }, { seq: 2, imageIndexes: [0] }, + ]]) + }) + + it('offloads a nested tool-result occurrence and leaves later images untouched', async () => { + const adapter = new ScriptedAdapter([offloadRequired(1), textResponse('sent')]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('offload-tool-result'), { provider: 'mock', model: 'mock' }) + const callId = ToolCallId('shot') + agent.session.append('turn/start', { turn: 0 }) + agent.session.append('assistant/message', { + turn: 0, + step: 1, + message: createAssistantMessage({ + content: [{ type: 'tool-call', id: callId, name: 'read_image', arguments: '{}' }], + source: { provider: 'mock', model: 'mock' }, + }), + stream: [], + }, { surfaceOp: 'append' }) + agent.session.append('tool/call', { turn: 0, step: 1, callId, name: 'read_image', arguments: '{}' }) + const result = agent.session.append('tool/result', { + turn: 0, + step: 1, + message: createToolResultMessage({ + callId, + content: [ + { type: 'tool-result', toolCallId: ToolCallId('empty'), content: [{ type: 'text', text: 'no image' }] }, + image('first'), + { type: 'tool-result', toolCallId: ToolCallId('inner'), content: [image('second')] }, + ], + isError: false, + }), + }, { surfaceOp: 'append' }) + agent.session.append('turn/end', { turn: 0, reason: { kind: 'completed' } }) + + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'send' }], source: { kind: 'user' } })) + await agent.whenIdle() + + expect(offloadedNames(adapter.requests[1]!)).toEqual(['first']) + expect(replacements(agent.session)).toHaveLength(0) + expect(decisions(agent.session)[0]?.data).toEqual({ targets: [{ seq: result.seq, imageIndexes: [0] }] }) + expect(agent.session.deriveEventMessage(result)?.source).toEqual(result.data.message.source) + }) + + it('advances across consecutive failures and preserves the first request snapshot', async () => { + const adapter = new ScriptedAdapter([offloadRequired(1), offloadRequired(1), textResponse('sent')]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('offload-repeat'), { provider: 'mock', model: 'mock' }) + agent.followup(createUserMessage({ + content: [image('first'), image('second'), image('third')], source: { kind: 'user' }, + })) + await agent.whenIdle() + expect(adapter.requests.map(offloadedNames)).toEqual([[], ['first'], ['first', 'second']]) + expect(decisions(agent.session).map(event => event.data.targets[0]?.imageIndexes)).toEqual([[0], [1]]) + expect(agent.session.surface.replaceGeneration).toBe(0) + expect(agent.session.surface.contentGeneration).toBe(2) + }) + + it('removes the recovery listener when its plugin is disposed', async () => { + const ctx = new Context() + contexts.push(ctx) + await mountAgentLoopTestDependencies(ctx) + const fiber = ctx.plugin(offload) + await fiber + await fiber.dispose() + const adapter = new ScriptedAdapter([offloadRequired(1)]) + ctx.llm.registerAdapter(['mock'], adapter) + await ctx.plugin(AgentLoop, { agents: [] }) + const agent = await ctx.agentLoop.create(SessionId('offload-unloaded'), { provider: 'mock', model: 'mock' }) + agent.followup(createUserMessage({ content: [image('a')], source: { kind: 'user' } })) + await agent.whenIdle() + expect(adapter.requests).toHaveLength(1) + expect(decisions(agent.session)).toEqual([]) + expect(ctx.waterfall('compaction/summary-error', { + session: agent.session, + sourceEventSeqs: agent.session.surface.nodes, + error: new LlmError('summary budget exceeded', IMAGE_OFFLOAD_REQUIRED_CODE, { offloadImages: 1 }), + }, () => false)).toBe(false) + expect(decisions(agent.session)).toEqual([]) + }) + + it('leaves every other failure to downstream recovery', async () => { + const adapter = new ScriptedAdapter([() => { + throw new LlmError('provider outage', 'SERVER') + }]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('offload-other-failure'), { provider: 'mock', model: 'mock' }) + const delegated: string[] = [] + ctx.on('agent/request-error', ({ failure }, next) => { + delegated.push(failure.code) + return next() + }) + agent.followup(createUserMessage({ content: [image('a')], source: { kind: 'user' } })) + await agent.whenIdle() + expect(delegated).toEqual(['SERVER']) + expect(replacements(agent.session)).toHaveLength(0) + }) + + it('delegates IMAGE_OFFLOAD_REQUIRED once nothing remains to offload', async () => { + const adapter = new ScriptedAdapter([offloadRequired(1)]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('offload-exhausted'), { provider: 'mock', model: 'mock' }) + const delegated: string[] = [] + ctx.on('agent/request-error', ({ failure }, next) => { + delegated.push(failure.code) + return next() + }) + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'no images' }], source: { kind: 'user' } })) + await agent.whenIdle() + expect(delegated).toEqual([IMAGE_OFFLOAD_REQUIRED_CODE]) + expect(replacements(agent.session)).toHaveLength(0) + expect(agent.session.snapshotEvents().at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'error' } } }) + }) +}) diff --git a/packages/compaction/compaction-image-offload/tests/projection.spec.ts b/packages/compaction/compaction-image-offload/tests/projection.spec.ts new file mode 100644 index 0000000000..36d0ec00df --- /dev/null +++ b/packages/compaction/compaction-image-offload/tests/projection.spec.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from 'vitest' +import { createAssistantMessage, createToolResultMessage, createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' +import type { ContentBlock, ImageBlock } from '@deepseek-ai/dsh-llm' +import { deriveEventMessage, foldSurface, Session, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session' +import type { SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session' +import { imageOffloadProjection } from '../src/projection.ts' + +function createSession(...args: Parameters): Session { + args[4] = [imageOffloadProjection] + return Session.create(...args) +} + +const image: ImageBlock = { + type: 'image', + attachment: { attachmentId: `sha256:${'a'.repeat(64)}` as never, mediaType: 'image/png', bytes: 1, width: 1, height: 1 }, +} + +function input(session: Session, content: ContentBlock[] = [image, image]) { + return session.append('user/message', createUserMessage({ content, source: { kind: 'user' } }), { surfaceOp: 'append' }) +} + +function marked(session: Session): boolean[] { + const result: boolean[] = [] + const visit = (content: readonly ContentBlock[]): void => { + for (const block of content) { + if (block.type === 'image') result.push(block.offloaded === true) + if (block.type === 'tool-result') visit(block.content) + } + } + for (const message of session.deriveMessages()) visit(message.content) + return result +} + +describe('durable image selections', () => { + it('preserves message identity, original data, and previously derived snapshots', () => { + const session = createSession(SessionId('images')) + const source = input(session, [ + { type: 'text', text: 'before' }, image, + { type: 'tool-result', toolCallId: ToolCallId('nested'), content: [image, { type: 'text', text: 'after' }] }, + { type: 'tool-result', toolCallId: ToolCallId('text'), content: [{ type: 'text', text: 'unchanged' }] }, + image, + ]) + const before = session.deriveMessages() + session.append('image/offload', { targets: [{ seq: source.seq, imageIndexes: [1] }] }) + expect(marked(session)).toEqual([false, true, false]) + const after = session.deriveMessages() + expect(after[0]?.id).toBe(before[0]?.id) + expect(after[0]).toBe(session.deriveEventMessage(source)) + expect(Object.isFrozen(after[0])).toBe(true) + expect(Object.isFrozen(after[0]?.content)).toBe(true) + expect(after[0]?.content[0]).toBe(before[0]?.content[0]) + expect(after[0]?.content[3]).toBe(before[0]?.content[3]) + expect(JSON.stringify(source)).not.toContain('offloaded') + expect(JSON.stringify(before)).not.toContain('offloaded') + expect(session.surface.nodes).toEqual([source.seq]) + expect(session.surface.replaceGeneration).toBe(0) + expect(session.surface.contentGeneration).toBe(1) + expect(session.deriveEventMessage(session.snapshotEvents().at(-1)!)).toBeNull() + session.append('image/offload', { targets: [{ seq: source.seq, imageIndexes: [0, 2] }] }) + expect(marked(session)).toEqual([true, true, true]) + expect(session.surface.contentGeneration).toBe(2) + expect(after[0]).not.toBe(session.deriveMessages()[0]) + }) + + it('reconstructs the same selections through pure folding, resume, restore, and fork', () => { + const session = createSession(SessionId('source')) + const source = input(session) + const before = session.snapshotEvents() + session.append('image/offload', { targets: [{ seq: source.seq, imageIndexes: [0] }] }) + const events = session.snapshotEvents() + const fold = foldSurface(events, [imageOffloadProjection]) + expect(deriveEventMessage(source, fold.projectedMessages)).toEqual(session.deriveMessages()[0]) + expect(marked(createSession(SessionId('before'), before))).toEqual([false, false]) + expect(marked(createSession(SessionId('resume'), events))).toEqual([true, false]) + const restored = Session.fromRestore(session.id, events, session.header, SessionLogOffset(0), 'shared-frozen', [imageOffloadProjection]) + expect(marked(restored)).toEqual([true, false]) + const childId = SessionId('child') + const child = createSession(childId, events, { + ...session.header, id: childId, parentSession: session.id, isSeeded: true, + }, SessionLogOffset(events.length)) + expect(marked(child)).toEqual([true, false]) + input(child, [image]) + expect(marked(child)).toEqual([true, false, false]) + expect(marked(session)).toEqual([true, false]) + }) + + it('counts all images in a tool result without changing its call identity', () => { + const session = createSession(SessionId('tool')) + const source = session.append('tool/result', { + turn: 1, step: 1, + message: createToolResultMessage({ callId: ToolCallId('shot'), isError: false, content: [image, image] }), + }, { surfaceOp: 'append' }) + session.append('image/offload', { targets: [{ seq: source.seq, imageIndexes: [1] }] }) + expect(marked(session)).toEqual([false, true]) + expect(session.deriveEventMessage(source)?.source).toEqual(source.data.message.source) + }) + + it.each([ + null, {}, { targets: [] }, { targets: 'bad' }, { targets: [null] }, + { targets: [{ seq: 0, imageIndexes: [] }] }, + { targets: [{ seq: 0, imageIndexes: [0], extra: true }] }, + { targets: [{ seq: 0, imageIndexes: [0] }], extra: true }, + { targets: [{ seq: -1, imageIndexes: [0] }] }, + { targets: [{ seq: 0, imageIndexes: [-1] }] }, + { targets: [{ seq: 0, imageIndexes: [0.5] }] }, + { targets: [{ seq: 0, imageIndexes: ['0'] }] }, + { targets: [{ seq: 0, imageIndexes: [1, 0] }] }, + { targets: [{ seq: 0, imageIndexes: [0, 0] }] }, + { targets: [{ seq: 0, imageIndexes: [2] }] }, + { targets: [{ seq: 1, imageIndexes: [0] }] }, + { targets: [{ seq: 0, imageIndexes: [0] }, { seq: 0, imageIndexes: [1] }] }, + ])('rejects malformed selections at append and replay without partial application: %j', (data) => { + const session = createSession(SessionId('invalid')) + input(session) + const events = session.snapshotEvents() + const candidate = { type: 'image/offload', seq: SessionSeq(1), time: 0, data } as SessionEvent + expect(() => session.append('image/offload', data as SessionEventMap['image/offload'])).toThrow(/image\/offload/) + expect(() => createSession(SessionId('seed'), [...events, candidate])).toThrow(/image\/offload/) + expect(() => foldSurface([...events, candidate], [imageOffloadProjection])).toThrow(/image\/offload/) + expect(session.snapshotEvents()).toEqual(events) + expect(marked(session)).toEqual([false, false]) + expect(session.surface.contentGeneration).toBe(0) + session.append('image/offload', { targets: [{ seq: SessionSeq(0), imageIndexes: [1] }] }) + expect(marked(session)).toEqual([false, true]) + }) + + it('rejects repeated offloads and shadowed or assistant targets', () => { + const session = createSession(SessionId('invalid-targets')) + const source = input(session) + session.append('image/offload', { targets: [{ seq: source.seq, imageIndexes: [0] }] }) + expect(() => session.append('image/offload', { targets: [{ seq: source.seq, imageIndexes: [0] }] })).toThrow(/already offloaded/) + session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'summary' }], source: { kind: 'user' } }), { + surfaceOp: { op: 'replace', startSeq: source.seq, endSeq: source.seq }, sourceEventSeqs: [source.seq], + }) + expect(() => session.append('image/offload', { targets: [{ seq: source.seq, imageIndexes: [1] }] })).toThrow(/not a current/) + const assistant = session.append('assistant/message', { + turn: 1, step: 1, stream: [], + message: createAssistantMessage({ content: [image], source: { provider: 'mock', model: 'mock' } }), + }, { surfaceOp: 'append' }) + expect(() => session.append('image/offload', { targets: [{ seq: assistant.seq, imageIndexes: [0] }] })).toThrow(/must be user\/message/) + }) +}) diff --git a/packages/compaction/compaction-image-offload/tsconfig.json b/packages/compaction/compaction-image-offload/tsconfig.json new file mode 100644 index 0000000000..26fb004b6b --- /dev/null +++ b/packages/compaction/compaction-image-offload/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../compaction" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/session" + }, + { + "path": "../../core/agent" + }, + { + "path": "../../util/values" + } + ] +} diff --git a/packages/compaction/compaction-tool-result-pruner/README.i18n.yaml b/packages/compaction/compaction-tool-result-pruner/README.i18n.yaml index 91860dfaa2..5f4ff48769 100644 --- a/packages/compaction/compaction-tool-result-pruner/README.i18n.yaml +++ b/packages/compaction/compaction-tool-result-pruner/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/compaction/compaction-tool-result-pruner/README.md -README.md: a7a41b49966304278b3b3a45babfe6311667e4a5 -README.zh.md: 7e3ba252ffecc366db3af3058bfdc7c4d2e2ab83 +README.md: 7b05154bc02035bb86933e77a60904613f72bbed +README.zh.md: 06ecb2b3a63f4511cc334878b5c02358c0fad0fd diff --git a/packages/compaction/compaction-tool-result-pruner/README.md b/packages/compaction/compaction-tool-result-pruner/README.md index a7a41b4996..7b05154bc0 100644 --- a/packages/compaction/compaction-tool-result-pruner/README.md +++ b/packages/compaction/compaction-tool-result-pruner/README.md @@ -41,7 +41,7 @@ With these rows, oversized tool results are trimmed automatically as part of con ### What gets trimmed -Every tool result whose text exceeds the threshold is replaced by a trimmed version: the configured head, a short "middle pruned" marker, and the configured tail. Rich content such as images and structured blocks keeps its order. The replacement keeps the tool call, step, errors, and metadata — only the text content changes. If a replacement cannot be recorded, the run fails and the trims already applied stay in place. +Every tool result whose text exceeds the threshold is replaced by a trimmed version: the configured head, a short "middle pruned" marker, and the configured tail. Rich content such as images and structured blocks keeps its order and all logged image-offload selections. The replacement keeps the tool call, step, errors, and metadata — only the text content changes. If a replacement cannot be recorded, the run fails and the trims already applied stay in place. ### Setting the size limits diff --git a/packages/compaction/compaction-tool-result-pruner/README.zh.md b/packages/compaction/compaction-tool-result-pruner/README.zh.md index 7e3ba252ff..06ecb2b3a6 100644 --- a/packages/compaction/compaction-tool-result-pruner/README.zh.md +++ b/packages/compaction/compaction-tool-result-pruner/README.zh.md @@ -41,7 +41,7 @@ kind: "package-reference" ### 什么会被修剪 -每个文本超过阈值的工具结果都会被替换为修剪版本:配置的头部、简短的「middle pruned」标记与配置的尾部。图片与结构化块等富内容保持原有顺序。替换保留工具调用、步骤、错误与元数据——只有文本内容发生变化。如果替换无法被记录,运行会失败,已应用的修剪仍会保留。 +每个文本超过阈值的工具结果都会被替换为修剪版本:配置的头部、简短的「middle pruned」标记与配置的尾部。图片与结构化块等富内容保持原有顺序,并保留日志中的所有图片省略选择。替换保留工具调用、步骤、错误与元数据——只有文本内容发生变化。如果替换无法被记录,运行会失败,已应用的修剪仍会保留。 ### 设置大小限制 diff --git a/packages/compaction/compaction-tool-result-pruner/package.json b/packages/compaction/compaction-tool-result-pruner/package.json index 93eedeba7f..b693fed200 100644 --- a/packages/compaction/compaction-tool-result-pruner/package.json +++ b/packages/compaction/compaction-tool-result-pruner/package.json @@ -46,6 +46,7 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-projection": "workspace:^", - "@deepseek-ai/dsh-token-meter": "workspace:^" + "@deepseek-ai/dsh-token-meter": "workspace:^", + "@deepseek-ai/dsh-compaction-image-offload": "workspace:^" } } diff --git a/packages/compaction/compaction-tool-result-pruner/src/index.ts b/packages/compaction/compaction-tool-result-pruner/src/index.ts index 6d8ef92298..333f560d75 100644 --- a/packages/compaction/compaction-tool-result-pruner/src/index.ts +++ b/packages/compaction/compaction-tool-result-pruner/src/index.ts @@ -145,13 +145,14 @@ export class ToolResultPruner extends Service { const pruned: PrunedEntry[] = [] let charsRemoved = 0 for (const { seq, event } of candidates) { - const result = event.data.message.content[0] + const original = session.deriveEventMessage(event) as ToolResultMessage + const result = original.content[0] const content = this.pruneContent(result.content) if (content === null) continue const charsBefore = this.measureContent(result.content) const charsAfter = this.measureContent(content) const message = freezeMessage({ - ...event.data.message, + ...original, content: [{ ...result, content, @@ -163,7 +164,7 @@ export class ToolResultPruner extends Service { session.append('compaction/prune', { shadowedRange: { start: seq, end: seq }, shadowedSeqs: [seq], - shadowedTokenCount: this.ctx.tokenMeter.estimateMessage(event.data.message), + shadowedTokenCount: this.ctx.tokenMeter.estimateMessage(original), }) const replacement = session.append('tool/result', { ...event.data, diff --git a/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts b/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts index a09d0a3c53..dc94a7c682 100644 --- a/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts +++ b/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts @@ -1,3 +1,4 @@ +import { imageOffloadProjection } from '@deepseek-ai/dsh-compaction-image-offload/projection' import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' @@ -5,6 +6,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId, + SessionSeq, } from '@deepseek-ai/dsh-session' import type { SurfaceEvent } from '@deepseek-ai/dsh-session' import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant' @@ -78,6 +80,24 @@ function appendToolStep( } describe('tool-result pruning configuration', () => { + it('preserves a logged image offload when pruning the same result later', () => { + const session = Session.create(SessionId('prune-offloaded'), undefined, undefined, undefined, [imageOffloadProjection]) + const seq = appendToolStep(session, 1, 'shot', [ + { type: 'text', text: 'x'.repeat(200) }, + { type: 'image', attachment: { + attachmentId: `sha256:${'a'.repeat(64)}` as never, mediaType: 'image/png', bytes: 1, width: 1, height: 1, + } }, + ]) + session.append('image/offload', { targets: [{ seq: SessionSeq(seq), imageIndexes: [0] }] }) + const pruned = service().pruneSession(session) + expect(pruned.pruned).toHaveLength(1) + const replacement = session.snapshotEvents().at(-1)! + expect(replacement.type).toBe('tool/result') + expect(JSON.stringify(session.deriveEventMessage(replacement))).toContain('"offloaded":true') + expect(JSON.stringify(Session.create(SessionId('restored-offloaded'), session.snapshotEvents(), undefined, undefined, [imageOffloadProjection]).deriveMessages())).toContain('"offloaded":true') + expect(JSON.stringify(session.eventAt(SessionSeq(seq)))).not.toContain('offloaded') + }) + it('resolves detached immutable defaults and partial overrides', () => { const raw = { thresholdChars: 100, headChars: 20, tailChars: 10 } const resolved = resolveConfig(raw) diff --git a/packages/compaction/compaction/README.i18n.yaml b/packages/compaction/compaction/README.i18n.yaml index a444c39af3..c3b4c9decc 100644 --- a/packages/compaction/compaction/README.i18n.yaml +++ b/packages/compaction/compaction/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/compaction/compaction/README.md -README.md: 101a549be11d94ec1e11a55645775f79afabdff8 -README.zh.md: 9e048fae5edf62679044e212b0bdd1c2f3753ebe +README.md: fecb3b6731108c493825f6fa0874ab865781fc2a +README.zh.md: 86e6a2935c408af7e247235378a022ad855c00a0 diff --git a/packages/compaction/compaction/README.md b/packages/compaction/compaction/README.md index 101a549be1..fecb3b6731 100644 --- a/packages/compaction/compaction/README.md +++ b/packages/compaction/compaction/README.md @@ -81,6 +81,8 @@ A backend that summarizes through `ctx.llm.stream()` must forward the abort sign ### Manual failure taxonomy +The synchronous `compaction/summary-error` waterfall lets a backend request durable input recovery for a failed summary and an explicit selection. A listener returns true only after recording progress, or calls `next()` to delegate. The backend checks cancellation and selection stability before dispatch, then re-derives and re-prices input before retrying. The event declaration documents its payload; image policy belongs to `dsh-compaction-image-offload`. + Expected manual failures throw `ManualCompactionError` with a stable `code` from a small closed set; only failures after the `compaction/start` marker are recorded — as a `compaction/end` carrying the error — while a `busy` rejection or pre-start cancellation leaves no record. The per-code semantics live in the [compaction subsystem reference](../../../docs/subsystems/compaction.md). diff --git a/packages/compaction/compaction/README.zh.md b/packages/compaction/compaction/README.zh.md index 9e048fae5e..86e6a2935c 100644 --- a/packages/compaction/compaction/README.zh.md +++ b/packages/compaction/compaction/README.zh.md @@ -81,6 +81,8 @@ kind: "package-reference" ### 手动失败分类 +同步的 `compaction/summary-error` waterfall 让后端为失败的摘要及明确选区请求持久输入恢复。监听器只有在记录实际进展后才返回 true,否则调用 `next()` 委派后续处理。后端在触发事件前检查取消和选区稳定性,重试前重新派生输入并计价。事件声明说明其载荷;图片策略由 `dsh-compaction-image-offload` 负责。 + 预期手动失败会抛出 `ManualCompactionError`,携带来自小型封闭集合的稳定 `code`;只有 `compaction/start` 标记之后发生的失败才会被记录——以携带错误的 `compaction/end` 形式——而 `busy` 拒绝或 start 之前的取消不会留下记录。每个错误码的语义见[压缩子系统参考](../../../docs/subsystems/compaction.zh.md)。 diff --git a/packages/compaction/compaction/src/index.ts b/packages/compaction/compaction/src/index.ts index cfbdd42209..eb8263f200 100644 --- a/packages/compaction/compaction/src/index.ts +++ b/packages/compaction/compaction/src/index.ts @@ -82,6 +82,22 @@ declare module '@deepseek-ai/cordis' { interface Context { compaction: CompactionEngine } + interface Events { + /** + * Recover a failed summary request by synchronously recording a durable + * change to its selected input. Return true only after making progress; + * the provider re-derives and re-prices the selection before retrying. + * Call next() when the failure cannot be recovered. Decisions survive a + * later summary failure or cancellation. + * @param payload.session - session containing the selected input. + * @param payload.sourceEventSeqs - selected message events in request order. + * @param payload.error - failure thrown by the summarizer. + * @param payload.signal - optional compaction cancellation signal. + * @param next - delegate to the next recovery listener. + * @mode waterfall + */ + 'compaction/summary-error'(payload: { session: Session; sourceEventSeqs: readonly SessionSeq[]; error: unknown; signal?: AbortSignal }, next: () => boolean): boolean + } } /** diff --git a/packages/compaction/compaction/src/invariant.ts b/packages/compaction/compaction/src/invariant.ts index 5682f066db..7880e5fb24 100644 --- a/packages/compaction/compaction/src/invariant.ts +++ b/packages/compaction/compaction/src/invariant.ts @@ -295,7 +295,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant openTurn: null, compaction: undefined, surfaceEvents, - surface: new SurfaceManager(surfaceEvents), + surface: new SurfaceManager(surfaceEvents, undefined, ctx.sessions.messageProjections), } traces.set(session, trace) // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index cff528aef5..f4879011cc 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/agent-loop/README.md -README.md: 780fed31a34113e01d7c98313bd88b5c322a8332 -README.zh.md: 766ff4a2e7287f6987b4443c19f3164d7e2cd2f3 +README.md: d494f3f6f7c68440d59ec2c1aa8fad2eae0bcda1 +README.zh.md: bb3ca764a688cc01f61aa9b90667312f7d765fb5 diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 780fed31a3..d494f3f6f7 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -90,7 +90,7 @@ The package is the one concrete implementation of the public `Agent` contract. I ### Request headers and adapter defaults -After `agent/request`, `ctx.llm.prepareCall()` validates adapter-owned fields and resolves reasoning-effort and output-token defaults under the active turn signal. The loop retains that exact adapter through resolution, `request/header` logging, and dispatch. It writes a full header for the first request, a changed envelope (config or tools — the prompt is not part of the header), an explicit message-series start, a request after surface replacement (an in-place prompt replacement or compaction), and resume; unchanged steps, retries, and ordinary later turns in the same series inherit the latest header, and an in-history prompt append is not a replacement, so the request that follows it inherits the header too. Beside the header, the loop logs `request/context` — provider, model, `contextWindow`, and the route's `systemPromptUpdate` mode from `prepareCall()` — only when one of those differs from the latest snapshot. Before the next waterfall, the loop removes adapter-default fields so the current route resolves them again, while explicit settings persist. An unhandled route still fails with `NO_ADAPTER`. +After `agent/request`, `ctx.llm.prepareCall()` validates adapter-owned fields and resolves reasoning-effort and output-token defaults under the active turn signal. The loop retains that exact adapter through resolution, `request/header` logging, and dispatch. It writes a full header for the first request, a changed envelope (config or tools — the prompt is not part of the header), an explicit message-series start, a request after a surface replacement or image-offload decision, and resume; unchanged steps, retries, and ordinary later turns in the same series inherit the latest header, and an in-history prompt append is not a replacement, so the request that follows it inherits the header too. Beside the header, the loop logs `request/context` — provider, model, `contextWindow`, and the route's `systemPromptUpdate` mode from `prepareCall()` — only when one of those differs from the latest snapshot. Before the next waterfall, the loop removes adapter-default fields so the current route resolves them again, while explicit settings persist. An unhandled route still fails with `NO_ADAPTER`. The loop deep-freezes each derived message identity on its first request and reuses that proof only within the same agent. Restored messages keep their identity; request construction does not freeze their containing event wrappers. Each request freezes its local canonical header, fresh message array, and envelope while leaving the cancellation signal live. The [request-freeze decision](../../../.agents/notes/implemented/simplification/2026-09-06-agent-request-freeze-evidence.md) explains ownership and measurement. @@ -118,7 +118,7 @@ The loop is the production acquisition point for session write handles. When `ct The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`. Its package-internal `ReactLoopInbox` constructor registers the standard `inbox` projection on the agent scope, then uses that projection for structural commands and loop-only claims. Registry reference counting keeps the shared key active until the last agent scope unloads. At a turn boundary it opens the durable turn, then atomically claims pending next-step input plus one queued prompt; between steps it claims only next-step input. The driver assembles the prompt and tools, projects runtime context, and runs `agent/pre-step`. A rejected decision or empty first batch opens no step. On the first attempt after acceptance, `step/start` precedes the `agent/request` waterfall and `prepareCall()`; neither async phase sees the pending system prompt or accepted users committed to history, and cancellation during either commits neither. For every attempt, the loop then synchronously reconciles the rendered prompt against the surviving `system/message` nodes using the prepared call capability, appends the accepted `user/message` batch only on the first attempt, logs the header and context as needed, and derives and freezes the request before streaming through that bound prepared call. Retries reuse the same rendered assembly without repeating assembly, `agent/pre-step`, or user admission. Reconciliation sees pre-step and retry compaction; a broken series consolidates the prompt at the head rather than appending an update after already-committed users. The request is `header.config`, `deriveMessages()`, and `header.tools`; it carries no `system` field. Each model attempt emits one process-local `start`, emits every `chunk` only after the matching durable assistant-frame settlement, and emits exactly one terminal `end`; final assembly or message-append failure settles it as `aborted`, while `committed` follows the durable `assistant/message`. Each successful model call appends one message anchor, and a cancelled stream appends an `interrupted: true` anchor with the delivered prefix so the next request contains what the user saw. Within a step, exclusive calls form barriers and parallel-safe calls use the bounded rolling pool; policy, durable results, and result context remain model-ordered. -Prompt admission uses the actual `prepareCall()` result, not the preceding `request/context`. With no system node, even an empty prompt is appended (reserving node 0 without a wire message). On an incapable route or at a new request series, a non-empty rendering is consolidated at the first system node: each non-empty later system node receives a logged per-node empty replacement, then the head is rewritten if needed. Dormant empty tails need no replacement and do not determine the effective text. Consolidation applies even when the latest effective text is unchanged. On a continuing `in-history` series, an unchanged effective prompt produces no event and a non-empty change appends. An empty rendering clears every non-empty later system node through a logged per-node empty replacement, then empties the head if needed, regardless of route or series state. No older instructions remain model-visible. An empty head with no active later system node means no prompt; repeated clears and resume leave it empty. A restored non-empty prompt follows the same route/series rule: a continuing capable route may append it, while an incapable route or new series refills the head. A step starts a series when the pre-step decision declares `startsRequestSeries`, when the surface replace generation changed since attachment or the last request (compaction or any replacement), or when visible tool schemas changed. Resume and a provider or model swap alone continue the series; the prepared route still governs admission. Per-node empty replacements preserve intervening history without a surface delete operation. +Prompt admission uses the actual `prepareCall()` result, not the preceding `request/context`. With no system node, even an empty prompt is appended (reserving node 0 without a wire message). On an incapable route or at a new request series, a non-empty rendering is consolidated at the first system node: each non-empty later system node receives a logged per-node empty replacement, then the head is rewritten if needed. Dormant empty tails need no replacement and do not determine the effective text. Consolidation applies even when the latest effective text is unchanged. On a continuing `in-history` series, an unchanged effective prompt produces no event and a non-empty change appends. An empty rendering clears every non-empty later system node through a logged per-node empty replacement, then empties the head if needed, regardless of route or series state. No older instructions remain model-visible. An empty head with no active later system node means no prompt; repeated clears and resume leave it empty. A restored non-empty prompt follows the same route/series rule: a continuing capable route may append it, while an incapable route or new series refills the head. A step starts a series when the pre-step decision declares `startsRequestSeries`, when `session.surface.contentGeneration` changed since attachment or the last request (a replacement or image-offload decision), or when visible tool schemas changed. Resume and a provider or model swap alone continue the series; the prepared route still governs admission. Per-node empty replacements preserve intervening history without a surface delete operation. ### Failure and cancellation diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index 766ff4a2e7..bb3ca764a6 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -90,7 +90,7 @@ const handle = await ctx.agents.create({ ### 请求 header 与适配器默认值 -`agent/request` 返回后,`ctx.llm.prepareCall()` 会在活跃轮次信号下校验适配器持有的字段,并解析推理强度和输出 token 默认值。循环会在解析、`request/header` 记录与分派期间保留同一个适配器。循环会为首次请求、变化的 envelope(配置或工具——提示词不属于 header)、显式消息序列起点、surface 替换(原地替换提示词或压缩(compaction))后的请求及恢复写入完整 header;同一序列内内容未变的步骤、重试与普通后续轮次继承最新 header,历史内追加提示词不是替换,因此紧随其后的请求同样继承 header。在 header 之外,循环还会记录 `request/context`——提供方、模型、`contextWindow` 以及来自 `prepareCall()` 的路由 `systemPromptUpdate` 模式——且仅在其中任何一项与最新快照不同时记录。下一次 waterfall 分发前,循环移除适配器默认字段,使当前路由重新解析它们;显式设置则保留。未处理的路由仍以 `NO_ADAPTER` 失败。 +`agent/request` 返回后,`ctx.llm.prepareCall()` 会在活跃轮次信号下校验适配器持有的字段,并解析推理强度和输出 token 默认值。循环会在解析、`request/header` 记录与分派期间保留同一个适配器。循环会为首次请求、变化的 envelope(配置或工具——提示词不属于 header)、显式消息序列起点、surface 替换或图片省略决定后的请求及恢复写入完整 header;同一序列内内容未变的步骤、重试与普通后续轮次继承最新 header,历史内追加提示词不是替换,因此紧随其后的请求同样继承 header。在 header 之外,循环还会记录 `request/context`——提供方、模型、`contextWindow` 以及来自 `prepareCall()` 的路由 `systemPromptUpdate` 模式——且仅在其中任何一项与最新快照不同时记录。下一次 waterfall 分发前,循环移除适配器默认字段,使当前路由重新解析它们;显式设置则保留。未处理的路由仍以 `NO_ADAPTER` 失败。 循环在每个派生消息对象首次进入请求时执行深冻结,并且仅在同一 agent 内复用该证明。恢复的消息保留对象身份;构造请求不会冻结包含消息的事件包装对象。每个请求都会冻结本地规范化 header、新消息数组和请求封装,同时保留取消信号的可变性。[请求冻结决策](../../../.agents/notes/implemented/simplification/2026-09-06-agent-request-freeze-evidence.zh.md)解释了所有权与测量依据。 @@ -118,7 +118,7 @@ const handle = await ctx.agents.create({ 驱动器在其整个生命周期内拥有一个 agent,并在 `ctx.agents.withInitiator(agent, ...)` 内运行。其包内部 `ReactLoopInbox` 构造函数在 agent 作用域上注册标准 `inbox` 投影,随后将该投影用于结构化命令与仅供 loop 使用的领取操作。注册表引用计数会使共享 key 持续有效,直至最后一个 agent 作用域卸载。在轮次边界,它先打开持久轮次,再原子领取待处理的 next-step 输入与一条排队提示词;在步骤之间则只领取 next-step 输入。驱动器组装提示词与工具、投影运行时上下文,并运行 `agent/pre-step`。被拒绝的决定或空的首批输入不打开步骤。接纳后的首次尝试先记录 `step/start`,再运行 `agent/request` waterfall 与 `prepareCall()`;这两个异步阶段都看不到待提交的系统提示词与已接纳用户消息进入历史,在任一阶段取消都不会提交这两者。每次尝试时,循环随后依据已准备调用的能力,同步将渲染后的提示词与存活的 `system/message` 节点协调一致、仅在首次尝试追加已接纳的 `user/message` 批次、按需记录 header 与 context,再派生并冻结请求,通过该绑定的已准备调用发起流式请求。重试复用同一份已渲染组装结果,不重复组装、`agent/pre-step` 或用户消息准入。协调过程可见 pre-step 与重试中的压缩;序列中断时将提示词归并到头部,而非在已提交用户消息之后追加更新。请求由 `header.config`、`deriveMessages()` 与 `header.tools` 构成,不携带 `system` 字段。每次模型尝试会发出一个进程本地 `start`,仅在匹配的持久 assistant-frame 结算之后发出各个 `chunk`,并恰好发出一个终态 `end`;最终组装或消息追加失败时以 `aborted` 结算,`committed` 则出现在持久 `assistant/message` 之后。每次成功的模型调用都恰好追加一个 message 锚点,被取消的流则追加带 `interrupted: true` 的锚点并携带已交付前缀,使下一次请求包含用户看到的内容。在步骤内,独占调用形成屏障,并行安全调用使用有界滚动池;策略、持久结果与结果上下文保持模型顺序。 -提示词准入依据实际的 `prepareCall()` 结果,而非先前的 `request/context`。没有系统节点时,即使提示词为空也追加(预留第 0 号节点,但不产生协议消息)。在不具备能力的路由上或新请求序列开始时,非空渲染文本归并到首个系统节点:每个非空的后续系统节点分别收到有日志记录的空内容替换,随后按需重写头节点。未生效的空尾节点无需替换,也不决定有效文本。即使最新有效文本未变,也执行归并。延续中的 `in-history` 序列在有效提示词不变时不产生事件,非空变更则追加。无论路由或序列状态如何,空渲染文本都会通过有日志记录的逐节点空内容替换清除每个非空的后续系统节点,再按需清空头节点。模型不会继续看到旧指令。空头节点且没有生效的后续系统节点表示没有提示词;重复清除与恢复会话都保持为空。重新提供的非空提示词遵循同一路由/序列规则:延续中的具备能力路由可以追加它,不具备能力的路由或新序列则重新填充头节点。以下情况开启序列:pre-step 决定声明 `startsRequestSeries`、surface 替换 generation 自附接或上次请求以来发生变化(压缩或任何替换)、可见工具 schema 变化。恢复与单纯的提供方或模型切换都延续序列;准入仍由已准备的路由决定。逐节点的空内容替换保留其间历史,无需 surface 删除操作。 +提示词准入依据实际的 `prepareCall()` 结果,而非先前的 `request/context`。没有系统节点时,即使提示词为空也追加(预留第 0 号节点,但不产生协议消息)。在不具备能力的路由上或新请求序列开始时,非空渲染文本归并到首个系统节点:每个非空的后续系统节点分别收到有日志记录的空内容替换,随后按需重写头节点。未生效的空尾节点无需替换,也不决定有效文本。即使最新有效文本未变,也执行归并。延续中的 `in-history` 序列在有效提示词不变时不产生事件,非空变更则追加。无论路由或序列状态如何,空渲染文本都会通过有日志记录的逐节点空内容替换清除每个非空的后续系统节点,再按需清空头节点。模型不会继续看到旧指令。空头节点且没有生效的后续系统节点表示没有提示词;重复清除与恢复会话都保持为空。重新提供的非空提示词遵循同一路由/序列规则:延续中的具备能力路由可以追加它,不具备能力的路由或新序列则重新填充头节点。以下情况开启序列:pre-step 决定声明 `startsRequestSeries`、`session.surface.contentGeneration` 自附接或上次请求以来发生变化(替换或图片省略决定)、可见工具 schema 变化。恢复与单纯的提供方或模型切换都延续序列;准入仍由已准备的路由决定。逐节点的空内容替换保留其间历史,无需 surface 删除操作。 ### 失败与取消 diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 06e1f51b57..e79fcd5e40 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -99,7 +99,7 @@ export class ReactLoopAgent implements Agent { public readonly options: AgentOptions, public readonly session: Session, ) { - this.requestSurfaceGeneration = session.surface.replaceGeneration + this.requestSurfaceGeneration = session.surface.contentGeneration this.dispatch = agentEvents(loopCtx, this) this.scope = createScope(loopCtx, this) this.ctx = this.scope.ctx @@ -364,7 +364,7 @@ export class ReactLoopAgent implements Agent { const commits = this.systemPrompt.project(renderedPrompt, { inHistory: preparedCall?.systemPromptUpdate === 'in-history', startsSeries: startsRequestSeries - || this.requestSurfaceGeneration !== this.session.surface.replaceGeneration + || this.requestSurfaceGeneration !== this.session.surface.contentGeneration || this.toolsChanged(assembly.tools), }) for (const { message, intent } of commits) { @@ -558,7 +558,7 @@ export class ReactLoopAgent implements Agent { signal: AbortSignal, ): GenerateOptions { const { session } = this - const surfaceGeneration = session.surface.replaceGeneration + const surfaceGeneration = session.surface.contentGeneration const header = canonicalHeader({ config, ...preparedCall === undefined ? {} : { adapterDefaults: preparedCall.adapterDefaults }, diff --git a/packages/core/session/README.i18n.yaml b/packages/core/session/README.i18n.yaml index c74112c5a6..583c52a88c 100644 --- a/packages/core/session/README.i18n.yaml +++ b/packages/core/session/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/session/README.md -README.md: 2368e20df012b16ed12f35de327db62c71ee3269 -README.zh.md: 84cc0aa35da11a5a782e1949e32852785fb681c4 +README.md: 8d07292287d7300b562751ad74de77397bd5b5db +README.zh.md: 28b6f1f02838420861a76409046c0e9a12bac1b5 diff --git a/packages/core/session/README.md b/packages/core/session/README.md index 2368e20df0..8d07292287 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -49,6 +49,8 @@ session.deriveMessages() // the derived model history Surface events (`system/message`, `user/message`, `assistant/message`, `tool/result`) require `surfaceOp` in both typed events and append input. A replacement uses exactly `{ op: 'replace', startSeq, endSeq }`, with inclusive `SessionSeq` endpoints in current surface order. An Assistant message embeds its exact compact provider stream and forbids `sourceEventSeqs`. Known log-only events forbid both metadata fields and never produce a message. +Plugins declare content-changing events with `@messageProjection` and register a pure definition through `ctx.sessions.registerMessageProjection()`. Session calls the definition before accepting an event and caches its immutable message updates. Missing definitions reject append and restore; unloading a used definition also blocks cached reads. Detached constructors and `foldSurface(events, projections)` require explicit definitions. Reconstructors pass the fold's `projectedMessages` to `deriveEventMessage()`; live instance methods apply the same projections automatically. [Plugin-owned message projections](../../../.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.md) defines ownership and offline assembly. + Append, seed/restore, and event adoption/snapshot reject any `header.system` and exactly empty optional request-header fields (`tools: []`, `adapterDefaults: {}`) instead of normalizing input. Tool-result `data.error` is allowed only when `message.content[0].isError === true`; failure identity remains optional. Rejected appends do not change the log, derived state, or event feed. Adoption validates event-local metadata but not referenced history or replacement membership. `system/message` holds the rendered system prompt: the first one is surface node 0, the prepared call capability governs admission, with a non-empty rendering consolidated at the first system node on an incapable route or appended after cached history inside a continuing `in-history` series; empty system nodes project to no message, so clearing the prompt requires logged empty replacements of all active system nodes, not just the latest; the surface fold rejects a replacement covering node 0 while it is a `system/message` unless the replacing event is itself a `system/message` over exactly that node, while later system nodes carry no protection and a compaction range may shadow them ([decision](../../../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md)). @@ -81,11 +83,11 @@ This section explains how the package realizes the behavior above; the observabl ### Design concept -The package is built on event sourcing: a `Session` is an append-only log of typed `SessionEvent`s, and everything else — model history, transcripts, telemetry, titles, persistence — derives from that stream. The surface is a derived projection: an incremental manager validates append candidates, advances the ordered view from committed events, and tracks a `replaceGeneration` that bumps on every committed rewrite. Model-visible means logged: anything that reaches a model request must be reconstructable from the log. Each model attempt that reaches settlement commits one event: `assistant/message` carries the assembled model-visible message plus its compact timed stream, while `assistant/attempt` retains a failed, retried, cancelled, or stream-error attempt without adding model history. A hard process loss before settlement leaves no durable attempt stream. +The package is built on event sourcing: a `Session` is an append-only log of typed `SessionEvent`s, and everything else — model history, transcripts, telemetry, titles, persistence — derives from that stream. The surface is a derived projection: an incremental manager validates append candidates, advances the ordered view from committed events, and tracks `replaceGeneration` for positional replacements and `contentGeneration` for replacements and plugin-owned message changes. Model-visible means logged: anything that reaches a model request must be reconstructable from the log. Each model attempt that reaches settlement commits one event: `assistant/message` carries the assembled model-visible message plus its compact timed stream, while `assistant/attempt` retains a failed, retried, cancelled, or stream-error attempt without adding model history. A hard process loss before settlement leaves no durable attempt stream. ### Request headers -`request/header` stores a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, `change`, or `series`. An explicit message-series start or a surface replacement writes a `series` snapshot when the envelope is unchanged; a simultaneous change uses `startsSeries: true`. Same-series steps, retries, and ordinary later turns inherit the latest snapshot. `adapterDefaults` distinguishes values resolved by the adapter from explicit settings, and `foldRequestHeader()` selects the latest snapshot. This self-contained record supports partial-window rendering and exact reconstruction at the cost of growth per message series; the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md) owns the detail. +`request/header` stores a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, `change`, or `series`. An explicit message-series start, a surface replacement, or a plugin-owned message change writes a `series` snapshot when the envelope is unchanged; a simultaneous change uses `startsSeries: true`. Same-series steps, retries, and ordinary later turns inherit the latest snapshot. `adapterDefaults` distinguishes values resolved by the adapter from explicit settings, and `foldRequestHeader()` selects the latest snapshot. This self-contained record supports partial-window rendering and exact reconstruction at the cost of growth per message series; the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md) owns the detail. ### Source map @@ -105,7 +107,7 @@ Every append uses the shared iterative `snapshotJsonValue()` pass, which reads, ### Derived history -`deriveMessages()` caches each surface node's projection once and returns a fresh array per call over shared, deep-frozen messages; each of the four surface event types (`system/message`, `user/message`, `assistant/message`, `tool/result`) projects its own message kind — the system-role prompt (an empty-content system node projects to no message), user content verbatim, the assembled assistant message with its provider and model, or a user-role tool result. Embedded Assistant streams and `assistant/attempt` events remain replay and diagnostic data only. A surface rewrite rebuilds the projection — there is no raw-log fallback, so the surface is the single source of derived history. +`deriveMessages()` caches deep-frozen projections and returns a fresh array per call. The four surface event types (`system/message`, `user/message`, `assistant/message`, `tool/result`) supply their recorded message identities and content; an empty-content system node projects to no message. Plugin-owned projections change derived content without mutating recorded messages. Replacements and projection decisions invalidate the cache. Embedded Assistant streams and `assistant/attempt` events remain replay and diagnostic data only. ### The request header @@ -135,7 +137,7 @@ The package-level contract is enough for most consumers; read these when you nee #### What the model sees -The model receives the complete messages from `system/message`, `user/message`, `assistant/message`, and `tool/result` surface entries verbatim, the system prompt first — identities, roles, sources, and content blocks are the same values established at creation, and projections never mint identities. Direct prompts and injected context remain separate `user/message` events whose sources preserve their attribution. Embedded streams, `assistant/attempt`, boundaries, and other log-only facts add no message. +The model receives the complete messages from `system/message`, `user/message`, `assistant/message`, and `tool/result` surface entries with logged projections applied, the system prompt first. Identities, roles, sources, and unmodified blocks retain their original values; projections never mint identities. Direct prompts and injected context remain separate `user/message` events whose sources preserve their attribution. Embedded streams, `assistant/attempt`, boundaries, and other log-only facts add no message. #### Token effect diff --git a/packages/core/session/README.zh.md b/packages/core/session/README.zh.md index 84cc0aa35d..28b6f1f028 100644 --- a/packages/core/session/README.zh.md +++ b/packages/core/session/README.zh.md @@ -49,6 +49,8 @@ session.deriveMessages() // the derived model history 表层事件(`system/message`、`user/message`、`assistant/message`、`tool/result`)在类型化事件与追加输入中都必须带有 `surfaceOp`。替换操作仅接受 `{ op: 'replace', startSeq, endSeq }`,端点为包含边界的 `SessionSeq`,按当前 surface 顺序解释。assistant 消息会嵌入精确、紧凑的提供方流,并禁止 `sourceEventSeqs`。已知仅日志事件禁止这两个元数据字段,且从不产生消息。 +插件用 `@messageProjection` 声明修改内容的事件,并通过 `ctx.sessions.registerMessageProjection()` 注册纯处理器。Session 在接受事件前调用处理器,并缓存其不可变消息更新。缺少处理器时拒绝追加和恢复,卸载已经使用的处理器后也会拒绝读取缓存。独立构造函数和 `foldSurface(events, projections)` 必须显式接收处理器。重建函数将折叠结果的 `projectedMessages` 传给 `deriveEventMessage()`,实时实例方法自动应用相同的投影。[插件拥有消息投影](../../../.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.zh.md)说明职责划分和离线装配。 + 追加、seed/restore 与事件 adoption/snapshot 会拒绝任何 `header.system` 及恰好为空的可选请求头字段(`tools: []`、`adapterDefaults: {}`),而不规范化输入。工具结果的 `data.error` 仅在 `message.content[0].isError === true` 时允许存在;失败标识仍是可选的。被拒绝的追加不会改变日志、派生状态或事件流。Adoption 校验事件局部元数据,但不校验所引用的历史或替换端点是否属于 surface。 `system/message` 承载渲染后的系统提示词:第一条是 surface 第 0 号节点,准入依据已准备调用的能力,不具备能力的路由将非空渲染文本归并到首个系统节点,延续中的 `in-history` 序列则在缓存历史之后追加;空系统节点不投影为消息,因此清除提示词必须为所有生效的系统节点记录空内容替换,而非仅替换最新节点;当第 0 号节点是 `system/message` 时,surface 折叠拒绝覆盖它的替换,除非替换事件本身是恰好覆盖该节点的 `system/message`,而后续系统节点不受保护,压缩范围可以遮蔽它们([决策](../../../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md))。 @@ -81,11 +83,11 @@ session.deriveMessages() // the derived model history ### 设计理念 -该包建立在事件溯源之上:`Session` 是类型化 `SessionEvent` 的仅追加日志,其他一切——模型历史、transcript(文本记录)、遥测、标题、持久化——都从这条流派生。surface 是派生投影:一个增量管理器校验追加候选、根据已提交事件推进有序视图,并跟踪每次已提交重写都会递增的 `replaceGeneration`。模型可见即已记录:任何到达模型请求的内容都必须能从日志重建。每个完成结算的模型尝试都会提交一个事件:`assistant/message` 携带组装后的模型可见 message 及其紧凑带时间 stream,`assistant/attempt` 则保留失败、重试、取消或 stream error attempt,且不添加模型历史。如果进程在 settlement 前硬中断,则不会留下持久 attempt stream。 +该包建立在事件溯源之上:`Session` 是类型化 `SessionEvent` 的仅追加日志,其他一切——模型历史、transcript(文本记录)、遥测、标题、持久化——都从这条流派生。surface 是派生投影:一个增量管理器校验追加候选、根据已提交事件推进有序视图,通过 `replaceGeneration` 跟踪位置替换,通过 `contentGeneration` 跟踪位置替换和插件拥有的消息变更。模型可见即已记录:任何到达模型请求的内容都必须能从日志重建。每个完成结算的模型尝试都会提交一个事件:`assistant/message` 携带组装后的模型可见 message 及其紧凑带时间 stream,`assistant/attempt` 则保留失败、重试、取消或 stream error attempt,且不添加模型历史。如果进程在 settlement 前硬中断,则不会留下持久 attempt stream。 ### 请求头 -`request/header` 存储非历史请求封装的完整规范快照,原因为 `initial`、`resume`、`change` 或 `series`。显式消息序列起点或表层替换会在请求封装不变时写入 `series` 快照;同时发生变化时使用 `startsSeries: true`。同一序列内的步骤、重试与普通后续轮次继承最新快照。`adapterDefaults` 区分由适配器解析的值与显式设置,`foldRequestHeader()` 选择最新快照。这种自包含记录以每个消息序列增加存储为代价,支持局部窗口渲染与精确重建;细节由[可重建请求 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md)负责。 +`request/header` 存储非历史请求封装的完整规范快照,原因为 `initial`、`resume`、`change` 或 `series`。显式消息序列起点、表层替换或插件拥有的消息变更会在请求封装不变时写入 `series` 快照;同时发生变化时使用 `startsSeries: true`。同一序列内的步骤、重试与普通后续轮次继承最新快照。`adapterDefaults` 区分由适配器解析的值与显式设置,`foldRequestHeader()` 选择最新快照。这种自包含记录以每个消息序列增加存储为代价,支持局部窗口渲染与精确重建;细节由[可重建请求 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md)负责。 ### 源码地图 @@ -105,7 +107,7 @@ session.deriveMessages() // the derived model history ### 派生历史 -`deriveMessages()` 把每个 surface 节点的投影缓存一次,每次调用都返回共享、深度冻结消息之上的新数组;四种 surface 事件类型(`system/message`、`user/message`、`assistant/message`、`tool/result`)各自投影自己的消息种类——system 角色的提示词(空内容的系统节点投影为无消息)、user 内容原样、带提供方与模型的组装 assistant 消息,或 user 角色的工具结果。嵌入式 Assistant stream 与 `assistant/attempt` 事件只保留回放和诊断数据。surface 重写会重建投影——不存在原始日志回退,因此 surface 是派生历史的唯一来源。 +`deriveMessages()` 缓存深度冻结的派生消息,每次调用返回新数组。四种 surface 事件类型(`system/message`、`user/message`、`assistant/message`、`tool/result`)提供记录的消息身份和内容,空内容的系统节点不派生消息。插件拥有的投影修改派生内容,不修改记录的消息。替换和投影决策使缓存失效。嵌入式 Assistant stream 与 `assistant/attempt` 事件只保留回放和诊断数据。 ### 请求头 @@ -135,7 +137,7 @@ session.deriveMessages() // the derived model history #### 模型看到什么 -模型会原样接收 `system/message`、`user/message`、`assistant/message` 与 `tool/result` surface 条目中的完整消息,系统提示词在先——标识、角色、来源与内容块都与创建时确定的值相同,投影从不生成标识。直接提示词与注入上下文仍是彼此独立的 `user/message` 事件,各事件的来源会保留其出处。嵌入式 stream、`assistant/attempt`、边界与其他仅日志事实不会添加消息。 +模型会接收 `system/message`、`user/message`、`assistant/message` 与 `tool/result` surface 条目中的消息,并应用日志中的投影,系统提示词在先。消息标识、角色、来源及未修改的内容块保持不变,投影不生成标识。直接提示词与注入上下文仍是独立的 `user/message` 事件,各事件的来源保留其出处。嵌入式 stream、`assistant/attempt`、边界与其他仅日志事实不添加消息。 #### Token 影响 diff --git a/packages/core/session/src/index.ts b/packages/core/session/src/index.ts index 12e9ed1918..548603f613 100644 --- a/packages/core/session/src/index.ts +++ b/packages/core/session/src/index.ts @@ -16,8 +16,8 @@ import type { Message } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, SessionLogOffset, SessionSeq } from './types.ts' import type { TypertLookup } from '@deepseek-ai/dsh-typert-protocol' import type { CreateSessionOptions, EpochHeader, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SessionId, SessionSeedEventState, SurfaceIntent, SurfaceEventType } from './types.ts' -import { deriveEventMessage, SurfaceManager, validateSessionEventData, validateSurfaceMetadata } from './surface.ts' -import type { SessionSurface } from './surface.ts' +import { SurfaceManager, validateSessionEventData, validateSurfaceMetadata } from './surface.ts' +import type { SessionSurface, SessionMessageProjection } from './surface.ts' import { foldRequestHeader } from './request-header.ts' export * from './types.ts' @@ -25,7 +25,7 @@ export { SessionPreparation } from './preparation.ts' export type { SessionPreparationOptions } from './preparation.ts' export type { AssistantMessage, SystemMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm' export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts' -export type { SessionSurface, SurfaceFoldReplacement, SurfaceFoldResult } from './surface.ts' +export type { SessionSurface, SurfaceFoldReplacement, SurfaceFoldResult, SessionMessageProjection, SessionMessageProjectionContext } from './surface.ts' export { deriveEventMessage, foldSurface, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEvent, isSurfaceEligibleType } from './surface.ts' export { canonicalHeader, foldRequestHeader, headerEquals } from './request-header.ts' export { KNOWN_SESSION_EVENT_TYPES } from './known-event-types.ts' @@ -446,7 +446,7 @@ const attachments = new WeakMap() export class Session { private log: SessionEvent[] = [] /** Single incremental owner of surface acceptance and projection state. */ - private readonly surfaceManager = new SurfaceManager(this.log) + private readonly surfaceManager: SurfaceManager /** The ordered surface over this session's event log. */ get surface(): SessionSurface { @@ -503,15 +503,18 @@ export class Session { * @param seed - optional borrowed replay or fork events. * @param header - optional borrowed storage metadata. * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header. + * @param projections - pure interpreters for plugin-owned message changes. * @returns a detached session. + * @throws when a seed event requires a missing message interpreter or fails validation. */ static create( id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader, inheritedEventCount?: SessionLogOffset, + projections?: readonly SessionMessageProjection[], ): Session { - return new Session(id, seed, header, 'snapshot', inheritedEventCount) + return new Session(id, seed, header, 'snapshot', inheritedEventCount, projections) } /** @@ -525,7 +528,9 @@ export class Session { * @param header - independently owned storage metadata. * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage. * @param eventState - aliasing state carried from the operation that produced the seed. + * @param projections - pure interpreters for plugin-owned message changes. * @returns a restored detached session. + * @throws when a seed event requires a missing message interpreter or fails validation. */ static fromRestore( id: SessionId, @@ -533,6 +538,7 @@ export class Session { header: SessionHeader, inheritedEventCount: SessionLogOffset, eventState: SessionSeedEventState, + projections?: readonly SessionMessageProjection[], ): Session { return new Session( id, @@ -540,6 +546,7 @@ export class Session { header, eventState, inheritedEventCount, + projections, ) } @@ -549,7 +556,9 @@ export class Session { header?: SessionHeader, mode: 'snapshot' | SessionSeedEventState = 'snapshot', suppliedInheritedEventCount?: SessionLogOffset, + projections: readonly SessionMessageProjection[] = [], ) { + this.surfaceManager = new SurfaceManager(this.log, SessionLogOffset(0), projections) const restoredHeader = mode === 'snapshot' ? undefined : validateRestoredSessionHeader(id, header) if (seed !== undefined) { // Validate the seed to the SAME invariants `append` enforces, so a @@ -808,7 +817,7 @@ export class Session { private derived: Message[] = [] /** Surface position (nodes projected) the cache has reached. */ private derivedNodes = 0 - /** {@link SurfaceManager.replaceGeneration} the cache was built under. */ + /** {@link SurfaceManager.contentGeneration} the cache was built under. */ private derivedGeneration = 0 /** @@ -818,21 +827,21 @@ export class Session { * append records its `surfaceOp`, so a raw event with no marker (a chunk, a * turn boundary) is correctly absent, and a compaction `replace` deletes the * shadowed nodes from the derivation. The projection rules are - * {@link deriveEventMessage}, folded per node. + * {@link deriveEventMessage}, with logged message projections applied + * without changing node membership or message identity. * - * CACHED: each surface node is projected exactly once, when first seen — a - * call costs O(new nodes), and a surface rewrite (a `replace`; - * {@link SessionSurface.replaceGeneration}) rebuilds. The returned array is + * CACHED: pure tail growth costs O(new nodes); a replacement or message projection + * ({@link SessionSurface.contentGeneration}) rebuilds. The returned array is * a fresh snapshot per call (later appends never grow an array a caller * already holds); the `Message` objects in it are SHARED and **deep-frozen**. - * Their content reuses the already frozen durable event data, so the cache - * needs no second deep clone and consumers still cannot mutate the log. + * Unchanged content reuses frozen event data; projected blocks are frozen + * derived copies. Consumers cannot mutate the log through either form. * @returns a fresh array of the shared, frozen derived history. */ deriveMessages(): Message[] { const surface = this.surface const nodes = surface.nodes - const generation = surface.replaceGeneration + const generation = surface.contentGeneration if (generation !== this.derivedGeneration) { this.derived = [] this.derivedNodes = 0 @@ -853,13 +862,13 @@ export class Session { } /** - * Instance face of the pure per-node `deriveEventMessage` export from - * `surface.ts`. + * Project one event with all committed message projections applied. + * The original durable event remains unchanged. * @param event - the event to project. * @returns the derived message, or null when the event produces none. */ deriveEventMessage(event: SessionEvent): Message | null { - return deriveEventMessage(event) + return this.surfaceManager.deriveEventMessage(event) } } @@ -899,6 +908,29 @@ export class SessionForkError extends Error { export class SessionStore extends Service { private store = new Map() private counter = 0 + private readonly projections: SessionMessageProjection[] = [] + + /** Borrowed definitions for detached replay; contributions live until their registering fibers unload. */ + get messageProjections(): readonly SessionMessageProjection[] { + return this.projections + } + + /** + * Register one event interpreter for live creation, restore, and fork. + * Disposing the contribution makes sessions that used it refuse further derivation. + * @param projection - pure definition owned by the event's plugin. + * @returns the fiber-owned disposer. + * @throws when another definition already owns this event type. + */ + registerMessageProjection(projection: SessionMessageProjection): () => Promise { + if (this.projections.some(item => item.type === projection.type)) { + throw new Error(`session message projection "${projection.type}" is already registered`) + } + return this.ctx.effect(() => { + this.projections.push(projection) + return () => { this.projections.splice(this.projections.indexOf(projection), 1) } + }, 'sessions.registerMessageProjection()') + } constructor(ctx: Context) { super(ctx, 'sessions') @@ -986,6 +1018,7 @@ export class SessionStore extends Service { options.meta, options.inheritedEventCount, eventState, + this.projections, ) case undefined: break @@ -1007,7 +1040,7 @@ export class SessionStore extends Service { ...meta?.delegationDepth === undefined ? {} : { delegationDepth: meta.delegationDepth }, ...meta?.agentPreset === undefined ? {} : { agentPreset: meta.agentPreset }, } - return Session.create(sessionId, seed, header, options?.inheritedEventCount) + return Session.create(sessionId, seed, header, options?.inheritedEventCount, this.projections) } /** diff --git a/packages/core/session/src/known-event-types.ts b/packages/core/session/src/known-event-types.ts index b38f532a5e..7eb3c5ea73 100644 --- a/packages/core/session/src/known-event-types.ts +++ b/packages/core/session/src/known-event-types.ts @@ -42,6 +42,7 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet = new Set([ 'goal/change', 'hook/invoked', 'hook/result', + 'image/offload', 'llm/retry', 'llm/retry-started', 'model/selection', @@ -79,3 +80,8 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet = new Set([ 'user/message', 'web/deepseek-search-llm-request', ]) + +/** Event types whose model-visible effects require an explicit pure interpreter. */ +export const MESSAGE_PROJECTION_EVENT_TYPES: ReadonlySet = new Set([ + 'image/offload', +]) diff --git a/packages/core/session/src/surface.ts b/packages/core/session/src/surface.ts index a702f9618a..9bc3d32a8c 100644 --- a/packages/core/session/src/surface.ts +++ b/packages/core/session/src/surface.ts @@ -10,14 +10,42 @@ import type { Message } from '@deepseek-ai/dsh-llm' import { SessionLogOffset, SessionSeq } from './types.ts' -import { KNOWN_SESSION_EVENT_TYPES } from './known-event-types.ts' +import { KNOWN_SESSION_EVENT_TYPES, MESSAGE_PROJECTION_EVENT_TYPES } from './known-event-types.ts' import type { SessionEvent, + SessionEventType, SessionSeqCursor, SurfaceEvent, SurfaceOp, } from './types.ts' +/** Readonly history immediately before a message-projection event. */ +export interface SessionMessageProjectionContext { + /** Current message-producing event sequences in model-visible order. */ + nodes: readonly SessionSeq[] + /** Contiguous event window; entries at or beyond the candidate seq are not committed inputs. */ + events: readonly SessionEvent[] + /** Absolute sequence of the window's first event. */ + baseSeq: SessionLogOffset + /** Previously projected messages keyed by their original event sequences. */ + messages: ReadonlyMap +} + +/** Pure interpretation of one plugin-owned event that changes existing message content. */ +export interface SessionMessageProjection { + /** Event interpreted by this definition; declare it with `@messageProjection` in SessionEventMap. */ + type: T + /** + * Validate the complete durable decision before returning any updates. Preserve + * message identities and publish immutable copies without mutating the input. + * @param event - candidate event, not yet applied to the supplied history. + * @param context - history preceding this decision. + * @returns changed current messages keyed by their original sequences. + * @throws when the durable decision cannot be applied to this history. + */ + project(event: SessionEvent, context: SessionMessageProjectionContext): ReadonlyMap +} + /** Runtime counterpart of the message-producing event union. */ const SURFACE_EVENT_TYPES = new Set([ 'system/message', @@ -79,17 +107,21 @@ export function isReplacementSurfaceEvent( /** * Project a single event into the LLM message it derives to, or null when it * produces none — a non-surface event (attempt, boundary, log-only record) or an - * empty-content assistant/message (which exists only to host usage). This is - * THE per-node projection rule: `Session.deriveMessages` folds it over the - * live surface, external reconstructors and pure projections fold the same - * function over a log prefix's surface to rebuild the exact messages any - * request was built from. The returned message is the already frozen message - * nested in the event wrapper and shared by delivery, durable history, and - * model requests. + * empty-content assistant/message (which exists only to host usage). A caller + * reconstructing model input supplies the same prefix's `projectedMessages` + * from {@link foldSurface}; without that map this function reads original + * event content. Session instance methods apply the live projection. Messages + * are immutable and unchanged content retains its durable identity. * @param event - the event to project. + * @param projectedMessages - message projections from the same log prefix's surface fold. * @returns the derived message, or null when the event produces none. */ -export function deriveEventMessage(event: SessionEvent): Message | null { +export function deriveEventMessage( + event: SessionEvent, + projectedMessages?: ReadonlyMap, +): Message | null { + const projected = projectedMessages?.get(event.seq) + if (projected !== undefined) return projected // Intentionally non-exhaustive: only message-producing events derive // history; turn/step boundaries, failed attempts, and errors are trace/replay // data. @@ -184,6 +216,8 @@ export interface SurfaceFoldResult { nodes: SessionSeq[] /** Replacement operations in event order. */ replacements: SurfaceFoldReplacement[] + /** Immutable projected messages, keyed by their original event sequences. */ + projectedMessages: ReadonlyMap } /** Readonly live projection of the message-producing session events. */ @@ -192,12 +226,17 @@ export interface SessionSurface { readonly nodes: readonly SessionSeq[] /** Monotonic count of committed positional replacements. */ readonly replaceGeneration: number + /** Monotonic count of committed replacements and plugin-owned message changes. */ + readonly contentGeneration: number } /** Mutable state shared by complete and incremental folds. */ interface SurfaceFoldState { nodes: SessionSeq[] replaceGeneration: number + contentGeneration: number + projectedMessages: Map + projections: Set } /** A validated replacement transition that has not mutated fold state yet. */ @@ -211,10 +250,11 @@ interface SurfaceReplacePlan extends SurfaceFoldReplacement { type SurfacePlan = | { kind: 'append'; seq: SessionSeq } | SurfaceReplacePlan + | { kind: 'project'; projection: SessionMessageProjection; messages: ReadonlyMap } /** Create an empty surface fold state. */ function createFoldState(): SurfaceFoldState { - return { nodes: [], replaceGeneration: 0 } + return { nodes: [], replaceGeneration: 0, contentGeneration: 0, projectedMessages: new Map(), projections: new Set() } } /** Whether a runtime value is a non-negative safe event sequence. */ @@ -424,11 +464,21 @@ function planSurfaceEvent( expectedSeq: SessionSeq, events: readonly SessionEvent[], baseSeq: SessionLogOffset, + projections: readonly SessionMessageProjection[], ): SurfacePlan | undefined { if (event.seq !== expectedSeq) { throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`) } const surfaceOp = validateSurfaceMetadata(event) + const projection = projections.find(item => item.type === event.type) + if (projection !== undefined) { + return { kind: 'project', projection, messages: projection.project(event, { + nodes: state.nodes, events, baseSeq, messages: state.projectedMessages, + }) } + } + if (MESSAGE_PROJECTION_EVENT_TYPES.has(event.type)) { + throw new Error(`session event "${event.type}" requires a message projection; load its owning plugin or supply its projection definition`) + } if (surfaceOp === undefined) return if (surfaceOp === 'append') { return { kind: 'append', seq: event.seq } @@ -453,8 +503,9 @@ function applySurfaceEvent( expectedSeq: SessionSeq, events: readonly SessionEvent[], baseSeq: SessionLogOffset, + projections: readonly SessionMessageProjection[], ): SurfaceFoldReplacement | undefined { - const plan = planSurfaceEvent(state, event, expectedSeq, events, baseSeq) + const plan = planSurfaceEvent(state, event, expectedSeq, events, baseSeq, projections) return applySurfacePlan(state, plan) } @@ -468,6 +519,11 @@ function applySurfacePlan( } else if (plan?.kind === 'replace') { state.nodes.splice(plan.startIdx, plan.endIdx - plan.startIdx + 1, plan.seq) state.replaceGeneration += 1 + state.contentGeneration += 1 + } else if (plan?.kind === 'project') { + for (const [seq, message] of plan.messages) state.projectedMessages.set(seq, message) + state.projections.add(plan.projection) + state.contentGeneration += 1 } if (plan?.kind !== 'replace') return return { @@ -481,10 +537,11 @@ function applySurfacePlan( /** * Replay a complete session log through the canonical surface fold. * @param events - session events in contiguous seq order. + * @param projections - pure interpreters for plugin-owned message changes; required definitions must be supplied. * @returns detached current sequences and replacement history. - * @throws when an event violates surface metadata, source-event references, range, or tool-result rewrite rules. + * @throws when an interpreter is missing or an event violates its projection, surface metadata, source attribution, or replacement rules. */ -export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult { +export function foldSurface(events: readonly SessionEvent[], projections: readonly SessionMessageProjection[] = []): SurfaceFoldResult { const state = createFoldState() const replacements: SurfaceFoldReplacement[] = [] for (const [index, event] of events.entries()) { @@ -494,10 +551,11 @@ export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult SessionSeq(index), events, SessionLogOffset(0), + projections, ) if (replacement !== undefined) replacements.push(replacement) } - return { nodes: [...state.nodes], replacements } + return { nodes: [...state.nodes], replacements, projectedMessages: new Map(state.projectedMessages) } } /** Incremental ordered surface view and append-boundary validator. */ @@ -512,10 +570,12 @@ export class SurfaceManager implements SessionSurface { /** * @param log - Contiguous complete log or loaded event window. * @param baseSeq - Absolute sequence of the window's first event. + * @param projections - live borrowed definitions; removing a used definition invalidates further reads. */ constructor( private log: readonly SessionEvent[], private readonly baseSeq: SessionLogOffset = SessionLogOffset(0), + private readonly projections: readonly SessionMessageProjection[] = [], ) { this._lastProcessedSeq = baseSeq === 0 ? -1 : SessionSeq(baseSeq - 1) } @@ -525,23 +585,44 @@ export class SurfaceManager implements SessionSurface { * @param event - candidate event that has not entered the log yet. */ validateNext(event: SessionEvent): void { + this._assertProjections() if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta() const expectedSeq = SessionSeq(this.baseSeq + this.log.length) this._pendingPlan = { event, expectedSeq, - plan: planSurfaceEvent(this._state, event, expectedSeq, this.log, this.baseSeq), + plan: planSurfaceEvent(this._state, event, expectedSeq, this.log, this.baseSeq, this.projections), } } /** Monotonic count of folded positional replacements. */ get replaceGeneration(): number { + this._assertProjections() if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta() return this._state.replaceGeneration } + /** Monotonic count of committed changes to existing model-visible content. */ + get contentGeneration(): number { + this._assertProjections() + if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta() + return this._state.contentGeneration + } + + /** + * Project one message with every committed message projection applied. + * @param event - message-producing or log-only event. + * @returns its immutable projected message, or null when it produces none. + */ + deriveEventMessage(event: SessionEvent): Message | null { + this._assertProjections() + if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta() + return deriveEventMessage(event, this._state.projectedMessages) + } + /** Surface event sequences in model-visible order. */ get nodes(): readonly SessionSeq[] { + this._assertProjections() if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta() return this._state.nodes } @@ -557,10 +638,25 @@ export class SurfaceManager implements SessionSurface { if (pending?.event === event && pending.expectedSeq === seq) { applySurfacePlan(this._state, pending.plan) } else { - applySurfaceEvent(this._state, event, SessionSeq(seq), this.log, this.baseSeq) + applySurfaceEvent(this._state, event, SessionSeq(seq), this.log, this.baseSeq, this.projections) } if (pending !== undefined && pending.expectedSeq <= seq) this._pendingPlan = undefined this._lastProcessedSeq = SessionSeq(seq) } } + + /** Cached messages cannot outlive the definitions that interpreted their log. */ + private _assertProjections(): void { + const candidate = this._pendingPlan + const pending = candidate !== undefined && this.log[candidate.expectedSeq - this.baseSeq] === candidate.event + ? candidate.plan : undefined + const required = pending?.kind === 'project' + ? [...this._state.projections, pending.projection] + : this._state.projections + for (const projection of required) { + if (!this.projections.includes(projection)) { + throw new Error(`session message projection "${projection.type}" was removed or replaced; restore the session with its owning plugin`) + } + } + } } diff --git a/packages/core/session/tests/gen-persistence-catalog.spec.ts b/packages/core/session/tests/gen-persistence-catalog.spec.ts index 4fee74793f..7c03a43dd0 100644 --- a/packages/core/session/tests/gen-persistence-catalog.spec.ts +++ b/packages/core/session/tests/gen-persistence-catalog.spec.ts @@ -13,6 +13,7 @@ import { collectLogEvents, collectSurfaceEventTypes, render, + renderKnownEventTypes, } from '../../../../scripts/gen-persistence-catalog.ts' /** Create a fixture scan root; `files` maps `packages/…`-relative paths to source. */ @@ -45,6 +46,18 @@ afterEach(() => { const OWNER_MANIFEST = '{ "name": "@deepseek-ai/dsh-session" }\n' describe('gen-persistence-catalog collectLogEvents', () => { + it('generates required interpreter types from plugin-owned event declarations', () => { + const events = collectLogEvents(make({ + 'packages/group/fix/src/types.ts': merge(' /**\n * Projects existing messages.\n * @messageProjection\n */\n \'fix/project\': { seq: number }'), + })) + expect(events[0]?.messageProjection).toBe(true) + expect(renderKnownEventTypes(annotateSurface(events, []))) + .toContain("MESSAGE_PROJECTION_EVENT_TYPES: ReadonlySet = new Set([\n 'fix/project',\n])") + expect(() => annotateSurface(events, ['fix/project'])).toThrow(/cannot declare both/) + const ordinary = events.map(({ messageProjection: _projection, ...event }) => event) + expect(renderKnownEventTypes(annotateSurface(ordinary, []))) + .toContain('MESSAGE_PROJECTION_EVENT_TYPES: ReadonlySet = new Set([\n])') + }) it('extracts a documented member of the owning top-level interface', () => { const events = collectLogEvents(make({ 'packages/core/fix/package.json': OWNER_MANIFEST, diff --git a/packages/core/session/tests/message-projections.spec.ts b/packages/core/session/tests/message-projections.spec.ts new file mode 100644 index 0000000000..055dac5ff6 --- /dev/null +++ b/packages/core/session/tests/message-projections.spec.ts @@ -0,0 +1,117 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import { createUserMessage } from '@deepseek-ai/dsh-llm' +import { deepFreeze } from '@deepseek-ai/dsh-util-values' +import SessionStore, { Session, SessionId, SessionSeq, SessionLogOffset, foldSurface, deriveEventMessage } from '../src/index.ts' +import type { SessionEvent, SessionMessageProjection } from '../src/index.ts' +import { MESSAGE_PROJECTION_EVENT_TYPES } from '../src/known-event-types.ts' +import { SurfaceManager } from '../src/surface.ts' + +declare module '@deepseek-ai/dsh-session/types' { + interface SessionEventMap { + 'test/project': { seq: SessionSeq; text: string } + } +} + +const projection: SessionMessageProjection<'test/project'> = { + type: 'test/project', + project(event, context) { + const source = context.events[event.data.seq - context.baseSeq]! + const original = deriveEventMessage(source, context.messages)! + if (event.data.text === 'reject') throw new Error('rejected decision') + return new Map([[source.seq, deepFreeze({ ...original, content: [{ type: 'text' as const, text: event.data.text }] })]]) + }, +} + +const contexts: Context[] = [] +afterEach(async () => { + await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) +}) + +function input(session: Session) { + return session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'original' }], source: { kind: 'user' } }), { surfaceOp: 'append' }) +} + +describe('plugin-owned message projections', () => { + it('refuses required events when no interpreter is supplied', () => { + const type = [...MESSAGE_PROJECTION_EVENT_TYPES][0]! + const event = { type, seq: SessionSeq(0), time: 0, data: {} } as SessionEvent + const session = Session.create(SessionId('missing')) + expect(() => session.append(event.type as 'test/project', event.data as never)).toThrow(/requires a message projection/) + expect(() => foldSurface([event])).toThrow(/requires a message projection/) + expect(() => Session.create(session.id, [event])).toThrow(/requires a message projection/) + expect(session.seq).toBe(0) + }) + + it('applies generic decisions atomically and replays them through detached folds', () => { + const definitions = [projection] + const session = Session.create(SessionId('pure'), undefined, undefined, undefined, definitions) + const source = input(session) + const original = session.deriveMessages() + expect(() => session.append('test/project', { seq: source.seq, text: 'reject' })).toThrow('rejected decision') + expect(session.deriveMessages()).toEqual(original) + session.append('test/project', { seq: source.seq, text: 'projected' }) + expect(session.deriveMessages()[0]?.content).toEqual([{ type: 'text', text: 'projected' }]) + expect(session.surface.replaceGeneration).toBe(0) + expect(session.surface.contentGeneration).toBe(1) + const folded = foldSurface(session.snapshotEvents(), definitions) + expect(deriveEventMessage(source, folded.projectedMessages)).toEqual(session.deriveMessages()[0]) + expect(source.data.content).toEqual([{ type: 'text', text: 'original' }]) + }) + + it('registers by fiber and refuses cached or pending decisions after disposal', async () => { + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SessionStore) + const fiber = await ctx.plugin({ + inject: ['sessions'], + apply(owner: Context) { owner.sessions.registerMessageProjection(projection) }, + }) + expect(() => ctx.sessions.registerMessageProjection(projection)).toThrow(/already registered/) + const live = ctx.sessions.create(SessionId('live')) + const source = input(live) + live.append('test/project', { seq: source.seq, text: 'changed' }) + const pending = ctx.sessions.create(SessionId('pending')) + input(pending) + pending.append('test/project', { seq: source.seq, text: 'changed' }) + const before = live.deriveMessages() + const child = ctx.sessions.fork(live) + expect(child.deriveMessages()).toEqual(before) + const restored = ctx.sessions.prepare(SessionId('restore'), { + seed: [...live.snapshotEvents()], meta: { ...live.header, id: SessionId('restore') }, + inheritedEventCount: live.inheritedEventCount, eventState: 'shared-frozen', + }) + expect(restored.deriveMessages()).toEqual(before) + await fiber.dispose() + expect(ctx.sessions.messageProjections).toEqual([]) + for (const session of [live, pending, child, restored]) { + expect(() => session.deriveMessages()).toThrow(/was removed or replaced/) + expect(() => session.deriveEventMessage(source)).toThrow(/was removed or replaced/) + expect(() => session.surface.replaceGeneration).toThrow(/was removed or replaced/) + expect(() => session.surface.contentGeneration).toThrow(/was removed or replaced/) + expect(() => session.append('turn/start', { turn: 1 })).toThrow(/was removed or replaced/) + } + expect(before[0]?.content).toEqual([{ type: 'text', text: 'changed' }]) + }) + + it('applies a supplied interpreter to a loaded window with absolute sequences', () => { + const session = Session.create(SessionId('window'), undefined, undefined, undefined, [projection]) + input(session) + const source = input(session) + session.append('test/project', { seq: source.seq, text: 'window' }) + const events = session.snapshotEvents().slice(1) + const surface = new SurfaceManager(events, SessionLogOffset(1), [projection]) + expect(surface.nodes).toEqual([source.seq]) + expect(surface.deriveEventMessage(source)?.content).toEqual([{ type: 'text', text: 'window' }]) + }) + + it('does not retain an interpreter for a candidate that never committed', () => { + const session = Session.create(SessionId('candidate')) + const source = input(session) + const definitions: SessionMessageProjection[] = [projection] + const surface = new SurfaceManager(session.snapshotEvents(), undefined, definitions) + surface.validateNext({ type: 'test/project', seq: SessionSeq(1), time: 0, data: { seq: source.seq, text: 'unused' } }) + definitions.length = 0 + expect(surface.deriveEventMessage(source)).toBe(source.data) + }) +}) diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index f343e0f1a5..ea086375d7 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -1150,6 +1150,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ 'client-ui-agent-preset AgentPresetLabel id \'agent-preset\'', 'client-ui-jobs JobListAction id \'job-list\'', 'client-ui-schedule ScheduleCatalogAction id \'schedule-catalog\'', + 'client-ui-sidebar-terminal TerminalRecovery', 'experimental-client-ui-agent-team TeamAction id \'agent-team\'', ], replaceRisk: 'none', @@ -2071,7 +2072,9 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ hookContext: '', slotInject: '', declaredBy: 'an entry in \'root\' (client-ui-layout), so it exists while that entry is mounted', - occupants: [], + occupants: [ + 'client-ui-sidebar-terminal TerminalCleanup', + ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'shell.overlay\', () => ctx.slots.register(\n { name: \'shell.overlay\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', source: 'packages/client/ui-layout/src/client/index.ts:91', @@ -2302,6 +2305,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ 'client-ui-sidebar-documentpreview TextPreview', 'client-ui-sidebar-files FilesBody', 'client-ui-sidebar-right GuideBody', + 'client-ui-sidebar-terminal TerminalBody', ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.pane.tab\', () => ctx.slots.register(\n { name: \'sidebar.right.pane.tab\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', @@ -2347,6 +2351,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ 'client-ui-sidebar-documentpreview TextTitle', 'client-ui-sidebar-files FilesTitle', 'client-ui-sidebar-right GuideTitle', + 'client-ui-sidebar-terminal TerminalTitle', ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.pane.tab.title\', () => ctx.slots.register(\n { name: \'sidebar.right.pane.tab.title\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index dc72b39e4e..f0e07bbf2d 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1897,6 +1897,13 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ summary: 'In-memory session store (`ctx.sessions`).', description: 'In-memory session store (`ctx.sessions`).\n\nPersistence is intentionally not implemented here — the agent lifecycle attaches a session-log writer to each published session\'s write handle; a session published outside that lifecycle persists nothing.', methods: [ + { + signature: 'registerMessageProjection(projection: SessionMessageProjection): () => Promise', + description: 'Register one event interpreter for live creation, restore, and fork. Disposing the contribution makes sessions that used it refuse further derivation.', + parameters: [{ name: 'projection', description: 'pure definition owned by the event\'s plugin.' }], + returns: 'the fiber-owned disposer.', + throws: ['when another definition already owns this event type.'], + }, { signature: 'create(id?: SessionId, options?: CreateSessionOptions): Session', description: 'Create a session owned by the calling fiber: disposing that fiber stops event notification and removes the session from the store. `options.seed` populates the session with a copy of those events (replay/fork); `options.meta` attaches creation metadata (validated absolute `cwd`, seed and parent lineage, and delegation depth) as the immutable SessionHeader (the store fills `version`/`id`/`createdAt`).\n\nFor an agent whose session must be torn down IN ORDER with its loop (so the loop\'s final events are published before the store attachment ends), do NOT use this — fold the session lifecycle into the agent\'s own effect via prepare + enter + announce (see `dsh-agent-loop`\'s creation transaction).', @@ -2445,6 +2452,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [{ name: 'command', description: 'absolute executable path or bare PATH name.' }, { name: 'env', description: 'explicit environment entries used for lookup.' }, { name: 'signal', description: 'aborts remote or local lookup.' }], returns: 'a canonical executable path.', }, + { + signature: 'abstract terminalEnvironment(signal?: AbortSignal): Promise', + description: 'Inspect shell-selection facts in the provider\'s execution environment.', + parameters: [{ name: 'signal', description: 'cancellation of remote environment inspection.' }], + returns: 'platform and preferred shell; executable lookup and allocation remain separate operations.', + }, { signature: 'abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle', description: 'Start one managed child process from a fully-specified spec; this seam applies no defaults.', @@ -2515,6 +2528,66 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, ], }, + { + key: 'terminalController', + summary: 'Typed Remote control of transient Session-owned terminal processes.', + description: 'Typed Remote control of transient Session-owned terminal processes.', + methods: [ + { + signature: '@Remote environment(agent: Agent, signal: AbortSignal): TerminalEnvironment', + description: 'Read the Session working directory and terminal limits without resolving a shell.', + parameters: [{ name: 'agent', description: 'Session owner supplied by the Gateway.' }, { name: 'signal', description: 'request cancellation.' }], + returns: 'the Session workspace directory and terminal limits.', + }, + { + signature: '@Remote shells(agent: Agent, signal: AbortSignal): Promise', + description: 'Discover installed shells in the Session\'s execution environment.', + parameters: [{ name: 'agent', description: 'Session owner supplied by the Gateway.' }, { name: 'signal', description: 'request cancellation.' }], + returns: 'verified profiles, with the configured or system default first.', + }, + { + signature: '@Remote list(sessionId: SessionId): WebTerminalInfo[]', + description: 'List retained terminals without resolving or activating an Agent.', + parameters: [{ name: 'sessionId', description: 'displayed Session identity, including offline history.' }], + returns: 'terminals retained for this Host lifetime.', + }, + { + signature: '@Remote async create(agent: Agent, request: TerminalCreateRequest, signal: AbortSignal): Promise', + description: 'Allocate an interactive shell once for a caller-generated identity.', + parameters: [{ name: 'agent', description: 'Session owner supplied by the Gateway.' }, { name: 'request', description: 'initial dimensions and idempotency identity.' }, { name: 'signal', description: 'allocation cancellation; committed terminals survive disconnection.' }], + returns: 'the existing or newly committed terminal.', + }, + { + signature: '@Remote({ mode: \'stream\' }) follow(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, signal: AbortSignal): AsyncIterable', + description: 'Attach to a terminal without binding its process lifetime to the transport.', + parameters: [{ name: 'agent', description: 'Session owner supplied by the Gateway.' }, { name: 'id', description: 'terminal identity.' }, { name: 'attachmentId', description: 'new exclusive input attachment.' }, { name: 'signal', description: 'physical stream cancellation.' }], + returns: 'screen recovery followed by output and metadata changes.', + }, + { + signature: '@Remote async write(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, data: string): Promise', + description: 'Deliver raw input, including Tab completion and control characters.', + parameters: [{ name: 'agent', description: 'Session owner supplied by the Gateway.' }, { name: 'id', description: 'terminal identity.' }, { name: 'attachmentId', description: 'current writable attachment.' }, { name: 'data', description: 'input bytes represented as UTF-8 text.' }], + returns: 'after provider input acceptance.', + }, + { + signature: '@Remote async resize(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, cols: number, rows: number): Promise', + description: 'Update the dimensions of the PTY and recovery screen.', + parameters: [{ name: 'agent', description: 'Session owner supplied by the Gateway.' }, { name: 'id', description: 'terminal identity.' }, { name: 'attachmentId', description: 'current writable attachment.' }, { name: 'cols', description: 'column count.' }, { name: 'rows', description: 'row count.' }], + returns: 'after the resize completes.', + }, + { + signature: '@Remote rename(agent: Agent, id: WebTerminalId, title: string): void', + description: 'Rename a terminal without changing its shell.', + parameters: [{ name: 'agent', description: 'Session owner supplied by the Gateway.' }, { name: 'id', description: 'terminal identity.' }, { name: 'title', description: 'nonempty display title, at most 120 characters.' }], + }, + { + signature: '@Remote async close(agent: Agent, id: WebTerminalId): Promise', + description: 'Close an identity to future creation and kill its process range; repeated closes succeed.', + parameters: [{ name: 'agent', description: 'Session owner supplied by the Gateway.' }, { name: 'id', description: 'terminal identity.' }], + returns: 'after provider cleanup succeeds. A failure retains the terminal for retry.', + }, + ], + }, { key: 'terminals', summary: 'In-process registry for replaceable PTY backends and exact-Agent sessions.', @@ -3272,6 +3345,14 @@ export const EVENT_API: readonly EventApiEntry[] = [ description: 'A command was registered or unregistered. This is an unfiltered registry notification because a global or scoped change may affect any UI view. Observer failures are contained and cannot veto the registry mutation.', parameters: [], }, + { + name: 'compaction/summary-error', + mode: 'waterfall', + signature: '\'compaction/summary-error\'(payload: { session: Session; sourceEventSeqs: readonly SessionSeq[]; error: unknown; signal?: AbortSignal }, next: () => boolean): boolean', + summary: 'Recover a failed summary request by synchronously recording a durable change to its selected input.', + description: 'Recover a failed summary request by synchronously recording a durable change to its selected input. Return true only after making progress; the provider re-derives and re-prices the selection before retrying. Call next() when the failure cannot be recovered. Decisions survive a later summary failure or cancellation.', + parameters: [{ name: 'payload', description: '.signal - optional compaction cancellation signal.' }, { name: 'next', description: 'delegate to the next recovery listener.' }], + }, { name: 'cordis/dynamic-package', mode: 'emit', @@ -4414,7 +4495,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ImageBlock', - declaration: 'export interface ImageBlock {\n type: \'image\';\n attachment: ImageAttachmentRef;\n}', + declaration: 'export interface ImageBlock {\n type: \'image\';\n attachment: ImageAttachmentRef;\n offloaded?: true;\n}', }, { name: 'ImageMediaType', @@ -4578,7 +4659,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'LlmFailure', - declaration: 'export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n}', + declaration: 'export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n readonly offloadImages?: number;\n}', }, { name: 'LlmImageRequestPrice', @@ -4586,7 +4667,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'LlmImageRequestPricing', - declaration: 'export interface LlmImageRequestPricing {\n priceImages(images: readonly ImageAttachmentRef[]): readonly LlmImageRequestPrice[];\n}', + declaration: 'export interface LlmImageRequestPricing {\n priceImages(images: readonly ImageBlock[]): readonly LlmImageRequestPrice[];\n}', }, { name: 'LlmModelContext', @@ -5138,7 +5219,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'Session', - declaration: 'export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n readonly inheritedEventCount: SessionLogOffset;\n get id(): SessionId;\n readonly firstLiveSeq: SessionLogOffset;\n static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader, inheritedEventCount?: SessionLogOffset): Session;\n static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader, inheritedEventCount: SessionLogOffset, eventState: SessionSeedEventState): Session;\n eventAt(seq: SessionSeq): SessionEvent | undefined;\n snapshotEvents(fromSeq: SessionLogOffset = SessionLogOffset(0), toSeqExclusive: SessionLogOffset = this.seq): readonly SessionEvent[];\n ownEvents(): readonly SessionEvent[];\n isOwnSeq(seq: SessionSeq): boolean;\n get seq(): SessionLogOffset;\n append(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent;\n requestHeader(): EpochHeader | undefined;\n requestContext(): RequestContext | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n}', + declaration: 'export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n readonly inheritedEventCount: SessionLogOffset;\n get id(): SessionId;\n readonly firstLiveSeq: SessionLogOffset;\n static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader, inheritedEventCount?: SessionLogOffset, projections?: readonly SessionMessageProjection[]): Session;\n static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader, inheritedEventCount: SessionLogOffset, eventState: SessionSeedEventState, projections?: readonly SessionMessageProjection[]): Session;\n eventAt(seq: SessionSeq): SessionEvent | undefined;\n snapshotEvents(fromSeq: SessionLogOffset = SessionLogOffset(0), toSeqExclusive: SessionLogOffset = this.seq): readonly SessionEvent[];\n ownEvents(): readonly SessionEvent[];\n isOwnSeq(seq: SessionSeq): boolean;\n get seq(): SessionLogOffset;\n append(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent;\n requestHeader(): EpochHeader | undefined;\n requestContext(): RequestContext | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n}', }, { name: 'SessionAccess', @@ -5364,6 +5445,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SessionLogSnapshot', declaration: 'export interface SessionLogSnapshot {\n session: SessionHeader;\n inheritedEventCount: SessionLogOffset;\n events: SessionEvent[];\n}', }, + { + name: 'SessionMessageProjection', + declaration: 'export interface SessionMessageProjection {\n type: T;\n project(event: SessionEvent, context: SessionMessageProjectionContext): ReadonlyMap;\n}', + }, + { + name: 'SessionMessageProjectionContext', + declaration: 'export interface SessionMessageProjectionContext {\n nodes: readonly SessionSeq[];\n events: readonly SessionEvent[];\n baseSeq: SessionLogOffset;\n messages: ReadonlyMap;\n}', + }, { name: 'SessionObservation', declaration: 'export interface SessionObservation extends Disposable {\n readonly source: \'live\' | \'prepared\';\n readonly header: SessionHeader;\n readonly inheritedEventCount: SessionLogOffsetType;\n readonly events: readonly SessionEvent[];\n readonly cursor: SessionSeqCursor;\n readonly revision?: SessionPersistenceRevision;\n readonly projections?: ProjectionSnapshot;\n retain(): SessionObservation;\n}', @@ -5546,7 +5635,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionSurface', - declaration: 'export interface SessionSurface {\n readonly nodes: readonly SessionSeq[];\n readonly replaceGeneration: number;\n}', + declaration: 'export interface SessionSurface {\n readonly nodes: readonly SessionSeq[];\n readonly replaceGeneration: number;\n readonly contentGeneration: number;\n}', }, { name: 'SessionSurfaceSnapshot', @@ -5936,13 +6025,17 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SubprocessStdio', declaration: 'export interface SubprocessStdio {\n stdin: SubprocessStdinMode;\n stdout: SubprocessOutputMode;\n stderr: SubprocessOutputMode;\n control?: \'pipe\';\n}', }, + { + name: 'SubprocessTerminalEnvironment', + declaration: 'export interface SubprocessTerminalEnvironment {\n platform: \'posix\' | \'windows\';\n defaultShell?: string;\n}', + }, { name: 'SubprocessTerminalForeground', declaration: 'export interface SubprocessTerminalForeground {\n processGroupId: number;\n inputWaiting: boolean;\n}', }, { name: 'SubprocessTerminalHandle', - declaration: 'export interface SubprocessTerminalHandle {\n readonly pid: number;\n readonly output: Readable;\n readonly done: Promise;\n write(data: string): Promise;\n inspectForeground(): Promise;\n signalForeground(signal: SubprocessTerminalSignal): Promise;\n terminate(): Promise;\n}', + declaration: 'export interface SubprocessTerminalHandle {\n readonly pid: number;\n readonly output: Readable;\n readonly done: Promise;\n write(data: string): Promise;\n resize(cols: number, rows: number): Promise;\n inspectForeground(): Promise;\n signalForeground(signal: SubprocessTerminalSignal): Promise;\n terminate(): Promise;\n}', }, { name: 'SubprocessTerminalSignal', @@ -5950,7 +6043,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SubprocessTerminalSpawnSpec', - declaration: 'export interface SubprocessTerminalSpawnSpec {\n argv: readonly string[];\n cwd: string;\n env?: Record | undefined;\n rows: number;\n cols: number;\n graceMs: number;\n signal?: AbortSignal | undefined;\n}', + declaration: 'export interface SubprocessTerminalSpawnSpec {\n argv: readonly string[];\n cwd: string;\n env?: Record | undefined;\n rows: number;\n cols: number;\n terminalType: string;\n graceMs: number;\n signal?: AbortSignal | undefined;\n}', }, { name: 'SurfaceEvent', @@ -6032,6 +6125,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'TeamWaitResult', declaration: 'export interface TeamWaitResult {\n readonly timedOut: boolean;\n}', }, + { + name: 'TerminalAttachmentId', + declaration: 'export type TerminalAttachmentId = Branded<\'TerminalAttachmentId\'>;', + }, { name: 'TerminalBackend', declaration: 'export interface TerminalBackend {\n readonly type: string;\n spawn(spec: TerminalBackendSpawnSpec): Promise;\n}', @@ -6048,6 +6145,18 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'TerminalCallView', declaration: 'export interface TerminalCallView {\n card: \'terminal\';\n title: string;\n description?: string;\n cwd?: string;\n}', }, + { + name: 'TerminalCreateRequest', + declaration: 'export interface TerminalCreateRequest {\n readonly shellPath?: string;\n readonly id: WebTerminalId;\n readonly cols: number;\n readonly rows: number;\n}', + }, + { + name: 'TerminalEnvironment', + declaration: 'export interface TerminalEnvironment {\n readonly cwd: string;\n readonly maxInputBytes: number;\n readonly maxCols: number;\n readonly maxRows: number;\n readonly scrollback: number;\n}', + }, + { + name: 'TerminalFrame', + declaration: 'export type TerminalFrame = {\n readonly type: \'snapshot\';\n readonly sequence: number;\n readonly screen: string;\n readonly info: WebTerminalInfo;\n} | {\n readonly type: \'output\';\n readonly sequence: number;\n readonly data: string;\n} | {\n readonly type: \'state\';\n readonly info: WebTerminalInfo;\n};', + }, { name: 'TerminalReadRequest', declaration: 'export interface TerminalReadRequest {\n offset?: number;\n count?: number;\n}', @@ -6092,6 +6201,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'TerminalSessionStatus', declaration: 'export type TerminalSessionStatus = {\n kind: \'running\';\n} | {\n kind: \'exited\';\n exitCode: number | null;\n signal: NodeJS.Signals | null;\n};', }, + { + name: 'TerminalShell', + declaration: 'export interface TerminalShell {\n readonly path: string;\n readonly args: readonly string[];\n readonly name: string;\n}', + }, { name: 'TerminalSignal', declaration: 'export type TerminalSignal = \'SIGINT\' | \'SIGTERM\' | \'SIGKILL\' | \'SIGTSTP\' | \'SIGHUP\';', @@ -6460,6 +6573,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'WebSource', declaration: 'export interface WebSource {\n url: string;\n title?: string;\n snippet?: string;\n publishedAt?: string;\n}', }, + { + name: 'WebTerminalId', + declaration: 'export type WebTerminalId = Branded<\'WebTerminalId\'>;', + }, + { + name: 'WebTerminalInfo', + declaration: 'export interface WebTerminalInfo {\n readonly id: WebTerminalId;\n readonly title: string;\n readonly shell: TerminalShell;\n readonly cwd: string;\n readonly cols: number;\n readonly rows: number;\n readonly state: \'running\' | \'exited\' | \'failed\';\n readonly exitCode: number | null;\n readonly error?: string;\n readonly controllerId?: TerminalAttachmentId;\n}', + }, { name: 'WebUpgradeRoute', declaration: 'export interface WebUpgradeRoute {\n path: string;\n handler: (req: IncomingMessage, socket: Duplex, head: Buffer) => void | Promise;\n}', diff --git a/packages/fs/fs-local/tests/fsio.spec.ts b/packages/fs/fs-local/tests/fsio.spec.ts index be3bef5b5b..40ed836309 100644 --- a/packages/fs/fs-local/tests/fsio.spec.ts +++ b/packages/fs/fs-local/tests/fsio.spec.ts @@ -110,7 +110,8 @@ describe('resolveLocalTarget', () => { await mkdir(join(dir, 'nested')) await writeFile(join(dir, 'created.txt'), 'native') const drive = parse(dir).root.slice(0, 2) - const driveCwd = `${drive}${relative(process.cwd(), dir)}` + // Resolve the temp drive independently when the checkout lives on another volume. + const driveCwd = `${drive}${relative(resolve(drive), dir)}` for (const cwd of [dir, driveCwd]) { for (const requested of ['created.txt', `${drive}nested/../created.txt`]) { const target = await resolveLocalTarget(cwd, requested) diff --git a/packages/fs/tool-fs-search/tests/tools.spec.ts b/packages/fs/tool-fs-search/tests/tools.spec.ts index 212cf5750f..22511aef39 100644 --- a/packages/fs/tool-fs-search/tests/tools.spec.ts +++ b/packages/fs/tool-fs-search/tests/tools.spec.ts @@ -148,6 +148,7 @@ class FakeHandle implements SubprocessHandle { * assert on the exact spawn specs and settled handles. */ class FakeSubprocess extends SubprocessRuntime { + async terminalEnvironment() { return { platform: 'posix' as const } } spawns: SubprocessSpawnSpec[] = [] override async resolveExecutable(command: string): Promise { return command } override spawnTerminal(): Promise { throw new Error('search tools spawn pipes, never terminals') } diff --git a/packages/host/frontend-static/tests/frontend-static.spec.ts b/packages/host/frontend-static/tests/frontend-static.spec.ts index 26034e7d6f..2a38750c06 100644 --- a/packages/host/frontend-static/tests/frontend-static.spec.ts +++ b/packages/host/frontend-static/tests/frontend-static.spec.ts @@ -141,7 +141,7 @@ describe('real Loader composition', () => { // Only the root and index path render index.html through registered taps. const untap = server.tapIndex(html => html.replace('', '')) - for (const path of ['/', '/index.html', '/?fixture']) { + for (const path of ['/', '/index.html', '/?view=test']) { const got = await request(port, path, authenticated()) expect(got.status).toBe(200) expect(got.type).toBe('text/html; charset=utf-8') diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index e2f3d50b08..9f9e1c3aa9 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md -README.md: c03fa8ab740f9ffd315ee84b029e132bc40882b4 -README.zh.md: 80630f363aa4a065f0a2bbedc033fbca03fc641f +README.md: bfa56e9b10772cd98bab934b75f32c7d651a57e3 +README.zh.md: 395b0106a920a4e31b98588d0e5a06ac19a9b745 diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index c03fa8ab74..bfa56e9b10 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -58,7 +58,7 @@ A request selects the route with `provider: deepseek-official`; the model id pas | `defaultContextWindow` | `1,000,000` | Capacity fallback for models without an exact value | | `models` | V41 Flash + V4 Flash + V4 Pro + V4 Flash Vision Exp | Advisory catalog shown by discovery consumers | | `streamIdleTimeoutMs` | `300,000` | Maximum provider idle time per outstanding stream read | -| `maxRequestFilesBytes` | `128 MiB` | High watermark for retained request-image bytes before oldest-first offload | +| `maxRequestFilesBytes` | `128 MiB` | File-mode request-image byte budget; a request whose retained images exceed it fails with `IMAGE_OFFLOAD_REQUIRED` | | `maxInlineRequestImageBytes` | `20 MiB` | Independent base64 fallback high watermark | | `maxImagesPerRequest` | `600` | High watermark for retained request-image count | | `imageOffloadByteQuantum` | `64 MiB` | Files-mode oldest-prefix removal quantum | @@ -175,7 +175,7 @@ The selected DeepSeek model receives the harness system prompt, message history, #### Token effect -Provider tokenization governs exact text and image-token input. The adapter declares per-route `imageRequestPricing`: it reproduces oldest-first image offload from durable byte lengths and prices each retained image at its projected dimensions with the published vision accounting (14px patch grid, 3:1 downsampling, 544×544 scale-up floor, 1024-token cap). This lets the token meter price image pressure before a request; reported usage remains authoritative. Reasoning passback carries every reasoned turn's chain of thought into later requests, while dropping over-budget images avoids paying those tokens again. Cache-read usage is reported when available. Messages totals include uncached input, output, cache-read, and cache-write tokens. Chat Completions uses `prompt_tokens + completion_tokens` and omits `totalTokens` if a supplied `total_tokens` disagrees. +Provider tokenization governs exact text and image-token input. The adapter declares per-route `imageRequestPricing`: it prices each occurrence selected by a logged image-offload decision as its placeholder text and each retained image at its projected dimensions with the published vision accounting (14px patch grid, 3:1 downsampling, 544×544 scale-up floor, 1024-token cap). This lets the token meter price image pressure before a request; reported usage remains authoritative. Reasoning passback carries every reasoned turn's chain of thought into later requests, while offloaded images stop costing visual tokens. A request whose retained occurrences exceed the file-mode or inline-fallback budget (`maxRequestFilesBytes`, `maxImagesPerRequest`, both quanta) at their exact request-version bytes fails with `IMAGE_OFFLOAD_REQUIRED` naming the additional oldest occurrences to offload, and `dsh-compaction-image-offload` records the selected occurrences in an `image/offload` event and retries. Cache-read usage is reported when available. Messages totals include uncached input, output, cache-read, and cache-write tokens. Chat Completions uses `prompt_tokens + completion_tokens` and omits `totalTokens` if a supplied `total_tokens` disagrees. #### KV Cache effect diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index 80630f363a..395b0106a9 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -58,7 +58,7 @@ kind: "package-reference" | `defaultContextWindow` | `1,000,000` | 无精确值模型的容量回退 | | `models` | V41 Flash + V4 Flash + V4 Pro + V4 Flash Vision Exp | 供发现消费方查看的建议性目录 | | `streamIdleTimeoutMs` | `300,000` | 单次流读取未完成的最大提供方空闲时间 | -| `maxRequestFilesBytes` | `128 MiB` | 按最旧优先卸载前保留的请求图片字节高水位 | +| `maxRequestFilesBytes` | `128 MiB` | file 模式请求图片字节预算,保留图片超过时请求以 `IMAGE_OFFLOAD_REQUIRED` 失败 | | `maxInlineRequestImageBytes` | `20 MiB` | 独立的 base64 回退高水位 | | `maxImagesPerRequest` | `600` | 保留请求图片数量的高水位 | | `imageOffloadByteQuantum` | `64 MiB` | Files 模式最旧前缀移除量子 | @@ -175,7 +175,7 @@ Files 模式通过 `maxRequestFilesBytes` 与 `maxImagesPerRequest` 限制保留 #### Token 影响 -提供方分词决定精确的文本与图片 token 输入。适配器声明按路由的 `imageRequestPricing`:它根据持久记录中的字节长度复现最旧优先的图片 offload,并按投影后的尺寸使用公开的视觉计量规则(14 px patch 网格、3:1 降采样、544×544 放大下限、单图 1024 token 上限)为每张保留图片计价。这使 token 计量服务可以在请求发出前为图片压力定价;上报的 usage 仍是权威值。推理回传会把每个推理轮次的思维链带进后续请求,而丢弃超预算图片会避免再次为它们付费。可用时报告缓存读取用量。Messages 的 token 总数包含未缓存输入、输出、缓存读取与缓存写入 token。Chat Completions 使用 `prompt_tokens + completion_tokens`,提供方给出的 `total_tokens` 不一致时省略 `totalTokens`。 +提供方分词决定精确的文本与图片 token 输入。适配器声明按路由的 `imageRequestPricing`:把日志中的图片省略决策选中的每个出现位置按其占位文本计价,并按投影后的尺寸使用公开的视觉计量规则(14 px patch 网格、3:1 降采样、544×544 放大下限、单图 1024 token 上限)为每张保留图片计价。这使 token 计量服务可以在请求发出前为图片压力定价;上报的 usage 仍是权威值。推理回传会把每个推理轮次的思维链带进后续请求,而已省略的图片不再消耗视觉 token。保留的出现位置按精确请求版本字节超过 file 模式或内联回退预算(`maxRequestFilesBytes`、`maxImagesPerRequest` 与两个量子)的请求,以 `IMAGE_OFFLOAD_REQUIRED` 失败并说明还需省略多少最老的出现位置,由 `dsh-compaction-image-offload` 用 `image/offload` 事件记录所选位置并重试。可用时报告缓存读取用量。Messages 的 token 总数包含未缓存输入、输出、缓存读取与缓存写入 token。Chat Completions 使用 `prompt_tokens + completion_tokens`,提供方给出的 `total_tokens` 不一致时省略 `totalTokens`。 #### KV Cache 影响 diff --git a/packages/llm/llm-deepseek/src/common/request-pricing.ts b/packages/llm/llm-deepseek/src/common/request-pricing.ts index 349a204d4a..df7416b369 100644 --- a/packages/llm/llm-deepseek/src/common/request-pricing.ts +++ b/packages/llm/llm-deepseek/src/common/request-pricing.ts @@ -1,16 +1,16 @@ /** - * Provider-side request-image pricing for DeepSeek routes: reproduces the - * adapter's deterministic request projection (per-model request target, - * oldest-first offload under the raw-byte and count budgets) and prices every - * retained image with the published vision-token accounting. Consumed - * synchronously by the token meter through `LlmAdapter.imageRequestPricing`; - * provider usage remains the authoritative anchor for completed requests. + * Provider-side request-image pricing for DeepSeek routes: prices every + * retained surface occurrence at its per-model request target with + * the published vision-token accounting, and every occurrence the surface + * marks offloaded as its placeholder text. Consumed synchronously by the + * token meter through `LlmAdapter.imageRequestPricing`; provider usage + * remains the authoritative anchor for completed requests. * * @module dsh-llm-deepseek/request-pricing */ -import { offloadedImageText, offloadedImagePrefixCount, requestImageHandleText, textOnlyImageText } from '@deepseek-ai/dsh-llm' -import type { ImageAttachmentAccessResolver, LlmImageRequestPrice, LlmImageRequestPricing } from '@deepseek-ai/dsh-llm' +import { offloadedImageText, requestImageHandleText, textOnlyImageText } from '@deepseek-ai/dsh-llm' +import type { ImageAttachmentAccessResolver, ImageBlock, LlmImageRequestPrice, LlmImageRequestPricing } from '@deepseek-ai/dsh-llm' import { longEdgeDimensions, requestImageDimensions } from '@deepseek-ai/dsh-attachment' import type { ImageAttachmentRef, ImageRequestTarget } from '@deepseek-ai/dsh-attachment' import { deepSeekImageTokens, deepSeekRequestImageDimensions } from './image-tokens.ts' @@ -69,21 +69,19 @@ export function resolveRequestImageTarget( * reproducing the `projectImagesForTextModel` substitution `LlmRuntime` * applies before dispatching to a route without the `image` modality. */ -function textOnlyPrice(ref: ImageAttachmentRef): LlmImageRequestPrice { - return { visualTokens: 0, text: textOnlyImageText(ref) } +function textOnlyPrice(block: ImageBlock): LlmImageRequestPrice { + return { visualTokens: 0, text: textOnlyImageText(block.attachment) } } /** * Build the request-image pricing for one DeepSeek route from a validated * connection snapshot. Uncatalogued and text-only models price every * occurrence as its deterministic text substitution; image-capable models - * reproduce the adapter's first-stage oldest-first offload from durable byte - * lengths and price retained images by their projected request dimensions, - * with each occurrence's handle or placeholder text built through the same - * access resolution the serializer uses. The base64 fallback's tighter inline - * budget is not reproduced, so a fallback request can only cost less than - * this estimate; access paths resolve at pricing time, so a path that changes - * before the request only shifts the text price by its own length. + * price an offloaded occurrence as its placeholder text and a retained one by + * its projected request dimensions, with each occurrence's handle or + * placeholder text built through the same access resolution the serializer + * uses. Access paths resolve at pricing time, so a path that changes before + * the request only shifts the text price by its own length. * @param connection - validated connection facts of the pricing resolution. * @param model - exact model id named by the request header. * @param resolveAccess - current execution-world access resolution shared with request serialization. @@ -98,28 +96,16 @@ export function deepSeekImageRequestPricing( if (catalogModel?.inputModalities?.includes('image') !== true) { return { priceImages: images => images.map(textOnlyPrice) } } - const maxBytes = resolveRequestImageMaxBytes(catalogModel) return { - priceImages: (images) => { - const offloaded = offloadedImagePrefixCount( - images.map(ref => Math.min(ref.bytes, maxBytes)), - { - maxBytes: connection.maxRequestFilesBytes, - maxImages: connection.maxImagesPerRequest, - byteQuantum: connection.imageOffloadByteQuantum, - countQuantum: connection.imageOffloadCountQuantum, - }, - ) - return images.map((ref, index) => { - if (index < offloaded) { - return { visualTokens: 0, text: offloadedImageText(ref, resolveAccess?.(ref)) } - } - const target = resolveRequestImageTarget(catalogModel, ref) - return { - visualTokens: deepSeekImageTokens(target.width, target.height), - text: requestImageHandleText(ref, target, resolveAccess?.(ref)), - } - }) - }, + priceImages: images => images.map(({ attachment: ref, offloaded }) => { + if (offloaded === true) { + return { visualTokens: 0, text: offloadedImageText(ref, resolveAccess?.(ref)) } + } + const target = resolveRequestImageTarget(catalogModel, ref) + return { + visualTokens: deepSeekImageTokens(target.width, target.height), + text: requestImageHandleText(ref, target, resolveAccess?.(ref)), + } + }), } } diff --git a/packages/llm/llm-deepseek/src/protocols/chat-completions/adapter.ts b/packages/llm/llm-deepseek/src/protocols/chat-completions/adapter.ts index 4297b1f680..3d2e50ea45 100644 --- a/packages/llm/llm-deepseek/src/protocols/chat-completions/adapter.ts +++ b/packages/llm/llm-deepseek/src/protocols/chat-completions/adapter.ts @@ -8,7 +8,7 @@ * @module dsh-llm-deepseek/adapter */ -import { attributionHeaders, contentHasImage, CONTEXT_WINDOW_EXCEEDED_CODE, isContextWindowExceededError, isQuotaExceededError, LlmAdapter, LlmError, offloadedImageText, offloadRequestImagesWithPolicy, ProviderRequestId, QUOTA_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm' +import { attributionHeaders, contentHasImage, CONTEXT_WINDOW_EXCEEDED_CODE, isContextWindowExceededError, isQuotaExceededError, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, @@ -32,7 +32,7 @@ import type { DeepSeekLlmApiJson, } from '@deepseek-ai/dsh-deepseek-llm-api-extensions' import { serializeRequest, serializeRequestWithImages } from './serialize.ts' -import { deepSeekImageRequestPricing, resolveRequestImageMaxBytes, resolveRequestImageTarget } from '../../common/request-pricing.ts' +import { deepSeekImageRequestPricing, resolveRequestImageTarget } from '../../common/request-pricing.ts' import { catalogModelInfo, modelInfo } from '../../common/model-info.ts' import type { DeepSeekAdapterOptions, DeepSeekCatalogModel, DeepSeekConnectionOptions } from '../../common/types.ts' import type { DeepSeekFileStore } from '../../common/file-store.ts' @@ -49,7 +49,7 @@ function collectImageRefs( refs: Map, ): void { for (const block of content) { - if (block.type === 'image') refs.set(block.attachment.attachmentId, block.attachment) + if (block.type === 'image' && block.offloaded !== true) refs.set(block.attachment.attachmentId, block.attachment) else if (block.type === 'tool-result') collectImageRefs(block.content, refs) } } @@ -270,21 +270,11 @@ export class ChatCompletionsAdapter extends LlmAdapter { const fileConnection = { baseURL: connection.baseURL, apiKey, protocol: connection.protocol } const model = connection.models.find(entry => entry.id === options.model) - const maxBytes = model === undefined ? undefined : resolveRequestImageMaxBytes(model) const resolveImageAccess = attachments === undefined ? undefined : (ref: ImageAttachmentRef): ImageAttachmentAccess | undefined => this.config.resolveImageAccess?.(attachments, ref) const imageAccessOptions = resolveImageAccess === undefined ? {} : { resolveImageAccess } - const requestMessages = maxBytes === undefined ? options.messages : offloadRequestImagesWithPolicy(options.messages, { - representation: 'raw', - maxBytes: connection.maxRequestFilesBytes, - maxImages: connection.maxImagesPerRequest, - byteQuantum: connection.imageOffloadByteQuantum, - countQuantum: connection.imageOffloadCountQuantum, - byteLength: ref => Math.min(ref.bytes, maxBytes), - placeholder: ref => offloadedImageText(ref, resolveImageAccess?.(ref)), - }) - const requestOptions = requestMessages === options.messages ? options : { ...options, messages: [...requestMessages] } + const requestOptions = options const requestImages = attachments === undefined || model === undefined ? new Map() : await prepareRequestImages(requestOptions, attachments, model, signal) diff --git a/packages/llm/llm-deepseek/src/protocols/chat-completions/serialize.ts b/packages/llm/llm-deepseek/src/protocols/chat-completions/serialize.ts index d96075888b..62e79bf94c 100644 --- a/packages/llm/llm-deepseek/src/protocols/chat-completions/serialize.ts +++ b/packages/llm/llm-deepseek/src/protocols/chat-completions/serialize.ts @@ -6,7 +6,7 @@ * @module dsh-llm-deepseek/serialize */ -import { contentHasImage, LlmError, offloadedImageText, offloadRequestImagesWithPolicy, requestImageHandleText } from '@deepseek-ai/dsh-llm' +import { contentHasImage, IMAGE_OFFLOAD_REQUIRED_CODE, LlmError, offloadedImageText, projectOffloadedImages, requestImageHandleText, requiredImageOffload } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, ImageAttachmentAccessResolver, Message } from '@deepseek-ai/dsh-llm' import type { ImageAttachmentRef, RequestImageAttachment } from '@deepseek-ai/dsh-attachment' import type { @@ -271,7 +271,7 @@ export function serializeMessages(messages: Message[]): WireMessage[] { * Serialize image-capable history after resolving durable attachments. * Consecutive tool results keep string `tool` messages and share one following * user message containing their images. - * @param messages - transient request history after request-size offloading. + * @param messages - request history whose offloaded occurrences are already placeholder text. * @param images - prepared request versions, one provider representation, and its budget. * @returns ordered DeepSeek wire messages. */ @@ -384,10 +384,40 @@ export function serializeRequest( return requestWithMessages(options, messages, defaults) } +/** + * Reject a request whose retained occurrences, at their exact request-version + * byte lengths under this representation, still exceed the route budget. The + * failure names how many more oldest retained occurrences need durable + * omission before the request can be retried. + */ +function assertRetainedImagesFit(messages: readonly Message[], images: ImageSerializationOptions): void { + const representation = images.representation.kind === 'file' ? 'raw' : 'base64' + const offloadImages = requiredImageOffload(messages, { + representation, + maxBytes: images.maxRequestImageBytes, + ...images.maxImagesPerRequest === undefined ? {} : { maxImages: images.maxImagesPerRequest }, + ...images.byteQuantum === undefined ? {} : { byteQuantum: images.byteQuantum }, + ...images.countQuantum === undefined ? {} : { countQuantum: images.countQuantum }, + }, (block) => { + const version = images.requestImages.get(block.attachment.attachmentId) + if (version === undefined) { + throw new LlmError(`DeepSeek request image ${block.attachment.attachmentId} was not prepared.`, 'INVALID_REQUEST') + } + return version.bytes + }) + if (offloadImages > 0) { + throw new LlmError( + `DeepSeek ${representation} request images exceed the route budget; ${offloadImages} more oldest occurrence(s) must be offloaded.`, + IMAGE_OFFLOAD_REQUIRED_CODE, + { offloadImages }, + ) + } +} + /** * Build one image-capable request while keeping durable bytes out of session - * messages. Oversized oldest images become per-image text after their - * exact request-version byte lengths are known and before provider serialization. + * messages. Offloaded occurrences become per-image text; retained occurrences + * must fit the route budget at their exact request-version byte lengths. * @param options - harness request containing image-capable user content. * @param images - request versions, optional current access resolver, and request bounds. * @param defaults - adapter-level thinking defaults. @@ -399,21 +429,11 @@ export async function serializeRequestWithImages( defaults: RequestDefaults = {}, ): Promise { assertSupportedImageRoles(options.messages) - const requestMessages = offloadRequestImagesWithPolicy(options.messages, { - representation: images.representation.kind === 'file' ? 'raw' : 'base64', - byteLength: (ref) => { - const version = images.requestImages.get(ref.attachmentId) - if (version === undefined) { - throw new LlmError(`DeepSeek request image ${ref.attachmentId} was not prepared.`, 'INVALID_REQUEST') - } - return version.bytes - }, - maxBytes: images.maxRequestImageBytes, - ...images.maxImagesPerRequest === undefined ? {} : { maxImages: images.maxImagesPerRequest }, - ...images.byteQuantum === undefined ? {} : { byteQuantum: images.byteQuantum }, - ...images.countQuantum === undefined ? {} : { countQuantum: images.countQuantum }, - placeholder: ref => offloadedImageText(ref, images.resolveImageAccess?.(ref)), - }) + assertRetainedImagesFit(options.messages, images) + const requestMessages = projectOffloadedImages( + options.messages, + ref => offloadedImageText(ref, images.resolveImageAccess?.(ref)), + ) const messages: WireMessage[] = [] if (options.system !== undefined) { messages.push({ role: 'system', content: options.system }) diff --git a/packages/llm/llm-deepseek/src/protocols/messages/adapter.ts b/packages/llm/llm-deepseek/src/protocols/messages/adapter.ts index 892709fcd6..fd3745f04d 100644 --- a/packages/llm/llm-deepseek/src/protocols/messages/adapter.ts +++ b/packages/llm/llm-deepseek/src/protocols/messages/adapter.ts @@ -109,7 +109,7 @@ export class DeepSeekMessagesAdapter extends LlmAdapter { continue } } - const history = inline ? inlineImages(messages, versions, connection, this.dependencies.imageAccess) : messages + const history = inline ? inlineImages(messages, versions, connection) : messages const body = serialize(options, connection, history, versions, this.dependencies.imageAccess, (reason) => { this.dependencies.onReplayDegrade?.({ provider: options.provider, model: options.model, reason }) }, fileIds) diff --git a/packages/llm/llm-deepseek/src/protocols/messages/images.ts b/packages/llm/llm-deepseek/src/protocols/messages/images.ts index 6e93f06f23..64b0933b12 100644 --- a/packages/llm/llm-deepseek/src/protocols/messages/images.ts +++ b/packages/llm/llm-deepseek/src/protocols/messages/images.ts @@ -1,10 +1,10 @@ /** Deterministic Messages image preparation for Files references and bounded inline fallback. */ import type { AttachmentStore, ImageAttachmentRef, RequestImageAttachment } from '@deepseek-ai/dsh-attachment' -import { contentHasImage, LlmError, offloadedImageText, offloadRequestImagesWithPolicy } from '@deepseek-ai/dsh-llm' +import { contentHasImage, IMAGE_OFFLOAD_REQUIRED_CODE, LlmError, offloadedImageText, projectOffloadedImages, requiredImageOffload } from '@deepseek-ai/dsh-llm' import type { ContentBlock, ImageAttachmentAccessResolver, Message } from '@deepseek-ai/dsh-llm' import type { DeepSeekConnectionOptions as Connection } from '../../common/types.ts' -import { resolveRequestImageMaxBytes, resolveRequestImageTarget } from '../../common/request-pricing.ts' +import { resolveRequestImageTarget } from '../../common/request-pricing.ts' import type { DeepSeekFileId } from '../../common/file-id.ts' import type { RequestFiles } from '../../common/request-files.ts' @@ -28,7 +28,7 @@ function* imageRefs(blocks: readonly ContentBlock[]): Generator }> { const versions = new Map() + const messages = projectOffloadedImages(history, ref => offloadedImageText(ref, access(ref))) if (!messages.some(message => contentHasImage(message.content))) return { messages, versions } const model = connection.models.find(entry => entry.id === modelId) if (model?.inputModalities?.includes('image') !== true || attachments === undefined) { @@ -49,39 +50,45 @@ export async function prepareImages( if (messages.some(message => message.role !== 'user' && contentHasImage(message.content))) { throw new LlmError('DeepSeek Messages supports images only in user messages and tool results', 'UNSUPPORTED_CONTENT') } - const offload = (input: readonly Message[], byteLength: (ref: ImageAttachmentRef) => number) => offloadRequestImagesWithPolicy(input, { - ...bounds(connection, 'raw'), - placeholder: ref => offloadedImageText(ref, access(ref)), - byteLength, - }) - const maxBytes = resolveRequestImageMaxBytes(model) - const retained = offload(messages, ref => Math.min(ref.bytes, maxBytes)) - for (const message of retained) { + for (const message of messages) { for (const ref of imageRefs(message.content)) { if (!versions.has(ref.attachmentId)) { versions.set(ref.attachmentId, await attachments.readImageRequest(ref, resolveRequestImageTarget(model, ref), signal)) } } } - return { messages: offload(retained, ref => (versions.get(ref.attachmentId) as RequestImageAttachment).bytes), versions } + assertImagesFit(messages, versions, connection, 'raw') + return { messages, versions } } -/** Apply the inline budget to normalized images after Files resolution fails. - * @param messages - history already reduced to the Files budget. +/** Require logged offload before retrying images that exceed the inline budget. + * @param messages - history already within the Files budget. * @param versions - normalized versions prepared for retained references. * @param connection - resolved inline bounds. - * @param access - current execution-world path resolver. - * @returns transient history within both byte and image-count limits. + * @returns unchanged history within both byte and image-count limits. */ export function inlineImages( messages: readonly Message[], versions: ReadonlyMap, - connection: Connection, access: ImageAttachmentAccessResolver, + connection: Connection, ): readonly Message[] { - return offloadRequestImagesWithPolicy(messages, { - ...bounds(connection, 'base64'), - byteLength: ref => (versions.get(ref.attachmentId) as RequestImageAttachment).bytes, - placeholder: ref => offloadedImageText(ref, access(ref)), - }) + assertImagesFit(messages, versions, connection, 'base64') + return messages +} + +/** Count additional oldest occurrences requiring durable offload at their exact represented bytes. */ +function assertImagesFit( + messages: readonly Message[], versions: ReadonlyMap, + connection: Connection, representation: 'raw' | 'base64', +): void { + const offloadImages = requiredImageOffload(messages, bounds(connection, representation), + block => (versions.get(block.attachment.attachmentId) as RequestImageAttachment).bytes) + if (offloadImages > 0) { + throw new LlmError( + `DeepSeek Messages ${representation} request images exceed the route budget; ${offloadImages} more oldest occurrence(s) must be offloaded.`, + IMAGE_OFFLOAD_REQUIRED_CODE, + { offloadImages }, + ) + } } /** Resolve retained images to Files ids, recording every occurrence for failure diagnostics. diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index 852c5fd4cb..e43c19c765 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -170,10 +170,10 @@ describe('request image target', () => { const adapter = adapterOf({ models: [{ id: 'vision', inputModalities: ['text', 'image'] }], }) - const priced = adapter.imageRequestPricing('deepseek-official', 'vision')?.priceImages([imageRef]) + const priced = adapter.imageRequestPricing('deepseek-official', 'vision')?.priceImages([{ type: 'image', attachment: imageRef }]) expect(priced).toHaveLength(1) expect(priced?.[0]!.visualTokens).toBeGreaterThan(0) - const textOnly = adapter.imageRequestPricing('deepseek-official', 'unlisted')?.priceImages([imageRef]) + const textOnly = adapter.imageRequestPricing('deepseek-official', 'unlisted')?.priceImages([{ type: 'image', attachment: imageRef }]) expect(textOnly?.[0]!.visualTokens).toBe(0) }) @@ -189,7 +189,7 @@ describe('request image target', () => { : undefined), prepareExtensions: noExtensions, }) - const priced = adapter.imageRequestPricing('deepseek-official', 'vision')?.priceImages([imageRef]) + const priced = adapter.imageRequestPricing('deepseek-official', 'vision')?.priceImages([{ type: 'image', attachment: imageRef }]) expect(priced?.[0]?.text).toContain('/world/img.png') }) }) @@ -448,7 +448,7 @@ describe('DeepSeekAdapter against a mock server', () => { expect(files.ensureUploaded).toHaveBeenCalledTimes(1) }) - it('reduces base64 fallback history from the configured high watermark to its half-size quantum', async () => { + it('fails a base64 fallback whose retained images exceed the inline bound with the count to offload', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const attachments = attachmentStoreOf(ref => Promise.resolve(requestImage(ref))).store const files = fileStoreOf(() => Promise.reject(new LlmError('Files unavailable', 'SERVER'))) @@ -459,20 +459,16 @@ describe('DeepSeekAdapter against a mock server', () => { models: [{ id: 'deepseek-v4-flash-vision-exp', inputModalities: ['text', 'image'] }], }, attachments, files.store) - await drain(adapter.stream({ + await expect(drain(adapter.stream({ provider: 'deepseek-official', model: 'deepseek-v4-flash-vision-exp', messages: [createUserMessage({ content: Array.from({ length: 21 }, () => ({ type: 'image' as const, attachment: imageRef })), source: { kind: 'plugin', plugin: 'test' }, })], - })) - - const body = JSON.stringify(server.requests[0]) - expect(body.match(/image omitted to fit request image limits/g)).toHaveLength(11) - expect(body.match(/"type":"image_url"/g)).toHaveLength(10) + }))).rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', message: expect.stringContaining('11 more') as string }) + expect(server.requests).toHaveLength(0) }) - it('discards partially resolved file ids and falls back with every retained image inline', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const secondRef = { ...imageRef, attachmentId: AttachmentId(`sha256:${'c'.repeat(64)}`) } @@ -599,7 +595,7 @@ describe('DeepSeekAdapter against a mock server', () => { expect(JSON.stringify(server.requests[0])).not.toContain('image_url') }) - it('does not prepare an old image removed by request offload', async () => { + it('does not prepare a surface-offloaded image and sends its placeholder instead', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const old = { ...imageRef, attachmentId: AttachmentId(`sha256:${'c'.repeat(64)}`), bytes: 3 } const recent = { ...imageRef, attachmentId: AttachmentId(`sha256:${'d'.repeat(64)}`), bytes: 3 } @@ -610,8 +606,6 @@ describe('DeepSeekAdapter against a mock server', () => { const adapter = adapterOf({ baseURL: server.url, models: [{ id: 'deepseek-v4-flash-vision-exp', inputModalities: ['text', 'image'] }], - maxRequestFilesBytes: 4, - imageOffloadByteQuantum: 2, }, attachmentMocks.store) await drain(adapter.stream({ @@ -619,7 +613,7 @@ describe('DeepSeekAdapter against a mock server', () => { model: 'deepseek-v4-flash-vision-exp', messages: [createUserMessage({ content: [ - { type: 'image', attachment: old }, + { type: 'image', attachment: old, offloaded: true }, { type: 'image', attachment: recent }, ], source: { kind: 'plugin', plugin: 'test' }, diff --git a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts index 0e2e4febdd..167a675388 100644 --- a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts +++ b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts @@ -219,16 +219,31 @@ describe('request-level dynamic configuration', () => { await assemble(ctx, { model: 'deepseek-flash', messages }) await ctx.settings.update(NS, { maxRequestFilesBytes: 4, imageOffloadByteQuantum: 2 }) - await assemble(ctx, { model: 'deepseek-flash', messages }) + // A request whose retained exact bytes exceed the tightened budget names the occurrences to offload. + const rejected = await assemble(ctx, { model: 'deepseek-flash', messages }) + expect(rejected.finish).toMatchObject({ + kind: 'error', + failure: { code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 }, + }) + await assemble(ctx, { + model: 'deepseek-flash', + messages: [createUserMessage({ + content: [ + { type: 'image', attachment: IMAGE_REF, offloaded: true }, + { type: 'image', attachment: IMAGE_REF }, + ], + source: { kind: 'plugin', plugin: 'test' }, + })], + }) const first = (server.requests[0] as { messages: Array<{ content: unknown }> }).messages[0]?.content const second = (server.requests[1] as { messages: Array<{ content: unknown }> }).messages[0]?.content + expect(server.requests).toHaveLength(2) expect(JSON.stringify(first).match(/"type":"file"/g)).toHaveLength(2) expect(JSON.stringify(second)).toContain('[image omitted to fit request image limits') expect(JSON.stringify(second)).toContain(MODEL_IMAGE_PATH) expect(JSON.stringify(second).match(/"type":"file"/g)).toHaveLength(1) }) - it('re-registers the route in place when the captured retry policy changes, without an empty-registry window', async () => { const dir = await home() const { ctx } = await boot(dir, { baseURL: 'http://127.0.0.1:1' }) diff --git a/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts b/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts index c88c9b2d18..5ecae3f9e8 100644 --- a/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts @@ -173,10 +173,10 @@ describe('Cordis provider composition', () => { const model = 'deepseek-v4-flash-vision-exp' const price = () => ctx.llm.imageRequestPricing('deepseek-official', model)! const dummy = { attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`), width: 1, height: 1, bytes: 3, mediaType: 'image/png' as const } - expect(price().priceImages([dummy])[0]?.text).toBeDefined() + expect(price().priceImages([{ type: 'image', attachment: dummy }])[0]?.text).toBeDefined() await ctx.plugin(LocalAttachments, { dshHome: home }) const attachment = await ctx.attachments.saveImage({ data: await readFile(new URL('fixtures/red.png', import.meta.url)), mediaType: 'image/png' }) - expect(price().priceImages([attachment])[0]?.text).not.toContain('/mounted/image.png') + expect(price().priceImages([{ type: 'image', attachment }])[0]?.text).not.toContain('/mounted/image.png') class MappedFiles extends Service { constructor(context: Context) { super(context, 'fs') } processPathFromHostPath(_path: string) { return '/mounted/image.png' } @@ -185,7 +185,7 @@ describe('Cordis provider composition', () => { const message = user() await chunks(ctx.llm.stream(options({ model, messages: [{ ...message, content: [...message.content, { type: 'image', attachment }] }] }))) expect(JSON.stringify(http.requests[0]?.body)).toContain('/mounted/image.png') - expect(price().priceImages([attachment])[0]?.text).toContain('/mounted/image.png') + expect(price().priceImages([{ type: 'image', attachment }])[0]?.text).toContain('/mounted/image.png') }) async function boot(...args: Parameters) { diff --git a/packages/llm/llm-deepseek/tests/messages/files.spec.ts b/packages/llm/llm-deepseek/tests/messages/files.spec.ts index 34aba64d37..1b5ee31941 100644 --- a/packages/llm/llm-deepseek/tests/messages/files.spec.ts +++ b/packages/llm/llm-deepseek/tests/messages/files.spec.ts @@ -8,6 +8,7 @@ import type { DeepSeekFileStore } from '../../src/common/file-store.ts' import { resolveAdapterOptions } from '../../src/config.ts' import type { Config } from '../../src/config.ts' import { DeepSeekMessagesAdapter } from '../../src/protocols/messages/adapter.ts' +import { prepareImages } from '../../src/protocols/messages/images.ts' import { providerErrorDetail } from '../../src/protocols/messages/transport.ts' import { chunks, options, prepareExtensions, sse, textEvents, user } from './helpers.ts' @@ -122,19 +123,62 @@ describe('Messages Files requests', () => { await chunks(h.adapter.stream(original)) expect(body(fetchImpl.mock.calls[0]?.[1]).match(/"file_id"/gu)).toHaveLength(2) h.ensureUploaded.mockRejectedValueOnce(new Error('offline')) - await chunks(h.adapter.stream(original)) + await expect(chunks(h.adapter.stream(original))).rejects.toMatchObject({ failure: { code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 } }) + expect(fetchImpl).toHaveBeenCalledTimes(1) + h.ensureUploaded.mockRejectedValueOnce(new Error('offline')) + await chunks(h.adapter.stream(options({ model, messages: [{ ...user(), content: [ + { type: 'image', attachment: ref, offloaded: true }, { type: 'image', attachment: second }, + ] }] }))) const fallback = body(fetchImpl.mock.calls[1]?.[1]) expect(fallback.match(/"type":"base64"/gu)).toHaveLength(1) expect(fallback).toContain('image omitted to fit request image limits') expect(JSON.stringify(original.messages)).toBe(saved) }) - it('does not read an old image removed by the Files budget and prices the same prefix', async () => { + it('reads and prices only the retained occurrences selected by the logged offload', async () => { vi.stubGlobal('fetch', async () => success()) const h = harness({ maxRequestFilesBytes: 3, imageOffloadByteQuantum: 1, maxImagesPerRequest: 2, imageOffloadCountQuantum: 1 }) - await chunks(h.adapter.stream(request([ref, second]))) + const images = [{ type: 'image' as const, attachment: ref, offloaded: true as const }, { type: 'image' as const, attachment: second }] + await chunks(h.adapter.stream(options({ model, messages: [{ ...user(), content: images }] }))) expect(h.readImageRequest).toHaveBeenCalledExactlyOnceWith(second, expect.anything(), expect.any(AbortSignal)) - expect(h.adapter.imageRequestPricing('deepseek-official', model).priceImages([ref, second]).map(image => image.visualTokens)).toEqual([0, expect.any(Number)]) + expect(h.adapter.imageRequestPricing('deepseek-official', model).priceImages(images).map(image => image.visualTokens)).toEqual([0, expect.any(Number)]) + }) + + it.each([ + { maxRequestFilesBytes: 5, imageOffloadByteQuantum: 1, maxImagesPerRequest: 2, imageOffloadCountQuantum: 1 }, + { maxRequestFilesBytes: 100, imageOffloadByteQuantum: 1, maxImagesPerRequest: 1, imageOffloadCountQuantum: 1 }, + ])('requires durable offload of repeated occurrences before upload for %j', async (config) => { + const fetchImpl = vi.fn(async () => success()) + vi.stubGlobal('fetch', fetchImpl) + const h = harness(config) + const original = request([ref, ref]) + const saved = JSON.stringify(original.messages) + await expect(chunks(h.adapter.stream(original))).rejects.toMatchObject({ failure: { code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 } }) + expect(h.readImageRequest).toHaveBeenCalledTimes(1) + expect(h.ensureUploaded).not.toHaveBeenCalled() + expect(fetchImpl).not.toHaveBeenCalled() + expect(JSON.stringify(original.messages)).toBe(saved) + }) + + it('uses exact prepared bytes when the durable reference fits the Files budget', async () => { + const h = harness({ maxRequestFilesBytes: 3, imageOffloadByteQuantum: 1, imageOffloadCountQuantum: 1 }) + h.readImageRequest.mockResolvedValue({ ...version(ref), bytes: 4, data: Uint8Array.of(1, 2, 3, 4) }) + await expect(chunks(h.adapter.stream(request()))).rejects.toMatchObject({ failure: { code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 } }) + expect(h.ensureUploaded).not.toHaveBeenCalled() + }) + + it('projects nested logged offloads without an attachment service or a vision model', async () => { + const messages = [createToolResultMessage({ callId: ToolCallId('offloaded'), isError: false, content: [ + { type: 'image', attachment: ref, offloaded: true }, + ] })] + const prepared = await prepareImages(messages, resolveAdapterOptions({}), 'text-model', undefined, + () => ({ readonlyPath: '/workspace/image.png' }), new AbortController().signal) + expect(prepared.versions.size).toBe(0) + expect(prepared.messages[0]?.content).toMatchObject([{ type: 'tool-result', content: [ + { type: 'text', text: expect.stringContaining('image omitted to fit request image limits') as string }, + ] }]) + expect(JSON.stringify(prepared.messages)).toContain('/workspace/image.png') + expect(messages[0]?.content).toMatchObject([{ type: 'tool-result', content: [{ type: 'image', offloaded: true }] }]) }) it('falls back when the Files deadline expires but never converts caller cancellation into another request', async () => { diff --git a/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts b/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts index 347030f16d..ada2e7207b 100644 --- a/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts +++ b/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts @@ -1,7 +1,7 @@ /** Request conversion and durable replay validation. */ import { describe, expect, it, vi } from 'vitest' import { createAssistantMessage, createMessage, createSystemMessage, createToolResultMessage, ReasoningEffortId, ToolCallId } from '@deepseek-ai/dsh-llm' -import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm' +import type { ContentBlock, GenerateOptions, ImageBlock, Message } from '@deepseek-ai/dsh-llm' import { AttachmentId, ImageVariantId } from '@deepseek-ai/dsh-attachment' import type { AttachmentStore, ImageAttachmentRef, RequestImageAttachment } from '@deepseek-ai/dsh-attachment' import { resolveAdapterOptions } from '../../src/config.ts' @@ -248,7 +248,7 @@ describe('validated configuration', () => { describe('Messages images', () => { const ref: ImageAttachmentRef = { attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`), mediaType: 'image/png', width: 1, height: 1, bytes: 3 } - const image: ContentBlock = { type: 'image', attachment: ref } + const image: ImageBlock = { type: 'image', attachment: ref } const version: RequestImageAttachment = { attachment: ref, variantId: ImageVariantId(`sha256:${'b'.repeat(64)}`), mediaType: 'image/png', bytes: 3, data: Uint8Array.of(1, 2, 3), width: 1, height: 1, depth: 'uchar', space: 'srgb', hasAlpha: false } const access = () => ({ readonlyPath: '/workspace/image.png' }) const model = 'deepseek-v4-flash-vision-exp' @@ -264,23 +264,30 @@ describe('Messages images', () => { { type: 'text', text: expect.stringContaining('/workspace/image.png') as string }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'AQID' } }, { type: 'text' }, { type: 'image' }, ] }) - expect(imagePricing(connection, model, access).priceImages([ref])[0]?.visualTokens).toBeGreaterThan(0) - expect(imagePricing(connection, MODEL, access).priceImages([ref])[0]?.visualTokens).toBe(0) + expect(imagePricing(connection, model, access).priceImages([image])[0]?.visualTokens).toBeGreaterThan(0) + expect(imagePricing(connection, MODEL, access).priceImages([image])[0]?.visualTokens).toBe(0) }) - it('offloads an oldest prefix using exact encoded bytes and preserves durable references', async () => { + it('requires logged offload at exact encoded bytes and preserves durable references', async () => { const config = resolveAdapterOptions({ maxInlineRequestImageBytes: 4, inlineImageOffloadByteQuantum: 1, maxImagesPerRequest: 2, imageOffloadCountQuantum: 1, }) const history = [result('a', [image, image])] const prepared = await prepareImages(history, config, model, attachments, access, signal) expect(prepared.messages[0]?.content[0]).toMatchObject({ content: [image, image] }) - expect(inlineImages(prepared.messages, prepared.versions, config, access)[0]?.content[0]).toMatchObject({ content: [{ type: 'text' }, { type: 'image' }] }) + expect(() => inlineImages(prepared.messages, prepared.versions, config)).toThrow(expect.objectContaining({ + failure: expect.objectContaining({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 }) as unknown, + })) + const offloaded: ImageBlock = { ...image, offloaded: true } + const retry = await prepareImages([result('a', [offloaded, image])], config, model, attachments, access, signal) + expect(inlineImages(retry.messages, retry.versions, config)[0]?.content[0]).toMatchObject({ content: [{ type: 'text' }, { type: 'image' }] }) expect(history[0]?.content[0]).toMatchObject({ content: [image, image] }) - expect(imagePricing(config, model, access).priceImages([ref, ref]).map(entry => entry.visualTokens)) + expect(imagePricing(config, model, access).priceImages([image, image]).map(entry => entry.visualTokens)) .toEqual([expect.any(Number), expect.any(Number)]) const large = { readImageRequest: async () => ({ ...version, bytes: 30, data: new Uint8Array(30) }) } as unknown as AttachmentStore const exact = await prepareImages([result('a', [image])], config, model, large, access, signal) - expect(inlineImages(exact.messages, exact.versions, config, access)[0]?.content[0]).toMatchObject({ content: [{ type: 'text' }] }) + expect(() => inlineImages(exact.messages, exact.versions, config)).toThrow(expect.objectContaining({ + failure: expect.objectContaining({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 }) as unknown, + })) }) it('rejects unsupported roles and unavailable image capabilities before HTTP', async () => { const history = [result('a', [image])] diff --git a/packages/llm/llm-deepseek/tests/request-pricing.spec.ts b/packages/llm/llm-deepseek/tests/request-pricing.spec.ts index a5f73da35d..bf2216d6c8 100644 --- a/packages/llm/llm-deepseek/tests/request-pricing.spec.ts +++ b/packages/llm/llm-deepseek/tests/request-pricing.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { offloadedImageText, requestImageHandleText, textOnlyImageText } from '@deepseek-ai/dsh-llm' +import type { ImageBlock } from '@deepseek-ai/dsh-llm' import { AttachmentId } from '@deepseek-ai/dsh-attachment' import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' import { deepSeekImageRequestPricing } from '../src/common/request-pricing.ts' @@ -22,6 +23,10 @@ function ref(name: string, width: number, height: number, bytes = 1024): ImageAt } } +function block(attachment: ImageAttachmentRef, offloaded?: true): ImageBlock { + return { type: 'image', attachment, ...offloaded === undefined ? {} : { offloaded } } +} + function connection(config: Omit = {}): ReturnType { return resolveAdapterOptions(Object.assign({ models: [VISION_MODEL] }, config)) } @@ -29,20 +34,20 @@ function connection(config: Omit = {}): ReturnType { it('prices an uncatalogued model as its text-only substitution', () => { const image = ref('photo', 1920, 1080) - const prices = deepSeekImageRequestPricing(connection(), 'unlisted').priceImages([image]) + const prices = deepSeekImageRequestPricing(connection(), 'unlisted').priceImages([block(image)]) expect(prices).toEqual([{ visualTokens: 0, text: textOnlyImageText(image) }]) }) it('prices a catalogued text-only model as its text-only substitution', () => { const image = ref('photo', 1920, 1080) const options = resolveAdapterOptions({ models: [{ id: 'text-only' }] }) - const prices = deepSeekImageRequestPricing(options, 'text-only').priceImages([image]) + const prices = deepSeekImageRequestPricing(options, 'text-only').priceImages([block(image)]) expect(prices).toEqual([{ visualTokens: 0, text: textOnlyImageText(image) }]) }) it('prices a retained image by its projected request dimensions plus its handle text', () => { const image = ref('photo', 1920, 1080) - const prices = deepSeekImageRequestPricing(connection(), 'vision').priceImages([image]) + const prices = deepSeekImageRequestPricing(connection(), 'vision').priceImages([block(image)]) expect(prices).toEqual([{ visualTokens: 968, text: requestImageHandleText(image, { width: 1708, height: 961 }), @@ -54,7 +59,7 @@ describe('DeepSeek request-image pricing', () => { [1, 8192, 1, 4096, 1024], ])('prices a %sx%s image at its per-side-capped %sx%s request dimensions', (width, height, cappedWidth, cappedHeight, tokens) => { const image = ref('thin', width, height) - const prices = deepSeekImageRequestPricing(connection(), 'vision').priceImages([image]) + const prices = deepSeekImageRequestPricing(connection(), 'vision').priceImages([block(image)]) expect(prices).toEqual([{ visualTokens: tokens, text: requestImageHandleText(image, { width: cappedWidth, height: cappedHeight }), @@ -63,7 +68,7 @@ describe('DeepSeek request-image pricing', () => { it('prices the sent dimensions when aspect-preserving projection changes the token grid', () => { const image = ref('portrait', 1224, 1429) - const prices = deepSeekImageRequestPricing(connection(), 'vision').priceImages([image]) + const prices = deepSeekImageRequestPricing(connection(), 'vision').priceImages([block(image)]) expect(prices).toEqual([{ visualTokens: 992, text: requestImageHandleText(image, { width: 1187, height: 1386 }), @@ -75,7 +80,7 @@ describe('DeepSeek request-image pricing', () => { const options = resolveAdapterOptions({ models: [{ ...VISION_MODEL, imagePixelBudget: 640_000 }], }) - const prices = deepSeekImageRequestPricing(options, 'vision').priceImages([image]) + const prices = deepSeekImageRequestPricing(options, 'vision').priceImages([block(image)]) expect(prices).toEqual([{ visualTokens: 422, text: requestImageHandleText(image, { width: 800, height: 800 }), @@ -87,7 +92,7 @@ describe('DeepSeek request-image pricing', () => { const options = resolveAdapterOptions({ models: [{ ...VISION_MODEL, imagePixelBudget: 'low' as const }], }) - const prices = deepSeekImageRequestPricing(options, 'vision').priceImages([image]) + const prices = deepSeekImageRequestPricing(options, 'vision').priceImages([block(image)]) expect(prices[0]!.visualTokens).toBe(184) }) @@ -95,10 +100,10 @@ describe('DeepSeek request-image pricing', () => { const access = { readonlyPath: '/world/attachments/photo.png' } const images = [ref('first', 800, 800), ref('second', 800, 800)] const prices = deepSeekImageRequestPricing( - connection({ maxImagesPerRequest: 1, imageOffloadCountQuantum: 1 }), + connection(), 'vision', () => access, - ).priceImages(images) + ).priceImages([block(images[0]!, true), block(images[1]!)]) expect(prices[0]).toEqual({ visualTokens: 0, text: offloadedImageText(images[0]!, access) }) expect(prices[1]).toEqual({ visualTokens: 422, @@ -107,12 +112,12 @@ describe('DeepSeek request-image pricing', () => { expect(prices[1]?.text).toContain('/world/attachments/photo.png') }) - it('prices count-offloaded oldest occurrences as their placeholder text', () => { + it('prices surface-offloaded occurrences as placeholder text and every retained one at its visual price', () => { const images = [ref('first', 800, 800), ref('second', 800, 800), ref('third', 800, 800)] const prices = deepSeekImageRequestPricing( - connection({ maxImagesPerRequest: 2, imageOffloadCountQuantum: 1 }), + connection({ maxImagesPerRequest: 1, imageOffloadCountQuantum: 1 }), 'vision', - ).priceImages(images) + ).priceImages([block(images[0]!, true), block(images[1]!), block(images[2]!)]) expect(prices).toEqual([ { visualTokens: 0, text: offloadedImageText(images[0]!) }, { visualTokens: 422, text: requestImageHandleText(images[1]!, { width: 800, height: 800 }) }, @@ -120,20 +125,12 @@ describe('DeepSeek request-image pricing', () => { ]) }) - it('caps each occurrence at the per-image byte target before the byte budget', () => { - // Each 5 MiB source counts as the 2 MiB request target, so a 4 MiB budget - // with a one-byte quantum removes exactly the oldest occurrence. - const oversized = 5 * 1024 * 1024 - const images = [ - ref('first', 800, 800, oversized), - ref('second', 800, 800, oversized), - ref('third', 800, 800, oversized), - ] + it('prices a retained oversized occurrence at its visual price: the surface, not the budget, decides offload', () => { + const oversized = ref('first', 800, 800, 5 * 1024 * 1024) const prices = deepSeekImageRequestPricing( connection({ maxRequestFilesBytes: 4 * 1024 * 1024, imageOffloadByteQuantum: 1 }), 'vision', - ).priceImages(images) - expect(prices.map(price => price.visualTokens)).toEqual([0, 422, 422]) - expect(prices[0]!.text).toBe(offloadedImageText(images[0]!)) + ).priceImages([block(oversized)]) + expect(prices.map(price => price.visualTokens)).toEqual([422]) }) }) diff --git a/packages/llm/llm-deepseek/tests/serialize.spec.ts b/packages/llm/llm-deepseek/tests/serialize.spec.ts index 6b12a33a96..25cbbbbb49 100644 --- a/packages/llm/llm-deepseek/tests/serialize.spec.ts +++ b/packages/llm/llm-deepseek/tests/serialize.spec.ts @@ -606,7 +606,7 @@ describe('image serialization', () => { ]) }) - it('offloads oldest images before reads and keeps the newest image', async () => { + it('projects surface-offloaded occurrences to placeholders without resolving them', async () => { const resolveFileId = fileResolver() const png = imageRef('image/png', 3) const jpeg = imageRef('image/jpeg', 3) @@ -618,7 +618,7 @@ describe('image serialization', () => { model: 'deepseek-v4-flash-vision-exp', messages: [createUserMessage({ content: [ - { type: 'image', attachment: png }, + { type: 'image', attachment: png, offloaded: true }, { type: 'image', attachment: jpeg }, ], source: { kind: 'plugin', plugin: 'test' }, @@ -639,22 +639,38 @@ describe('image serialization', () => { expect(resolveFileId).toHaveBeenCalledTimes(1) expect(resolveFileId.mock.calls[0]?.[0]).toMatchObject({ attachment: { mediaType: 'image/jpeg' } }) }) - - it('drops base64 history from a 20-unit high watermark to a 10-unit low watermark', async () => { + it('rejects retained inline images beyond the bound with the quantized count to offload', async () => { const ref = imageRef('image/png', 3) - const wire = await serializeRequestWithImages(request({ + // 21 retained 3-byte images encode to 84 base64 bytes against an 80-byte bound with a + // 40-byte quantum: the removal crosses the quantum at the eleventh oldest occurrence. + await expect(serializeRequestWithImages(request({ model: 'deepseek-v4-flash-vision-exp', messages: [createUserMessage({ content: Array.from({ length: 21 }, () => ({ type: 'image' as const, attachment: ref })), source: { kind: 'plugin', plugin: 'test' }, })], - }), inlineImageOptions([ref], 80, 40)) + }), inlineImageOptions([ref], 80, 40))).rejects.toMatchObject({ + code: 'IMAGE_OFFLOAD_REQUIRED', + failure: { offloadImages: 11 }, + }) + }) + it('counts only retained occurrences against the bound', async () => { + const ref = imageRef('image/png', 3) + const wire = await serializeRequestWithImages(request({ + model: 'deepseek-v4-flash-vision-exp', + messages: [createUserMessage({ + content: [ + ...Array.from({ length: 11 }, () => ({ type: 'image' as const, attachment: ref, offloaded: true as const })), + ...Array.from({ length: 10 }, () => ({ type: 'image' as const, attachment: ref })), + ], + source: { kind: 'plugin', plugin: 'test' }, + })], + }), inlineImageOptions([ref], 80, 40)) const content = wire.messages[0]?.content expect(JSON.stringify(content).match(/image omitted to fit request image limits/g)).toHaveLength(11) expect(JSON.stringify(content).match(/"type":"image_url"/g)).toHaveLength(10) }) - it('rejects an unprepared image while computing exact request bytes', async () => { const ref = imageRef() await expect(serializeRequestWithImages(request({ diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml index eef2e60af8..988a7298f0 100644 --- a/packages/llm/llm-pi-ai/README.i18n.yaml +++ b/packages/llm/llm-pi-ai/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md -README.md: 7f52d3f04634c805fc69b4c00365b9843d308252 -README.zh.md: 05a13fe0dc097ad911a4872412a694258939f860 +README.md: a49281b5ddc63c1fe4ce628857ff6f267dc498cd +README.zh.md: 78aed7db98e217691351f49f3f8ee41e74177e17 diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index 7f52d3f046..a49281b5dd 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -83,7 +83,7 @@ Each profile may set a `retryPolicy`; omission uses normal mode with five retrie | `defaultMaxTokens` | `32,768` | Output-cap fallback for undescribed models | | `requestImagePixelBudget` | `4,194,304` | Total-pixel budget for each deterministic request image | | `requestImageMaxBytes` | `1 MiB` | Encoded-byte target for each request image before base64 expansion | -| `maxRequestImageBytes` | `20 MiB` | Aggregate base64 image-payload bound with oldest-first offload | +| `maxRequestImageBytes` | `20 MiB` | Aggregate base64 image-payload bound; a request whose retained images exceed it fails with `IMAGE_OFFLOAD_REQUIRED` | | `retryPolicy` | normal, 5 retries | Provider-owned retry policy executed by `dsh-llm-retry` | The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-llm-pi-ai) is the exhaustive source for every accepted field and its JSDoc. @@ -180,7 +180,7 @@ Read these pages when the package-level contract is not enough. They move from t #### What the model sees -The selected catalog model receives one system prompt (`GenerateOptions.system`, otherwise the text of a leading `system` history message; a leading system message with empty text sends none), the remaining history, tools, and sampling fields supported by pi-ai's common streaming API. Each retained image is preceded by text naming its complete attachment id and actual request dimensions. When the current execution filesystem maps the attachment provider's host object, the text also carries a read-only normalized-object path and warns that normalization or request projection may have resized or re-encoded the upload. When accumulated base64 image payload exceeds the route's `maxRequestImageBytes`, each offloaded image keeps its own identity and currently resolved access in replacement text. Offloaded normalized attachments are not read or transformed. Provider-native replay metadata is restored only when the adapter validates it for the historical content. +The selected catalog model receives one system prompt (`GenerateOptions.system`, otherwise the text of a leading `system` history message; a leading system message with empty text sends none), the remaining history, tools, and sampling fields supported by pi-ai's common streaming API. Each retained image is preceded by text naming its complete attachment id and actual request dimensions. When the current execution filesystem maps the attachment provider's host object, the text also carries a read-only normalized-object path and warns that normalization or request projection may have resized or re-encoded the upload. Each occurrence selected by a logged image-offload decision keeps its own identity and currently resolved access in replacement text, and its normalized attachment is not read or transformed. When the retained occurrences' exact base64 payload still exceeds the route's `maxRequestImageBytes`, the call fails with `IMAGE_OFFLOAD_REQUIRED` so `dsh-compaction-image-offload` records the selected occurrences in an `image/offload` event and retries the step. Provider-native replay metadata is restored only when the adapter validates it for the historical content. #### Token effect @@ -188,7 +188,7 @@ Provider tokenization governs exact input. Retained images add the stable attach #### KV Cache effect -Conversion preserves logical request order, while image handles and offload placeholders add model-visible text. A changed execution-world path rewrites a historical handle and can prevent reuse from that image even when attachment identity and request bytes stay stable. Changing adapter instance, provider, model, or another upstream token has the same suffix effect. Crossing the image bound replaces an earlier image with placeholder text, so reuse ends at that message until the offloaded prefix stabilizes. +Conversion preserves logical request order, while image handles and offload placeholders add model-visible text. A changed execution-world path rewrites a historical handle and can prevent reuse from that image even when attachment identity and request bytes stay stable. Changing adapter instance, provider, model, or another upstream token has the same suffix effect. An offload decision turns an earlier image into placeholder text, so reuse ends at that message; the omission never reverts, so the prefix stays stable afterwards. ### Provider response @@ -211,7 +211,7 @@ Recorded response content appends to the next request and does not invalidate it These limits define where the adapter stops and future work begins. They are current package constraints, not a general pi-ai comparison or a task backlog. -- **`maxRequestImageBytes` counts base64 image payload only** — text, tools, descriptors, and JSON structure ride outside the bound, so it must sit below the gateway's request-body cap with headroom. Offload is a deterministic request projection and is not recorded as a session event. +- **`maxRequestImageBytes` counts base64 image payload only** — text, tools, descriptors, and JSON structure ride outside the bound, so it must sit below the gateway's request-body cap with headroom. - **A sign-in lives only in the process that started it** — an authorization attempt is not durable, so reloading the page mid-login abandons it and the human starts over. Signing out is `deleteRecord` on the stored record, which forgets it locally without telling the issuer. - **Provider-native discovery answers through this plugin's ambient context** — a route naming no credential defers to the catalog provider's own resolution, which asks for environment values (`AZURE_OPENAI_API_KEY`, `AWS_PROFILE`, and each provider's own set) and for local credential files. Both questions are answered here: the credential seam is consulted before the process environment, and file existence is checked against the host process's filesystem with `~` expanded. What it cannot do is *read* a credential file's contents — a provider that parses `~/.aws/credentials` itself does so directly, outside the seam. - **Settings can add or override routes, not remove composition routes** — the user layer merges over the composition base, so deleting a `cordis.yml`-provided provider is a composition change. diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md index 05a13fe0dc..78aed7db98 100644 --- a/packages/llm/llm-pi-ai/README.zh.md +++ b/packages/llm/llm-pi-ai/README.zh.md @@ -83,7 +83,7 @@ kind: "package-reference" | `defaultMaxTokens` | `32,768` | 未描述模型的输出上限回退 | | `requestImagePixelBudget` | `4,194,304` | 每张确定性请求图片的总像素预算 | | `requestImageMaxBytes` | `1 MiB` | 每张请求图片在 base64 扩展前的编码字节目标 | -| `maxRequestImageBytes` | `20 MiB` | 带最旧优先卸载的 base64 图片载荷总上限 | +| `maxRequestImageBytes` | `20 MiB` | base64 图片载荷总上限,保留图片超过时请求以 `IMAGE_OFFLOAD_REQUIRED` 失败 | | `retryPolicy` | normal,5 次重试 | 由 `dsh-llm-retry` 执行的提供方自有重试策略 | 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-llm-pi-ai)是每个受支持字段及其 JSDoc 的穷尽式真源。 @@ -180,7 +180,7 @@ Settings 写入会在合并组合层与用户层后严格校验每个新增或 #### 模型看到什么 -所选目录模型会收到一条系统提示词(`GenerateOptions.system`,否则取历史中首条 `system` 消息的文本;首条 system 消息文本为空时不发送系统提示词)、其余历史、工具与 pi-ai 通用流式 API 支持的采样字段。每张保留图片前都会有文本,注明其完整附件 id 与实际请求尺寸。当前执行文件系统可以映射附件提供方的宿主对象时,该文本还会携带只读规范化对象路径,并警告规范化或请求投影可能缩放或重新编码上传内容。当累计 base64 图片载荷超过路由的 `maxRequestImageBytes` 时,每张卸载图片都会在替换文本中保留自己的身份与当前已解析访问方式。卸载的规范化附件不会读取或变换。提供方原生回放元数据只在适配器针对历史内容校验通过后恢复。 +所选目录模型会收到一条系统提示词(`GenerateOptions.system`,否则取历史中首条 `system` 消息的文本;首条 system 消息文本为空时不发送系统提示词)、其余历史、工具与 pi-ai 通用流式 API 支持的采样字段。每张保留图片前都会有文本,注明其完整附件 id 与实际请求尺寸。当前执行文件系统可以映射附件提供方的宿主对象时,该文本还会携带只读规范化对象路径,并警告规范化或请求投影可能缩放或重新编码上传内容。日志中的图片省略决策选中的每个出现位置都会在替换文本中保留自己的身份与当前已解析访问方式,其规范化附件不会读取或变换。当保留的出现位置按精确 base64 载荷仍超过路由的 `maxRequestImageBytes` 时,调用以 `IMAGE_OFFLOAD_REQUIRED` 失败,由 `dsh-compaction-image-offload` 用 `image/offload` 事件记录所选位置并重试步骤。提供方原生回放元数据只在适配器针对历史内容校验通过后恢复。 #### Token 影响 @@ -188,7 +188,7 @@ Settings 写入会在合并组合层与用户层后严格校验每个新增或 #### KV Cache 影响 -转换保持逻辑请求顺序,图片句柄与卸载占位符则会添加模型可见文本。即使附件身份与请求字节保持稳定,执行世界路径变化也会改写历史句柄,并可能从该图片起阻止复用。更换适配器实例、提供方、模型或其他上游 token 具有相同的后缀影响。越过图片上限会把较早图片替换为占位文本,因此复用在该消息处结束,直到被卸载前缀稳定。 +转换保持逻辑请求顺序,图片句柄与卸载占位符则会添加模型可见文本。即使附件身份与请求字节保持稳定,执行世界路径变化也会改写历史句柄,并可能从该图片起阻止复用。更换适配器实例、提供方、模型或其他上游 token 具有相同的后缀影响。一次省略决策会把较早图片换成占位文本,因此复用在该消息处结束;省略永不回退,此后前缀保持稳定。 ### 提供方响应 @@ -211,7 +211,7 @@ pi-ai 事件变成 harness 的推理、文本、工具调用、用量与 finish 这些限制说明适配器在哪里停止、由未来工作接续。它们是当前包约束,不是通用 pi-ai 对比或任务积压。 -- **`maxRequestImageBytes` 只计算 base64 图片载荷**——文本、工具、描述符与 JSON 结构在该上限之外,因此它必须留有余量地低于网关请求体上限。卸载是确定性请求投影,不会记录为会话事件。 +- **`maxRequestImageBytes` 只计算 base64 图片载荷**,文本、工具、描述符与 JSON 结构在该上限之外,因此它必须留有余量地低于网关请求体上限。 - **登录只存在于发起它的进程中**——授权尝试不持久,因此登录中途刷新页面会放弃它,用户需要重新开始。退出登录是对已存储记录执行 `deleteRecord`,只在本地忘记它,不会告知签发方。 - **提供方原生发现经本插件的 ambient context 回答**——不点名凭据的路由交由目录提供方自身解析,它会询问环境值(`AZURE_OPENAI_API_KEY`、`AWS_PROFILE` 及各提供方自有集合)与本地凭据文件。两个问题都在这里得到回答:凭据 seam 先于进程环境被查询,文件存在性则针对宿主进程的文件系统以 `~` 展开后检查。它做不到的是*读取*凭据文件内容——自行解析 `~/.aws/credentials` 的提供方会直接读取,不经该 seam。 - **设置可以新增或覆盖路由,不能移除组合路由**——用户层覆盖组合 base,因此删除 `cordis.yml` 提供的提供方属于组合变更。 diff --git a/packages/llm/llm-pi-ai/src/context.ts b/packages/llm/llm-pi-ai/src/context.ts index 8126bfc04d..d16e842845 100644 --- a/packages/llm/llm-pi-ai/src/context.ts +++ b/packages/llm/llm-pi-ai/src/context.ts @@ -5,7 +5,7 @@ */ import { brandString } from '@deepseek-ai/dsh-brand' -import { contentHasImage, LlmError, offloadedImageText, offloadRequestImagesWithPolicy, requestImageHandleText } from '@deepseek-ai/dsh-llm' +import { contentHasImage, IMAGE_OFFLOAD_REQUIRED_CODE, LlmError, offloadedImageText, projectOffloadedImages, requestImageHandleText, requiredImageOffload } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, ImageAttachmentAccessResolver, Message, ToolCallId } from '@deepseek-ai/dsh-llm' import type { AttachmentId, @@ -95,8 +95,11 @@ function collectImageRefs( refs: Map, ): void { for (const block of blocks) { - if (block.type === 'image') refs.set(block.attachment.attachmentId, block.attachment) - else if (block.type === 'tool-result') collectImageRefs(block.content, refs) + if (block.type === 'image') { + if (block.offloaded !== true) refs.set(block.attachment.attachmentId, block.attachment) + } else if (block.type === 'tool-result') { + collectImageRefs(block.content, refs) + } } } @@ -220,7 +223,7 @@ export interface PiImageRequestContext { attachments: AttachmentStore /** Resolve current tool access separately from deterministic request-image versions. */ resolveImageAccess: ImageAttachmentAccessResolver - /** Request-level bound on base64-encoded image payload; omission leaves every image in place. */ + /** Request-level bound on the base64-encoded payload of retained images; omission leaves the bound unchecked. */ maxRequestImageBytes?: number /** Route pixel and raw encoded-byte budgets. */ requestImagePolicy?: PiImageRequestBudget @@ -255,10 +258,11 @@ export function toPiContext( ): PiContext /** * Convert harness history to a pi-ai Context while resolving durable images. - * Tool result names are recovered from preceding assistant tool calls. When - * the accumulated base64 image payload exceeds `maxRequestImageBytes`, the - * oldest images are replaced by text placeholders until the request fits, so - * an image-heavy session keeps clearing gateway request-size caps. + * Tool result names are recovered from preceding assistant tool calls. Image + * occurrences the surface marks offloaded become text placeholders; when the + * retained occurrences' exact base64 payload still exceeds + * `maxRequestImageBytes`, the call fails with `IMAGE_OFFLOAD_REQUIRED` naming + * how many more oldest occurrences must be offloaded. * @param options - the harness request; `options.system`, else a leading `system` message, maps to pi-ai's single `systemPrompt` slot. * @param images - attachment provider, current path resolver, and request limits. * @param onReplayDegrade - forwarded to {@link toPiAssistant} for each assistant message. @@ -291,21 +295,25 @@ async function toPiContextWithImages( } assertSupportedImageRoles(options.messages) const split = splitSystemPrompt(options) - const requestMessages = offloadRequestImagesWithPolicy(split.messages, { - representation: 'base64', - ...maxRequestImageBytes === undefined ? {} : { maxBytes: maxRequestImageBytes }, - byteQuantum: 1, - byteLength: ref => Math.min(ref.bytes, requestImagePolicy.maxBytes), - placeholder: ref => offloadedImageText(ref, resolveImageAccess(ref)), - }) - const requestImages = await prepareRequestImages(requestMessages, attachments, requestImagePolicy, options.signal) - const exactMessages = offloadRequestImagesWithPolicy(requestMessages, { - representation: 'base64', - ...maxRequestImageBytes === undefined ? {} : { maxBytes: maxRequestImageBytes }, - byteQuantum: 1, - byteLength: ref => (requestImages.get(ref.attachmentId) as RequestImageAttachment).bytes, - placeholder: ref => offloadedImageText(ref, resolveImageAccess(ref)), - }) + const requestImages = await prepareRequestImages(split.messages, attachments, requestImagePolicy, options.signal) + if (maxRequestImageBytes !== undefined) { + const offloadImages = requiredImageOffload( + split.messages, + { representation: 'base64', maxBytes: maxRequestImageBytes }, + block => (requestImages.get(block.attachment.attachmentId) as RequestImageAttachment).bytes, + ) + if (offloadImages > 0) { + throw new LlmError( + `pi-ai request images exceed the ${maxRequestImageBytes}-byte base64 bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`, + IMAGE_OFFLOAD_REQUIRED_CODE, + { offloadImages }, + ) + } + } + const exactMessages = projectOffloadedImages( + split.messages, + ref => offloadedImageText(ref, resolveImageAccess(ref)), + ) const toolNames = new Map() const messages: PiMessage[] = [] diff --git a/packages/llm/llm-pi-ai/tests/context.spec.ts b/packages/llm/llm-pi-ai/tests/context.spec.ts index 59f952ad86..47942b0099 100644 --- a/packages/llm/llm-pi-ai/tests/context.spec.ts +++ b/packages/llm/llm-pi-ai/tests/context.spec.ts @@ -257,20 +257,20 @@ describe('pi-ai request context conversion', () => { }) }) - it('replaces the oldest images with placeholders once the request payload bound is exceeded', async () => { + it('projects surface-offloaded occurrences to placeholders and prepares only retained ones', async () => { const readImageRequest = vi.fn((value: ImageAttachmentRef) => ( Promise.resolve(requestImage(value, Uint8Array.of(1, 2, 3))) )) const store = projectionStore(readImageRequest) const sized: ImageAttachmentRef = { ...ref, bytes: 3 } const callId = ToolCallId('shot-call') - // Three 3-byte images cost 4 base64 characters each (12 total); a bound of - // 8 forces exactly the oldest one out, including one nested in a tool result. + // The nested occurrence is offloaded on the surface; the two retained 3-byte images cost + // 4 base64 characters each and fit the 8-byte bound exactly. const context = await toPiContext(request([ user([{ type: 'tool-result', toolCallId: callId, - content: [{ type: 'image', attachment: sized }], + content: [{ type: 'image', attachment: sized, offloaded: true }], }]), user([{ type: 'image', attachment: sized }, { type: 'text', text: 'newer' }]), user([{ type: 'image', attachment: sized }]), @@ -305,8 +305,7 @@ describe('pi-ai request context conversion', () => { ]) expect(readImageRequest).toHaveBeenCalledTimes(1) }) - - it('does not prepare an old image removed by the conservative request projection', async () => { + it('does not prepare a surface-offloaded image', async () => { const old = { ...ref, attachmentId: AttachmentId(`sha256:${'c'.repeat(64)}`), bytes: 3 } const recent = { ...ref, attachmentId: AttachmentId(`sha256:${'d'.repeat(64)}`), bytes: 3 } const readImageRequest = vi.fn((value: ImageAttachmentRef) => { @@ -315,7 +314,7 @@ describe('pi-ai request context conversion', () => { }) const context = await toPiContext(request([user([ - { type: 'image', attachment: old }, + { type: 'image', attachment: old, offloaded: true }, { type: 'image', attachment: recent }, ])]), imageContext(projectionStore(readImageRequest), { maxRequestImageBytes: 4 })) @@ -330,16 +329,26 @@ describe('pi-ai request context conversion', () => { expect(readImageRequest).toHaveBeenCalledTimes(1) expect(readImageRequest.mock.calls[0]?.[0]).toEqual(recent) }) - - it('uses independently resolved access when exact encoded bytes require offload', async () => { + it('fails with the count to offload when exact encoded bytes exceed the bound', async () => { const sized: ImageAttachmentRef = { ...ref, bytes: 3 } - const access = { readonlyPath: '/tmp/dsh-normalized-image' } const readImageRequest = vi.fn((value: ImageAttachmentRef) => Promise.resolve({ ...requestImage(value, Uint8Array.of(1, 2, 3, 4)), })) - const context = await toPiContext(request([ + await expect(toPiContext(request([ user([{ type: 'image', attachment: sized }]), + ]), imageContext(projectionStore(readImageRequest), { maxRequestImageBytes: 4 }))) + .rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', failure: { offloadImages: 1 } }) + expect(readImageRequest).toHaveBeenCalledTimes(1) + }) + + it('renders an offloaded occurrence with independently resolved access', async () => { + const sized: ImageAttachmentRef = { ...ref, bytes: 3 } + const access = { readonlyPath: '/tmp/dsh-normalized-image' } + const readImageRequest = vi.fn() + + const context = await toPiContext(request([ + user([{ type: 'image', attachment: sized, offloaded: true }]), ]), imageContext(projectionStore(readImageRequest), { maxRequestImageBytes: 4, resolveImageAccess: () => access, @@ -350,10 +359,9 @@ describe('pi-ai request context conversion', () => { content: offloadedImageText(sized, access), timestamp: 0, }]) - expect(readImageRequest).toHaveBeenCalledTimes(1) + expect(readImageRequest).not.toHaveBeenCalled() }) - - it('keeps every image at exactly the payload bound and drops all of them when even the newest cannot fit', async () => { + it('keeps every image at exactly the payload bound and rejects a single image that cannot fit', async () => { const sized: ImageAttachmentRef = { ...ref, bytes: 3 } const exact = await toPiContext(request([ user([{ type: 'image', attachment: sized }]), @@ -376,17 +384,12 @@ describe('pi-ai request context conversion', () => { Promise.resolve(requestImage(value, new Uint8Array(300))) )) const store = projectionStore(readImageRequest) - const oversized = await toPiContext(request([ + await expect(toPiContext(request([ user([{ type: 'image', attachment: { ...ref, bytes: 300 } }]), - ]), imageContext(store, { maxRequestImageBytes: 8 })) - // All-text content collapses to the string form; the placeholder still reaches the model. - expect(oversized.messages).toEqual([ - { role: 'user', content: offloadedImageText({ ...ref, bytes: 300 }), timestamp: 0 }, - ]) - expect(readImageRequest).not.toHaveBeenCalled() + ]), imageContext(store, { maxRequestImageBytes: 8 }))) + .rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', failure: { offloadImages: 1 } }) }) - - it('offloads repeated image-block occurrences by position rather than shared object identity', async () => { + it('projects repeated image-block occurrences by their own surface mark', async () => { const sized: ImageAttachmentRef = { ...ref, bytes: 3 } const shared: ContentBlock = { type: 'image', attachment: sized } const readImageRequest = vi.fn((value: ImageAttachmentRef) => ( @@ -394,11 +397,11 @@ describe('pi-ai request context conversion', () => { )) const store = projectionStore(readImageRequest) const aliased = await toPiContext( - request([user([shared, shared])]), + request([user([{ ...shared, offloaded: true }, shared])]), imageContext(store, { maxRequestImageBytes: 4 }), ) const replayed = await toPiContext(request([user([ - { type: 'image', attachment: { ...sized } }, + { type: 'image', attachment: { ...sized }, offloaded: true }, { type: 'image', attachment: { ...sized } }, ])]), imageContext(store, { maxRequestImageBytes: 4 })) @@ -415,7 +418,6 @@ describe('pi-ai request context conversion', () => { expect(replayed.messages).toEqual(expected) expect(readImageRequest).toHaveBeenCalledTimes(2) }) - it('keeps empty text-only users while separating result-only messages', () => { const callId = ToolCallId('unknown-call') expect(toPiContext(request([ diff --git a/packages/llm/llm/README.i18n.yaml b/packages/llm/llm/README.i18n.yaml index d01cdc0abd..f24e25706f 100644 --- a/packages/llm/llm/README.i18n.yaml +++ b/packages/llm/llm/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm/README.md -README.md: 75a5d3e1d51ada12338a41e54b42ad8d6d21c279 -README.zh.md: 6c0d42dc1cb01fe8d5fc01d3384e909b6cdc044a +README.md: 8e4e547190a039959f7db29898945f2ed68ff874 +README.zh.md: 9164f4a6e6107e439b732539cddeb14716bc3fdc diff --git a/packages/llm/llm/README.md b/packages/llm/llm/README.md index 75a5d3e1d5..8e4e547190 100644 --- a/packages/llm/llm/README.md +++ b/packages/llm/llm/README.md @@ -95,13 +95,13 @@ The service is built on one separation: **the logical contract is provider-neutr | [`src/call-config.ts`](src/call-config.ts) | Call-config validation, adapter-default materialization, and request freezing | | [`src/retry-policy.ts`](src/retry-policy.ts) | Provider-owned retry policy resolution (normal and always modes) | | [`src/error.ts`](src/error.ts) | `HarnessError`/`LlmError` taxonomy and provider-neutral failure codes | -| [`src/content.ts`](src/content.ts) | Shared file and image projection helpers, including request-image offloading | +| [`src/content.ts`](src/content.ts) | Shared file and image helpers: content walks, file projection, image offload accounting, and offloaded-image projection | | [`src/api-key.ts`](src/api-key.ts) | Credential format check shared by every adapter | | [`src/adapter-failure.ts`](src/adapter-failure.ts) | Failure normalization into terminal finish chunks | ### Main flow -A request is validated against its exact model's capability — context window, output default, reasoning efforts, input modalities, and `systemPromptUpdate` mode — and any adapter-configured defaults are materialized, then the whole request is deep-frozen. `prepareCall()` binds those facts, detached context, and retry policy to the exact adapter generation that performs terminal dispatch, so HMR or dynamic settings cannot combine one generation's image capability with another generation's endpoint. An image-capable adapter projects durable references into route-specific request versions; `resolveImageAttachmentAccess()` separately maps an attachment provider's optional host object into the current tool execution world without changing the request image or its `variantId`. A text-only route receives deterministic per-image placeholders, including nested tool-result images, without rewriting append-only session history. Durable `FileBlock` references never reach any adapter: request assembly replaces each one, nested tool-result occurrences included, with deterministic handle text naming the file and its saved read-only path, resolved through the mounted attachment and filesystem providers. `ctx.llm.fileRequestText(ref)` exposes that exact synchronous projection to request measurement. `offloadRequestImagesWithPolicy()` removes oldest images deterministically by raw or base64 size and count or byte quanta; the pure `offloadedImagePrefixCount()` exposes that decision so route-owned request pricing can reproduce it without building the projection. Adapters that charge visual tokens declare per-route `imageRequestPricing`, which `ctx.llm.imageRequestPricing(provider, model)` resolves synchronously for the token meter. Dispatch goes through the `llm/stream` waterfall, then chunks return as token-level deltas and every adapter outcome reaches the consumer as one terminal `finish` chunk. +A request is validated against its exact model's capability — context window, output default, reasoning efforts, input modalities, and `systemPromptUpdate` mode — and any adapter-configured defaults are materialized, then the whole request is deep-frozen. `prepareCall()` binds those facts, detached context, and retry policy to the exact adapter generation that performs terminal dispatch, so HMR or dynamic settings cannot combine one generation's image capability with another generation's endpoint. An image-capable adapter projects durable references into route-specific request versions; `resolveImageAttachmentAccess()` separately maps an attachment provider's optional host object into the current tool execution world without changing the request image or its `variantId`. A text-only route receives deterministic per-image placeholders, including nested tool-result images, without rewriting append-only session history. Durable `FileBlock` references never reach any adapter: request assembly replaces each one, nested tool-result occurrences included, with deterministic handle text naming the file and its saved read-only path, resolved through the mounted attachment and filesystem providers. `ctx.llm.fileRequestText(ref)` exposes that exact synchronous projection to request measurement. An image occurrence derived with `offloaded: true` reaches every route as placeholder text through `projectOffloadedImages()`. An image-capable route whose retained occurrences exceed its `LlmImageRequestBudget` at their exact bytes fails with `IMAGE_OFFLOAD_REQUIRED` naming the additional oldest occurrences (`requiredImageOffload()`), never with an unlogged projection; `dsh-compaction-image-offload` logs the selected occurrences in one `image/offload` event and retries. Adapters that charge visual tokens declare per-route `imageRequestPricing`, which `ctx.llm.imageRequestPricing(provider, model)` resolves synchronously for the token meter. Dispatch goes through the `llm/stream` waterfall, then chunks return as token-level deltas and every adapter outcome reaches the consumer as one terminal `finish` chunk. File detection reads current content, including nested tool results, on every request without caching message identities or freeze state. The [file-scan decision](../../../.agents/notes/implemented/simplification/2026-09-07-file-content-scan.md) records the measured traversal cost. @@ -141,7 +141,7 @@ None, as the LLM service adds no content; adapters choose when to add the shared #### KV Cache effect -Reasoning-effort materialization preserves the assembled request prefix. Image identity and request-preview text are deterministic, while an optional execution-world path is resolved for each request; a changed path or image-offload boundary can prevent reuse from that image. +Reasoning-effort materialization preserves the assembled request prefix. Image identity and request-preview text are deterministic, while an optional execution-world path is resolved for each request; a changed path or an offload decision can prevent reuse from that image. ## Known Limitations and Deferred Work diff --git a/packages/llm/llm/README.zh.md b/packages/llm/llm/README.zh.md index 6c0d42dc1c..9164f4a6e6 100644 --- a/packages/llm/llm/README.zh.md +++ b/packages/llm/llm/README.zh.md @@ -95,13 +95,13 @@ for await (const chunk of ctx.llm.stream({ | [`src/call-config.ts`](src/call-config.ts) | 调用配置校验、适配器默认值填入与请求冻结 | | [`src/retry-policy.ts`](src/retry-policy.ts) | 提供方自有重试策略解析(normal 与 always 模式) | | [`src/error.ts`](src/error.ts) | `HarnessError`/`LlmError` 分类体系与提供方无关失败 code | -| [`src/content.ts`](src/content.ts) | 共享文件与图片投影辅助函数,包括请求图片卸载 | +| [`src/content.ts`](src/content.ts) | 共享文件与图片辅助函数:内容遍历、文件投影、图片卸载计数与已卸载图片投影 | | [`src/api-key.ts`](src/api-key.ts) | 每个适配器共享的凭据格式校验 | | [`src/adapter-failure.ts`](src/adapter-failure.ts) | 把失败归一化为终止 finish 分片 | ### 主流程 -请求会对照其精确模型的能力——上下文窗口、输出默认值、推理强度、输入模态与 `systemPromptUpdate` 模式——校验,填入任何适配器配置的默认值,然后整个请求被深度冻结。`prepareCall()` 把这些事实、分离的上下文与重试策略绑定到执行最终分发的精确适配器代次,因此 HMR(热模块替换)或动态设置无法把一个代次的图片能力与另一代次的端点混用。支持图片的适配器把持久引用投影为路由专用请求版本;`resolveImageAttachmentAccess()` 会单独把附件提供方的可选宿主对象映射进当前工具执行世界,而不改变请求图片或其 `variantId`。纯文本路由接收确定性的逐图片占位符,包括嵌套工具结果图片,而不会改写仅追加会话历史。持久 `FileBlock` 引用永远不会到达任何适配器:请求组装把每个引用(包括嵌套工具结果中的出现)替换为确定性句柄文本,指出文件与其只读保存路径,路径经由挂载的附件与文件系统提供方解析。`ctx.llm.fileRequestText(ref)` 向请求计量公开相同的同步投影。`offloadRequestImagesWithPolicy()` 按原始字节或 base64 大小以及图片数或字节步长,确定性地从最旧图片开始移除;纯函数 `offloadedImagePrefixCount()` 公开同一决策,使路由所属的请求定价无需构建投影即可复现它。对视觉 token 收费的适配器声明按路由的 `imageRequestPricing`,`ctx.llm.imageRequestPricing(provider, model)` 为 token meter 同步解析它。分发经过 `llm/stream` waterfall(瀑布式事件),随后分片以 token 级增量返回,每个适配器结果都以唯一一个终止 `finish` 分片到达消费方。 +请求会对照其精确模型的能力校验,包括上下文窗口、输出默认值、推理强度、输入模态与 `systemPromptUpdate` 模式,填入任何适配器配置的默认值,然后整个请求被深度冻结。`prepareCall()` 把这些事实、分离的上下文与重试策略绑定到执行最终分发的精确适配器代次,因此 HMR(热模块替换)或动态设置无法把一个代次的图片能力与另一代次的端点混用。支持图片的适配器把持久引用投影为路由专用请求版本;`resolveImageAttachmentAccess()` 会单独把附件提供方的可选宿主对象映射进当前工具执行世界,而不改变请求图片或其 `variantId`。纯文本路由接收确定性的逐图片占位符,包括嵌套工具结果图片,而不会改写仅追加会话历史。持久 `FileBlock` 引用永远不会到达任何适配器:请求组装把每个引用(包括嵌套工具结果中的出现)替换为确定性 句柄文本,指出文件与其只读保存路径,路径经由挂载的附件与文件系统提供方解析。`ctx.llm.fileRequestText(ref)` 向请求计量公开相同的同步投影。派生后带有 `offloaded: true` 的图片出现位置,经 `projectOffloadedImages()` 以占位文本到达每条路由。支持图片的路由在保留的出现位置按精确字节超过其 `LlmImageRequestBudget` 时,以 `IMAGE_OFFLOAD_REQUIRED` 失败并说明还需省略多少最老的出现位置(`requiredImageOffload()`),绝不发送未记录的投影;`dsh-compaction-image-offload` 用一条 `image/offload` 事件记录所选位置并重试。对视觉 token 收费的适配器声明按路由的 `imageRequestPricing`,`ctx.llm.imageRequestPricing(provider, model)` 为 token meter 同步解析它。分发经过 `llm/stream` waterfall(瀑布式事件),随后分片以 token 级增量返回,每个适配器结果都以唯一一个终止 `finish` 分片到达消费方。 文件检测在每次请求时读取当前内容,包括嵌套工具结果,不缓存消息身份或冻结状态。[文件扫描决策](../../../.agents/notes/implemented/simplification/2026-09-07-file-content-scan.zh.md)记录了实测遍历成本。 @@ -141,7 +141,7 @@ for await (const chunk of ctx.llm.stream({ #### KV Cache 影响 -推理强度的具体化会保留已组装请求前缀。图片身份与请求预览文本是确定性的,可选执行世界路径则按请求解析;路径变化或图片卸载边界变化可能从该图片起阻止复用。 +推理强度的具体化会保留已组装请求前缀。图片身份与请求预览文本是确定性的,可选执行世界路径则按请求解析;路径变化或一次省略决策可能从该图片起阻止复用。 ## 已知限制与延期工作 diff --git a/packages/llm/llm/src/adapter-failure.ts b/packages/llm/llm/src/adapter-failure.ts index d11ee2e52b..98dba446d1 100644 --- a/packages/llm/llm/src/adapter-failure.ts +++ b/packages/llm/llm/src/adapter-failure.ts @@ -69,18 +69,21 @@ function failureSnapshot(value: unknown): LlmFailure | undefined { const status = candidate.status const providerRetryAfterMs = candidate.providerRetryAfterMs const requestId = candidate.requestId + const offloadImages = candidate.offloadImages if (typeof message !== 'string' || message.length === 0 || typeof code !== 'string' || code.length === 0 || (status !== undefined && (!Number.isInteger(status) || status < 100 || status > 599)) || (providerRetryAfterMs !== undefined && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0)) - || (requestId !== undefined && (typeof requestId !== 'string' || requestId.length === 0))) return undefined + || (requestId !== undefined && (typeof requestId !== 'string' || requestId.length === 0)) + || (offloadImages !== undefined && (!Number.isSafeInteger(offloadImages) || offloadImages <= 0))) return undefined return Object.freeze({ message, code, ...status === undefined ? {} : { status }, ...providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs }, ...requestId === undefined ? {} : { requestId }, + ...offloadImages === undefined ? {} : { offloadImages }, }) } catch (_sdkFailureGetter) { return undefined diff --git a/packages/llm/llm/src/content.ts b/packages/llm/llm/src/content.ts index c6d3a447e6..b5c3e59f3d 100644 --- a/packages/llm/llm/src/content.ts +++ b/packages/llm/llm/src/content.ts @@ -1,6 +1,6 @@ /** Content-block structure helpers. @module @deepseek-ai/dsh-llm/content */ -import type { ContentBlock } from './types.ts' +import type { ContentBlock, ImageBlock, LlmImageRequestBudget } from './types.ts' import type { Message } from './message.ts' import type { AttachmentStore, FileAttachmentRef, ImageAttachmentRef, ImageMediaType, RequestImageAttachment, @@ -209,58 +209,33 @@ function base64Length(bytes: number): number { return Math.ceil(bytes / 3) * 4 } -/** Byte accounting and quantized removal policy for one request representation. */ -export interface RequestImageOffloadPolicy { - /** Image count accepted by the route; omission leaves count unbounded. */ - maxImages?: number - /** Accumulated image bytes accepted by the route; omission leaves bytes unbounded. */ - maxBytes?: number - /** Number of excess images removed as one deterministic step. */ - countQuantum?: number - /** Number of excess bytes removed as one deterministic step. */ - byteQuantum?: number - /** Whether byte accounting uses raw file bytes or inline base64 length. */ - representation: 'raw' | 'base64' - /** Resolve the encoded request-version length; omission uses normalized attachment bytes. */ - byteLength?: (ref: ImageAttachmentRef) => number - /** Build the model-visible replacement for each omitted attachment. */ - placeholder: (ref: ImageAttachmentRef) => string -} - -/** Collect represented image lengths in request and nested-block order. */ -function collectImageLengths( - blocks: readonly ContentBlock[], - lengths: number[], - policy: RequestImageOffloadPolicy, -): void { - for (const block of blocks) { - if (block.type === 'image') { - const bytes = policy.byteLength === undefined - ? block.attachment.bytes - : policy.byteLength(block.attachment) - lengths.push(policy.representation === 'base64' ? base64Length(bytes) : bytes) - } else if (block.type === 'tool-result') { - collectImageLengths(block.content, lengths, policy) - } +/** + * Visit every image occurrence of typed content in message order, including + * nested tool-result content. + * @param content - typed model content blocks. + * @param visit - called once per occurrence. + */ +function visitImageBlocks(content: readonly ContentBlock[], visit: (block: ImageBlock) => void): void { + for (const block of content) { + if (block.type === 'image') visit(block) + else if (block.type === 'tool-result') visitImageBlocks(block.content, visit) } } -/** Replace the first `remaining.count` image occurrences without mutating durable messages. */ -function replaceOldestImages( +/** Replace every offloaded occurrence, including nested tool results, with its placeholder. */ +function replaceOffloadedImages( blocks: readonly ContentBlock[], - remaining: { count: number }, placeholder: (ref: ImageAttachmentRef) => string, ): ContentBlock[] { let next: ContentBlock[] | undefined for (const [index, block] of blocks.entries()) { - if (block.type === 'image' && remaining.count > 0) { - remaining.count -= 1 + if (block.type === 'image' && block.offloaded === true) { next ??= blocks.slice(0, index) next.push({ type: 'text', text: placeholder(block.attachment) }) continue } if (block.type === 'tool-result') { - const content = replaceOldestImages(block.content, remaining, placeholder) + const content = replaceOffloadedImages(block.content, placeholder) if (content !== block.content) { next ??= blocks.slice(0, index) next.push({ ...block, content }) @@ -272,6 +247,83 @@ function replaceOldestImages( return next ?? blocks as ContentBlock[] } +/** + * Project the surface's offloaded occurrences into deterministic text for one + * request. The offloaded set is a durable surface fact, so every route sends + * the same set; only the placeholder text is route-owned. + * @param messages - derived request history. + * @param placeholder - build the model-visible replacement for one offloaded attachment. + * @returns the original list when nothing is offloaded, otherwise shallow message copies with placeholders. + */ +export function projectOffloadedImages( + messages: readonly Message[], + placeholder: (ref: ImageAttachmentRef) => string, +): readonly Message[] { + return messages.map((message) => { + const content = replaceOffloadedImages(message.content, placeholder) + return content === message.content ? message : { ...message, content } + }) +} + +/** + * Number of oldest retained image occurrences one route budget removes, in + * whole count and byte quanta, once the budget is exceeded. The result depends + * only on the represented lengths, so every route names the count the same + * way. + * @param lengths - represented byte length of every retained occurrence, oldest first. + * @param budget - count/byte budgets and removal quanta; unbounded when absent. + * @returns how many leading occurrences to offload. + */ +function offloadedImagePrefixCount( + lengths: readonly number[], + budget: Pick, +): number { + const total = lengths.reduce((sum, bytes) => sum + bytes, 0) + const excessCount = budget.maxImages === undefined ? 0 : Math.max(0, lengths.length - budget.maxImages) + const excessBytes = budget.maxBytes === undefined ? 0 : Math.max(0, total - budget.maxBytes) + if (excessCount === 0 && excessBytes === 0) return 0 + const countQuantum = budget.countQuantum ?? 1 + const byteQuantum = budget.byteQuantum ?? 1 + const removeCount = excessCount === 0 ? 0 : Math.ceil(excessCount / countQuantum) * countQuantum + const removeBytes = excessBytes === 0 ? 0 : Math.ceil(excessBytes / byteQuantum) * byteQuantum + let count = 0 + let removedBytes = 0 + for (const imageBytes of lengths) { + const byteTargetMet = removeBytes === 0 + || (byteQuantum === 1 ? removedBytes >= removeBytes : removedBytes > removeBytes) + if (count >= removeCount && byteTargetMet) break + removedBytes += imageBytes + count += 1 + } + return count +} + +/** + * Number of oldest retained occurrences a route must still offload before a + * derived request fits its budget at the exact byte length the route sends; + * zero when the request fits. A route fails with `IMAGE_OFFLOAD_REQUIRED` + * carrying this count instead of offloading on its own. + * @param messages - derived request history carrying the surface's `offloaded` marks. + * @param budget - route representation, budgets, and removal quanta. + * @param versionBytes - exact request-version byte length of one retained occurrence. + * @returns how many more leading retained occurrences to offload. + */ +export function requiredImageOffload( + messages: readonly Message[], + budget: Pick, + versionBytes: (block: ImageBlock) => number, +): number { + const lengths: number[] = [] + for (const message of messages) { + visitImageBlocks(message.content, (block) => { + if (block.offloaded === true) return + const bytes = versionBytes(block) + lengths.push(budget.representation === 'base64' ? base64Length(bytes) : bytes) + }) + } + return offloadedImagePrefixCount(lengths, budget) +} + /** Replace every image occurrence, including nested tool results, for a text-only model. */ function replaceImagesForTextModel(blocks: readonly ContentBlock[]): ContentBlock[] { let next: ContentBlock[] | undefined @@ -306,62 +358,3 @@ export function projectImagesForTextModel(messages: readonly Message[]): readonl return content === message.content ? message : { ...message, content } }) } - -/** - * Number of oldest image occurrences one request projection removes, in whole - * count and byte quanta, once a route budget is exceeded. The result depends - * only on the represented lengths, so provider request pricing reproduces the - * exact serialization decision without building the projected messages. - * @param lengths - represented byte length of every occurrence, in request order. - * @param policy - count/byte budgets and removal quanta; unbounded when absent. - * @returns how many leading occurrences the projection replaces with placeholders. - */ -export function offloadedImagePrefixCount( - lengths: readonly number[], - policy: Pick, -): number { - const total = lengths.reduce((sum, bytes) => sum + bytes, 0) - const excessCount = policy.maxImages === undefined ? 0 : Math.max(0, lengths.length - policy.maxImages) - const excessBytes = policy.maxBytes === undefined ? 0 : Math.max(0, total - policy.maxBytes) - if (excessCount === 0 && excessBytes === 0) return 0 - const countQuantum = policy.countQuantum ?? 1 - const byteQuantum = policy.byteQuantum ?? 1 - const removeCount = excessCount === 0 ? 0 : Math.ceil(excessCount / countQuantum) * countQuantum - const removeBytes = excessBytes === 0 ? 0 : Math.ceil(excessBytes / byteQuantum) * byteQuantum - let count = 0 - let removedBytes = 0 - for (const imageBytes of lengths) { - const byteTargetMet = removeBytes === 0 - || (byteQuantum === 1 ? removedBytes >= removeBytes : removedBytes > removeBytes) - if (count >= removeCount && byteTargetMet) break - removedBytes += imageBytes - count += 1 - } - return count -} - -/** - * Return a deterministic transient projection whose oldest images are replaced - * in whole count and byte quanta after a route budget is exceeded. The target - * depends only on complete durable history: at 129 one-megabyte images under - * a 128 MiB bound with a 64 MiB quantum, the oldest 65 images are removed so - * 64 MiB remain; that removed prefix stays fixed until total history exceeds - * 192 MiB. - * @param messages - complete request history, oldest first. - * @param policy - route representation, budgets, and removal quanta. - * @returns original messages below both bounds, otherwise shallow copies with deterministic placeholders. - */ -export function offloadRequestImagesWithPolicy( - messages: readonly Message[], - policy: RequestImageOffloadPolicy, -): readonly Message[] { - const lengths: number[] = [] - for (const message of messages) collectImageLengths(message.content, lengths, policy) - const count = offloadedImagePrefixCount(lengths, policy) - if (count === 0) return messages - const remaining = { count } - return messages.map((message) => { - const content = replaceOldestImages(message.content, remaining, policy.placeholder) - return content === message.content ? message : { ...message, content } - }) -} diff --git a/packages/llm/llm/src/error.ts b/packages/llm/llm/src/error.ts index 4b3e25c75f..477735d178 100644 --- a/packages/llm/llm/src/error.ts +++ b/packages/llm/llm/src/error.ts @@ -161,3 +161,12 @@ export function errorChain(value: unknown): string { export function isHarnessError(value: unknown): value is HarnessError { return value instanceof HarnessError } + +/** + * Canonical code for a request an image-capable route cannot send until more + * of its images are offloaded. The failure's `offloadImages` names how many + * more of the oldest retained occurrences must be offloaded; + * `dsh-compaction-image-offload` records an `image/offload` selection before + * the agent or summarizer retries with freshly derived input. + */ +export const IMAGE_OFFLOAD_REQUIRED_CODE = 'IMAGE_OFFLOAD_REQUIRED' diff --git a/packages/llm/llm/src/index.ts b/packages/llm/llm/src/index.ts index 367688c0d1..bc5b5d6eb3 100644 --- a/packages/llm/llm/src/index.ts +++ b/packages/llm/llm/src/index.ts @@ -82,6 +82,8 @@ export interface LlmErrorOptions extends ErrorOptions { providerRetryAfterMs?: number /** Non-empty opaque provider request id. */ requestId?: ProviderRequestId + /** Positive count of additional oldest retained image occurrences to offload; only with `IMAGE_OFFLOAD_REQUIRED`. */ + offloadImages?: number } /** @@ -120,6 +122,7 @@ export class LlmError extends HarnessError { ...options?.status === undefined ? {} : { status: options.status }, ...options?.providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs: options.providerRetryAfterMs }, ...options?.requestId === undefined ? {} : { requestId: options.requestId }, + ...options?.offloadImages === undefined ? {} : { offloadImages: options.offloadImages }, }) } } diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts index 23a56e73a3..eaa4e0462a 100644 --- a/packages/llm/llm/src/types.ts +++ b/packages/llm/llm/src/types.ts @@ -48,6 +48,13 @@ export interface LlmFailure { readonly providerRetryAfterMs?: number /** Opaque provider-issued request identifier for diagnostics. */ readonly requestId?: ProviderRequestId + /** + * With code `IMAGE_OFFLOAD_REQUIRED`: how many more of the oldest retained + * image occurrences the route needs offloaded before the same request fits + * its exact byte accounting. `dsh-compaction-image-offload` records the + * selected occurrences in an `image/offload` event and retries the step. + */ + readonly offloadImages?: number } /** Plain text visible to the end user. */ @@ -72,6 +79,12 @@ export interface ImageBlock { type: 'image' /** Immutable bytes and intrinsic display metadata owned by the attachment service. */ attachment: ImageAttachmentRef + /** + * Derived from a durable image-offload decision or preserved by a message + * rewrite. Every route sends placeholder text naming the image and its + * available read-only path instead of image bytes. + */ + offloaded?: true } /** @@ -186,10 +199,11 @@ export interface LlmImageRequestPrice { export interface LlmImageRequestPricing { /** * Price every image occurrence of one request projection. - * @param images - durable image references in request order, one entry per occurrence. + * @param images - surface image blocks in request order, one entry per occurrence; an `offloaded` block + * is priced as its placeholder text. * @returns one price per occurrence, aligned by index with `images`. */ - priceImages(images: readonly ImageAttachmentRef[]): readonly LlmImageRequestPrice[] + priceImages(images: readonly ImageBlock[]): readonly LlmImageRequestPrice[] } /** Display metadata for one registered provider route. */ @@ -317,6 +331,26 @@ export interface LlmModelContext { contextWindow: number } +/** + * Request-image budget one exact image-capable route enforces over the + * retained occurrences' exact request-version bytes; for the `base64` + * representation each byte count expands to its encoded length. A route whose + * retained occurrences exceed the budget fails the request with + * `IMAGE_OFFLOAD_REQUIRED` naming the additional occurrences to offload. + */ +export interface LlmImageRequestBudget { + /** Whether the route accounts raw file bytes or inline base64 length. */ + representation: 'raw' | 'base64' + /** Accumulated represented image bytes the route accepts; absent leaves bytes unbounded. */ + maxBytes?: number + /** Image occurrences the route accepts; absent leaves the count unbounded. */ + maxImages?: number + /** Represented bytes removed as one deterministic advance step; absent removes the minimum. */ + byteQuantum?: number + /** Occurrences removed as one deterministic advance step; absent removes the minimum. */ + countQuantum?: number +} + /** Display metadata for one adapter-owned reasoning effort. */ export interface LlmReasoningEffortInfo { /** Opaque stable value accepted by {@link GenerateOptions.reasoningEffort}. */ diff --git a/packages/llm/llm/tests/content.spec.ts b/packages/llm/llm/tests/content.spec.ts index 2868f4f871..61821d8f9a 100644 --- a/packages/llm/llm/tests/content.spec.ts +++ b/packages/llm/llm/tests/content.spec.ts @@ -8,28 +8,19 @@ import { fileHandleText, projectFilesToText, offloadedImageText, - offloadedImagePrefixCount, - offloadRequestImagesWithPolicy, projectImagesForTextModel, + projectOffloadedImages, + requiredImageOffload, resolveImageAttachmentAccess, requestImageHandleText, } from '../src/index.ts' -import type { ContentBlock, Message } from '../src/index.ts' +import type { ContentBlock } from '../src/index.ts' const source = { kind: 'plugin' as const, plugin: 'test' } const OMITTED = '[omitted]' -function offloadBase64(messages: readonly Message[], maxBytes: number | undefined): readonly Message[] { - return offloadRequestImagesWithPolicy(messages, { - representation: 'base64', - ...maxBytes === undefined ? {} : { maxBytes }, - byteQuantum: 1, - placeholder: () => OMITTED, - }) -} - -function image(bytes: number): Extract { +function image(bytes: number, offloaded?: true): Extract { return { type: 'image', attachment: { @@ -39,158 +30,105 @@ function image(bytes: number): Extract { width: 1, height: 1, }, + ...offloaded === undefined ? {} : { offloaded }, } } -describe('base64 request-image offload', () => { - it('preserves every image when no payload bound is configured', () => { +describe('requiredImageOffload traversal', () => { + it('visits image occurrences at every tool-result depth in message order', () => { + const seen: number[] = [] + const content: ContentBlock[] = [ + { type: 'text', text: 'before' }, + image(1), + { + type: 'tool-result', + toolCallId: ToolCallId('shot'), + content: [ + { type: 'text', text: 'x' }, + image(2), + { + type: 'tool-result', + toolCallId: ToolCallId('nested'), + content: [{ type: 'tool-result', toolCallId: ToolCallId('deep'), content: [image(4)] }], + }, + ], + }, + image(3), + ] + expect(requiredImageOffload([createUserMessage({ content, source })], + { representation: 'raw', maxImages: 0 }, (block) => { + seen.push(block.attachment.bytes) + return block.attachment.bytes + })).toBe(4) + expect(seen).toEqual([1, 2, 4, 3]) + }) +}) + +describe('projectOffloadedImages', () => { + it('keeps messages without offloaded occurrences by identity', () => { const messages = [createUserMessage({ content: [image(300)], source })] - expect(offloadBase64(messages, undefined)).toBe(messages) + const projected = projectOffloadedImages(messages, () => OMITTED) + expect(projected[0]).toBe(messages[0]) }) - it('preserves the original request when its base64 payload fits exactly', () => { - const messages = [createUserMessage({ content: [image(3), image(3)], source })] - expect(offloadBase64(messages, 8)).toBe(messages) - }) - - it('keeps five 3 MiB images at 20 MiB and offloads the oldest after one more raw byte', () => { - const rawImageBytes = 3 * 1024 * 1024 - const maxRequestImageBytes = 20 * 1024 * 1024 - const exact = [createUserMessage({ - content: Array.from({ length: 5 }, () => image(rawImageBytes)), - source, - })] - expect(offloadBase64(exact, maxRequestImageBytes)).toBe(exact) - - const over = [createUserMessage({ - content: [image(rawImageBytes + 1), ...Array.from({ length: 4 }, () => image(rawImageBytes))], - source, - })] - expect(offloadBase64(over, maxRequestImageBytes)[0]?.content).toEqual([ - { type: 'text', text: OMITTED }, - ...Array.from({ length: 4 }, () => image(rawImageBytes)), - ]) - }) - - it('replaces the oldest nested occurrences without mutating durable messages', () => { - const shared = image(3) + it('replaces marked top-level and nested occurrences with route placeholders', () => { const messages = [ createUserMessage({ - content: [{ - type: 'tool-result', - toolCallId: ToolCallId('shot'), - content: [shared], - }], + content: [{ type: 'tool-result', toolCallId: ToolCallId('shot'), content: [image(3, true)] }], source, }), - createUserMessage({ content: [shared, image(3)], source }), + createUserMessage({ content: [image(3, true), image(3)], source }), ] - - const fitted = offloadBase64(messages, 8) - expect(fitted).not.toBe(messages) - expect(fitted[0]?.content).toEqual([{ + const projected = projectOffloadedImages(messages, ref => `${OMITTED}:${ref.bytes}`) + expect(projected[0]?.content).toEqual([{ type: 'tool-result', toolCallId: ToolCallId('shot'), - content: [{ type: 'text', text: OMITTED }], + content: [{ type: 'text', text: `${OMITTED}:3` }], }]) - expect(fitted[1]?.content).toEqual([shared, image(3)]) - expect(messages[0]?.content[0]).toMatchObject({ type: 'tool-result', content: [shared] }) + expect(projected[1]?.content).toEqual([{ type: 'text', text: `${OMITTED}:3` }, image(3)]) + expect(messages[1]?.content[0]).toEqual(image(3, true)) }) - it('replaces a single image that cannot fit', () => { - const messages = [createUserMessage({ content: [image(300)], source })] - expect(offloadBase64(messages, 8)[0]?.content) - .toEqual([{ type: 'text', text: OMITTED }]) - }) - - it('keeps unchanged nested content while replacing a later image', () => { + it('keeps unchanged nested content while replacing a later occurrence', () => { const nested = { type: 'tool-result' as const, toolCallId: ToolCallId('text-only'), content: [{ type: 'text' as const, text: 'kept' }], } - const messages = [createUserMessage({ content: [nested, image(3)], source })] - expect(offloadBase64(messages, 1)[0]?.content).toEqual([ + const messages = [createUserMessage({ content: [nested, image(3, true)], source })] + expect(projectOffloadedImages(messages, () => OMITTED)[0]?.content).toEqual([ nested, { type: 'text', text: OMITTED }, ]) }) }) -describe('offloadedImagePrefixCount', () => { +describe('requiredImageOffload', () => { + const request = (lengths: number[], offloaded: number[] = []) => [createUserMessage({ + content: lengths.map((bytes, index) => image(bytes, offloaded.includes(index) ? true : undefined)), + source, + })] + const bytesOf = (block: Extract): number => block.attachment.bytes + const raw = { representation: 'raw' as const } + it('removes nothing under unbounded budgets and whole quanta past them', () => { const lengths = [4, 4, 4, 4] - expect(offloadedImagePrefixCount(lengths, {})).toBe(0) - expect(offloadedImagePrefixCount(lengths, { maxBytes: 16 })).toBe(0) - expect(offloadedImagePrefixCount(lengths, { maxImages: 4 })).toBe(0) + expect(requiredImageOffload(request(lengths), raw, bytesOf)).toBe(0) + expect(requiredImageOffload(request(lengths), { ...raw, maxBytes: 16 }, bytesOf)).toBe(0) + expect(requiredImageOffload(request(lengths), { ...raw, maxImages: 4 }, bytesOf)).toBe(0) // One excess image rounds up to the whole count quantum. - expect(offloadedImagePrefixCount([...lengths, 4], { maxImages: 4, countQuantum: 2 })).toBe(2) + expect(requiredImageOffload(request([...lengths, 4]), { ...raw, maxImages: 4, countQuantum: 2 }, bytesOf)).toBe(2) // One excess byte removes a whole byte quantum, crossing the second image. - expect(offloadedImagePrefixCount([...lengths, 1], { maxBytes: 16, byteQuantum: 5 })).toBe(2) - }) -}) - -describe('offloadRequestImagesWithPolicy', () => { - it('drops 129 MiB to 64 MiB and keeps the removed prefix stable through 192 MiB', () => { + expect(requiredImageOffload(request([...lengths, 1]), { ...raw, maxBytes: 16, byteQuantum: 5 }, bytesOf)).toBe(2) + // 129 one-mebibyte images under a 128 MiB bound with a 64 MiB quantum offload the oldest 65. const mib = 1024 * 1024 - const project = (count: number) => offloadRequestImagesWithPolicy([ - createUserMessage({ content: Array.from({ length: count }, () => image(mib)), source }), - ], { - representation: 'raw', - maxBytes: 128 * mib, - byteQuantum: 64 * mib, - placeholder: () => OMITTED, - })[0]?.content - - expect(project(128)?.filter(block => block.type === 'image')).toHaveLength(128) - expect(project(129)?.filter(block => block.type === 'text')).toHaveLength(65) - expect(project(192)?.filter(block => block.type === 'text')).toHaveLength(65) - expect(project(193)?.filter(block => block.type === 'text')).toHaveLength(129) + const budget = { ...raw, maxBytes: 128 * mib, byteQuantum: 64 * mib } + expect(requiredImageOffload(request(Array.from({ length: 129 }, () => mib)), budget, bytesOf)).toBe(65) }) - it('rounds a count excess up to a 20-image removal step', () => { - const projected = offloadRequestImagesWithPolicy([ - createUserMessage({ content: Array.from({ length: 601 }, () => image(1)), source }), - ], { - representation: 'raw', - maxImages: 600, - countQuantum: 20, - placeholder: () => OMITTED, - }) - expect(projected[0]?.content.filter(block => block.type === 'text')).toHaveLength(20) - expect(projected[0]?.content.filter(block => block.type === 'image')).toHaveLength(581) - }) - - it('uses route-owned request byte lengths when supplied', () => { - const messages = [createUserMessage({ content: [image(100), image(100)], source })] - const projected = offloadRequestImagesWithPolicy(messages, { - representation: 'raw', - maxBytes: 3, - byteLength: () => 2, - placeholder: () => OMITTED, - }) - expect(projected[0]?.content).toEqual([ - { type: 'text', text: OMITTED }, - image(100), - ]) - }) - - it('builds a distinct placeholder from each omitted attachment', () => { - const first = image(3) - const second = image(3) - first.attachment = { ...first.attachment, name: 'first.png' } - second.attachment = { ...second.attachment, name: 'second.png' } - const projected = offloadRequestImagesWithPolicy([ - createUserMessage({ content: [first, second], source }), - ], { - representation: 'raw', - maxBytes: 3, - placeholder: ref => `omitted:${ref.name}`, - }) - expect(projected[0]?.content).toEqual([ - { type: 'text', text: 'omitted:first.png' }, - second, - ]) + it('skips offloaded occurrences and accounts inline bytes by their base64 length', () => { + expect(requiredImageOffload(request([3, 3, 3]), { representation: 'base64', maxBytes: 8 }, bytesOf)).toBe(1) + expect(requiredImageOffload(request([3, 3, 3], [0]), { representation: 'base64', maxBytes: 8 }, bytesOf)).toBe(0) }) }) diff --git a/packages/llm/token-meter/README.i18n.yaml b/packages/llm/token-meter/README.i18n.yaml index 6d29caf63e..98030bed78 100644 --- a/packages/llm/token-meter/README.i18n.yaml +++ b/packages/llm/token-meter/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/token-meter/README.md -README.md: 266aa8c53ffcc849967f90469822503ff3c897a9 -README.zh.md: 9018daa31b5190e25732467ac027e8fffc918da9 +README.md: 37b9614a6c684a27d44b8b8bb65108029d58612f +README.zh.md: f2fb79132ec7deded9051a85db90cc1995a5f336 diff --git a/packages/llm/token-meter/README.md b/packages/llm/token-meter/README.md index 266aa8c53f..37b9614a6c 100644 --- a/packages/llm/token-meter/README.md +++ b/packages/llm/token-meter/README.md @@ -40,7 +40,7 @@ const { totalTokens, surfaceTokens, nodes } = ctx.tokenMeter.measure(session) const price = ctx.tokenMeter.estimateMessage(message) ``` -Each measurement resolves the effective envelope's provider/model through the optional `llm` service. Image occurrences use the routed request's visual-token price plus model-visible text when the adapter declares pricing; other routes keep the fixed heuristic. File occurrences use the exact route-independent handle text that the same `llm` service resolves for adapter dispatch, including its current execution-world path or explicit no-path message. Each node also carries route-independent `heuristicTokens` for replacement shadow prices. Provider usage is reused only when the latest successful call's canonical request envelope matches the measured envelope and its total is no lower than that call's full route-priced anchor; otherwise the complete current envelope and surface are estimated. Surface changes stay signed relative to a matching anchor repriced under the same route, including negative deltas after shrinking replacements. +Each measurement resolves the effective envelope's provider/model through the optional `llm` service. Image occurrences use the routed request's visual-token price plus model-visible text when the adapter declares pricing, and occurrences selected by an `image/offload` event are priced as the route's placeholder text exactly as the surface sends them; other routes keep the fixed heuristic. File occurrences use the exact route-independent handle text that the same `llm` service resolves for adapter dispatch, including its current execution-world path or explicit no-path message. Each node also carries route-independent `heuristicTokens` for replacement shadow prices. Provider usage is reused only when the latest successful call's canonical request envelope matches the measured envelope and its total is no lower than that call's full route-priced anchor; otherwise the complete current envelope and surface are estimated. Surface changes stay signed relative to a matching anchor repriced under the same route, including negative deltas after shrinking replacements. The measurement anchor includes the priced surface immediately before the successful `assistant/message`, including system and user messages admitted after `step/start` and replacements made before a retry. With unchanged durable output, the completed call has zero surface delta: its prompt is already included in provider usage. Later surface changes remain signed deltas against that anchor. @@ -48,6 +48,8 @@ The measurement anchor includes the priced surface immediately before the succes When the composition provides `ctx.sessionProjections`, token-meter registers three projection units. `tokenUsage` carries the complete durable log's `uncachedInputTokens`, `outputTokens`, `cacheReadTokens`, and `cacheWriteTokens`. A final assistant-message sample replaces streaming usage from the same attempt; `llm/retry-started` ends that replacement scope, so a retry in the same step contributes another billed attempt. `contextPressure` carries optional `pressureTokens` (the newest provider-reported prompt size), optional `projectedTokens` (what the next request's prompt would cost), and optional `contextWindow` from the newest `request/context` record. `contextBreakdown` carries heuristic `systemTokens`, `toolsTokens`, and `messageTokens` — the context's composition, not its provider-billed size. Unloading the plugin removes all three keys. +Image offload reprices existing node identities while preserving prior usage anchors. The fixed reference heuristic excludes `offloaded` metadata, so an offload decision does not change `contextBreakdown` or the scalar heuristic total; route-aware measurement replaces the selected visual prices with placeholder text. + `contextBreakdown` classifies the last nonempty surviving `system/message` in surface order as `systemTokens`; empty dormant nodes contribute nothing, and no nonempty system means zero. `messageTokens` includes every other visible node, including superseded prompts. Their sum always equals `measure().nodes[].heuristicTokens`, including after unmetered replacements, compaction, and per-node prompt clearing. `toolsTokens` follows the latest `request/header`. All three use the fixed heuristic, not route image pricing or file-handle projection; they are approximate composition, not billing or `projectedTokens`. `deriveTurnTokenUsage(events)` folds one complete turn into exact per-attempt and whole-turn usage for browser consumers. It returns no result when lifecycle evidence is missing, counts are unsafe, or exact totals conflict; each corresponding aggregate appears only when every participating attempt reports its optional cache, reasoning, or route value. @@ -98,7 +100,7 @@ Each `measure()` call synchronizes the fold to the current durable tail, then re ### Projection semantics -`contextBreakdown` retains plain-JSON `{ seq, heuristicTokens, system }` entries in surface order and reuses the measurement plan/commit fold. Its state and surface transitions are O(current retained surface), not O(1) and not O(total historical log); replaced entries and message bodies are not retained. State version 4 invalidates scalar checkpoints and replays the log. `contextPressure` remains the scalar shadow-price consumer: replacements without adjacent claims contribute zero delta. The usage fold retains one last-sample slot because legal logs never report usage for an earlier step after a later step reports usage. +`contextBreakdown` retains plain-JSON `{ seq, heuristicTokens, system }` entries in surface order and reuses the measurement plan/commit fold. Its state and surface transitions are O(current retained surface), not O(1) and not O(total historical log); replaced entries and message bodies are not retained. State version 5 invalidates checkpoints priced with offload metadata. `contextPressure` remains the scalar shadow-price consumer: replacements without adjacent claims contribute zero delta. The usage fold retains one last-sample slot because legal logs never report usage for an earlier step after a later step reports usage.
diff --git a/packages/llm/token-meter/README.zh.md b/packages/llm/token-meter/README.zh.md index 9018daa31b..f2fb79132e 100644 --- a/packages/llm/token-meter/README.zh.md +++ b/packages/llm/token-meter/README.zh.md @@ -40,7 +40,7 @@ const { totalTokens, surfaceTokens, nodes } = ctx.tokenMeter.measure(session) const price = ctx.tokenMeter.estimateMessage(message) ``` -每次测量都会通过可选的 `llm` 服务解析生效 envelope 的提供方/模型。适配器声明图片定价时,图片出现处使用路由请求的视觉 token 价格加模型可见文本;其他路由保持固定启发式规则。文件出现处使用同一个 `llm` 服务为适配器分发解析的确切、与路由无关的句柄文本,其中包含当前执行世界路径或明确的无路径说明。每个节点还携带与路由无关的 `heuristicTokens`,供替换影子价使用。只有当最新成功调用的规范请求 envelope 与已测量 envelope 匹配、且其总量不低于该调用完整路由定价锚点时,才复用提供方用量;否则会对完整当前 envelope 与表面做估算。表面变更保持相对于按同一路由重新定价的匹配锚点的带符号值,包括缩减替换后的负 delta。 +每次测量都会通过可选的 `llm` 服务解析生效 envelope 的提供方/模型。适配器声明图片定价时,图片出现处使用路由请求的视觉 token 价格加模型可见文本,`image/offload` 事件选中的出现位置则按路由的占位文本计价,与表层实际发送的一致;其他路由保持固定启发式规则。文件出现处使用同一个 `llm` 服务为适配器分发解析的确切、与路由无关的 句柄文本,其中包含当前执行世界路径或明确的无路径说明。每个节点还携带与路由无关的 `heuristicTokens`,供替换影子价使用。只有当最新成功调用的规范请求 envelope 与已测量 envelope 匹配、且其总量不低于该调用完整路由定价锚点时,才复用提供方用量;否则会对完整当前 envelope 与表面做估算。表面变更保持相对于按同一路由重新定价的匹配锚点的带符号值,包括缩减替换后的负 delta。 测量锚点包含成功的 `assistant/message` 之前的已计价表面,包括 `step/start` 之后接纳的系统与用户消息,以及重试之前执行的替换。持久输出未变时,完成调用的表面增量为零:其提示词已包含在提供方用量中。后续表面变更仍是相对于该锚点的带符号增量。 @@ -48,6 +48,8 @@ const price = ctx.tokenMeter.estimateMessage(message) 当组合提供 `ctx.sessionProjections` 时,token-meter 注册三个投影单元。`tokenUsage` 携带完整持久日志中的 `uncachedInputTokens`、`outputTokens`、`cacheReadTokens` 与 `cacheWriteTokens`。最终 assistant 消息样本会替换同一次尝试的流式用量;`llm/retry-started` 会结束该替换范围,因此同一步骤中的重试会贡献另一次计费用量。`contextPressure` 携带可选 `pressureTokens`(提供方报告的最新提示词规模)、可选 `projectedTokens`(下一个请求的提示词将花费多少)与来自最新一条 `request/context` 记录的可选 `contextWindow`。`contextBreakdown` 携带启发式 `systemTokens`、`toolsTokens` 与 `messageTokens`——上下文的构成,而非提供方计费规模。卸载插件会移除全部三个键。 +图片省略重新计算现有节点的价格,同时保留此前的用量锚点。固定引用启发式规则不计入 `offloaded` 元数据,因此一次省略决定不改变 `contextBreakdown` 或标量启发式总量,按路由的测量则把所选图片的视觉价格换成占位文本价格。 + `contextBreakdown` 把 surface 顺序中最后一个非空且存活的 `system/message` 归入 `systemTokens`;休眠的空节点不贡献 token,没有非空系统消息时为零。`messageTokens` 包含其余所有可见节点,包括被取代的提示词。两者之和始终等于 `measure().nodes[].heuristicTokens`,未计量替换、压缩和逐节点清空提示词之后也成立。`toolsTokens` 跟随最新 `request/header`。三个数字都使用固定启发式规则,而非路由图片定价或文件句柄投影;它们是近似构成,不是计费数据或 `projectedTokens`。 `deriveTurnTokenUsage(events)` 为浏览器消费方把一个完整轮次折叠为精确的逐次尝试与整轮用量。生命周期证据缺失、计数不安全或精确总量矛盾时不返回结果;只有每次参与的尝试都报告可选缓存、推理或路由值时,相应汇总才会出现。 @@ -98,7 +100,7 @@ const price = ctx.tokenMeter.estimateMessage(message) ### 投影语义 -`contextBreakdown` 按 surface 顺序保留纯 JSON 的 `{ seq, heuristicTokens, system }` 条目,并复用测量服务的 plan/commit fold。其状态与 surface 转换成本为 O(当前保留 surface),不是 O(1),也不是 O(完整历史日志);被替换条目和消息正文不保留。状态版本 4 使标量检查点失效并重放日志。`contextPressure` 仍是标量影子价消费方:没有相邻 claim 的替换贡献零增量。用量 fold 保留一个最后样本槽,因为合法日志不会在更晚步骤报告用量后再次报告更早步骤的用量。 +`contextBreakdown` 按 surface 顺序保留纯 JSON 的 `{ seq, heuristicTokens, system }` 条目,并复用测量服务的 plan/commit fold。其状态与 surface 转换成本为 O(当前保留 surface),不是 O(1),也不是 O(完整历史日志);被替换条目和消息正文不保留。状态版本 5 使计入省略元数据的检查点失效。`contextPressure` 仍是标量影子价消费方:没有相邻 claim 的替换贡献零增量。用量 fold 保留一个最后样本槽,因为合法日志不会在更晚步骤报告用量后再次报告更早步骤的用量。
diff --git a/packages/llm/token-meter/package.json b/packages/llm/token-meter/package.json index 44223f3a96..263910342e 100644 --- a/packages/llm/token-meter/package.json +++ b/packages/llm/token-meter/package.json @@ -37,7 +37,8 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", - "@deepseek-ai/dsh-session-projection": "workspace:^" + "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/dsh-compaction-image-offload": "workspace:^" }, "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:^", @@ -50,6 +51,7 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", - "@deepseek-ai/dsh-session-projection": "workspace:^" + "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/dsh-compaction-image-offload": "workspace:^" } } diff --git a/packages/llm/token-meter/src/breakdown-projection.ts b/packages/llm/token-meter/src/breakdown-projection.ts index d97338d47a..dd6d4a5d55 100644 --- a/packages/llm/token-meter/src/breakdown-projection.ts +++ b/packages/llm/token-meter/src/breakdown-projection.ts @@ -47,7 +47,7 @@ type ContextBreakdownState = z.infer */ export const contextBreakdownProjectionDefinition = { key: 'contextBreakdown', - stateVersion: 4, + stateVersion: 5, stateSchema: contextBreakdownStateSchema, init: (): ContextBreakdownState => ({ nodes: [], diff --git a/packages/llm/token-meter/src/estimate.ts b/packages/llm/token-meter/src/estimate.ts index c82d621242..99ebbb2aaf 100644 --- a/packages/llm/token-meter/src/estimate.ts +++ b/packages/llm/token-meter/src/estimate.ts @@ -21,11 +21,16 @@ export const ROLE_OVERHEAD = 4 /** * Structural JSON price of one block outside the typed pricing arms: the * fixed heuristic for merge-extended blocks and for image references, whose - * request price is route-owned rather than fixed. + * request price is route-owned rather than fixed. Image offload marks do not + * change this reference-only heuristic; route pricing owns their placeholders. * @param block - block to price without mutation. * @returns heuristic tokens for the block's JSON structure. */ export function estimateStructuralBlock(block: ContentBlock): number { + if (block.type === 'image') { + const { offloaded: _offloaded, ...reference } = block + return BLOCK_OVERHEAD + Math.ceil(JSON.stringify(reference).length / CHARS_PER_TOKEN) + } return BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN) } diff --git a/packages/llm/token-meter/src/index.ts b/packages/llm/token-meter/src/index.ts index 2693af5913..f721d26219 100644 --- a/packages/llm/token-meter/src/index.ts +++ b/packages/llm/token-meter/src/index.ts @@ -5,6 +5,7 @@ */ import { Context, Service } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-compaction-image-offload/projection' import z from '@deepseek-ai/schemastery' import { assembleAssistantStream } from '@deepseek-ai/dsh-llm' import type { LlmImageRequestPricing, LlmRuntime, Message, TokenUsage } from '@deepseek-ai/dsh-llm' @@ -249,6 +250,18 @@ export class TokenMeter extends Service { let nextAnchor = state.anchor switch (event.type) { + case 'image/offload': { + const offloaded = new Map(event.data.targets.map(target => [target.seq, new Set(target.imageIndexes)])) + state.surface = state.surface.map((node) => { + const indexes = offloaded.get(node.seq) + if (indexes === undefined) return node + return { + ...node, + images: node.images.map((image, index) => (indexes.has(index) ? { ...image, offloaded: true as const } : image)), + } + }) + break + } case 'request/header': nextHeader = canonicalHeader(event.data.header) break diff --git a/packages/llm/token-meter/src/surface-fold.ts b/packages/llm/token-meter/src/surface-fold.ts index d0cccea947..7cea95adb0 100644 --- a/packages/llm/token-meter/src/surface-fold.ts +++ b/packages/llm/token-meter/src/surface-fold.ts @@ -16,8 +16,7 @@ import { deriveEventMessage } from '@deepseek-ai/dsh-session' import type { SessionSeq, SurfaceEvent } from '@deepseek-ai/dsh-session' -import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm' -import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' +import type { ContentBlock, ImageBlock, Message } from '@deepseek-ai/dsh-llm' import { estimateMessage, estimateStructuralBlock } from './estimate.ts' type FileAttachmentRef = Extract['attachment'] @@ -32,8 +31,8 @@ export interface MeterSurfaceNode { readonly imageStructuralTokens: number /** Structural JSON price replaced when request assembly projects files to text. */ readonly fileStructuralTokens: number - /** Durable image occurrences in message order; empty for image-free nodes. */ - readonly images: readonly ImageAttachmentRef[] + /** Durable image blocks in message order, `offloaded` marks included; empty for image-free nodes. */ + readonly images: readonly ImageBlock[] /** Durable file occurrences in message order; empty for file-free nodes. */ readonly files: readonly FileAttachmentRef[] } @@ -53,14 +52,14 @@ export interface SurfaceTokenPlan { /** Collect projected attachment occurrences and their structural prices. */ function collectProjectedAttachments( blocks: readonly ContentBlock[], - images: ImageAttachmentRef[], + images: ImageBlock[], files: FileAttachmentRef[], ): { readonly imageTokens: number; readonly fileTokens: number } { let imageTokens = 0 let fileTokens = 0 for (const block of blocks) { if (block.type === 'image') { - images.push(block.attachment) + images.push(block) imageTokens += estimateStructuralBlock(block) } else if (block.type === 'file') { files.push(block.attachment) @@ -87,7 +86,7 @@ function analyzeNode(seq: SessionSeq, message: Message | null): MeterSurfaceNode } } const heuristicTokens = estimateMessage(message) - const images: ImageAttachmentRef[] = [] + const images: ImageBlock[] = [] const files: FileAttachmentRef[] = [] const structural = collectProjectedAttachments(message.content, images, files) return { diff --git a/packages/llm/token-meter/src/usage-projection.ts b/packages/llm/token-meter/src/usage-projection.ts index 815baff91d..2238316fab 100644 --- a/packages/llm/token-meter/src/usage-projection.ts +++ b/packages/llm/token-meter/src/usage-projection.ts @@ -172,7 +172,7 @@ export const tokenUsageProjectionDefinition = { */ export const contextPressureProjectionDefinition = { key: 'contextPressure', - stateVersion: 4, + stateVersion: 5, stateSchema: contextPressureStateSchema, init: () => ({ surfaceTokens: 0 }), apply: (state, event) => { diff --git a/packages/llm/token-meter/tests/context-breakdown-projection.spec.ts b/packages/llm/token-meter/tests/context-breakdown-projection.spec.ts index dc1fcfb19e..3c7081a9b2 100644 --- a/packages/llm/token-meter/tests/context-breakdown-projection.spec.ts +++ b/packages/llm/token-meter/tests/context-breakdown-projection.spec.ts @@ -405,7 +405,7 @@ describe('contextBreakdown session projection', () => { ctx.sessionProjections.checkpoint(session), )) as ReturnType const row = checkpoint['contextBreakdown']! - expect(row.ver).toBe(4) + expect(row.ver).toBe(5) expect(ctx.sessionProjections.viewCheckpoint(checkpoint).contextBreakdown).toEqual(projected(ctx, session)) const replacement = session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'summary' }], source: { kind: 'user' }, @@ -424,7 +424,7 @@ describe('contextBreakdown session projection', () => { stale, session.snapshotEvents(), SessionLogOffset(0), session.header, session.inheritedEventCount, ) expect(replayed.snapshot.values.contextBreakdown).toEqual(projected(ctx, session)) - expect(replayed.checkpoint['contextBreakdown']?.ver).toBe(4) + expect(replayed.checkpoint['contextBreakdown']?.ver).toBe(5) const invalid = { ...checkpoint, contextBreakdown: { @@ -458,7 +458,7 @@ describe('contextBreakdown session projection', () => { systemTokens: 8, toolsTokens: staleValue.toolsTokens, messageTokens: 9, }) expect(restored.checkpoint).toEqual(current) - expect(restored.checkpoint['contextBreakdown']?.ver).toBe(4) + expect(restored.checkpoint['contextBreakdown']?.ver).toBe(5) } finally { await ctx.fiber.dispose() } diff --git a/packages/llm/token-meter/tests/route-pricing.spec.ts b/packages/llm/token-meter/tests/route-pricing.spec.ts index 6488552253..360d1b6352 100644 --- a/packages/llm/token-meter/tests/route-pricing.spec.ts +++ b/packages/llm/token-meter/tests/route-pricing.spec.ts @@ -1,3 +1,4 @@ +import { imageOffloadProjection } from '@deepseek-ai/dsh-compaction-image-offload/projection' import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { @@ -85,7 +86,7 @@ async function harness(pricing: (model: string) => LlmImageRequestPricing | unde const llm = new LlmRuntime(ctx) llm.registerAdapter(['mock'], new PricingAdapter(pricing)) const meter = new TokenMeter(ctx) - return { ctx, meter, session: Session.create(SessionId('route-priced')) } + return { ctx, meter, session: Session.create(SessionId('route-priced'), undefined, undefined, undefined, [imageOffloadProjection]) } } /** Route price of one image-bearing message under the fixed pricing double. */ @@ -202,6 +203,37 @@ describe('request projection pricing', () => { expect(unknownRoute.nodes[0]!.tokens).toBe(estimateMessage(message)) }) + it('reprices logged offloads without changing surface node identities or heuristic totals', async () => { + const placeholder = '[offloaded]' + const markedPricing: LlmImageRequestPricing = { + priceImages: images => images.map(block => (block.offloaded === true + ? { visualTokens: 0, text: placeholder } + : { visualTokens: VISUAL_TOKENS, text: HANDLE_TEXT })), + } + const { ctx, meter, session } = await harness(() => markedPricing) + session.append('turn/start', { turn: 1 }) + const older = imageMessage('older') + const newer = imageMessage('newer') + const olderSeq = session.append('user/message', older, { surfaceOp: 'append' }).seq + session.append('user/message', newer, { surfaceOp: 'append' }) + session.append('request/header', { header: header('vision'), reason: 'initial' }) + const before = meter.measure(session) + expect(before.nodes.map(node => node.tokens)).toEqual([routedMessageTokens(older), routedMessageTokens(newer)]) + + session.append('image/offload', { targets: [{ seq: olderSeq, imageIndexes: [0] }] }) + const after = meter.measure(session) + const imageFree = estimateMessage({ ...older, content: older.content.filter(block => block.type !== 'image') }) + expect(after.nodes[0]!.tokens).toBe(imageFree + estimateContent([{ type: 'text', text: placeholder }])) + expect(after.nodes[1]!.tokens).toBe(routedMessageTokens(newer)) + expect(after.totalTokens).toBeLessThan(before.totalTokens) + expect(after.nodes.map(node => node.seq)).toEqual(before.nodes.map(node => node.seq)) + expect(after.nodes.map(node => node.heuristicTokens)).toEqual(before.nodes.map(node => node.heuristicTokens)) + const breakdown = ctx.sessionProjections.snapshot(session).values.contextBreakdown + expect(breakdown?.messageTokens).toBe(after.nodes.reduce((total, node) => total + node.heuristicTokens, 0)) + const restored = Session.create(SessionId('offloaded-restored'), session.snapshotEvents(), undefined, undefined, [imageOffloadProjection]) + expect(meter.measure(restored).surfaceTokens).toBe(after.surfaceTokens) + }) + it('fails loud when a route answers a mismatched occurrence count', async () => { const broken: LlmImageRequestPricing = { priceImages: () => [] } const { meter, session } = await harness(() => broken) @@ -211,6 +243,37 @@ describe('request projection pricing', () => { .toThrow('route image pricing answered 0 prices for 1 occurrences') }) + it('offloads one of two equal attachments without rewriting the prior usage anchor', async () => { + const placeholder = '[offloaded]' + const { ctx, meter, session } = await harness(() => ({ + priceImages: images => images.map(block => block.offloaded === true + ? { visualTokens: 0, text: placeholder } + : { visualTokens: VISUAL_TOKENS, text: HANDLE_TEXT }), + })) + try { + const ref = imageRef('same') + const source = session.append('user/message', createUserMessage({ + source: { kind: 'user' }, + content: [{ type: 'image', attachment: ref }, { type: 'image', attachment: ref }], + }), { surfaceOp: 'append' }) + appendSuccessfulCall(session, header('vision'), { inputTokens: 5000, outputTokens: 10 }) + const before = meter.measure(session) + session.append('image/offload', { targets: [{ seq: source.seq, imageIndexes: [0] }] }) + const after = meter.measure(session) + const saving = VISUAL_TOKENS + estimateContent([{ type: 'text', text: HANDLE_TEXT }]) + - estimateContent([{ type: 'text', text: placeholder }]) + expect(after.baseline).toEqual(before.baseline) + expect(after.surfaceDeltaTokens - before.surfaceDeltaTokens).toBe(-saving) + expect(after.totalTokens).toBe(before.totalTokens - saving) + expect(after.nodes[0]?.heuristicTokens).toBe(before.nodes[0]?.heuristicTokens) + const projection = session.deriveMessages()[0]! + expect(projection.content).toEqual([{ type: 'image', attachment: ref, offloaded: true }, { type: 'image', attachment: ref }]) + expect(estimateMessage(projection)).toBe(after.nodes[0]?.heuristicTokens) + } finally { + await ctx.fiber.dispose() + } + }) + it('prices nested tool-result images through the same route pricing', async () => { const { meter, session } = await harness(() => fixedPricing) const nested = createUserMessage({ diff --git a/packages/llm/token-meter/tests/token-usage-projection.spec.ts b/packages/llm/token-meter/tests/token-usage-projection.spec.ts index d57d3f8d11..34c00ab3dc 100644 --- a/packages/llm/token-meter/tests/token-usage-projection.spec.ts +++ b/packages/llm/token-meter/tests/token-usage-projection.spec.ts @@ -439,7 +439,7 @@ describe('contextPressure session projection', () => { const checkpoint = JSON.parse(JSON.stringify( ctx.sessionProjections.checkpoint(session), )) as ReturnType - expect(checkpoint.contextPressure?.ver).toBe(4) + expect(checkpoint.contextPressure?.ver).toBe(5) await meterFiber.dispose() expect(ctx.sessionProjections.snapshot(session).values).not.toHaveProperty('contextPressure') diff --git a/packages/llm/token-meter/tsconfig.json b/packages/llm/token-meter/tsconfig.json index 1d8de4abf6..51e81f5e71 100644 --- a/packages/llm/token-meter/tsconfig.json +++ b/packages/llm/token-meter/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../../session/session-projection" + }, + { + "path": "../../compaction/compaction-image-offload" } ] } diff --git a/packages/session-query/session-query-sqlite/src/index.ts b/packages/session-query/session-query-sqlite/src/index.ts index e0b24b3c39..7c95afa28d 100644 --- a/packages/session-query/session-query-sqlite/src/index.ts +++ b/packages/session-query/session-query-sqlite/src/index.ts @@ -662,8 +662,6 @@ export class SqliteSessionQueryEngine extends SessionQueryEngine { offset, ] assertPortableBindingCount(bindings.length) - // The browser fixture mirrors these rank keys in - // `packages/client/connection/src/client/fixture.ts`; update both together. return this._requireDb().prepare(` ${selected.sql}, filtered AS ( diff --git a/packages/session-query/session-query/package.json b/packages/session-query/session-query/package.json index e383b1b413..17eb37f1ae 100644 --- a/packages/session-query/session-query/package.json +++ b/packages/session-query/session-query/package.json @@ -58,5 +58,8 @@ "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-session-projection-cache": "workspace:^", "@deepseek-ai/cordis": "workspace:^" + }, + "dependencies": { + "@deepseek-ai/dsh-session-format-catalog": "workspace:^" } } diff --git a/packages/session-query/session-query/src/documents.ts b/packages/session-query/session-query/src/documents.ts index f51ae6b733..8788c8ea35 100644 --- a/packages/session-query/session-query/src/documents.ts +++ b/packages/session-query/session-query/src/documents.ts @@ -1,5 +1,6 @@ /** Shared event metadata and semantic-document projection. */ +import { currentSessionMessageProjections } from '@deepseek-ai/dsh-session-format-catalog/message-projections' import { foldSurface } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionId, SessionSeq } from '@deepseek-ai/dsh-session' import type { SessionEventRecord, SessionEventSearchDocument, SessionEventSurface } from './types.ts' @@ -56,7 +57,7 @@ export function buildSessionEventSearchDocuments( function classifySurface(events: readonly SessionEvent[]): Map { let folded: ReturnType try { - folded = foldSurface(events) + folded = foldSurface(events, currentSessionMessageProjections) } catch (error: unknown) { throw new SessionQueryError( /* v8 ignore next -- foldSurface throws Error instances */ diff --git a/packages/session-query/session-query/src/index.ts b/packages/session-query/session-query/src/index.ts index 44360042e9..838ce4c74f 100644 --- a/packages/session-query/session-query/src/index.ts +++ b/packages/session-query/session-query/src/index.ts @@ -4,6 +4,7 @@ * @module @deepseek-ai/dsh-session-query */ +import { currentSessionMessageProjections } from '@deepseek-ai/dsh-session-format-catalog/message-projections' import { Context, Service } from '@deepseek-ai/cordis' import { Session, @@ -187,6 +188,7 @@ export abstract class SessionQueryEngine extends Service { loaded.events, loaded.header, loaded.inheritedEventCount, + currentSessionMessageProjections, ) return { session: structuredClone(loaded.header), diff --git a/packages/session-query/session-query/src/tracing.ts b/packages/session-query/session-query/src/tracing.ts index 2424073e27..0cd3fccfbb 100644 --- a/packages/session-query/session-query/src/tracing.ts +++ b/packages/session-query/session-query/src/tracing.ts @@ -1,5 +1,6 @@ /** One-shot session-lineage and event-relationship tracing helpers. */ +import { currentSessionMessageProjections } from '@deepseek-ai/dsh-session-format-catalog/message-projections' import { foldSurface, isSurfaceEvent, snapshotSessionEvent } from '@deepseek-ai/dsh-session' import type { SessionEvent, @@ -183,7 +184,7 @@ function analyzeEventLog( ): EventLogAnalysis { let folded: ReturnType try { - folded = foldSurface(events) + folded = foldSurface(events, currentSessionMessageProjections) } catch (error: unknown) { throw new SessionQueryError( /* v8 ignore next -- foldSurface throws Error instances */ diff --git a/packages/session-query/session-query/tsconfig.json b/packages/session-query/session-query/tsconfig.json index ab19084ff9..3d6b2bd489 100644 --- a/packages/session-query/session-query/tsconfig.json +++ b/packages/session-query/session-query/tsconfig.json @@ -37,6 +37,9 @@ }, { "path": "../../session/session-projection-cache" + }, + { + "path": "../../session/session-format-catalog" } ] } diff --git a/packages/session/session-format-catalog/README.i18n.yaml b/packages/session/session-format-catalog/README.i18n.yaml index b009c83e2b..523dc45f64 100644 --- a/packages/session/session-format-catalog/README.i18n.yaml +++ b/packages/session/session-format-catalog/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-format-catalog/README.md -README.md: 8cebff66b4d788792075784dfc5c59a6da92a400 -README.zh.md: 5190c19927dfa917f7855f28a687711b724f6411 +README.md: 8232a28b2c2c31c0176adb62e07ed16a804791e8 +README.zh.md: 2c4c86da50640f672842d98e6cc254dca08a3470 diff --git a/packages/session/session-format-catalog/README.md b/packages/session/session-format-catalog/README.md index 8cebff66b4..8232a28b2c 100644 --- a/packages/session/session-format-catalog/README.md +++ b/packages/session/session-format-catalog/README.md @@ -44,7 +44,7 @@ Import `sessionFormatCatalog` from the package root. JSONL and fixture readers c Production historical reads select `{ recovery: 'recoverable', validation: 'transformed' }`. Worker and fixture verification select `{ recovery: 'strict', validation: 'current' }`. Transformed validation runs the released-current rules after migration but deliberately skips installed semantic validation for input that is already current. -The catalog contains all supported historical readers directly. A profile cannot add, remove, or reorder an edge by mounting a feature plugin. Its peer dependency on `dsh-session` supplies the installed current event vocabulary and current restoration rules, while historical edge validators remain frozen. +The catalog contains all supported historical readers directly. A profile cannot add, remove, or reorder an edge by mounting a feature plugin. Its peer dependency on `dsh-session` supplies the installed current event vocabulary and current restoration rules, while historical edge validators remain frozen. The browser-safe `./message-projections` export assembles current plugin-owned interpreters for detached constructors and surface folds; it does not mount recovery listeners. ----- diff --git a/packages/session/session-format-catalog/README.zh.md b/packages/session/session-format-catalog/README.zh.md index 5190c19927..2c4c86da50 100644 --- a/packages/session/session-format-catalog/README.zh.md +++ b/packages/session/session-format-catalog/README.zh.md @@ -44,7 +44,7 @@ const eventRecords = current.events.map(sessionFormatCatalog.encodeCurrentEvent) Production 历史读取使用 `{ recovery: 'recoverable', validation: 'transformed' }`。Worker 与 fixture 校验使用 `{ recovery: 'strict', validation: 'current' }`。Transformed validation 会在迁移后执行已发布 current 规则,但对已经是 current 的输入有意跳过已安装语义校验。 -该目录直接包含所有受支持的历史读取器。Profile 无法通过挂载功能插件来添加、移除或重新排列迁移边。它通过对 `dsh-session` 的对等依赖(peer dependency)获得已安装的当前事件词表与当前还原规则,而历史迁移边校验器保持冻结。 +该目录直接包含所有受支持的历史读取器。Profile 无法通过挂载功能插件来添加、移除或重新排列迁移边。它通过对 `dsh-session` 的对等依赖(peer dependency)获得已安装的当前事件词表与当前还原规则,而历史迁移边校验器保持冻结。浏览器安全的 `./message-projections` 导出为独立构造函数和 surface 折叠装配当前插件拥有的处理器,不挂载恢复监听器。 ----- diff --git a/packages/session/session-format-catalog/package.json b/packages/session/session-format-catalog/package.json index f3ea35a6c8..a34af0786d 100644 --- a/packages/session/session-format-catalog/package.json +++ b/packages/session/session-format-catalog/package.json @@ -19,10 +19,15 @@ "default": "./lib/index.js" }, "./src/*": "./src/*", - "./package.json": "./package.json" + "./package.json": "./package.json", + "./message-projections": { + "types": "./lib/types/message-projections.d.ts", + "default": "./lib/types/message-projections.js" + } }, "files": [ "lib/index.js", + "lib/types/**/*.js", "lib/types/**/*.d.ts" ], "license": "MIT", @@ -30,7 +35,8 @@ "@deepseek-ai/dsh-session-format": "workspace:^", "@deepseek-ai/dsh-session-format-v0-to-v1": "workspace:^", "@deepseek-ai/dsh-session-format-v1-to-v2": "workspace:^", - "@deepseek-ai/dsh-session-format-v2-to-v3": "workspace:^" + "@deepseek-ai/dsh-session-format-v2-to-v3": "workspace:^", + "@deepseek-ai/dsh-compaction-image-offload": "workspace:^" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^", diff --git a/packages/session/session-format-catalog/src/current.ts b/packages/session/session-format-catalog/src/current.ts index a8b0f3f011..70fb574590 100644 --- a/packages/session/session-format-catalog/src/current.ts +++ b/packages/session/session-format-catalog/src/current.ts @@ -8,6 +8,7 @@ import { } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' import type { SessionFormatArtifact, SessionFormatHeader } from '@deepseek-ai/dsh-session-format' +import { currentSessionMessageProjections } from './message-projections.ts' /** * Validate current logical metadata through the installed Session package. @@ -46,5 +47,6 @@ export function validateInstalledCurrentSessionArtifact(artifact: SessionFormatA artifact.header as unknown as SessionHeader, SessionLogOffset(artifact.inheritedEventCount), 'detached', + currentSessionMessageProjections, ) } diff --git a/packages/session/session-format-catalog/src/message-projections.ts b/packages/session/session-format-catalog/src/message-projections.ts new file mode 100644 index 0000000000..d011783c12 --- /dev/null +++ b/packages/session/session-format-catalog/src/message-projections.ts @@ -0,0 +1,7 @@ +/** Pure first-party message interpreters for detached current-format replay, including browser readers. */ + +import { imageOffloadProjection } from '@deepseek-ai/dsh-compaction-image-offload/projection' +import type { SessionMessageProjection } from '@deepseek-ai/dsh-session/surface' + +/** Installed interpretation definitions; recovery listeners are mounted separately by their owning plugins. */ +export const currentSessionMessageProjections: readonly SessionMessageProjection[] = [imageOffloadProjection] diff --git a/packages/session/session-format-catalog/tests/catalog.spec.ts b/packages/session/session-format-catalog/tests/catalog.spec.ts index 0c1d748615..335f406e89 100644 --- a/packages/session/session-format-catalog/tests/catalog.spec.ts +++ b/packages/session/session-format-catalog/tests/catalog.spec.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest' import type { SessionFormatEvent } from '@deepseek-ai/dsh-session-format' import { sessionFormatCatalog } from '../src/index.ts' +import { currentSessionMessageProjections } from '../src/message-projections.ts' +import { MESSAGE_PROJECTION_EVENT_TYPES } from '@deepseek-ai/dsh-session/src/known-event-types.ts' +import { validateInstalledCurrentSessionArtifact } from '../src/current.ts' +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import { createUserMessage } from '@deepseek-ai/dsh-llm' function deepFreeze(value: T): T { if (value !== null && typeof value === 'object') { @@ -11,6 +16,17 @@ function deepFreeze(value: T): T { } describe('first-party Session format catalog', () => { + it('supplies every required current message interpreter and validates its durable references', () => { + expect(currentSessionMessageProjections.map(projection => projection.type).sort()) + .toEqual([...MESSAGE_PROJECTION_EVENT_TYPES].sort()) + const session = Session.create(SessionId('projection-catalog')) + session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'no image' }], source: { kind: 'user' } }), { surfaceOp: 'append' }) + const artifact = { + header: { ...session.header, delegationDepth: 0 }, inheritedEventCount: 0, + events: [...session.snapshotEvents() as unknown as SessionFormatEvent[], { type: 'image/offload', seq: 1, time: 0, data: { targets: [{ seq: 0, imageIndexes: [0] }] } }], + } + expect(() => { validateInstalledCurrentSessionArtifact(artifact) }).toThrow(/image index 0 does not exist/) + }) it('statically owns the complete adjacent v0 to v3 chain', () => { const header = { type: 'session', diff --git a/packages/session/session-format-catalog/tsconfig.json b/packages/session/session-format-catalog/tsconfig.json index cba30e5c55..42a9cc2ff3 100644 --- a/packages/session/session-format-catalog/tsconfig.json +++ b/packages/session/session-format-catalog/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "../session-format-v2-to-v3" + }, + { + "path": "../../compaction/compaction-image-offload" } ] } diff --git a/packages/session/session-persistence-jsonl/src/generation.ts b/packages/session/session-persistence-jsonl/src/generation.ts index 1e1fd9aa0f..1e5ded6771 100644 --- a/packages/session/session-persistence-jsonl/src/generation.ts +++ b/packages/session/session-persistence-jsonl/src/generation.ts @@ -7,6 +7,7 @@ * @module @deepseek-ai/dsh-session-persistence-jsonl/generation */ +import { currentSessionMessageProjections } from '@deepseek-ai/dsh-session-format-catalog/message-projections' import { createHash, randomBytes } from 'node:crypto' import { link as fsLink, @@ -536,6 +537,7 @@ async function verifyCurrentGeneration( generation.meta, generation.inheritedEventCount, 'detached', + currentSessionMessageProjections, ) assertCurrentAssistantStreams(generation.events) return { diff --git a/packages/session/session-telemetry-otel/src/index.ts b/packages/session/session-telemetry-otel/src/index.ts index 0722845a3b..48792dbe67 100644 --- a/packages/session/session-telemetry-otel/src/index.ts +++ b/packages/session/session-telemetry-otel/src/index.ts @@ -259,7 +259,7 @@ export class OpenTelemetrySessionBackend extends SessionTelemetryBackend { if (committed === undefined) return const session = Session.fromRestore( snapshot.meta.id, snapshot.events, snapshot.meta, snapshot.inheritedEventCount, - 'detached', + 'detached', ctx.sessions.messageProjections, ) // fromRestore appends a lifecycle marker that this submission did not commit. if (isFeedback(session, committed)) coordinator.captureSession(session, committed.seq) diff --git a/packages/shell/pwsh-local/tests/executor.spec.ts b/packages/shell/pwsh-local/tests/executor.spec.ts index 227eac7b6a..1e21231a26 100644 --- a/packages/shell/pwsh-local/tests/executor.spec.ts +++ b/packages/shell/pwsh-local/tests/executor.spec.ts @@ -188,6 +188,7 @@ describe('resolvePwshPath and candidatePwshPaths (pure, every platform)', () => describe('spawn construction (pure, every platform)', () => { /** A subprocess service that records spawn specs and settles instantly. */ class CapturingSubprocessRuntime extends SubprocessRuntime { + async terminalEnvironment() { return { platform: 'posix' as const } } specs: SubprocessSpawnSpec[] = [] done: Promise = Promise.resolve({ exitCode: 0, signal: null }) stderrText = '' diff --git a/packages/ssh/ssh/src/helper-processes.ts b/packages/ssh/ssh/src/helper-processes.ts index e52945378b..c672a2deba 100644 --- a/packages/ssh/ssh/src/helper-processes.ts +++ b/packages/ssh/ssh/src/helper-processes.ts @@ -331,6 +331,19 @@ export class RemoteProcesses { return terminal.signalForeground(z.enum(['SIGINT', 'SIGTERM', 'SIGKILL', 'SIGTSTP', 'SIGHUP']).parse(value)) } + /** + * Resize an allocated terminal without replacing its process. + * @param id - terminal reservation. + * @param cols - positive terminal width. + * @param rows - positive terminal height. + * @returns after the local provider accepts the dimensions. + */ + async resizeTerminal(id: SshProcessId, cols: number, rows: number): Promise { + const terminal = this.record(id).terminal + if (terminal === undefined) throw new Error('SSH handle does not own a terminal') + await terminal.resize(cols, rows) + } + /** Stop every owned process on lease expiry or disconnect. */ async close(): Promise { if (this.closing) return diff --git a/packages/ssh/ssh/src/helper.ts b/packages/ssh/ssh/src/helper.ts index 61c1c82404..519d68a4a2 100644 --- a/packages/ssh/ssh/src/helper.ts +++ b/packages/ssh/ssh/src/helper.ts @@ -6,13 +6,14 @@ import type { Readable, Writable } from 'node:stream' import { Context } from '@deepseek-ai/cordis' import { FsError, type FsTarget, type FsWriteIntent, type FsVersion } from '@deepseek-ai/dsh-fs' import { SandboxedFileSystem } from '@deepseek-ai/dsh-fs-sandbox' +import { SubprocessExecutableNotFoundError } from '@deepseek-ai/dsh-subprocess' import { LocalSubprocessRuntime } from '@deepseek-ai/dsh-subprocess-local' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy' import { SessionProjectionRegistry } from '@deepseek-ai/dsh-session-projection' import type { SandboxExecutionPolicy, SandboxPolicy } from '@deepseek-ai/dsh-sandbox' import { z } from 'zod' -import { SshRpcPeer, SSH_MAX_PROCESS_HANDLES, SSH_MAX_TEXT_STREAMS, SSH_PROTOCOL_VERSION } from './protocol.ts' +import { SshRpcPeer, RemoteOperationError, SSH_MAX_PROCESS_HANDLES, SSH_MAX_TEXT_STREAMS, SSH_PROTOCOL_VERSION } from './protocol.ts' import { RemoteProcesses } from './helper-processes.ts' import { editSchema, environmentSchema, intentSchema, policySchema, processIdSchema, remotePath, targetSchema, textStreamIdSchema } from './schemas.ts' import type { SshTextStreamId } from './schemas.ts' @@ -114,6 +115,14 @@ export async function runSshHelper(transport: HelperTransport): Promise { if (method === 'process.done') return processes.done(processIdRequest.parse(raw).id) if (method === 'process.wait') return processes.wait(processIdRequest.parse(raw).id, signal) if (method === 'process.terminate') { await processes.terminate(processIdRequest.parse(raw).id); return null } + if (method === 'terminal.environment') { object.parse(raw); return ctx.subprocess.terminalEnvironment(signal) } + if (method === 'terminal.resize') { + const input = z.object({ + id: processIdSchema, cols: z.number().int().positive(), rows: z.number().int().positive(), + }).strict().parse(raw) + await processes.resizeTerminal(input.id, input.cols, input.rows) + return null + } if (method === 'terminal.write' || method === 'terminal.inspect' || method === 'terminal.signal') { const input = z.object({ id: processIdSchema, value: z.string().optional() }).strict().parse(raw) return processes.terminal(input.id, method === 'terminal.write' ? 'write' : method === 'terminal.inspect' ? 'inspect' : 'signal', input.value) @@ -123,7 +132,12 @@ export async function runSshHelper(transport: HelperTransport): Promise { const env = input.env === undefined ? undefined : Object.fromEntries( Object.entries(input.env).filter((entry): entry is [string, string] => entry[1] !== null), ) - return ctx.subprocess.resolveExecutable(input.command, env, signal) + try { + return await ctx.subprocess.resolveExecutable(input.command, env, signal) + } catch (error) { + if (error instanceof SubprocessExecutableNotFoundError) throw new RemoteOperationError(error.message, 'SUBPROCESS_EXECUTABLE_NOT_FOUND') + throw error + } } if (method === 'sandbox') { const input = z.object({ argv: z.array(z.string()).min(1), policy: policySchema }).strict().parse(raw) diff --git a/packages/ssh/ssh/src/schemas.ts b/packages/ssh/ssh/src/schemas.ts index 23b754067e..1b7db7dc92 100644 --- a/packages/ssh/ssh/src/schemas.ts +++ b/packages/ssh/ssh/src/schemas.ts @@ -48,7 +48,9 @@ export const spawnSchema = z.object({ stderr: z.union([z.literal('pipe'), z.literal('inherit'), collection]), control: z.literal('pipe').optional(), }).strict().optional(), - terminal: z.object({ rows: z.number().int().positive(), cols: z.number().int().positive() }).strict().optional(), + terminal: z.object({ + terminalType: z.string().min(1), rows: z.number().int().positive(), cols: z.number().int().positive(), + }).strict().optional(), }).strict().refine(value => (value.stdio === undefined) !== (value.terminal === undefined), 'select ordinary or terminal execution') /** Connection handshake binds sockets and workspace to one helper process. */ export const helloSchema = z.object({ protocol: z.literal(1), hash: z.string().regex(/^[0-9a-f]{64}$/), platform: z.enum(['linux', 'darwin']), nodeVersion: z.string(), node: remotePath, root: remotePath, workspace: remotePath, bootstrapHash: z.string().regex(/^[0-9a-f]{64}$/).optional() }).strict() diff --git a/packages/ssh/ssh/tests/helper-processes.spec.ts b/packages/ssh/ssh/tests/helper-processes.spec.ts index a7b42bd9d8..c94ffb7c8a 100644 --- a/packages/ssh/ssh/tests/helper-processes.spec.ts +++ b/packages/ssh/ssh/tests/helper-processes.spec.ts @@ -19,7 +19,7 @@ vi.mock('node:fs/promises', async (original) => { const outcome = { exitCode: 0, signal: null } const ordinaryRequest = { argv: ['target'], cwd: '/tmp', graceMs: 20, stdio: { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' } } -const terminalRequest = { argv: ['target'], cwd: '/tmp', graceMs: 20, terminal: { rows: 24, cols: 80 } } +const terminalRequest = { argv: ['target'], cwd: '/tmp', graceMs: 20, terminal: { terminalType: 'dumb', rows: 24, cols: 80 } } async function harness() { const root = await mkdtemp('/tmp/dsh-ssh-owner-') @@ -76,10 +76,12 @@ function terminal() { const inspectForeground = vi.fn(async () => undefined) const signalForeground = vi.fn(async () => 42) const terminate = vi.fn(async () => { output.end(); completion.resolve(outcome) }) + const resize = vi.fn(async (_cols: number, _rows: number) => {}) const handle: SubprocessTerminalHandle = { + resize, pid: 42, output, done: completion.promise, write, inspectForeground, signalForeground, terminate, } - return { handle, completion, output, write, inspectForeground, signalForeground, terminate } + return { handle, completion, output, write, resize, inspectForeground, signalForeground, terminate } } describe.skipIf(process.platform === 'win32')('SSH helper process settlement', () => { @@ -99,6 +101,8 @@ describe.skipIf(process.platform === 'win32')('SSH helper process settlement', ( await expect(test.owner.start(run.id)).rejects.toThrow('already requested') await test.owner.terminal(run.id, 'write', 'input') expect(child.write).toHaveBeenCalledWith('input') + await test.owner.resizeTerminal(run.id, 120, 40) + expect(child.resize).toHaveBeenCalledWith(120, 40) expect(await test.owner.terminal(run.id, 'inspect')).toBeNull() child.inspectForeground.mockResolvedValueOnce({ processGroupId: 42, inputWaiting: true }) expect(await test.owner.terminal(run.id, 'inspect')).toEqual({ processGroupId: 42, inputWaiting: true }) @@ -144,6 +148,7 @@ describe.skipIf(process.platform === 'win32')('SSH helper process settlement', ( const run = await test.prepare({ ...ordinaryRequest, env: { VALUE: 'kept', REMOVED: null }, stdio: { ...ordinaryRequest.stdio, stdin: 'pipe' } }) await expect(test.owner.done(run.id)).rejects.toThrow('has not started') await expect(test.owner.wait(run.id)).rejects.toThrow('was not started') + await expect(test.owner.resizeTerminal(run.id, 80, 24)).rejects.toThrow('does not own a terminal') const bytes: Buffer[] = [] child.stdin.on('data', (chunk: Buffer) => { bytes.push(Buffer.from(chunk)) }) const ended = once(child.stdin, 'end') diff --git a/packages/ssh/ssh/tests/helper-runtime.spec.ts b/packages/ssh/ssh/tests/helper-runtime.spec.ts index 1e4aa21070..3336986a8e 100644 --- a/packages/ssh/ssh/tests/helper-runtime.spec.ts +++ b/packages/ssh/ssh/tests/helper-runtime.spec.ts @@ -93,6 +93,9 @@ describe.skipIf(process.platform === 'win32')('SSH helper runtime', () => { await expect(test.client.request('sandbox', { argv: ['true'], policy: { mode: 'danger-full-access', workspaceRoot: test.root } }, z.unknown())).rejects.toThrow('does not need') expect(await test.client.request('executable', { command: process.execPath, env: { REMOVED: null } }, z.string())).toBe(process.execPath) expect(await test.client.request('executable', { command: process.execPath }, z.string())).toBe(process.execPath) + expect(await test.client.request('terminal.environment', {}, z.unknown())).toMatchObject({ platform: 'posix' }) + await expect(test.client.request('executable', { command: `${test.root}/absent` }, z.string())).rejects.toMatchObject({ code: 'SUBPROCESS_EXECUTABLE_NOT_FOUND' }) + await expect(test.client.request('executable', { command: './relative' }, z.string())).rejects.toThrow('relative') const wrapped = await test.client.request('sandbox', { argv: ['true'], policy: policy(test.root) }, z.looseObject({ argv: z.array(z.string()), enforcement: z.enum(['full', 'partial']) })) expect(wrapped.argv).toEqual(['confined', 'true']) expect(confine).toHaveBeenCalledWith(['true'], policy(test.root), expect.any(AbortSignal)) diff --git a/packages/ssh/ssh/tests/live.e2e.ts b/packages/ssh/ssh/tests/live.e2e.ts index 045ea794d6..6fd71de3c5 100644 --- a/packages/ssh/ssh/tests/live.e2e.ts +++ b/packages/ssh/ssh/tests/live.e2e.ts @@ -191,7 +191,7 @@ describe.skipIf(!enabled)('POSIX SSH runtime acceptance', () => { const test = await setup() try { const terminal = await test.ctx.subprocess.spawnTerminal({ - argv: ['/bin/bash', '--noprofile', '--norc', '-i'], cwd: test.root, rows: 24, cols: 80, graceMs: 500, + terminalType: 'dumb', argv: ['/bin/bash', '--noprofile', '--norc', '-i'], cwd: test.root, rows: 24, cols: 80, graceMs: 500, }) const output: string[] = [] const received = Promise.withResolvers() diff --git a/packages/ssh/ssh/tests/stream-capability.spec.ts b/packages/ssh/ssh/tests/stream-capability.spec.ts index fc1ca16187..9729a9f50d 100644 --- a/packages/ssh/ssh/tests/stream-capability.spec.ts +++ b/packages/ssh/ssh/tests/stream-capability.spec.ts @@ -21,7 +21,7 @@ describe.skipIf(process.platform === 'win32')('SSH stream capabilities', () => { const owner = new RemoteProcesses(new Context(), root, 1, 5000) let stream: Socket | undefined try { - const prepared = await owner.prepare({ argv: ['true'], cwd: root, graceMs: 100, terminal: { rows: 24, cols: 80 } }) + const prepared = await owner.prepare({ argv: ['true'], cwd: root, graceMs: 100, terminal: { terminalType: 'dumb', rows: 24, cols: 80 } }) const endpoint = prepared.streams.terminal! const raw = createConnection(endpoint.path) await once(raw, 'connect') @@ -40,7 +40,7 @@ describe.skipIf(process.platform === 'win32')('SSH stream capabilities', () => { const owner = new RemoteProcesses(new Context(), root, 1, 5000) const sockets: Socket[] = [] try { - const prepared = await owner.prepare({ argv: ['true'], cwd: root, graceMs: 100, terminal: { rows: 24, cols: 80 } }) + const prepared = await owner.prepare({ argv: ['true'], cwd: root, graceMs: 100, terminal: { terminalType: 'dumb', rows: 24, cols: 80 } }) const endpoint = prepared.streams.terminal! const raw = [createConnection(endpoint.path), createConnection(endpoint.path)] await Promise.all(raw.map(socket => once(socket, 'connect'))) diff --git a/packages/ssh/ssh/tests/stream-lifetime.spec.ts b/packages/ssh/ssh/tests/stream-lifetime.spec.ts index 9f88a2224e..2feeb1d52e 100644 --- a/packages/ssh/ssh/tests/stream-lifetime.spec.ts +++ b/packages/ssh/ssh/tests/stream-lifetime.spec.ts @@ -12,7 +12,7 @@ describe('SSH TLS cancellation ownership', () => { const root = await mkdtemp('/tmp/dsh-ssh-tls-life-') const owner = new RemoteProcesses(new Context(), root, 1, 5000) try { - const prepared = await owner.prepare({ argv: ['true'], cwd: root, graceMs: 100, terminal: { rows: 24, cols: 80 } }) + const prepared = await owner.prepare({ argv: ['true'], cwd: root, graceMs: 100, terminal: { terminalType: 'dumb', rows: 24, cols: 80 } }) const endpoint = prepared.streams.terminal! const raw = createConnection({ path: endpoint.path, allowHalfOpen: true }) raw.on('error', () => {}) diff --git a/packages/ssh/subprocess-ssh/README.i18n.yaml b/packages/ssh/subprocess-ssh/README.i18n.yaml index df7872748e..4b47b611a5 100644 --- a/packages/ssh/subprocess-ssh/README.i18n.yaml +++ b/packages/ssh/subprocess-ssh/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/ssh/subprocess-ssh/README.md -README.md: ea647cd15b97ae281e145c5ac3afbf1e4cc4740c -README.zh.md: 455da9cade52540eb69581ddc3ec858553c444a5 +README.md: 15afa74f041e9944036c451dacaaedb53ecd9ff9 +README.zh.md: 3417f184fbcb702edeadf3b510663b1a9a58192f diff --git a/packages/ssh/subprocess-ssh/README.md b/packages/ssh/subprocess-ssh/README.md index ea647cd15b..15afa74f04 100644 --- a/packages/ssh/subprocess-ssh/README.md +++ b/packages/ssh/subprocess-ssh/README.md @@ -33,6 +33,8 @@ A direct exit reported by `done` does not prove managed-range quiescence; `waitF ----- +Terminal shell facts and executable verification come from the remote helper. Lookup misses remain distinct from SSH failures. PTY creation forwards the caller's `terminalType`, and `resize()` updates the remote terminal without replacing its process. + ## Understand the implementation diff --git a/packages/ssh/subprocess-ssh/README.zh.md b/packages/ssh/subprocess-ssh/README.zh.md index 455da9cade..3417f184fb 100644 --- a/packages/ssh/subprocess-ssh/README.zh.md +++ b/packages/ssh/subprocess-ssh/README.zh.md @@ -33,6 +33,8 @@ kind: "package-reference" ----- +终端 shell 信息与可执行文件验证来自远端 helper。查找未命中与 SSH 故障分别报告。PTY 创建传递调用方的 `terminalType`,`resize()` 更新远端终端尺寸,不替换进程。 + ## 理解实现 diff --git a/packages/ssh/subprocess-ssh/src/index.ts b/packages/ssh/subprocess-ssh/src/index.ts index da69932102..ea2ba64907 100644 --- a/packages/ssh/subprocess-ssh/src/index.ts +++ b/packages/ssh/subprocess-ssh/src/index.ts @@ -2,13 +2,13 @@ import { Duplex, PassThrough, type Readable, type Writable } from 'node:stream' import type { Socket } from 'node:net' import { Context } from '@deepseek-ai/cordis' -import { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess' -import type { SubprocessCollectedOutputs, SubprocessHandle, SubprocessOutcome, SubprocessOutputMode, SubprocessSpawnSpec, SubprocessTerminalHandle, SubprocessTerminalSignal, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess' +import { SubprocessRuntime, SubprocessExecutableNotFoundError } from '@deepseek-ai/dsh-subprocess' +import type { SubprocessCollectedOutputs, SubprocessHandle, SubprocessOutcome, SubprocessOutputMode, SubprocessSpawnSpec, SubprocessTerminalHandle, SubprocessTerminalEnvironment, SubprocessTerminalSignal, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess' import { OutputCollector } from '@deepseek-ai/dsh-subprocess-local/output' import type { SshConnection } from '@deepseek-ai/dsh-ssh' import { doneSchema, foregroundSchema, outputSnapshotFrameLimit, outputSnapshotSchema, preparedSchema, remotePath, streamEndpointSchema } from '@deepseek-ai/dsh-ssh/schemas' import type { SshProcessId } from '@deepseek-ai/dsh-ssh/schemas' -import { SshRpcPeer } from '@deepseek-ai/dsh-ssh/protocol' +import { SshRpcPeer, RemoteOperationError } from '@deepseek-ai/dsh-ssh/protocol' import { z } from 'zod' function environment(env?: NodeJS.ProcessEnv): Record | undefined { @@ -252,8 +252,23 @@ export class SshSubprocessRuntime extends SubprocessRuntime { }) } - override resolveExecutable(command: string, env?: Readonly>, signal?: AbortSignal): Promise { - return this.ctx.ssh.request('executable', { command, env }, remotePath, signal) + override async resolveExecutable(command: string, env?: Readonly>, signal?: AbortSignal): Promise { + try { + return await this.ctx.ssh.request('executable', { command, env }, remotePath, signal) + } catch (error) { + if (error instanceof RemoteOperationError && error.code === 'SUBPROCESS_EXECUTABLE_NOT_FOUND') { + throw new SubprocessExecutableNotFoundError(error.message, { cause: error }) + } + throw error + } + } + + override terminalEnvironment(signal?: AbortSignal): Promise { + return this.ctx.ssh.request('terminal.environment', {}, z.object({ + platform: z.enum(['posix', 'windows']), defaultShell: z.string().optional(), + }).strict().transform(value => ({ platform: value.platform, + ...(value.defaultShell === undefined ? {} : { defaultShell: value.defaultShell }), + })), signal) } override spawn(spec: SubprocessSpawnSpec): SubprocessHandle { @@ -279,7 +294,7 @@ export class SshSubprocessRuntime extends SubprocessRuntime { const ssh = this.ctx.ssh const prepared = await ssh.request('process.prepare', { argv: spec.argv, cwd: spec.cwd, env: environment(spec.env), graceMs: spec.graceMs, - terminal: { rows: spec.rows, cols: spec.cols }, + terminal: { rows: spec.rows, cols: spec.cols, terminalType: spec.terminalType }, }, preparedSchema, signal) const id = prepared.id let socket: Socket | undefined @@ -298,6 +313,7 @@ export class SshSubprocessRuntime extends SubprocessRuntime { const abort = (): void => { void handle.terminate().catch(() => { void ssh.dispose().catch(() => {}) }) } const handle: SubprocessTerminalHandle = { pid: started.pid, output, done, + resize: async (cols, rows) => { await ssh.request('terminal.resize', { id, cols, rows }, z.null()) }, write: async (data) => { await ssh.request('terminal.write', { id, value: data }, z.null()) }, inspectForeground: async () => await ssh.request('terminal.inspect', { id }, foregroundSchema) ?? undefined, signalForeground: (signal: SubprocessTerminalSignal) => ssh.request('terminal.signal', { id, value: signal }, z.number().int().positive()), diff --git a/packages/ssh/subprocess-ssh/tests/lifecycle.spec.ts b/packages/ssh/subprocess-ssh/tests/lifecycle.spec.ts index 8f62ee1cea..4444a7e5eb 100644 --- a/packages/ssh/subprocess-ssh/tests/lifecycle.spec.ts +++ b/packages/ssh/subprocess-ssh/tests/lifecycle.spec.ts @@ -5,7 +5,7 @@ import { describe, expect, it, vi } from 'vitest' import { SshSubprocessRuntime } from '../src/index.ts' const id = 'a7b17d9c-5ebf-40d8-9e82-3d1ddae8517b' -const spec = { argv: ['bash'], cwd: '/workspace', rows: 24, cols: 80, graceMs: 100 } +const spec = { terminalType: 'dumb', argv: ['bash'], cwd: '/workspace', rows: 24, cols: 80, graceMs: 100 } describe('SSH terminal allocation lifecycle', () => { it.each(['prepare', 'connect', 'start'])('joins a pending %s before completing provider disposal', async (stage) => { diff --git a/packages/ssh/subprocess-ssh/tests/shell-discovery.spec.ts b/packages/ssh/subprocess-ssh/tests/shell-discovery.spec.ts new file mode 100644 index 0000000000..7af2872b28 --- /dev/null +++ b/packages/ssh/subprocess-ssh/tests/shell-discovery.spec.ts @@ -0,0 +1,58 @@ +/** Shell facts and lookup failures retain remote execution semantics. */ +import { Context } from '@deepseek-ai/cordis' +import { SubprocessExecutableNotFoundError } from '@deepseek-ai/dsh-subprocess' +import { RemoteOperationError } from '@deepseek-ai/dsh-ssh/protocol' +import { expect, it, vi } from 'vitest' +import { z } from 'zod' +import { SshSubprocessRuntime } from '../src/index.ts' + +it('validates remote shell facts and distinguishes a lookup miss from transport failure', async () => { + const ctx = new Context() + let facts: unknown = { platform: 'posix', defaultShell: '/remote/bin/zsh' } + let failure: Error | undefined + const request = vi.fn(async (method: string, _params: unknown, schema: z.ZodType): Promise => { + if (failure !== undefined) throw failure + return schema.parse(method === 'terminal.environment' ? facts : '/remote/bin/bash') + }) + ctx.provide('ssh', { request } as never) + const runtime = new SshSubprocessRuntime(ctx) + const signal = new AbortController().signal + try { + expect(await runtime.terminalEnvironment(signal)).toEqual(facts) + expect(request).toHaveBeenCalledWith('terminal.environment', {}, expect.anything(), signal) + facts = { platform: 'posix' } + expect(await runtime.terminalEnvironment()).toEqual(facts) + facts = { platform: 'untrusted' } + await expect(runtime.terminalEnvironment()).rejects.toBeInstanceOf(z.ZodError) + expect(await runtime.resolveExecutable('bash', undefined, signal)).toBe('/remote/bin/bash') + failure = new RemoteOperationError('not installed', 'SUBPROCESS_EXECUTABLE_NOT_FOUND') + await expect(runtime.resolveExecutable('absent')).rejects.toBeInstanceOf(SubprocessExecutableNotFoundError) + failure = new RemoteOperationError('permission', 'EACCES') + await expect(runtime.resolveExecutable('bash')).rejects.toBe(failure) + failure = new Error('SSH disconnected') + await expect(runtime.resolveExecutable('bash')).rejects.toBe(failure) + } finally { await ctx.fiber.dispose() } +}) + +it.skipIf(process.platform === 'win32')('queries the real helper and resizes its PTY without replacing the shell', async () => { + const { createHelperHarness } = await import('../../ssh/tests/fixtures/helper.ts') + const helper = await createHelperHarness() + const ctx = new Context() + ctx.provide('ssh', helper.connection as never) + const runtime = new SshSubprocessRuntime(ctx) + try { + expect(await runtime.terminalEnvironment()).toMatchObject({ platform: 'posix' }) + await expect(runtime.resolveExecutable(`${helper.root}/absent`)).rejects.toBeInstanceOf(SubprocessExecutableNotFoundError) + const terminal = await runtime.spawnTerminal({ argv: ['/bin/sh', '-i'], cwd: helper.root, cols: 80, rows: 24, terminalType: 'xterm-256color', graceMs: 100 }) + let output = '' + terminal.output.on('data', (chunk: Buffer) => { output += chunk.toString() }) + await terminal.resize(120, 40) + await terminal.write('printf "REMOTE_TERM:%s\\n" "$TERM"; stty size\n') + await expect.poll(() => output).toContain('REMOTE_TERM:xterm-256color') + await expect.poll(() => output).toMatch(/40\s+120/u) + await terminal.terminate() + await terminal.done + } finally { + try { await ctx.fiber.dispose() } finally { await helper.close() } + } +}) diff --git a/packages/ssh/subprocess-ssh/tests/terminal-behavior.spec.ts b/packages/ssh/subprocess-ssh/tests/terminal-behavior.spec.ts index b29902f6e4..7ec4e4b15c 100644 --- a/packages/ssh/subprocess-ssh/tests/terminal-behavior.spec.ts +++ b/packages/ssh/subprocess-ssh/tests/terminal-behavior.spec.ts @@ -8,7 +8,7 @@ import { z } from 'zod' import { SshSubprocessRuntime } from '../src/index.ts' const id = 'aa910b47-e7d7-467b-8421-3331569dd02b' -const spec: SubprocessTerminalSpawnSpec = { argv: ['bash'], cwd: '/remote/workspace', rows: 24, cols: 80, graceMs: 100 } +const spec: SubprocessTerminalSpawnSpec = { argv: ['bash'], cwd: '/remote/workspace', terminalType: 'dumb', rows: 24, cols: 80, graceMs: 100 } const completion = { outcome: { exitCode: 0, signal: null }, spills: {}, collected: {} } async function setup(options: { @@ -42,7 +42,7 @@ async function setup(options: { if (options.terminateFailure !== undefined) throw options.terminateFailure finished.resolve(completion) value = null - } else if (method === 'terminal.write') value = null + } else if (method === 'terminal.write' || method === 'terminal.resize') value = null else if (method === 'terminal.inspect') value = foreground else if (method === 'terminal.signal') value = 321 else throw new Error(`Unexpected terminal request ${method}`) @@ -89,13 +89,15 @@ describe('SSH terminal behavior', () => { test.remote.write('terminal output') expect(String((await data)[0])).toBe('terminal output') await handle.write('input\n') + await handle.resize(120, 40) + expect(test.calls.find(call => call.method === 'terminal.resize')?.params).toEqual({ id, cols: 120, rows: 40 }) expect(await handle.inspectForeground()).toBeUndefined() test.setForeground({ processGroupId: 321, inputWaiting: true }) expect(await handle.inspectForeground()).toEqual({ processGroupId: 321, inputWaiting: true }) expect(await handle.signalForeground('SIGINT')).toBe(321) expect(test.calls.find(call => call.method === 'process.prepare')?.params).toEqual({ argv: ['bash'], cwd: spec.cwd, env: { KEEP: 'value' }, graceMs: 100, - terminal: { rows: 24, cols: 80 }, + terminal: { terminalType: 'dumb', rows: 24, cols: 80 }, }) expect(test.calls.find(call => call.method === 'terminal.write')?.params).toEqual({ id, value: 'input\n' }) expect(test.calls.find(call => call.method === 'terminal.signal')?.params).toEqual({ id, value: 'SIGINT' }) diff --git a/packages/subagent/subagent/tests/list-children.spec.ts b/packages/subagent/subagent/tests/list-children.spec.ts index 6d83648d23..2794f825bd 100644 --- a/packages/subagent/subagent/tests/list-children.spec.ts +++ b/packages/subagent/subagent/tests/list-children.spec.ts @@ -35,10 +35,12 @@ import { seedStoredSession } from './persistence-helpers.ts' type Script = ConstructorParameters[0] const roots: string[] = [] +const projectionCacheDisposers: Array<() => Promise> = [] const persistenceDisposers: Array<() => Promise> = [] const projCacheRoots: string[] = [] afterEach(async () => { + await Promise.all(projectionCacheDisposers.splice(0).map(dispose => dispose())) await Promise.all(persistenceDisposers.splice(0).map(dispose => dispose())) for (const root of projCacheRoots.splice(0)) rmSync(root, { recursive: true, force: true }) for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) @@ -64,7 +66,8 @@ async function setup( await ctx.plugin(Storage) await ctx.plugin({ name: storageJsonName, inject: storageJsonInject, apply: storageJsonApply, Config: storageJsonConfig }, { root }) await ctx.plugin({ name: storageDomainName, inject: storageDomainInject, apply: storageDomainApply, Config: storageDomainConfig }, { backend: 'json' }) - await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) + const cache = await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) + projectionCacheDisposers.push(() => cache.dispose()) } await ctx.plugin(TestSessionQuery) await ctx.plugin(SubagentRuntime) diff --git a/packages/subprocess/subprocess-local/README.i18n.yaml b/packages/subprocess/subprocess-local/README.i18n.yaml index 8bc3e74eb6..4e2a3943ad 100644 --- a/packages/subprocess/subprocess-local/README.i18n.yaml +++ b/packages/subprocess/subprocess-local/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subprocess/subprocess-local/README.md -README.md: 7673b72a3afeef7f411c7e11bfacdc65ead299be -README.zh.md: 7960e9e75cf99444a91bed421621a9df36872cc7 +README.md: 5379aff31cdcf036614a6664ce59330d319bd459 +README.zh.md: f61596ac94b7f1270275cb93593c90a281c9ca90 diff --git a/packages/subprocess/subprocess-local/README.md b/packages/subprocess/subprocess-local/README.md index 7673b72a3a..5379aff31c 100644 --- a/packages/subprocess/subprocess-local/README.md +++ b/packages/subprocess/subprocess-local/README.md @@ -29,7 +29,7 @@ Mount the provider beside its consumers and start processes exactly as the subpr ### Mounting the provider -Load the provider in the same composition as its consumers. It has no config fields: every choice arrives on the spawn request, so deployment-varying decisions stay with the caller's configuration. +Load the provider in the same composition as its consumers. It has no config fields: every choice arrives on the spawn request, so deployment-varying decisions stay with the caller's configuration. `terminalEnvironment()` reads a nonempty `SHELL` on POSIX, falling back to the account login shell, or a nonempty `ComSpec` on Windows. Empty values are omitted so the consumer can choose its platform fallback. ```yaml - name: '@deepseek-ai/dsh-subprocess-local' @@ -121,6 +121,8 @@ Read these pages when the provider-level contract is not enough. They move from ----- +Terminal allocation advertises the caller-provided `terminalType` through TERM and node-pty. Dynamic resize updates the existing PTY. Output backpressure pauses native reads until the consumer drains; explicit termination resumes a paused reader to receive the exit notification. + ## Model Experience diff --git a/packages/subprocess/subprocess-local/README.zh.md b/packages/subprocess/subprocess-local/README.zh.md index 7960e9e75c..f61596ac94 100644 --- a/packages/subprocess/subprocess-local/README.zh.md +++ b/packages/subprocess/subprocess-local/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" ### 挂载提供方 -在与消费方相同的组合中加载本提供方。它没有任何配置字段:每项选择都随 spawn 请求到达,因此随部署变化的决策留在调用方的配置里。 +在与消费方相同的组合中加载本提供方。它没有任何配置字段:每项选择都随 spawn 请求到达,因此随部署变化的决策留在调用方的配置里。 `terminalEnvironment()` 在 POSIX 读取非空的 `SHELL`,缺失时使用账户登录 shell;在 Windows 读取非空的 `ComSpec`。空值会被省略,由消费者选择平台回退。 ```yaml - name: '@deepseek-ai/dsh-subprocess-local' @@ -121,6 +121,8 @@ spill 文件以 `0600` 权限、`O_EXCL` 与随机名称在 `0700` 每进程目 ----- +终端分配通过 TERM 和 node-pty 使用调用者指定的 `terminalType`。动态 resize 更新已有 PTY。输出背压会暂停原生读取,待消费者排空后恢复;显式终止会恢复暂停的读取,以接收退出通知。 + ## 模型体验 diff --git a/packages/subprocess/subprocess-local/src/index.ts b/packages/subprocess/subprocess-local/src/index.ts index 0a7a9a15c5..44a15359cb 100644 --- a/packages/subprocess/subprocess-local/src/index.ts +++ b/packages/subprocess/subprocess-local/src/index.ts @@ -10,16 +10,18 @@ import { constants } from 'node:fs' import { access, stat } from 'node:fs/promises' +import { userInfo } from 'node:os' import { delimiter, extname, isAbsolute, resolve } from 'node:path' import type { Duplex } from 'node:stream' import { Context } from '@deepseek-ai/cordis' import * as nodePty from 'node-pty' import type { IPtyForkOptions } from 'node-pty' -import { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess' +import { SubprocessRuntime, SubprocessExecutableNotFoundError } from '@deepseek-ai/dsh-subprocess' import type { SubprocessHandle, SubprocessSpawnSpec, SubprocessTerminalHandle, + SubprocessTerminalEnvironment, SubprocessTerminalSpawnSpec, } from '@deepseek-ai/dsh-subprocess' import { @@ -156,7 +158,7 @@ export class LocalSubprocessRuntime extends SubprocessRuntime { } } signal?.throwIfAborted() - throw new Error(absolute + throw new SubprocessExecutableNotFoundError(absolute ? `subprocess-local: command ${JSON.stringify(command)} is not an executable file` : `subprocess-local: command ${JSON.stringify(command)} was not found on PATH`) } @@ -240,6 +242,15 @@ export class LocalSubprocessRuntime extends SubprocessRuntime { ) } + /** @inheritdoc */ + // oxlint-disable-next-line typescript/require-await -- Keep the provider promise rejection semantics for cancelled inspection. + async terminalEnvironment(signal?: AbortSignal): Promise { + signal?.throwIfAborted() + const platform = process.platform === 'win32' ? 'windows' : 'posix' + const defaultShell = platform === 'windows' ? process.env.ComSpec || undefined : process.env.SHELL || userInfo().shell || undefined + return { platform, ...defaultShell === undefined ? {} : { defaultShell } } + } + // Local PTY allocation is synchronous, but the provider contract permits remote asynchronous allocation. // oxlint-disable-next-line typescript/require-await -- Preserve promise rejection semantics at the async provider contract. async spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise { @@ -250,11 +261,11 @@ export class LocalSubprocessRuntime extends SubprocessRuntime { spec.signal?.throwIfAborted() const env = targetEnvironment(spec) const options: IPtyForkOptions = { - name: 'dumb', + name: spec.terminalType, rows: spec.rows, cols: spec.cols, cwd: spec.cwd, - env, + env: { ...env, TERM: spec.terminalType }, } const inspector = this.terminalInspector ?? createProcessInspector() const containmentMode = this.selectContainmentMode('terminal') @@ -262,7 +273,7 @@ export class LocalSubprocessRuntime extends SubprocessRuntime { ? prepareLinuxTerminalScope(spec, { ...env, PWD: spec.cwd, - TERM: 'dumb', + TERM: spec.terminalType, }) : undefined if (scope !== undefined) { diff --git a/packages/subprocess/subprocess-local/src/terminal.ts b/packages/subprocess/subprocess-local/src/terminal.ts index 8d165dbd20..3f9f9c9811 100644 --- a/packages/subprocess/subprocess-local/src/terminal.ts +++ b/packages/subprocess/subprocess-local/src/terminal.ts @@ -65,6 +65,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle { private cleanup: Promise | undefined private managedOwnerCleaned = false private exited = false + private outputPaused = false private trackedDescendants: ProcessIdentity[] = [] /** The spawned shell's start identity; scans stop adopting members once the root pid no longer carries it. */ private readonly rootIdentity: ProcessIdentity | undefined @@ -86,7 +87,19 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle { this.pid = terminal.pid this.rootIdentity = inspector.snapshot().tree(this.pid).find(member => member.pid === this.pid) this.done = this.outcome.promise - this.dataDisposable = terminal.onData((data) => { this.output.write(Buffer.from(data, 'utf8')) }) + const resume = (): void => { + if (!this.outputPaused) return + this.outputPaused = false + if (!this.exited) terminal.resume() + } + this.output.on('drain', resume) + this.output.once('close', () => { this.output.off('drain', resume) }) + this.dataDisposable = terminal.onData((data) => { + if (!this.output.write(Buffer.from(data, 'utf8')) && this.cleanup === undefined && !this.outputPaused) { + this.outputPaused = true + terminal.pause() + } + }) this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => { if (this.exited) return this.exited = true @@ -115,6 +128,12 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle { this.terminal.write(data) } + // oxlint-disable-next-line typescript/require-await -- Provider operations share promise rejection semantics. + async resize(cols: number, rows: number): Promise { + if (this.exited) throw new Error('terminal process has exited') + this.terminal.resize(cols, rows) + } + // Local inspection is synchronous; the seam returns a promise for remote transports. // oxlint-disable-next-line typescript/require-await -- Preserve promise rejection semantics at the async provider contract. async inspectForeground(): Promise { @@ -154,6 +173,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle { terminate(): Promise { if (this.cleanup !== undefined) return this.cleanup + if (this.outputPaused) { this.outputPaused = false; this.terminal.resume() } const cleanup = this.closeOnce() this.cleanup = cleanup void cleanup.catch(() => { this.cleanup = undefined }) diff --git a/packages/subprocess/subprocess-local/tests/fixtures/process-exit-host.ts b/packages/subprocess/subprocess-local/tests/fixtures/process-exit-host.ts index 721c5134c3..080b503152 100644 --- a/packages/subprocess/subprocess-local/tests/fixtures/process-exit-host.ts +++ b/packages/subprocess/subprocess-local/tests/fixtures/process-exit-host.ts @@ -48,6 +48,7 @@ if (kind === 'ordinary') { cwd: process.cwd(), rows: 24, cols: 80, + terminalType: 'dumb', graceMs: 30_000, }) } diff --git a/packages/subprocess/subprocess-local/tests/linux-scope.spec.ts b/packages/subprocess/subprocess-local/tests/linux-scope.spec.ts index 99a2890ae1..091edb8884 100644 --- a/packages/subprocess/subprocess-local/tests/linux-scope.spec.ts +++ b/packages/subprocess/subprocess-local/tests/linux-scope.spec.ts @@ -288,10 +288,14 @@ describe('Linux scope establishment and quiescence', () => { result.owner.cleanup?.() }) - it('accepts request consumption followed by rapid --collect unload as stopped', async () => { + it.each([undefined, 'pipe'] as const)('accepts request consumption and rapid --collect unload with control %s', async (control) => { const states = [activeUnit(), unloadedUnit()] - const { child, result, requestPath } = launch(async () => states.shift() ?? missingUnit()) - expect(consumeLinuxLaunchRequest(requestPath)).toEqual({ cwd: '/target', env: { TARGET: 'yes' } }) + const controlOptions = control === undefined ? {} : { control } + const { child, result, requestPath } = launch(async () => states.shift() ?? missingUnit(), {}, { + ...spec(), stdio: { ...spec().stdio, ...controlOptions }, + }) + expect(consumeLinuxLaunchRequest(requestPath)).toEqual({ cwd: '/target', env: { TARGET: 'yes' }, ...controlOptions }) + expect(result.control).toBe(control === undefined ? undefined : child.control) const waiting = result.owner.waitForExit() child.exit(0, null) await expect(result.direct).resolves.toEqual({ exitCode: 0, signal: null }) @@ -953,6 +957,7 @@ describe('Linux PTY bootstrap reuse', () => { env: { TARGET: 'yes' }, rows: 24, cols: 80, + terminalType: 'dumb', graceMs: 100, } as const diff --git a/packages/subprocess/subprocess-local/tests/local.spec.ts b/packages/subprocess/subprocess-local/tests/local.spec.ts index d15ce5e2ea..ef04b338f9 100644 --- a/packages/subprocess/subprocess-local/tests/local.spec.ts +++ b/packages/subprocess/subprocess-local/tests/local.spec.ts @@ -1,4 +1,6 @@ import { PassThrough } from 'node:stream' +import os from 'node:os' +import { syncBuiltinESMExports } from 'node:module' import { describe, expect, it, vi } from 'vitest' import { basename, dirname, relative, resolve } from 'node:path' import { Context } from '@deepseek-ai/cordis' @@ -41,6 +43,57 @@ function spec(command: string, overrides: Partial = {}): Su } describe('LocalSubprocessRuntime', () => { + it('discovers the platform shell without inventing a missing default and honors cancellation', async () => { + let loginShell: string | null = '/account/shell' + const userInfo = vi.spyOn(os, 'userInfo').mockImplementation(() => ({ + uid: 1, gid: 1, username: 'terminal-user', homedir: '/home/terminal-user', shell: loginShell, + })) + let fiber: Awaited> | undefined + let restorePlatform: (() => void) | undefined + try { + syncBuiltinESMExports() + const ctx = new Context() + fiber = await ctx.plugin(LocalSubprocessRuntime) + const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux') + restorePlatform = () => { platform.mockRestore() } + vi.stubEnv('SHELL', '/environment/shell') + await expect(ctx.subprocess.terminalEnvironment()).resolves.toEqual({ + platform: 'posix', defaultShell: '/environment/shell', + }) + expect(userInfo).not.toHaveBeenCalled() + vi.stubEnv('SHELL', undefined) + await expect(ctx.subprocess.terminalEnvironment()).resolves.toEqual({ + platform: 'posix', defaultShell: '/account/shell', + }) + vi.stubEnv('SHELL', '') + await expect(ctx.subprocess.terminalEnvironment()).resolves.toEqual({ platform: 'posix', defaultShell: '/account/shell' }) + loginShell = '' + await expect(ctx.subprocess.terminalEnvironment()).resolves.toEqual({ platform: 'posix' }) + loginShell = null + await expect(ctx.subprocess.terminalEnvironment()).resolves.toEqual({ platform: 'posix' }) + platform.mockReturnValue('win32') + vi.stubEnv('ComSpec', 'C:\\Windows\\System32\\cmd.exe') + await expect(ctx.subprocess.terminalEnvironment()).resolves.toEqual({ + platform: 'windows', defaultShell: 'C:\\Windows\\System32\\cmd.exe', + }) + vi.stubEnv('ComSpec', undefined) + await expect(ctx.subprocess.terminalEnvironment()).resolves.toEqual({ platform: 'windows' }) + vi.stubEnv('ComSpec', '') + await expect(ctx.subprocess.terminalEnvironment()).resolves.toEqual({ platform: 'windows' }) + platform.mockReturnValue('linux') + userInfo.mockClear() + const reason = new Error('terminal inspection cancelled') + await expect(ctx.subprocess.terminalEnvironment(AbortSignal.abort(reason))).rejects.toBe(reason) + expect(userInfo).not.toHaveBeenCalled() + } finally { + restorePlatform?.() + vi.unstubAllEnvs() + userInfo.mockRestore() + syncBuiltinESMExports() + await fiber?.dispose() + } + }) + it('places the host-exit finalizer before listeners that predate the service', async () => { const baseline = new Set(process.listeners('exit')) const prior = vi.fn() @@ -224,7 +277,7 @@ describe('LocalSubprocessRuntime', () => { const ctx = new Context() const fiber = await ctx.plugin(LocalSubprocessRuntime) const base: SubprocessTerminalSpawnSpec = { - argv: ['bash'], cwd: process.cwd(), rows: 24, cols: 80, graceMs: 10, + argv: ['bash'], cwd: process.cwd(), rows: 24, cols: 80, terminalType: 'dumb', graceMs: 10, } await expect(ctx.subprocess.spawnTerminal({ ...base, argv: [] })).rejects.toThrow('must contain a program') await expect(ctx.subprocess.spawnTerminal({ ...base, argv: [''] })).rejects.toThrow('must contain a program') @@ -241,6 +294,7 @@ describe('LocalSubprocessRuntime', () => { output: new PassThrough(), done: Promise.resolve({ exitCode: 0, signal: null }), write: async () => {}, + resize: async () => {}, inspectForeground: async () => undefined, signalForeground: async () => 1, terminate, @@ -268,6 +322,7 @@ describe('LocalSubprocessRuntime', () => { output: new PassThrough(), done: Promise.resolve({ exitCode: 0, signal: null }), write: async () => {}, + resize: async () => {}, inspectForeground: async () => undefined, signalForeground: async () => 1, terminate: vi.fn(async () => { throw firstFailure }), @@ -320,6 +375,7 @@ describe('LocalSubprocessRuntime', () => { output: new PassThrough(), done: Promise.resolve({ exitCode: 0, signal: null }), write: async () => {}, + resize: async () => {}, inspectForeground: async () => undefined, signalForeground: async () => 1, terminate: vi.fn(async () => { throw failure }), @@ -399,7 +455,7 @@ describe('LocalSubprocessRuntime', () => { const fiber = await ctx.plugin(IsolatedLocalSubprocessRuntime) const service = ctx.subprocess as InstanceType const handle = await ctx.subprocess.spawnTerminal({ - argv: ['shell'], cwd: process.cwd(), rows: 24, cols: 80, graceMs: 1, + argv: ['shell'], cwd: process.cwd(), rows: 24, cols: 80, terminalType: 'dumb', graceMs: 1, }) expect((service as unknown as { terminals: Set }).terminals.size).toBe(1) exitListener?.({ exitCode: 0 }) @@ -498,6 +554,7 @@ describe('LocalSubprocessRuntime', () => { cwd: targetCwd, rows: 24, cols: 80, + terminalType: 'dumb', graceMs: 10, env: { PWD: '/stale-parent-cwd', TERM: 'xterm-256color', TARGET_VALUE: 'preserved' }, }) @@ -593,7 +650,7 @@ describe('LocalSubprocessRuntime', () => { runtime.terminalInspector = inspector await expect(runtime.spawnTerminal({ - argv: ['shell'], cwd: process.cwd(), rows: 24, cols: 80, graceMs: 10, + argv: ['shell'], cwd: process.cwd(), rows: 24, cols: 80, terminalType: 'dumb', graceMs: 10, })).rejects.toBe(launchFailure) expect(cleanup).toHaveBeenCalledOnce() } finally { @@ -643,7 +700,7 @@ describe('LocalSubprocessRuntime', () => { signalProcess: () => {}, } const handle = await ctx.subprocess.spawnTerminal({ - argv: ['shell'], cwd: process.cwd(), rows: 24, cols: 80, graceMs: 1, + argv: ['shell'], cwd: process.cwd(), rows: 24, cols: 80, terminalType: 'dumb', graceMs: 1, }) const terminate = vi.spyOn(handle, 'terminate') exitListener?.({ exitCode: 0 }) diff --git a/packages/subprocess/subprocess-local/tests/native-containment.spec.ts b/packages/subprocess/subprocess-local/tests/native-containment.spec.ts index 8b4eb7369f..d64240044b 100644 --- a/packages/subprocess/subprocess-local/tests/native-containment.spec.ts +++ b/packages/subprocess/subprocess-local/tests/native-containment.spec.ts @@ -253,6 +253,7 @@ while :; do sleep 60; done cwd: scratch, rows: 24, cols: 80, + terminalType: 'dumb', graceMs: 100, }) const output = captureTerminalOutput(handle) diff --git a/packages/subprocess/subprocess-local/tests/terminal.spec.ts b/packages/subprocess/subprocess-local/tests/terminal.spec.ts index 7b02470f7c..bf34abdd79 100644 --- a/packages/subprocess/subprocess-local/tests/terminal.spec.ts +++ b/packages/subprocess/subprocess-local/tests/terminal.spec.ts @@ -13,6 +13,9 @@ import type { SubprocessTerminalSignal } from '@deepseek-ai/dsh-subprocess' class FakePty { pid = 123 + readonly pause = vi.fn() + readonly resume = vi.fn() + readonly resize = vi.fn() readonly writes: string[] = [] readonly kills: string[] = [] autoExitOnKill = true @@ -114,6 +117,41 @@ function makeHandle(pty: FakePty, inspector: ProcessInspector, graceMs: number): } describe('LocalTerminalHandle', () => { + it('pauses native output until the consumer drains and resumes before termination', async () => { + const pty = new FakePty() + const handle = makeHandle(pty, new FakeInspector(), 10) + const chunk = 'x'.repeat(handle.output.readableHighWaterMark + handle.output.writableHighWaterMark) + pty.emitData(chunk) + expect(pty.pause).toHaveBeenCalledOnce() + const chunks: Buffer[] = [] + handle.output.on('data', (data: Buffer) => { chunks.push(data) }) + await vi.waitFor(() => { expect(pty.resume).toHaveBeenCalledOnce() }) + expect(Buffer.concat(chunks).toString()).toBe(chunk) + handle.output.pause() + pty.emitData(chunk) + expect(pty.pause).toHaveBeenCalledTimes(2) + await handle.terminate() + handle.output.emit('drain') + expect(pty.resume).toHaveBeenCalledTimes(2) + handle.output.destroy() + }) + + it('does not resume native output when a pending drain follows the PTY exit', async () => { + const pty = new FakePty() + const handle = makeHandle(pty, new FakeInspector(), 10) + try { + pty.emitData('x'.repeat(handle.output.readableHighWaterMark + handle.output.writableHighWaterMark)) + expect(pty.pause).toHaveBeenCalledOnce() + pty.emitExit(0) + await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null }) + handle.output.emit('drain') + expect(pty.resume).not.toHaveBeenCalled() + } finally { + await handle.terminate() + handle.output.destroy() + } + }) + it('terminates a managed range with TERM when it stops within the grace period', async () => { vi.useFakeTimers() const pty = new FakePty() @@ -419,6 +457,8 @@ describe('LocalTerminalHandle', () => { pty.emitData('hello €') await handle.write('input\r') + await handle.resize(100, 30) + expect(pty.resize).toHaveBeenCalledWith(100, 30) expect(pty.writes).toEqual(['input\r']) expect(await handle.inspectForeground()).toEqual({ processGroupId: 456, inputWaiting: true }) expect(inspector.stdinChecks).toEqual([[456, 123]]) @@ -427,6 +467,7 @@ describe('LocalTerminalHandle', () => { pty.emitExit(7, 9) pty.emitExit(0) + await expect(handle.resize(80, 24)).rejects.toThrow('terminal process has exited') expect(await handle.done).toEqual({ exitCode: null, signal: 'SIGKILL' }) await handle.terminate() expect(Buffer.concat(chunks).toString('utf8')).toBe('hello €') diff --git a/packages/subprocess/subprocess/README.i18n.yaml b/packages/subprocess/subprocess/README.i18n.yaml index bac6edcaea..13d8005605 100644 --- a/packages/subprocess/subprocess/README.i18n.yaml +++ b/packages/subprocess/subprocess/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subprocess/subprocess/README.md -README.md: ccb48051981f57eee9830cf9d3696d0293cae5d8 -README.zh.md: eca1530759ee923c7b588983330b9bcf22fbaf45 +README.md: 5ce671d0fc25eb2acab80ea364f7ad94a0ce3d8f +README.zh.md: c8d1f0ea1ea80f6bd23845c8b9b7e7d505561b81 diff --git a/packages/subprocess/subprocess/README.md b/packages/subprocess/subprocess/README.md index ccb4805198..5ce671d0fc 100644 --- a/packages/subprocess/subprocess/README.md +++ b/packages/subprocess/subprocess/README.md @@ -127,6 +127,8 @@ Read these pages when the package-level contract is not enough. They move from t ----- +Terminal consumers use `terminalEnvironment()` to read the provider platform and preferred shell, and `resolveExecutable()` to verify candidates. A completed lookup miss throws `SubprocessExecutableNotFoundError`; transport failures remain distinct. `spawnTerminal` requires `terminalType` and initial dimensions, and its handle supports `resize(cols, rows)` without reallocating the process. + ## Model Experience diff --git a/packages/subprocess/subprocess/README.zh.md b/packages/subprocess/subprocess/README.zh.md index eca1530759..c8d1f0ea1e 100644 --- a/packages/subprocess/subprocess/README.zh.md +++ b/packages/subprocess/subprocess/README.zh.md @@ -127,6 +127,8 @@ spawn 会立即返回活动句柄,而不公开目标身份。`done` 独立报 ----- +终端消费者通过 `terminalEnvironment()` 读取 provider 平台和首选 shell,通过 `resolveExecutable()` 验证候选。确定未找到可执行文件时抛出 `SubprocessExecutableNotFoundError`,传输故障仍单独报告。`spawnTerminal` 要求 `terminalType` 和初始尺寸,返回的 handle 通过 `resize(cols, rows)` 调整尺寸,不重新分配进程。 + ## 模型体验 diff --git a/packages/subprocess/subprocess/src/index.ts b/packages/subprocess/subprocess/src/index.ts index 0cda343664..c914b48ede 100644 --- a/packages/subprocess/subprocess/src/index.ts +++ b/packages/subprocess/subprocess/src/index.ts @@ -12,7 +12,7 @@ import { Context, Service } from '@deepseek-ai/cordis' import { proxyEnvironmentForChild } from '@deepseek-ai/dsh-http-proxy' import { DSH_ENV_PREFIX } from './types.ts' import type { SubprocessHandle, SubprocessSpawnSpec } from './types.ts' -import type { SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from './types.ts' +import type { SubprocessTerminalEnvironment, SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from './types.ts' export { DSH_ENV_PREFIX } from './types.ts' export type { @@ -30,6 +30,7 @@ export type { SubprocessStdinMode, SubprocessStdio, SubprocessTerminalForeground, + SubprocessTerminalEnvironment, SubprocessTerminalHandle, SubprocessTerminalSignal, SubprocessTerminalSpawnSpec, @@ -134,6 +135,13 @@ export abstract class SubprocessRuntime extends Service { signal?: AbortSignal, ): Promise + /** + * Inspect shell-selection facts in the provider's execution environment. + * @param signal - cancellation of remote environment inspection. + * @returns platform and preferred shell; executable lookup and allocation remain separate operations. + */ + abstract terminalEnvironment(signal?: AbortSignal): Promise + /** * Start one managed child process from a fully-specified spec; this seam * applies no defaults. @@ -154,3 +162,15 @@ export abstract class SubprocessRuntime extends Service { } export default SubprocessRuntime + +/** Executable lookup completed without finding an executable file. */ +export class SubprocessExecutableNotFoundError extends Error { + /** + * @param message - provider-specific lookup diagnostic. + * @param options - original provider failure, when available. + */ + constructor(message: string, options?: ErrorOptions) { + super(message, options) + this.name = 'SubprocessExecutableNotFoundError' + } +} diff --git a/packages/subprocess/subprocess/src/types.ts b/packages/subprocess/subprocess/src/types.ts index 0d5b084f91..1274c08399 100644 --- a/packages/subprocess/subprocess/src/types.ts +++ b/packages/subprocess/subprocess/src/types.ts @@ -202,6 +202,14 @@ export interface SubprocessHandle { */ export type SubprocessTerminalSignal = 'SIGINT' | 'SIGTERM' | 'SIGKILL' | 'SIGTSTP' | 'SIGHUP' +/** Shell-selection facts from the subprocess provider's execution environment. */ +export interface SubprocessTerminalEnvironment { + /** Operating-system family that interprets executable paths and shell arguments. */ + platform: 'posix' | 'windows' + /** Login or environment-selected shell, when the provider can resolve one. */ + defaultShell?: string +} + /** A fully specified terminal-process spawn. */ export interface SubprocessTerminalSpawnSpec { /** Executable and arguments; `argv[0]` is the program. */ @@ -214,6 +222,8 @@ export interface SubprocessTerminalSpawnSpec { rows: number /** Initial terminal column count. */ cols: number + /** Terminal emulation advertised to the child through TERM. */ + terminalType: string /** TERM-to-KILL cleanup grace for the complete terminal session. */ graceMs: number /** Cancellation of terminal allocation; a published handle owns its later lifetime. */ @@ -246,6 +256,12 @@ export interface SubprocessTerminalHandle { * @param data - text to deliver without implicit newline conversion. */ write(data: string): Promise + /** + * Change the terminal dimensions and notify its foreground application. + * @param cols - positive terminal column count. + * @param rows - positive terminal row count. + */ + resize(cols: number, rows: number): Promise /** * Inspect the current foreground process group. * @returns its id and input-wait fact, or undefined when no foreground group can be resolved. diff --git a/packages/subprocess/subprocess/tests/service.spec.ts b/packages/subprocess/subprocess/tests/service.spec.ts index 02ee5be4d5..d76f6c56b9 100644 --- a/packages/subprocess/subprocess/tests/service.spec.ts +++ b/packages/subprocess/subprocess/tests/service.spec.ts @@ -16,6 +16,7 @@ import type { * is all an implementation owes the abstract class. */ class StubSubprocessRuntime extends SubprocessRuntime { + async terminalEnvironment() { return { platform: 'posix' as const } } async resolveExecutable(command: string): Promise { return `/bin/${command}` } @@ -43,6 +44,7 @@ class StubSubprocessRuntime extends SubprocessRuntime { output: new PassThrough(), done: Promise.resolve({ exitCode: 0, signal: null }), write: async () => {}, + resize: async () => {}, inspectForeground: async () => ({ processGroupId: 1, inputWaiting: true }), signalForeground: async () => 1, terminate: async () => {}, diff --git a/packages/terminal/terminal-bash/src/index.ts b/packages/terminal/terminal-bash/src/index.ts index 35eb5b89bc..2371121f59 100644 --- a/packages/terminal/terminal-bash/src/index.ts +++ b/packages/terminal/terminal-bash/src/index.ts @@ -201,6 +201,7 @@ export class BashTerminalBackend implements TerminalBackend { env: childEnvironment(spec, this.config.shellDialect), rows: this.config.rows, cols: this.config.cols, + terminalType: 'dumb', graceMs: this.config.disposeGraceMs, signal: spec.signal, }) diff --git a/packages/terminal/terminal-bash/tests/index.spec.ts b/packages/terminal/terminal-bash/tests/index.spec.ts index ec81bc1609..7030b72e3e 100644 --- a/packages/terminal/terminal-bash/tests/index.spec.ts +++ b/packages/terminal/terminal-bash/tests/index.spec.ts @@ -72,6 +72,7 @@ function terminalHandle(): SubprocessTerminalHandle { output, done: Promise.resolve({ exitCode: 0, signal: null }), write: async () => {}, + resize: async () => {}, inspectForeground: async () => ({ processGroupId: 123, inputWaiting: true }), signalForeground: async () => 123, terminate: async () => { output.end() }, @@ -79,6 +80,7 @@ function terminalHandle(): SubprocessTerminalHandle { } class StubSubprocessRuntime extends SubprocessRuntime { + async terminalEnvironment() { return { platform: 'posix' as const } } async resolveExecutable(command: string): Promise { return command } spawn(_spec: SubprocessSpawnSpec): SubprocessHandle { throw new Error('unused') } async spawnTerminal(_spec: SubprocessTerminalSpawnSpec): Promise { @@ -359,6 +361,7 @@ describe('BashTerminalBackend startup rollback', () => { output, done: outcome.promise, write: async () => {}, + resize: async () => {}, inspectForeground: async () => ({ processGroupId: 123, inputWaiting: true }), signalForeground: async () => 123, async terminate() { diff --git a/packages/terminal/terminal-bash/tests/session.spec.ts b/packages/terminal/terminal-bash/tests/session.spec.ts index b28c579375..d8051db3b6 100644 --- a/packages/terminal/terminal-bash/tests/session.spec.ts +++ b/packages/terminal/terminal-bash/tests/session.spec.ts @@ -86,6 +86,8 @@ class FakeTerminal implements SubprocessTerminalHandle { }) } + async resize(_cols: number, _rows: number): Promise {} + async write(data: string): Promise { if (this.throwWrite) throw new Error('write failed') this.writes.push(data) diff --git a/packages/test-support/client-runtime/README.i18n.yaml b/packages/test-support/client-runtime/README.i18n.yaml index 534cc2596a..962229c870 100644 --- a/packages/test-support/client-runtime/README.i18n.yaml +++ b/packages/test-support/client-runtime/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/test-support/client-runtime/README.md -README.md: 20f6454012d0d8e4c78d9a0ea0df960d22a71f5a -README.zh.md: d9c6888f17df3f37e699bb6be0f7ee44ce82d373 +README.md: 014bcbf8bc0ed9be596e2a20c07f57c80aaaf7dc +README.zh.md: a14a5281c58a5ea4a8a49e1a826397ae91e5f115 diff --git a/packages/test-support/client-runtime/README.md b/packages/test-support/client-runtime/README.md index 20f6454012..014bcbf8bc 100644 --- a/packages/test-support/client-runtime/README.md +++ b/packages/test-support/client-runtime/README.md @@ -40,7 +40,7 @@ expect(view.container).toMatchSnapshot() await runtime.dispose() ``` -`mount` prechecks required services and fails loud when one is missing — `provide(name, value)` supplies an extra service first. The runtime provides an unavailable `fileUpload` stub so assemblies can mount; replace `runtime.fileUpload.upload` before mounting when a test exercises upload behavior. `storeOf(key, scopeKey)` returns the live store instance the renderer hands a slot's component for identity and action-driven-write assertions. +`mount` prechecks required services and fails loud when one is missing — `provide(name, value)` supplies an extra service first. The runtime provides a `fileUpload` stub that rejects every call until a suite replaces `runtime.fileUpload.upload`. `storeOf(key, scopeKey)` returns the live store instance the renderer hands a slot's component for identity and action-driven-write assertions. The optional render options select a keyed entry with `entryKey` or a list item with `only`; `view.update(owner)` retains that selection. `runtime.panelInfo` supplies the default `usePanelInfo` source with no global panel selected. Release it with `releasePanelInfoSource()` before mounting the production Layout owner. `dispose()` releases both default Workspace and panel-info root sources; early release is idempotent and does not remove replacement owners. @@ -66,7 +66,7 @@ expect(view.getByRole('alert')).toHaveTextContent('goal/not-found') ### Whole-client tier -The slot tier above mounts one feature against doubles. The whole-client tier boots the real assembly: `TestClient.start(plan, mock, options)` installs `{ rpc: mock.rpc }` as `globalThis.__DSH_TRANSPORT__`, imports every roster row's `/client` module in-process (or takes the plan's `provide` replacement), synthesizes the boot graph with `graphFromRoster` and hands the loaded modules to the production module system, boots through the production `bootClient`, optionally mounts `uiRenderer`, and waits for `ctx.connection.state === 'connected'`. It lives behind a deep import so slot-tier specs never load it: +The slot tier above mounts one feature against doubles. The whole-client tier boots the real assembly: `TestClient.start(plan, mock, options)` imports every roster row's `/client` module in-process (or takes the plan's `provide` replacement), binds `{ rpc: mock.rpc }` to that client's Connection module, synthesizes the boot graph with `graphFromRoster` and hands the loaded modules to the production module system, boots through the production `bootClient`, optionally mounts `uiRenderer`, and waits for `ctx.connection.state === 'connected'`. It lives behind a deep import so slot-tier specs never load it: ```text // @vitest-environment jsdom @@ -87,7 +87,7 @@ The [generic Remote proxy](../remote-mock/README.md#remote-proxy) supports every ### Roster and startup behavior -`webApp` is the `web` profile's browser roster, read when the assembly entry is first imported from its bundles (`dsh-base`, then `dsh-web-app`) as the launcher composes them, except that a patch matching nothing throws here where the launcher warns: each bundle's `dsh.bundle.patch` list is parsed with the include plugin's YAML dialect and composed by its `applyEntryPatches`, and every enabled row whose package declares `dsh.client.platform === 'web'` becomes a row carrying that declaration's `inject` and `immediately`; `bundleRoster(bundles)` does the same for any bundle list. Nothing is copied from the bundles, so a bundle change is visible at the next test run. `webApp.closure(names)` keeps the named rows plus everything they inject, transitively (the rows a spec needs to boot those plugins as the bundle composes them), `webApp.pick(names)` and `webApp.without(names)` cut it down by hand, all three throw on unknown names, and `ClientRoster.of(rows)` builds one inline. `remoteDefaultResponses` holds default responses for exactly the Remote endpoints the roster calls while booting with no sessions, no workspaces, and default settings; a spec layers its own `RemoteTable` on top with `mock.load(table)`, and any call without a rule fails the test at `dispose()` through `mock.assertNoUnmatched()`. `mount` requires a roster that provides `uiRenderer`; `start` fails loud otherwise instead of returning an empty container. `client.connection` is the roster's Connection service (no `Context` augmentation declares it), and `connectTimeoutMs` bounds the readiness wait, whose failure message lists the mock log. `reload(name)` rebuilds one Loader entry the way client-hmr does (registry-first teardown, then `entry.refresh()`), under the worker's boot turn with this client's carrier installed so a rebuilt `connection` row reads its own mock; `unload(name)` removes it and waits for plugin cleanup; `flush()` settles React inside `act`. jsdom implements neither `EventSource` (client-hmr opens one at apply) nor `ResizeObserver` (layout components observe size at mount), so `start` installs inert stubs for whichever global is absent and `dispose` removes exactly those — a jsdom gap, not a product requirement. The `@deepseek-ai/dsh-api-remotes` row is dropped from every roster: its generated Remote clients exist only in built `lib/`, and `remote.` is what the tier replaces anyway. `start` instead provides one contract-free proxy per `remote.` service the roster injects (plus the namespaces the mock has rules for at that point; a namespace first registered later has no proxy); `ctx.remote..(...args)` becomes a call on the endpoint `/` carrying the positional args, a stream when the mock registered a `stream()` script and a unary call otherwise, with the generated client's outcome folding (`gateway/internal` for carrier throws, `gateway/cancelled` on abort). An endpoint without a rule is still dispatched, so the mock logs it and `dispose()` fails the test. +`webApp` is the `web` profile's browser roster, read when the assembly entry is first imported from its bundles (`dsh-base`, then `dsh-web-app`) as the launcher composes them, except that a patch matching nothing throws here where the launcher warns: each bundle's `dsh.bundle.patch` list is parsed with the include plugin's YAML dialect and composed by its `applyEntryPatches`, and every enabled row whose package declares `dsh.client.platform === 'web'` becomes a row carrying that declaration's `inject` and `immediately`; `bundleRoster(bundles)` does the same for any bundle list. Nothing is copied from the bundles, so a bundle change is visible at the next test run. `webApp.closure(names)` keeps the named rows plus everything they inject, transitively (the rows a spec needs to boot those plugins as the bundle composes them), `webApp.pick(names)` and `webApp.without(names)` cut it down by hand, all three throw on unknown names, and `ClientRoster.of(rows)` builds one inline. `remoteDefaultResponses` holds default responses for exactly the Remote endpoints the roster calls while booting with no sessions, no workspaces, and default settings; a spec layers its own `RemoteTable` on top with `mock.load(table)`, and any call without a rule fails the test at `dispose()` through `mock.assertNoUnmatched()`. `mount` requires a roster that provides `uiRenderer`; `start` fails loud otherwise instead of returning an empty container. `client.connection` is the roster's Connection service (no `Context` augmentation declares it), and `connectTimeoutMs` bounds the readiness wait, whose failure message lists the mock log. `reload(name)` rebuilds one Loader entry the way client-hmr does (registry-first teardown, then `entry.refresh()`); each client retains its own module system and instance-bound Connection replacement, so starts and reloads can overlap without changing a page global. `unload(name)` removes an entry and waits for plugin cleanup; `flush()` settles React inside `act`. jsdom implements neither `EventSource` (client-hmr opens one at apply) nor `ResizeObserver` (layout components observe size at mount), so `start` reference-counts inert stubs for whichever global is absent and `dispose` removes exactly those — a jsdom gap, not a client identity channel. The `@deepseek-ai/dsh-api-remotes` row is dropped from every roster: its generated Remote clients exist only in built `lib/`, and `remote.` is what the tier replaces anyway. `start` instead provides one contract-free proxy per `remote.` service the roster injects (plus the namespaces the mock has rules for at that point; a namespace first registered later has no proxy); `ctx.remote..(...args)` becomes a call on the endpoint `/` carrying the positional args, a stream when the mock registered a `stream()` script and a unary call otherwise, with the generated client's outcome folding (`gateway/internal` for carrier throws, `gateway/cancelled` on abort). An endpoint without a rule is still dispatched, so the mock logs it and `dispose()` fails the test. ### When to use it @@ -126,7 +126,7 @@ The bench copies no production logic: it mounts the production `SlotRegistry`, p | [`src/settings-scope.ts`](src/settings-scope.ts) | `stubSettingsScope` with test-driven publications and a write spy | | [`src/assembly/roster.ts`](src/assembly/roster.ts) | `ClientRosterRow`, `ClientRoster` (`of`/`closure`/`pick`/`without`), the `AssemblyPlan` it annotates, and `graphFromRoster` | | [`src/assembly/modules.ts`](src/assembly/modules.ts) | Source `/client` imports and replacements, registered through the production module facade's pending factory queue | -| [`src/assembly/test-client.ts`](src/assembly/test-client.ts) | `TestClient`: transport install, jsdom shims, `bootClient`, mount, readiness wait, `reload`/`unload`/`dispose` | +| [`src/assembly/test-client.ts`](src/assembly/test-client.ts) | `TestClient`: instance-bound Connection, shared jsdom shims, `bootClient`, mount, readiness wait, `reload`/`unload`/`dispose` | | [`src/assembly/vitest.ts`](src/assembly/vitest.ts) | Test-scoped `mock` and lazy `start` fixtures | | [`src/assembly/remote-default-responses.ts`](src/assembly/remote-default-responses.ts) | `remoteDefaultResponses`: default responses of the Remote endpoints the roster calls at boot | | [`src/assembly/remote-proxies.ts`](src/assembly/remote-proxies.ts) | Contract-free `remote.` proxies over the Connection: `remoteNamespacesOf`, `remoteProxiesPlugin` | diff --git a/packages/test-support/client-runtime/README.zh.md b/packages/test-support/client-runtime/README.zh.md index d9c6888f17..a14a5281c5 100644 --- a/packages/test-support/client-runtime/README.zh.md +++ b/packages/test-support/client-runtime/README.zh.md @@ -40,7 +40,7 @@ expect(view.container).toMatchSnapshot() await runtime.dispose() ``` -`mount` 会预检必需服务,缺失时自明报错——先用 `provide(name, value)` 提供额外服务。运行时会提供不可用的 `fileUpload` 替身,使装配可以挂载;测试上传行为时,需要在挂载前替换 `runtime.fileUpload.upload`。`storeOf(key, scopeKey)` 返回渲染器交给 slot 组件的实时存储实例,用于身份与动作驱动写入断言。 +`mount` 会预检必需服务,缺失时自明报错——先用 `provide(name, value)` 提供额外服务。运行时提供的 `fileUpload` 替身会拒绝每次调用,直到测试套件替换 `runtime.fileUpload.upload`。`storeOf(key, scopeKey)` 返回渲染器交给 slot 组件的实时存储实例,用于身份与动作驱动写入断言。 可选渲染参数通过 `entryKey` 选择 keyed 条目,或通过 `only` 选择 list 条目;`view.update(owner)` 保留该选择。`runtime.panelInfo` 提供默认的 `usePanelInfo` 数据源,初始不选中全局面板。挂载生产 Layout 所有者之前,先调用 `releasePanelInfoSource()` 释放该数据源。`dispose()` 同时释放默认的工作区与面板信息根数据源;提前释放是幂等的,不会移除替代它们的所有者。 @@ -66,7 +66,7 @@ expect(view.getByRole('alert')).toHaveTextContent('goal/not-found') ### 整体档 -上面的 slot 档把一个功能挂在替身上。整体档起真实装配:`TestClient.start(plan, mock, options)` 把 `{ rpc: mock.rpc }` 装到 `globalThis.__DSH_TRANSPORT__`,进程内 import 每个 roster 行的 `/client` 模块(或取计划里的 `provide` 替换),用 `graphFromRoster` 合成启动图并把已加载模块交给生产模块系统,经生产 `bootClient` 启动,按需挂载 `uiRenderer`,再等 `ctx.connection.state === 'connected'`。它藏在深 import 后面,slot 档测试永不加载它: +上面的 slot 档把一个功能挂在替身上。整体档起真实装配:`TestClient.start(plan, mock, options)` 进程内 import 每个 roster 行的 `/client` 模块(或取计划里的 `provide` 替换),把 `{ rpc: mock.rpc }` 绑定到该客户端的 Connection 模块,用 `graphFromRoster` 合成启动图并把已加载模块交给生产模块系统,经生产 `bootClient` 启动,按需挂载 `uiRenderer`,再等 `ctx.connection.state === 'connected'`。它藏在深 import 后面,slot 档测试永不加载它: ```text // @vitest-environment jsdom @@ -87,7 +87,7 @@ test('registers into the sidebar', async ({ remote, start }) => { ### Roster 与启动行为 -`webApp` 是 `web` profile 的浏览器 roster,首次 import 装配入口时从它的 bundle(先 `dsh-base`、再 `dsh-web-app`)按启动器的方式现读,只是匹配不到任何行的补丁在这里抛错、启动器只警告:每个 bundle 的 `dsh.bundle.patch` 列表用 include 插件的 YAML 方言解析、用它的 `applyEntryPatches` 合成,每个未禁用且其包声明 `dsh.client.platform === 'web'` 的行成为一行,带上该声明的 `inject` 与 `immediately`;`bundleRoster(bundles)` 对任意 bundle 列表做同样的事。没有任何东西从 bundle 拷贝出来,bundle 一改下次跑测试就能看见。`webApp.closure(names)` 保留点名的行及其传递注入的全部行(即按 bundle 组合方式起这些插件所需的行),`webApp.pick(names)` 与 `webApp.without(names)` 手工裁剪,三者都对未知名字抛错,`ClientRoster.of(rows)` 内联构造一份。`remoteDefaultResponses` 是 roster 在没有 session、没有 workspace、默认设置下启动时恰好会打的那些 Remote 端点的默认响应;测试用 `mock.load(table)` 在其上叠加自己的 `RemoteTable`,任何没有规则的调用都会在 `dispose()` 时经 `mock.assertNoUnmatched()` 让测试失败。`mount` 要求 roster 提供 `uiRenderer`;否则 `start` 响亮失败而不是返回一个空容器。`client.connection` 是 roster 的 Connection 服务(没有任何 `Context` 增强声明它),`connectTimeoutMs` 限定等就绪的时长,超时消息列出 mock log。`reload(name)` 按 client-hmr 的方式重建一个 Loader entry(先拆 registry,再 `entry.refresh()`),并在 worker 的启动轮次内装上本客户端的载体,重建的 `connection` 行因此读到自己的 mock;`unload(name)` 移除它并等待插件清理完成;`flush()` 在 `act` 内让 React 落定。jsdom 既没有 `EventSource`(client-hmr 在 apply 时打开一个)也没有 `ResizeObserver`(布局组件挂载时观察尺寸),所以 `start` 对缺失的全局装惰性桩、`dispose` 只移除它装的那些——这是 jsdom 的缺口,不是产品需求。每个 roster 里的 `@deepseek-ai/dsh-api-remotes` 行都会被去掉:它生成的 Remote 客户端只存在于构建后的 `lib/`,而 `remote.` 正是本档要替掉的东西。`start` 改为给 roster 注入的每个 `remote.` 服务(加上此刻 mock 登记过规则的命名空间;之后才首次登记的命名空间没有代理)提供一个无契约代理;`ctx.remote..(...args)` 变成对端点 `/` 的调用,携带位置参数,mock 登记了 `stream()` 脚本的走流、否则走一元,并沿用生成客户端的结果折叠(载体抛错折成 `gateway/internal`,中止折成 `gateway/cancelled`)。没有规则的端点照样发出,所以 mock 会记下它、`dispose()` 让测试失败。 +`webApp` 是 `web` profile 的浏览器 roster,首次 import 装配入口时从它的 bundle(先 `dsh-base`、再 `dsh-web-app`)按启动器的方式现读,只是匹配不到任何行的补丁在这里抛错、启动器只警告:每个 bundle 的 `dsh.bundle.patch` 列表用 include 插件的 YAML 方言解析、用它的 `applyEntryPatches` 合成,每个未禁用且其包声明 `dsh.client.platform === 'web'` 的行成为一行,带上该声明的 `inject` 与 `immediately`;`bundleRoster(bundles)` 对任意 bundle 列表做同样的事。没有任何东西从 bundle 拷贝出来,bundle 一改下次跑测试就能看见。`webApp.closure(names)` 保留点名的行及其传递注入的全部行(即按 bundle 组合方式起这些插件所需的行),`webApp.pick(names)` 与 `webApp.without(names)` 手工裁剪,三者都对未知名字抛错,`ClientRoster.of(rows)` 内联构造一份。`remoteDefaultResponses` 是 roster 在没有 session、没有 workspace、默认设置下启动时恰好会打的那些 Remote 端点的默认响应;测试用 `mock.load(table)` 在其上叠加自己的 `RemoteTable`,任何没有规则的调用都会在 `dispose()` 时经 `mock.assertNoUnmatched()` 让测试失败。`mount` 要求 roster 提供 `uiRenderer`;否则 `start` 响亮失败而不是返回一个空容器。`client.connection` 是 roster 的 Connection 服务(没有任何 `Context` 增强声明它),`connectTimeoutMs` 限定等就绪的时长,超时消息列出 mock log。`reload(name)` 按 client-hmr 的方式重建一个 Loader entry(先拆 registry,再 `entry.refresh()`);每个客户端都保留自己的模块系统和绑定实例的 Connection 替换,因此启动与重载可以重叠而无需改写页面全局变量。`unload(name)` 移除 entry 并等待插件清理完成,`flush()` 在 `act` 内让 React 落定。jsdom 既没有 `EventSource`(client-hmr 在 apply 时打开一个)也没有 `ResizeObserver`(布局组件挂载时观察尺寸),所以 `start` 对缺失的全局惰性桩做引用计数,`dispose` 只移除它装的那些——这是 jsdom 的缺口,不是客户端身份通道。每个 roster 里的 `@deepseek-ai/dsh-api-remotes` 行都会被去掉:它生成的 Remote 客户端只存在于构建后的 `lib/`,而 `remote.` 正是本档要替掉的东西。`start` 改为给 roster 注入的每个 `remote.` 服务(加上此刻 mock 登记过规则的命名空间;之后才首次登记的命名空间没有代理)提供一个无契约代理;`ctx.remote..(...args)` 变成对端点 `/` 的调用,携带位置参数,mock 登记了 `stream()` 脚本的走流、否则走一元,并沿用生成客户端的结果折叠(载体抛错折成 `gateway/internal`,中止折成 `gateway/cancelled`)。没有规则的端点照样发出,所以 mock 会记下它、`dispose()` 让测试失败。 ### 何时使用 @@ -126,7 +126,7 @@ test('registers into the sidebar', async ({ remote, start }) => { | [`src/settings-scope.ts`](src/settings-scope.ts) | 带测试驱动发布与写入 spy 的 `stubSettingsScope` | | [`src/assembly/roster.ts`](src/assembly/roster.ts) | `ClientRosterRow`、`ClientRoster`(`of`/`closure`/`pick`/`without`)、它所标注的 `AssemblyPlan`,以及 `graphFromRoster` | | [`src/assembly/modules.ts`](src/assembly/modules.ts) | 源码 `/client` 导入及替换,通过生产模块 facade 的待注册工厂队列登记 | -| [`src/assembly/test-client.ts`](src/assembly/test-client.ts) | `TestClient`:装传输、jsdom 桩、`bootClient`、挂载、等就绪、`reload`/`unload`/`dispose` | +| [`src/assembly/test-client.ts`](src/assembly/test-client.ts) | `TestClient`:绑定实例的 Connection、共享 jsdom 桩、`bootClient`、挂载、等就绪、`reload`/`unload`/`dispose` | | [`src/assembly/vitest.ts`](src/assembly/vitest.ts) | 测试级 `mock` 与懒启动 `start` fixture | | [`src/assembly/remote-default-responses.ts`](src/assembly/remote-default-responses.ts) | `remoteDefaultResponses`:roster 启动期 Remote 端点的默认响应 | | [`src/assembly/remote-proxies.ts`](src/assembly/remote-proxies.ts) | 经 Connection 的无契约 `remote.` 代理:`remoteNamespacesOf`、`remoteProxiesPlugin` | diff --git a/packages/test-support/client-runtime/src/assembly/test-client.ts b/packages/test-support/client-runtime/src/assembly/test-client.ts index fd99de38fd..ecadd61fea 100644 --- a/packages/test-support/client-runtime/src/assembly/test-client.ts +++ b/packages/test-support/client-runtime/src/assembly/test-client.ts @@ -1,13 +1,16 @@ /** * Whole-client test carrier: boots an {@link AssemblyPlan} through the * production `bootClient` over an in-process module table, with a - * `RemoteMock` installed as the Connection carrier through `__DSH_TRANSPORT__.rpc`. + * `RemoteMock` bound to that client's Connection plugin instance. * @module @deepseek-ai/dsh-client-test-runtime/src/assembly/test-client */ import { Context, type Plugin } from '@deepseek-ai/cordis' import type { Entry } from '@deepseek-ai/cordis-plugin-loader' import { tearDownEntryFiber } from '@deepseek-ai/dsh-client-hmr/client' -import type { ClientTransportHooks, ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client' +import { + installConnection, + type ConnectionHandle, +} from '@deepseek-ai/dsh-client-connection/client' import { bootClient } from '@deepseek-ai/dsh-client-web/src/boot-client.ts' import { mountClient } from '@deepseek-ai/dsh-client-web/src/mount.ts' import type { RemoteMock } from '@deepseek-ai/dsh-remote-mock' @@ -29,73 +32,35 @@ export interface TestClientOptions { readonly connectTimeoutMs?: number } -/** Page global the connection plugin reads its carrier from. */ -interface TransportGlobal { - __DSH_TRANSPORT__?: ClientTransportHooks -} - -const transportGlobal = globalThis as TransportGlobal - /** - * Process globals every live client in this worker shares: the transport the - * `connection` plugin reads at apply, and the jsdom shims. The first holder - * installs them and remembers what was there; the last release removes the - * shims and restores the transport. A holder installs its transport for its - * own boot and for each rebuild it runs, both under the worker's boot turn, so - * overlapping clients and out-of-order disposal neither clobber a booting or - * rebuilding client nor leak into later tests. + * jsdom shims shared by every live client in this worker. The first holder + * installs missing shims and the last release removes exactly those shims. */ -class SharedGlobals { +class SharedJsdomShims { private holders = 0 - private previousTransport: ClientTransportHooks | undefined private removeShims: (() => void) | undefined /** - * Point the transport global at `transport` for the boot or rebuild about to run; the holder must already hold. - * @param transport - carrier the next `connection` apply reads. - */ - install(transport: ClientTransportHooks): void { - transportGlobal.__DSH_TRANSPORT__ = transport - } - - /** - * Hold the globals with `transport` installed. - * @param transport - carrier the next boot reads. + * Hold the shims for one client. * @returns the release for this holder. */ - acquire(transport: ClientTransportHooks): () => void { + acquire(): () => void { if (this.holders === 0) { - this.previousTransport = transportGlobal.__DSH_TRANSPORT__ this.removeShims = installJsdomShims() } this.holders += 1 - this.install(transport) return () => { this.holders -= 1 if (this.holders > 0) return this.removeShims?.() this.removeShims = undefined - if (this.previousTransport === undefined) delete transportGlobal.__DSH_TRANSPORT__ - else transportGlobal.__DSH_TRANSPORT__ = this.previousTransport - this.previousTransport = undefined } } } -const sharedGlobals = new SharedGlobals() +const sharedJsdomShims = new SharedJsdomShims() -/** - * Boots and entry rebuilds run one at a time per worker: the transport global - * must stay the acting client's until its `connection` row applies. - */ -let bootTurn: Promise = Promise.resolve() - -/** Run `work` as the next boot turn; its failure is the caller's, never the next turn's. */ -function takeTurn(work: () => Promise): Promise { - const turn = bootTurn.then(work) - bootTurn = turn.catch(() => undefined) - return turn -} +const CONNECTION_PACKAGE = '@deepseek-ai/dsh-client-connection' /** Default readiness budget; the mock answers `$events` immediately, so a miss means a boot-time fixture is absent. */ const DEFAULT_CONNECT_TIMEOUT_MS = 5_000 @@ -194,16 +159,20 @@ async function awaitConnected(ctx: Context, mock: RemoteMock, timeoutMs: number) /** A booted client under test. */ export class TestClient { /** - * Load the roster's modules, then, holding this worker's boot turn, install - * the mock as the Connection carrier and the jsdom shims and boot through - * `bootClient` over the synthesized boot graph; afterwards optionally mount - * and wait for the connection. The `@deepseek-ai/dsh-api-remotes` row is + * Load the roster's modules, bind this client's mock to its Connection row, + * hold the jsdom shims, and boot through `bootClient` over the synthesized + * boot graph; afterwards optionally mount and wait for the connection. The + * bound row replaces only the page-global input adapter: both paths call + * `installConnection`, while this path supplies the mock carrier, uses + * default recovery timings, and captures the current page hostname once for + * later reloads. A caller-provided Connection row remains unchanged and owns + * its readiness behavior. The `@deepseek-ai/dsh-api-remotes` row is * dropped from the roster: its generated Remote clients exist only in built * `lib/`, and the `remote.` services the roster injects (plus the * namespaces the mock has rules for at this point) are provided as * contract-free proxies over the same Connection instead; a `provide` entry * for that row is refused. On any failure the context is disposed, an owned - * mount removed, and this client's hold on the globals released before the + * mount removed, and this client's hold on the shims released before the * original error is rethrown. * @param plan - roster and annotations. * @param mock - Remote mock answering every Gateway call. @@ -223,7 +192,9 @@ export class TestClient { ? plan.roster.without([REMOTES_PACKAGE]) : plan.roster const ctx = new Context() - const transport: ClientTransportHooks = { rpc: mock.rpc } + const pageLocation = typeof location === 'undefined' + ? undefined + : { hostname: location.hostname } let mountPoint: MountPoint = { element: undefined, owned: false } let release: (() => void) | undefined const restore = (): void => { @@ -231,14 +202,24 @@ export class TestClient { release?.() } try { - const modules = await loadPluginModules({ ...plan, roster }) + const modules = new Map(await loadPluginModules({ ...plan, roster })) + const connection = modules.get(CONNECTION_PACKAGE) + if (connection !== undefined && plan.provide?.[CONNECTION_PACKAGE] === undefined) { + modules.set(CONNECTION_PACKAGE, { + ...connection, + apply: (connectionCtx) => { + installConnection(connectionCtx, { + transport: { rpc: mock.rpc }, + ...(pageLocation === undefined ? {} : { location: pageLocation }), + }) + }, + }) + } mountPoint = resolveMountPoint(options.mount) - await takeTurn(async () => { - release = sharedGlobals.acquire(transport) - const system = createInProcessModules(graphFromRoster(roster.rows), modules) - ctx.plugin(remoteProxiesPlugin(remoteNamespacesOf(modules.values(), mock), mock) as unknown as Plugin) - await bootClient({ ctx, modules: system, manifest: system.manifest }) - }) + release = sharedJsdomShims.acquire() + const system = createInProcessModules(graphFromRoster(roster.rows), modules) + ctx.plugin(remoteProxiesPlugin(remoteNamespacesOf(modules.values(), mock), mock) as unknown as Plugin) + await bootClient({ ctx, modules: system, manifest: system.manifest }) if (mountPoint.element !== undefined) { if (ctx.get('uiRenderer') === undefined) { throw new Error('client-test-runtime: mount requested, but the roster provides no `uiRenderer`') @@ -270,11 +251,6 @@ export class TestClient { private readonly restore: () => void, ) {} - /** The carrier this client's `connection` row reads when it applies. */ - private get transport(): ClientTransportHooks { - return { rpc: this.mock.rpc } - } - /** The roster's Connection service (no `Context` augmentation declares it); throws when the roster provides none. */ get connection(): ConnectionHandle { return connectionOf(this.ctx) @@ -286,20 +262,18 @@ export class TestClient { } /** - * Rebuild one Loader entry: client-hmr's registry-first fiber teardown, then `entry.refresh()`. The rebuild - * takes the worker's boot turn with this client's carrier installed, so a rebuilt `connection` row reads its - * own mock even while another client is live. Requires a live client: after `dispose()` the Loader holds no - * entries and the lookup throws before anything is installed. + * Rebuild one Loader entry: client-hmr's registry-first fiber teardown, then + * `entry.refresh()`. Each client's module table retains its own instance-bound + * Connection plugin, so reloads do not coordinate through process globals. + * Requires a live client: after `dispose()` the Loader holds no entries and + * the lookup throws before teardown. * @param name - package name of the row. */ async reload(name: string): Promise { const entry = this.entryOf(name) - await takeTurn(async () => { - sharedGlobals.install(this.transport) - await tearDownEntryFiber(entry) - await entry.refresh() - await this.ctx.loader.await() - }) + await tearDownEntryFiber(entry) + await entry.refresh() + await this.ctx.loader.await() } /** @@ -315,7 +289,7 @@ export class TestClient { /** * Dispose the plugin tree, then drop an owned mount and release this - * client's hold on the shared globals even when the tree fails to dispose, + * client's hold on the shared jsdom shims even when the tree fails to dispose, * then `mock.assertNoUnmatched()` last so its failure is the test's reason * without skipping the cleanup; when both the tree and the check fail, one * error carries both messages. The first call owns the teardown and reports diff --git a/packages/test-support/client-runtime/src/index.ts b/packages/test-support/client-runtime/src/index.ts index 8472514901..34614b0205 100644 --- a/packages/test-support/client-runtime/src/index.ts +++ b/packages/test-support/client-runtime/src/index.ts @@ -113,8 +113,6 @@ export interface FeatureHandle { /** Mutable fail-loud file-upload stub installed by {@link SlotTestRuntime}. */ export interface TestFileUpload { - /** Availability reported to the feature under test. */ - available: boolean /** Test-supplied upload behavior; the default rejects every call. */ upload: (sessionId: SessionId, ...args: unknown[]) => Promise } @@ -245,7 +243,6 @@ export class SlotTestRuntime { this.sessions = new TestSessions(this.stabilizer, ctx) this.workspaces = new TestWorkspaces(this.stabilizer) this.fileUpload = { - available: false, upload: () => Promise.reject(new Error('client test runtime: file upload is not stubbed')), } ctx.provide('sessions', this.sessions) diff --git a/packages/test-support/client-runtime/tests/assembly-test-client-node.client.spec.ts b/packages/test-support/client-runtime/tests/assembly-test-client-node.client.spec.ts index 26afee3272..748b6d3eab 100644 --- a/packages/test-support/client-runtime/tests/assembly-test-client-node.client.spec.ts +++ b/packages/test-support/client-runtime/tests/assembly-test-client-node.client.spec.ts @@ -19,7 +19,7 @@ describe('TestClient (node environment)', () => { await client.flush() }, 60_000) - it('releases the shared globals even when the plugin tree fails to dispose, and still rethrows', async () => { + it('closes the client even when the plugin tree fails to dispose, and still rethrows', async () => { const mock = RemoteMock.create().load(remoteDefaultResponses) const client = await TestClient.start({ roster: API_ROSTER }, mock) const dispose = client.ctx.fiber.dispose.bind(client.ctx.fiber) @@ -38,11 +38,10 @@ describe('TestClient (node environment)', () => { await client.dispose() // idempotent after a failed teardown }) - it('rethrows a row that fails to apply, releases the globals, and lets the next boot take its turn', async () => { + it('rethrows a row that fails to apply and lets the next client boot', async () => { const failing = { apply(): void { throw new Error('apply boom') } } await expect(TestClient.start({ roster: TYPERT_ONLY, provide: { '@deepseek-ai/dsh-typert-registry': failing } }, RemoteMock.create())) .rejects.toThrow(/apply boom|typert-registry/) - expect(globals.__DSH_TRANSPORT__).toBeUndefined() const mock = RemoteMock.create().load(remoteDefaultResponses) const client = await TestClient.start({ roster: API_ROSTER }, mock) onTestFinished(() => client.dispose()) @@ -53,7 +52,6 @@ describe('TestClient (node environment)', () => { const roster = webApp.closure(['@deepseek-ai/dsh-api-remotes']) await expect(TestClient.start({ roster, provide: { '@deepseek-ai/dsh-api-remotes': { apply() {} } } }, RemoteMock.create())) .rejects.toThrow('@deepseek-ai/dsh-api-remotes cannot be provided; its remote. services are the tier\'s proxies') - expect(globals.__DSH_TRANSPORT__).toBeUndefined() }) it('reports unmatched requests alongside a failed teardown instead of hiding them', async () => { @@ -84,24 +82,22 @@ describe('TestClient (node environment)', () => { await first }) - it('refuses to mount without a DOM before installing the transport', async () => { + it('refuses to mount without a DOM', async () => { await expect(TestClient.start({ roster: API_ROSTER }, RemoteMock.create(), { mount: true })) .rejects.toThrow('mount requires a DOM') - expect(globals.__DSH_TRANSPORT__).toBeUndefined() }) - it('fails loud, then restores the transport, when the roster cannot provide a connection', async () => { + it('fails loud when the roster cannot provide a connection', async () => { await expect(TestClient.start({ roster: TYPERT_ONLY }, RemoteMock.create())) .rejects.toThrow('provides no `connection` service') - expect(globals.__DSH_TRANSPORT__).toBeUndefined() }) - it('skips readiness on request and restores a pre-existing transport on dispose', async () => { + it('skips readiness on request and leaves a pre-existing page transport untouched', async () => { const previous: ClientTransportHooks = { fetch: () => Promise.reject(new Error('unused')) } globals.__DSH_TRANSPORT__ = previous onTestFinished(() => { delete globals.__DSH_TRANSPORT__ }) const client = await TestClient.start({ roster: TYPERT_ONLY }, RemoteMock.create(), { awaitConnected: false }) - expect(globals.__DSH_TRANSPORT__).not.toBe(previous) + expect(globals.__DSH_TRANSPORT__).toBe(previous) expect(client.ctx.get('typert')).toBeDefined() expect(() => client.connection).toThrow('provides no `connection` service') await client.dispose() diff --git a/packages/test-support/client-runtime/tests/assembly-test-client.client.spec.ts b/packages/test-support/client-runtime/tests/assembly-test-client.client.spec.ts index b4d28d9fc6..9768bf06ee 100644 --- a/packages/test-support/client-runtime/tests/assembly-test-client.client.spec.ts +++ b/packages/test-support/client-runtime/tests/assembly-test-client.client.spec.ts @@ -2,22 +2,24 @@ /** * TestClient over the web profile's roster read from its bundles: production * `bootClient` over in-process modules, every Remote call answered by a - * `RemoteMock` installed as the Connection carrier, mount, HMR-style reload, + * `RemoteMock` bound to that client's Connection instance, mount, HMR-style reload, * unload, and fail-loud teardown. */ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import { RemoteMock, ok, openStream } from '@deepseek-ai/dsh-remote-mock' import { describe, expect, it, onTestFinished, vi } from 'vitest' -import type { AssemblyPlan, TestClientOptions } from '../src/assembly/index.ts' +import type { AssemblyPlan, ClientPluginModule, TestClientOptions } from '../src/assembly/index.ts' import { ClientRoster, TestClient, remoteDefaultResponses, webApp } from '../src/assembly/index.ts' /** The Gateway client and what it injects: the Typert registry and the Connection. */ const API_ROSTER = webApp.closure(['@deepseek-ai/dsh-api-gateway']) +const MODULES = '@deepseek-ai/dsh-client-modules' const SIDEBAR = '@deepseek-ai/dsh-client-ui-sidebar' +const PARALLEL_PROBE = '@deepseek-ai/dsh-client-test-parallel-probe' /** Declared by ui-sidebar, whose SlotMap merge is outside this package's compilation face. */ const SIDEBAR_SETTINGS = 'sidebar.settings' as never const BRAND = '@deepseek-ai/dsh-client-ui-brand-official' -const globals = globalThis as { __DSH_TRANSPORT__?: unknown; EventSource?: unknown; ResizeObserver?: unknown } +const globals = globalThis as { EventSource?: unknown; ResizeObserver?: unknown } /** The whole roster's first boot pays the cold module transform of every plugin package. */ const COLD_BOOT_TIMEOUT_MS = 60_000 @@ -37,12 +39,12 @@ describe('TestClient (jsdom)', () => { const container = client.container! expect(document.body.contains(container)).toBe(true) expect(container.childElementCount).toBeGreaterThan(0) - expect(globals.__DSH_TRANSPORT__).toBeDefined() + expect('__DSH_TRANSPORT__' in globalThis).toBe(false) expect(globals.EventSource).toBeDefined() expect(globals.ResizeObserver).toBeDefined() await client.dispose() expect(document.body.contains(container)).toBe(false) - expect(globals.__DSH_TRANSPORT__).toBeUndefined() + expect('__DSH_TRANSPORT__' in globalThis).toBe(false) expect(globals.EventSource).toBeUndefined() expect(globals.ResizeObserver).toBeUndefined() await client.dispose() @@ -60,13 +62,14 @@ describe('TestClient (jsdom)', () => { expect(globals.EventSource).toBeUndefined() }) - it('boots overlapping clients one at a time onto their own mocks and keeps the shared globals until the last dispose', async () => { + it('boots separate client instances against their own mocks and keeps shared shims until the last dispose', async () => { const mockA = RemoteMock.create().load(remoteDefaultResponses).unary('session/rename', ok({ title: 'a', seq: 1 })) const mockB = RemoteMock.create().load(remoteDefaultResponses).unary('session/rename', ok({ title: 'b', seq: 1 })) const [a, b] = await Promise.all([ TestClient.start({ roster: API_ROSTER }, mockA), TestClient.start({ roster: API_ROSTER }, mockB), ]) + onTestFinished(() => a.dispose()) onTestFinished(() => b.dispose()) const rename = async (client: TestClient): Promise => (client.ctx as unknown as { remote: { session: { rename(request: unknown): Promise } } }).remote.session.rename({ sessionId: 's', title: 't' }) @@ -74,20 +77,71 @@ describe('TestClient (jsdom)', () => { await expect(rename(b)).resolves.toEqual({ ok: true, value: { title: 'b', seq: 1 } }) expect(mockA.log.calls('session/rename')).toHaveLength(1) expect(mockB.log.calls('session/rename')).toHaveLength(1) - // A rebuilt connection row reads its own mock even after another client installed the transport last. await a.reload('@deepseek-ai/dsh-client-connection') await vi.waitFor(() => { expect(a.connection.state.getSnapshot()).toBe('connected') }) await expect(rename(a)).resolves.toEqual({ ok: true, value: { title: 'a', seq: 1 } }) expect(mockA.log.calls('session/rename')).toHaveLength(2) expect(mockB.log.calls('session/rename')).toHaveLength(1) await a.dispose() - expect(globals.__DSH_TRANSPORT__).toBeDefined() + expect('__DSH_TRANSPORT__' in globalThis).toBe(false) expect(globals.EventSource).toBeDefined() await b.dispose() - expect(globals.__DSH_TRANSPORT__).toBeUndefined() + expect('__DSH_TRANSPORT__' in globalThis).toBe(false) expect(globals.EventSource).toBeUndefined() }) + it('boots two plugin trees concurrently instead of serializing the worker', async () => { + let arrivals = 0 + const gate = Promise.withResolvers() + const probe: ClientPluginModule = { + async apply() { + arrivals += 1 + await gate.promise + }, + } + const roster = ClientRoster.of([ + { name: MODULES, inject: [], immediately: true }, + { name: PARALLEL_PROBE, inject: [], immediately: false }, + ]) + const first = TestClient.start({ roster, provide: { [PARALLEL_PROBE]: probe } }, RemoteMock.create(), { awaitConnected: false }) + const second = TestClient.start({ roster, provide: { [PARALLEL_PROBE]: probe } }, RemoteMock.create(), { awaitConnected: false }) + const starts = Promise.allSettled([first, second]) + let overlapFailure: unknown + try { + await vi.waitFor(() => { expect(arrivals).toBe(2) }, { timeout: 5_000 }) + } catch (error) { + overlapFailure = error + } finally { + gate.resolve(undefined) + } + const results = await starts + for (const result of results) { + if (result.status === 'fulfilled') onTestFinished(() => result.value.dispose()) + } + const rejected = results.find(result => result.status === 'rejected') + if (rejected?.status === 'rejected') { + const reason: unknown = rejected.reason + throw reason + } + if (overlapFailure !== undefined) throw overlapFailure + }, COLD_BOOT_TIMEOUT_MS) + + it('reloads the bootstrap modules row against its own Loader internal', async () => { + const roster = ClientRoster.of([{ name: MODULES, inject: [], immediately: true }]) + const a = await TestClient.start({ roster }, RemoteMock.create(), { awaitConnected: false }) + onTestFinished(() => a.dispose()) + const b = await TestClient.start({ roster }, RemoteMock.create(), { awaitConnected: false }) + onTestFinished(() => b.dispose()) + const modulesA = a.ctx.modules + const modulesB = b.ctx.modules + expect(modulesA).not.toBe(modulesB) + expect(modulesA).toBe(a.ctx.loader.internal) + expect(modulesB).toBe(b.ctx.loader.internal) + await a.reload(MODULES) + expect(a.ctx.modules).toBe(modulesA) + expect(b.ctx.modules).toBe(modulesB) + }) + it('boots the api subset without a mount and exposes the typed Remote', async () => { const client = await started({ roster: API_ROSTER }) expect(client.container).toBeUndefined() @@ -100,7 +154,6 @@ describe('TestClient (jsdom)', () => { await expect(TestClient.start({ roster: API_ROSTER }, RemoteMock.create().load(remoteDefaultResponses), { mount: true })) .rejects.toThrow('mount requested, but the roster provides no `uiRenderer`') expect(document.body.childElementCount).toBe(before) - expect(globals.__DSH_TRANSPORT__).toBeUndefined() }) it('creates no mount element when the roster cannot be loaded', async () => { @@ -108,7 +161,6 @@ describe('TestClient (jsdom)', () => { const before = document.body.childElementCount await expect(TestClient.start({ roster }, RemoteMock.create(), { mount: true })).rejects.toThrow() expect(document.body.childElementCount).toBe(before) - expect(globals.__DSH_TRANSPORT__).toBeUndefined() }) it('mounts into a caller-supplied element and leaves it in place on dispose', async () => { @@ -189,7 +241,6 @@ describe('TestClient (jsdom)', () => { const mock = RemoteMock.create().stream('$events', openStream([])) await expect(TestClient.start({ roster }, mock, { connectTimeoutMs: 300 })) .rejects.toThrow(/connection state is \S+ after 300ms; unmatched: \[unary workspace\/follow\]; streams: \[.*\$events \(open\).*\]/) - expect(globals.__DSH_TRANSPORT__).toBeUndefined() expect(globals.EventSource).toBeUndefined() }) }) diff --git a/packages/test-support/client-runtime/tests/helpers.client.spec.tsx b/packages/test-support/client-runtime/tests/helpers.client.spec.tsx index 389c1a0d90..f96849d9e4 100644 --- a/packages/test-support/client-runtime/tests/helpers.client.spec.tsx +++ b/packages/test-support/client-runtime/tests/helpers.client.spec.tsx @@ -105,7 +105,6 @@ describe('fixture helpers', () => { it('rejects an upload until a suite replaces the default stub', async () => { const runtime = await SlotTestRuntime.create() - expect(runtime.fileUpload.available).toBe(false) await expect(runtime.fileUpload.upload('fixture-session' as SessionId)).rejects.toThrow('file upload is not stubbed') await runtime.dispose() }) diff --git a/packages/test-support/llm-replay/src/index.ts b/packages/test-support/llm-replay/src/index.ts index 82f1d94baa..14d1a5764a 100644 --- a/packages/test-support/llm-replay/src/index.ts +++ b/packages/test-support/llm-replay/src/index.ts @@ -32,7 +32,7 @@ import type { SystemPromptUpdate, TokenUsage, } from '@deepseek-ai/dsh-llm' -import { LlmAdapter, LlmError, ReasoningEffortId, expandAssistantStream, requestImageHandleText, resolveRetryPolicy } from '@deepseek-ai/dsh-llm' +import { LlmAdapter, LlmError, ReasoningEffortId, expandAssistantStream, offloadedImageText, requestImageHandleText, resolveRetryPolicy } from '@deepseek-ai/dsh-llm' import { assertNever } from '@deepseek-ai/dsh-util-values' const PACKED_CHUNK_ROW_TYPES = new Set(['text-chunks', 'reasoning-chunks', 'tool-call-chunks']) @@ -879,10 +879,9 @@ class ReplayAdapter extends LlmAdapter { const visualTokens = configured?.models?.find(candidate => candidate.id === model)?.imageRequestTokens if (visualTokens === undefined) return undefined return { - priceImages: images => images.map(ref => ({ - visualTokens, - text: requestImageHandleText(ref, { width: ref.width, height: ref.height }), - })), + priceImages: images => images.map(({ attachment: ref, offloaded }) => (offloaded === true + ? { visualTokens: 0, text: offloadedImageText(ref) } + : { visualTokens, text: requestImageHandleText(ref, { width: ref.width, height: ref.height }) })), } } diff --git a/packages/test-support/llm-replay/tests/llm-replay.spec.ts b/packages/test-support/llm-replay/tests/llm-replay.spec.ts index 4e97358b94..816d9c30a6 100644 --- a/packages/test-support/llm-replay/tests/llm-replay.spec.ts +++ b/packages/test-support/llm-replay/tests/llm-replay.spec.ts @@ -2159,9 +2159,13 @@ describe('apply (the plugin entry)', () => { width: 640, height: 480, } as never - const priced = pricing?.priceImages([ref, ref]) - expect(priced?.map(price => price.visualTokens)).toEqual([384, 384]) - expect(priced?.every(price => price.text.includes('640x480px'))).toBe(true) + const priced = pricing?.priceImages([ + { type: 'image', attachment: ref }, + { type: 'image', attachment: ref, offloaded: true }, + ]) + expect(priced?.map(price => price.visualTokens)).toEqual([384, 0]) + expect(priced?.[0]?.text).toContain('640x480px') + expect(priced?.[1]?.text).toContain('image omitted to fit request image limits') expect(ctx.llm.imageRequestPricing('deepseek', 'plain')).toBeUndefined() }) diff --git a/packages/test-support/remote-mock/README.i18n.yaml b/packages/test-support/remote-mock/README.i18n.yaml index 66722a563c..b34a83ebed 100644 --- a/packages/test-support/remote-mock/README.i18n.yaml +++ b/packages/test-support/remote-mock/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/test-support/remote-mock/README.md -README.md: c3182b61a67aa6af799fb4fbac85083fbe805e59 -README.zh.md: 8199bc748d96df34de28d225db1289d76be6eed2 +README.md: 82979e37052aecee6ec716702f634d25c969f5ad +README.zh.md: 201ca011410dd5a4d65b6ebdfb0c47f23742b0d3 diff --git a/packages/test-support/remote-mock/README.md b/packages/test-support/remote-mock/README.md index c3182b61a6..82979e3705 100644 --- a/packages/test-support/remote-mock/README.md +++ b/packages/test-support/remote-mock/README.md @@ -26,7 +26,7 @@ English | [中文](README.zh.md) ### When to use it -Use it when a spec boots real client plugins that talk to `ctx.remote` and wants to script the Host side by endpoint name: whole-client jsdom specs through `__DSH_TRANSPORT__`, and unit specs that call `dispatch` / `open` directly. Endpoints are the Gateway's wire names (`session/page`, `settings/describe`); `args` is the caller's positional argument list with a trailing `AbortSignal` removed; a value is whatever the test registers and is answered unchanged. The only declaration is whether an endpoint is unary (`unary`) or a stream (`stream`). +Use it when a spec boots real client plugins that talk to `ctx.remote` and wants to script the Host side by endpoint name: whole-client specs bind `mock.rpc` to their Connection instance, while unit specs may call `mock.remote`, `dispatch`, or `open` directly. Endpoints are the Gateway's wire names (`session/page`, `settings/describe`); `args` is the caller's positional argument list with a trailing `AbortSignal` removed; a value is whatever the test registers and is answered unchanged. The only declaration is whether an endpoint is unary (`unary`) or a stream (`stream`). ### Use the Remote proxy @@ -77,7 +77,7 @@ A failed stream rejects the consumer's next read with the given `Error`. Consume ### Connect a client -`mock.rpc` is the `ClientConnectionRpc` face: install it as `globalThis.__DSH_TRANSPORT__ = { rpc: mock.rpc }` and the production `connection` plugin uses it in place of the HTTP caller, so every Remote call reaches `dispatch` and every stream `open` with no envelopes in between. Payloads carry `{ args }` as the whole-client proxies send them (an array) or as the Gateway's own endpoints send them (one object, delivered as one positional arg); a call whose signal aborts rejects with the abort reason. `RemoteMock.create()` registers one stream, `$events`, that answers the Gateway client's opening with `{ type: 'ready', clientId, host: { home } }` (host from `RemoteMockOptions.host`, default `/home/mock`) and stays open, which is what lets the assembled client reach `connected`; a spec overrides or fails it like any other stream. +`mock.rpc` is the `ClientConnectionRpc` face. Pass it as `{ transport: { rpc: mock.rpc } }` to the Connection installer, or let `TestClient` bind it to its instance, so every Remote call reaches `dispatch` and every stream reaches `open` with no envelopes in between. Payloads carry `{ args }` as the whole-client proxies send them (an array) or as the Gateway's own endpoints send them (one object, delivered as one positional arg); a call whose signal aborts rejects with the abort reason. `RemoteMock.create()` registers one stream, `$events`, that answers the Gateway client's opening with `{ type: 'ready', clientId, host: { home } }` (host from `RemoteMockOptions.host`, default `/home/mock`) and stays open, which is what lets the assembled client reach `connected`; a spec overrides or fails it like any other stream. ### Observe and assert @@ -129,7 +129,7 @@ None; this package neither assembles nor sends a provider request. -- **In-process carrier only** — `rpc` serves a client in the same realm through `__DSH_TRANSPORT__.rpc`; no HTTP or WebSocket carrier for browser-lane specs is provided. +- **In-process carrier only** — `rpc` serves a Connection instance in the same realm; no HTTP or WebSocket carrier for browser-lane specs is provided. - **Values cross by reference** — answers and stream items reach the client unserialized, so a non-JSON value that the real wire would reject passes through unchanged. - **Values are not checked** — a unary answer must be the result the caller reads (`{ ok, value }` or `{ ok: false, error }`); the mock passes it through unchanged and does not check those fields. - **No payload matching** — rules match on endpoint only; discriminate on business arguments inside a handler. diff --git a/packages/test-support/remote-mock/README.zh.md b/packages/test-support/remote-mock/README.zh.md index 8199bc748d..201ca01141 100644 --- a/packages/test-support/remote-mock/README.zh.md +++ b/packages/test-support/remote-mock/README.zh.md @@ -26,7 +26,7 @@ kind: "package-library" ### 何时使用 -当测试要启动与 `ctx.remote` 对话的真实客户端插件、并想按端点名脚本化 Host 侧时使用它:经 `__DSH_TRANSPORT__` 的整体 jsdom 测试,以及直接调用 `dispatch` / `open` 的单元测试。端点是 Gateway 的 wire 名(`session/page`、`settings/describe`);`args` 是调用方的位置参数列表,末尾的 `AbortSignal` 已剥掉;值就是测试登记的东西,原样应答。唯一的声明是端点是一元(`unary`)还是流(`stream`)。 +当测试要启动与 `ctx.remote` 对话的真实客户端插件、并想按端点名脚本化 Host 侧时使用它:整体客户端测试把 `mock.rpc` 绑定到各自的 Connection 实例,单元测试也可以直接调用 `mock.remote`、`dispatch` 或 `open`。端点是 Gateway 的 wire 名(`session/page`、`settings/describe`);`args` 是调用方的位置参数列表,末尾的 `AbortSignal` 已剥掉;值就是测试登记的东西,原样应答。唯一的声明是端点是一元(`unary`)还是流(`stream`)。 ### 使用 Remote Proxy @@ -77,7 +77,7 @@ await mock.streams.drained('session/follow') ### 接上客户端 -`mock.rpc` 是 `ClientConnectionRpc` 面:装成 `globalThis.__DSH_TRANSPORT__ = { rpc: mock.rpc }`,生产的 `connection` 插件就用它替代 HTTP 调用方,每次 Remote 调用直达 `dispatch`、每条流直达 `open`,中间没有信封。payload 携带 `{ args }`——整机代理发数组、Gateway 自身端点发一个对象(到达时是一个位置参数);signal 中止的调用以中止原因 reject。`RemoteMock.create()` 登记一条流 `$events`,用 `{ type: 'ready', clientId, host: { home } }`(host 来自 `RemoteMockOptions.host`,默认 `/home/mock`)应答 Gateway 客户端的打开并保持打开——这正是整机能达到 `connected` 的原因;测试可以像任何流一样覆盖或让它失败。 +`mock.rpc` 是 `ClientConnectionRpc` 面。把它作为 `{ transport: { rpc: mock.rpc } }` 传给 Connection 安装函数,或让 `TestClient` 将其绑定到自身实例,每次 Remote 调用就会直达 `dispatch`、每条流直达 `open`,中间没有信封。payload 携带 `{ args }`——整机代理发数组、Gateway 自身端点发一个对象(到达时是一个位置参数);signal 中止的调用以中止原因 reject。`RemoteMock.create()` 登记一条流 `$events`,用 `{ type: 'ready', clientId, host: { home } }`(host 来自 `RemoteMockOptions.host`,默认 `/home/mock`)应答 Gateway 客户端的打开并保持打开——这正是整机能达到 `connected` 的原因;测试可以像任何流一样覆盖或让它失败。 ### 观察与断言 @@ -129,7 +129,7 @@ await mock.streams.drained('session/follow') -- **仅进程内载体**——`rpc` 经 `__DSH_TRANSPORT__.rpc` 服务同一 realm 的客户端;不提供给浏览器车道测试用的 HTTP 或 WebSocket 载体。 +- **仅进程内载体**——`rpc` 服务同一 realm 中的 Connection 实例;不提供给浏览器车道测试用的 HTTP 或 WebSocket 载体。 - **值按引用传递**——应答与流项都未经序列化就到达客户端,真实线路会拒绝的非 JSON 值在这里原样通过。 - **不校验值**——一元应答必须是调用方读取的结果(`{ ok, value }` 或 `{ ok: false, error }`);mock 原样传递它,不检查这些字段。 - **不做 payload 匹配**——规则只按端点匹配;在 handler 内按业务参数判别。 diff --git a/packages/test-support/remote-mock/src/index.ts b/packages/test-support/remote-mock/src/index.ts index 9c78ee3a27..d7c1850c2e 100644 --- a/packages/test-support/remote-mock/src/index.ts +++ b/packages/test-support/remote-mock/src/index.ts @@ -2,7 +2,7 @@ * Endpoint-named mock for Typert Remote traffic: a table of unary answers and * stream scripts keyed by `/`, scripted-stream control, a * carrier log, and the Connection carrier face the `connection` plugin - * accepts through `__DSH_TRANSPORT__.rpc`. Values are whatever the test + * accepts through explicit installation or a page transport. Values are whatever the test * registers; the only declaration is whether an endpoint is unary or a * stream. Browser-safe: no DOM, React, or Node imports, no runtime import * from another harness package. diff --git a/packages/test-support/remote-mock/src/remote-mock.ts b/packages/test-support/remote-mock/src/remote-mock.ts index 00d10668a3..fca6a7a09c 100644 --- a/packages/test-support/remote-mock/src/remote-mock.ts +++ b/packages/test-support/remote-mock/src/remote-mock.ts @@ -90,8 +90,7 @@ class MissingUnaryRule extends Error {} /** * Endpoint-named Remote mock. `dispatch` / `open` are the core; `rpc` is the - * same core as the Connection carrier the `connection` plugin accepts through - * `__DSH_TRANSPORT__.rpc`. + * same core as the decoded carrier accepted by the Connection installer. */ export class RemoteMock { /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1593d53335..975d75afed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -619,6 +619,9 @@ importers: '@deepseek-ai/dsh-client-store': specifier: workspace:^ version: link:../../packages/client/store + '@deepseek-ai/dsh-client-test-runtime': + specifier: workspace:^ + version: link:../../packages/test-support/client-runtime '@deepseek-ai/dsh-client-ui-dockkit': specifier: workspace:^ version: link:../../packages/client/ui-dockkit @@ -649,6 +652,12 @@ importers: '@deepseek-ai/dsh-pwsh-local': specifier: workspace:^ version: link:../../packages/shell/pwsh-local + '@deepseek-ai/dsh-subprocess-local': + specifier: workspace:^ + version: link:../../packages/subprocess/subprocess-local + '@deepseek-ai/dsh-remote-mock': + specifier: workspace:^ + version: link:../../packages/test-support/remote-mock '@types/node': specifier: ^22.0.0 version: 22.20.0 @@ -893,6 +902,9 @@ importers: '@deepseek-ai/dsh-host-webserver': specifier: workspace:^ version: link:../../host/webserver + '@deepseek-ai/dsh-remote-mock': + specifier: workspace:^ + version: link:../../test-support/remote-mock '@deepseek-ai/dsh-typert-registry': specifier: workspace:^ version: link:../../typert/registry @@ -936,6 +948,9 @@ importers: '@deepseek-ai/dsh-api-settings-controller': specifier: workspace:^ version: link:../settings-controller + '@deepseek-ai/dsh-api-terminal-controller': + specifier: workspace:^ + version: link:../terminal-controller '@deepseek-ai/dsh-api-workspace-controller': specifier: workspace:^ version: link:../workspace-controller @@ -1176,6 +1191,67 @@ importers: specifier: workspace:^ version: link:../../typert/protocol + packages/api/terminal-controller: + dependencies: + '@deepseek-ai/dsh-deque': + specifier: workspace:^ + version: link:../../util/deque + '@deepseek-ai/dsh-typert-protocol': + specifier: workspace:^ + version: link:../../typert/protocol + '@deepseek-ai/schemastery': + specifier: link:../../../vendor/schemastery + version: link:../../../vendor/schemastery + '@xterm/addon-serialize': + specifier: ^0.14.0 + version: 0.14.0 + '@xterm/headless': + specifier: ^6.0.0 + version: 6.0.0 + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-api-gateway': + specifier: workspace:^ + version: link:../gateway + '@deepseek-ai/dsh-brand': + specifier: workspace:^ + version: link:../../util/brand + '@deepseek-ai/dsh-client-store': + specifier: workspace:^ + version: link:../../client/store + '@deepseek-ai/dsh-fs': + specifier: workspace:^ + version: link:../../fs/fs + '@deepseek-ai/dsh-sandbox': + specifier: workspace:^ + version: link:../../sandbox/sandbox + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-projection': + specifier: workspace:^ + version: link:../../session/session-projection + '@deepseek-ai/dsh-subprocess': + specifier: workspace:^ + version: link:../../subprocess/subprocess + '@deepseek-ai/dsh-subprocess-local': + specifier: workspace:^ + version: link:../../subprocess/subprocess-local + '@deepseek-ai/dsh-util-crypto': + specifier: workspace:^ + version: link:../../util/crypto + packages/api/workspace-controller: dependencies: '@deepseek-ai/dsh-deque': @@ -1426,6 +1502,9 @@ importers: '@deepseek-ai/dsh-compaction-basic': specifier: workspace:^ version: link:../../compaction/compaction-basic + '@deepseek-ai/dsh-compaction-image-offload': + specifier: workspace:^ + version: link:../../compaction/compaction-image-offload '@deepseek-ai/dsh-compaction-tool-result-pruner': specifier: workspace:^ version: link:../../compaction/compaction-tool-result-pruner @@ -1823,6 +1902,9 @@ importers: '@deepseek-ai/dsh-api-settings-controller': specifier: workspace:^ version: link:../../api/settings-controller + '@deepseek-ai/dsh-api-terminal-controller': + specifier: workspace:^ + version: link:../../api/terminal-controller '@deepseek-ai/dsh-api-workspace-controller': specifier: workspace:^ version: link:../../api/workspace-controller @@ -1952,6 +2034,9 @@ importers: '@deepseek-ai/dsh-client-ui-sidebar-right': specifier: workspace:^ version: link:../../client/ui-sidebar-right + '@deepseek-ai/dsh-client-ui-sidebar-terminal': + specifier: workspace:^ + version: link:../../client/ui-sidebar-terminal '@deepseek-ai/dsh-client-ui-skill': specifier: workspace:^ version: link:../../client/ui-skill @@ -2080,21 +2165,12 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis - '@deepseek-ai/dsh-api-session-controller': - specifier: workspace:^ - version: link:../../api/session-controller '@deepseek-ai/dsh-attachment': specifier: workspace:^ version: link:../../attachment/attachment '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand - '@deepseek-ai/dsh-commands': - specifier: workspace:^ - version: link:../../interaction/commands - '@deepseek-ai/dsh-host-directory-picker': - specifier: workspace:^ - version: link:../../host/directory-picker '@deepseek-ai/dsh-host-webserver': specifier: workspace:^ version: link:../../host/webserver @@ -2104,15 +2180,6 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session - '@deepseek-ai/dsh-settings': - specifier: workspace:^ - version: link:../../settings/settings - '@deepseek-ai/dsh-tool-todo': - specifier: workspace:^ - version: link:../../todo/tool-todo - '@deepseek-ai/dsh-util-values': - specifier: workspace:^ - version: link:../../util/values packages/client/file-upload: dependencies: @@ -3151,6 +3218,9 @@ importers: '@deepseek-ai/dsh-message-feedback': specifier: workspace:^ version: link:../../feedback/message-feedback + '@deepseek-ai/dsh-remote-mock': + specifier: workspace:^ + version: link:../../test-support/remote-mock '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -4106,6 +4176,9 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session + '@deepseek-ai/dsh-util-crypto': + specifier: workspace:^ + version: link:../../util/crypto '@testing-library/react': specifier: ^16.1.0 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -4131,6 +4204,78 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + packages/client/ui-sidebar-terminal: + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:^ + version: link:../../api/remotes + '@deepseek-ai/dsh-api-terminal-controller': + specifier: workspace:^ + version: link:../../api/terminal-controller + '@deepseek-ai/dsh-client-locale': + specifier: workspace:^ + version: link:../locale + '@deepseek-ai/dsh-client-store': + specifier: workspace:^ + version: link:../store + '@deepseek-ai/dsh-client-test-runtime': + specifier: workspace:^ + version: link:../../test-support/client-runtime + '@deepseek-ai/dsh-client-ui-conversation': + specifier: workspace:^ + version: link:../ui-conversation + '@deepseek-ai/dsh-client-ui-dockkit': + specifier: workspace:^ + version: link:../ui-dockkit + '@deepseek-ai/dsh-client-ui-layout': + specifier: workspace:^ + version: link:../ui-layout + '@deepseek-ai/dsh-client-ui-primitives': + specifier: workspace:^ + version: link:../ui-primitives + '@deepseek-ai/dsh-client-ui-renderer': + specifier: workspace:^ + version: link:../ui-renderer + '@deepseek-ai/dsh-client-ui-session': + specifier: workspace:^ + version: link:../ui-session + '@deepseek-ai/dsh-client-ui-sidebar-right': + specifier: workspace:^ + version: link:../ui-sidebar-right + '@deepseek-ai/dsh-client-ui-slots': + specifier: workspace:^ + version: link:../ui-slots + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@testing-library/react': + specifier: ^16.1.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/react': + specifier: ~18.3.1 + version: 18.3.31 + '@types/react-dom': + specifier: ~18.3.0 + version: 18.3.7(@types/react@18.3.31) + '@xterm/addon-fit': + specifier: ^0.11.0 + version: 0.11.0 + '@xterm/xterm': + specifier: ^6.0.0 + version: 6.0.0 + clsx: + specifier: ^2.0.0 + version: 2.1.1 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + packages/client/ui-skill: devDependencies: '@deepseek-ai/cordis': @@ -4771,6 +4916,9 @@ importers: '@deepseek-ai/dsh-compaction': specifier: workspace:^ version: link:../compaction + '@deepseek-ai/dsh-compaction-image-offload': + specifier: workspace:^ + version: link:../compaction-image-offload '@deepseek-ai/dsh-compaction-tool-result-pruner': specifier: workspace:^ version: link:../compaction-tool-result-pruner @@ -4796,6 +4944,49 @@ importers: specifier: workspace:^ version: link:../../core/tools + packages/compaction/compaction-image-offload: + dependencies: + '@deepseek-ai/dsh-util-values': + specifier: workspace:^ + version: link:../../util/values + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-agent-loop': + specifier: workspace:^ + version: link:../../core/agent-loop + '@deepseek-ai/dsh-agent-loop-testkit': + specifier: workspace:^ + version: link:../../test-support/agent-loop-testkit + '@deepseek-ai/dsh-attachment': + specifier: workspace:^ + version: link:../../attachment/attachment + '@deepseek-ai/dsh-compaction': + specifier: workspace:^ + version: link:../compaction + '@deepseek-ai/dsh-compaction-basic': + specifier: workspace:^ + version: link:../compaction-basic + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-llm-deepseek': + specifier: workspace:^ + version: link:../../llm/llm-deepseek + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-projection': + specifier: workspace:^ + version: link:../../session/session-projection + '@deepseek-ai/dsh-token-meter': + specifier: workspace:^ + version: link:../../llm/token-meter + packages/compaction/compaction-tool-result-pruner: dependencies: '@deepseek-ai/dsh-util-values': @@ -4817,6 +5008,9 @@ importers: '@deepseek-ai/dsh-compaction': specifier: workspace:^ version: link:../compaction + '@deepseek-ai/dsh-compaction-image-offload': + specifier: workspace:^ + version: link:../compaction-image-offload '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../runtime-diagnostics/invariants @@ -7721,6 +7915,9 @@ importers: '@deepseek-ai/dsh-compaction': specifier: workspace:^ version: link:../../compaction/compaction + '@deepseek-ai/dsh-compaction-image-offload': + specifier: workspace:^ + version: link:../../compaction/compaction-image-offload '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../llm @@ -8427,6 +8624,10 @@ importers: version: 18.3.1 packages/session-query/session-query: + dependencies: + '@deepseek-ai/dsh-session-format-catalog': + specifier: workspace:^ + version: link:../../session/session-format-catalog devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -8590,6 +8791,9 @@ importers: packages/session/session-format-catalog: dependencies: + '@deepseek-ai/dsh-compaction-image-offload': + specifier: workspace:^ + version: link:../../compaction/compaction-image-offload '@deepseek-ai/dsh-session-format': specifier: workspace:^ version: link:../session-format @@ -11724,6 +11928,9 @@ importers: '@deepseek-ai/dsh-compaction-basic': specifier: workspace:^ version: link:../../packages/compaction/compaction-basic + '@deepseek-ai/dsh-compaction-image-offload': + specifier: workspace:^ + version: link:../../packages/compaction/compaction-image-offload '@deepseek-ai/dsh-compaction-tool-result-pruner': specifier: workspace:^ version: link:../../packages/compaction/compaction-tool-result-pruner @@ -15356,9 +15563,18 @@ packages: resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} engines: {node: '>=10.0.0'} + '@xterm/addon-fit@0.11.0': + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} + + '@xterm/addon-serialize@0.14.0': + resolution: {integrity: sha512-uteyTU1EkrQa2Ux6P/uFl2fzmXI46jy5uoQMKEOM0fKTyiW7cSn0WrFenHm5vO5uEXX/GpwW/FgILvv3r0WbkA==} + '@xterm/headless@6.0.0': resolution: {integrity: sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw==} + '@xterm/xterm@6.0.0': + resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} + '@yao-pkg/pkg-fetch@3.6.4': resolution: {integrity: sha512-2JXvS9HbMudLlzEjSaJ7bLNnF/WlTv7iaTAJp2Tk8pBsgwCyL9p4rIN8cztHlyZF7qVzr1EaBjd7DQooFk0azQ==} hasBin: true @@ -22112,8 +22328,14 @@ snapshots: '@xmldom/xmldom@0.8.15': {} + '@xterm/addon-fit@0.11.0': {} + + '@xterm/addon-serialize@0.14.0': {} + '@xterm/headless@6.0.0': {} + '@xterm/xterm@6.0.0': {} + '@yao-pkg/pkg-fetch@3.6.4': dependencies: picocolors: 1.1.1 diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index 2bdb15d15f..0307f96e86 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -31,6 +31,7 @@ "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-compaction": "workspace:^", "@deepseek-ai/dsh-compaction-basic": "workspace:^", + "@deepseek-ai/dsh-compaction-image-offload": "workspace:^", "@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^", "@deepseek-ai/dsh-cordis-host-runner": "workspace:^", "@deepseek-ai/dsh-credentials": "workspace:^", diff --git a/scripts/fixtures/python-snapshot-image-offload.mjs b/scripts/fixtures/python-snapshot-image-offload.mjs new file mode 100644 index 0000000000..a4b767ccdf --- /dev/null +++ b/scripts/fixtures/python-snapshot-image-offload.mjs @@ -0,0 +1,43 @@ +/** One admitted image and one authored failure for the Python SDK event snapshot. */ + +/** Cordis fixture identity. */ +export const name = 'python-snapshot-image-offload' + +/** Admission and request hooks supplied by the shipped SDK profile. */ +export const inject = ['attachments', 'agents', 'llm'] + +/** + * @param {import('@deepseek-ai/cordis').Context} ctx - Scenario-local host context. + * @param {{ parentSessionId: string }} config - The one session receiving the authored failure. + */ +export function apply(ctx, config) { + let injected = false + let failed = false + ctx.on('agent/pre-step', async ({ agent }, next) => { + const decision = await next() + if (agent.id !== config.parentSessionId || injected || decision.kind === 'reject') return decision + const content = await ctx.attachments.admitPromptContent([{ + type: 'image', + mediaType: 'image/png', + data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', + }]) + injected = true + return { + ...decision, + messages: [...decision.messages, { id: 'python-snapshot-image', role: 'user', content, source: { kind: 'plugin', plugin: name } }], + } + }) + ctx.on('llm/stream', async function* (options, next) { + if (options.sessionId !== config.parentSessionId || options.purpose !== undefined || failed) { + yield* next() + return + } + failed = true + yield { + type: 'finish', + reason: { kind: 'error', failure: { + code: 'IMAGE_OFFLOAD_REQUIRED', message: 'authored image budget failure', offloadImages: 1, + } }, + } + }) +} diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index a539250909..ff2edacad4 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -128,6 +128,7 @@ export const SERVICE_PAGE: Record = { workspaceRegistry: 'workspace.md', workspaceController: 'workspace.md', workspaceFiles: 'workspace.md', + terminalController: 'workspace.md', directoryPickerController: 'workspace.md', } @@ -150,6 +151,7 @@ export const SERVICE_PAGE: Record = { * to a model as `cordis_runtime_inspect what:"client"`). */ export const SERVICE_WALK_EXEMPTIONS: Record = { + webTerminals: 'client-side terminal view models — packages/api/terminal-controller/README.md owns the API', appReady: 'not a service: launcher-provided successful-startup signal — packages/boot/cmdline/README.md owns the launcher contract', appExit: 'not a service: launcher-provided bounded process-exit callback — packages/boot/cmdline/README.md owns the launcher contract', cmdlineArgs: 'not a service: launcher-provided immutable app argument accessor — packages/boot/cmdline/README.md owns the launcher contract', @@ -199,6 +201,7 @@ export const EVENT_SCOPE_PAGE: Record = { 'api-session': 'session.md', 'approval': 'approval.md', 'commands': 'commands.md', + 'compaction': 'compaction.md', 'cordis': 'extensions.md', 'authorization': 'credentials.md', 'credentials': 'credentials.md', @@ -358,6 +361,7 @@ export const LINK_MAP: Readonly> = { SessionSelectModelRequest: 'session.md', SessionSelectModelValue: 'session.md', SessionSummary: 'session.md', + SessionMessageProjection: 'session.md', SessionUpdateQueueRequest: 'session.md', SessionUpdateQueueValue: 'session.md', EncodedFileUploadRequest: 'attachment.md', @@ -401,6 +405,7 @@ export const LINK_MAP: Readonly> = { SubprocessOutputReader: 'subprocess.md', SubprocessSpawnSpec: 'subprocess.md', SubprocessTerminalHandle: 'subprocess.md', + SubprocessTerminalEnvironment: 'subprocess.md', SubprocessTerminalSpawnSpec: 'subprocess.md', PtcRunRequest: 'ptc-runtime.md', PtcRunSpec: 'ptc-runtime.md', @@ -786,6 +791,13 @@ export const TYPE_LINK_EXEMPTIONS: Readonly> = { WorkspaceFileRange: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md', WorkspaceFileStat: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md', WorkspaceFileText: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md', + TerminalShell: 'Browser terminal shell profiles are owned by packages/api/terminal-controller/README.md', + TerminalEnvironment: 'Browser terminal environment fields are owned by packages/api/terminal-controller/README.md', + WebTerminalInfo: 'Browser terminal metadata is owned by packages/api/terminal-controller/README.md', + TerminalCreateRequest: 'Browser terminal allocation fields are owned by packages/api/terminal-controller/README.md', + TerminalAttachmentId: 'Browser terminal input ownership is owned by packages/api/terminal-controller/README.md', + TerminalFrame: 'Browser terminal stream frames are owned by packages/api/terminal-controller/README.md', + WebTerminalId: 'Browser terminal identity is owned by packages/api/terminal-controller/README.md', } /** Repository data policy consumed by the Cordis catalog projector. */ diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index b3638b72b3..5860dd8d55 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -226,6 +226,13 @@ const SERVICE_ROLES: ServiceRole[] = [ mode: 'core', note: 'Serves stat, paged text, byte windows, directory listings, and the change feed for files inside a Session\'s workspace root, confined by lstat, containment, and a stat re-check.', }, + { + key: 'terminalController', + pkg: 'api-terminal-controller', + title: 'Session interactive terminal Remote controller', + mode: 'core', + note: 'Owns user terminal processes, default shell resolution and bounded screen recovery through the subprocess provider and typed Remote transport.', + }, { key: 'workspaceController', pkg: 'api-workspace-controller', diff --git a/scripts/gen-persistence-catalog.ts b/scripts/gen-persistence-catalog.ts index a824615482..7f0be31113 100644 --- a/scripts/gen-persistence-catalog.ts +++ b/scripts/gen-persistence-catalog.ts @@ -156,6 +156,11 @@ export function renderKnownEventTypes(events: AnnotatedLogEventEntry[]): string ...names.map(name => ` '${name}',`), '])', '', + '/** Event types whose model-visible effects require an explicit pure interpreter. */', + 'export const MESSAGE_PROJECTION_EVENT_TYPES: ReadonlySet = new Set([', + ...events.filter(event => event.messageProjection).map(event => ` '${event.name}',`).sort(), + '])', + '', ].join('\n') } diff --git a/scripts/package-dependency-policy.ts b/scripts/package-dependency-policy.ts index fa1db54c75..835c252675 100644 --- a/scripts/package-dependency-policy.ts +++ b/scripts/package-dependency-policy.ts @@ -54,6 +54,7 @@ const SAFE_HOST_DEPENDENCY_EXPORTS = { /** Runtime exports that require every consumer to resolve the provider's shared peer instance. */ const PEER_REQUIRED_HOST_EXPORTS = { + '@deepseek-ai/dsh-subprocess': ['SubprocessExecutableNotFoundError'], '@deepseek-ai/dsh-scope': ['carrierKeyOf', 'scopeOf', 'scopeTarget'], '@deepseek-ai/dsh-session': ['SESSION_FORMAT_VERSION'], '@deepseek-ai/dsh-session-persistence': ['SessionPersistenceNotFoundError'], diff --git a/scripts/persistence-catalog-source.ts b/scripts/persistence-catalog-source.ts index 01a42c51b9..01eb702dd5 100644 --- a/scripts/persistence-catalog-source.ts +++ b/scripts/persistence-catalog-source.ts @@ -37,6 +37,8 @@ export interface LogEventEntry { doc: string /** Source pointer `packages/…/file.ts:line` of the declaration. */ source: string + /** Whether interpretation requires a plugin-owned message projection. */ + messageProjection?: true } /** A {@link LogEventEntry} plus its surface-eligibility badge. */ @@ -198,7 +200,8 @@ export function collectLogEvents(scanRoot: string = root): LogEventEntry[] { violations.push(`${where} has no description prose. Say what the event records and what its payload means — the JSDoc becomes the catalog entry.`) } const declaration = declarationText(text, sf, member) - entries.push({ name, scope: name.split('/')[0] ?? name, payload, declaration, doc, source: src }) + const messageProjection = ts.getJSDocTags(member).some(tag => tag.tagName.text === 'messageProjection') + entries.push({ name, scope: name.split('/')[0] ?? name, payload, declaration, doc, source: src, ...messageProjection ? { messageProjection: true as const } : {} }) } } } @@ -295,5 +298,10 @@ export function annotateSurface(events: LogEventEntry[], surfaceTypes: string[]) throw new Error(`gen-persistence-catalog: SurfaceEventType member(s) ${stale.map(t => `'${t}'`).join(', ')} name no declared log event (stale union member?).`) } const surface = new Set(surfaceTypes) + for (const event of events) { + if (event.messageProjection && surface.has(event.name)) { + throw new Error(`gen-persistence-catalog: '${event.name}' cannot declare both a message projection and surface operations.`) + } + } return events.map(e => ({ ...e, surface: surface.has(e.name) })) } diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 03232d696c..af31bc9de2 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -1330,6 +1330,9 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool) sessions = dsh_home / "sessions" patch = write_advanced_profile_patch(root, "snapshot.patch.yml", sessions) feedback_patch = write_profile_patch(root, "feedback.patch.yml", sessions, [{"insert": [ + {"id": "snapshot-image-offload", "name": ( + Path(__file__).resolve().parent / "fixtures/python-snapshot-image-offload.mjs" + ).as_uri(), "config": {"parentSessionId": SNAPSHOT_SESSION_ID}}, {"id": "snapshot-workflow-order", "name": ( Path(__file__).resolve().parent / "fixtures/python-snapshot-workflow-order.mjs" ).as_uri(), "config": { @@ -1362,6 +1365,12 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool) result = harness.run(SNAPSHOT_PROMPT, session_id=SNAPSHOT_SESSION_ID) assert result.final_response == SNAPSHOT_FINAL_TEXT, result.final_response + offloads = [event for event in result.events if event.get("type") == "image/offload"] + if len(offloads) != 1 or "surfaceOp" in offloads[0]: + raise AssertionError(f"advanced snapshot expected one standalone image offload: {offloads}") + targets = offloads[0]["data"]["targets"] + if len(targets) != 1 or targets[0]["imageIndexes"] != [0]: + raise AssertionError(f"advanced snapshot selected unexpected image occurrences: {targets}") feedback_types = [event.get("type") for event in result.events if str(event.get("type")).startswith("feedback/")] if feedback_types != ["feedback/record", "feedback/record", "feedback/message-put", "feedback/message-put", "feedback/message-delete"]: diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index 8465aeb0ad..29f5074a00 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -129,9 +129,35 @@ "surfaceOp": "append" }, { - "type": "request/header", + "type": "user/message", "seq": 10, "time": 0, + "data": { + "id": "{{messageId}}", + "role": "user", + "content": [ + { + "type": "image", + "attachment": { + "attachmentId": "sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640", + "mediaType": "image/png", + "width": 1, + "height": 1, + "bytes": 69 + } + } + ], + "source": { + "kind": "plugin", + "plugin": "python-snapshot-image-offload" + } + }, + "surfaceOp": "append" + }, + { + "type": "request/header", + "seq": 11, + "time": 0, "data": { "header": { "config": { @@ -165,7 +191,7 @@ }, { "type": "request/context", - "seq": 11, + "seq": 12, "time": 0, "data": { "provider": "deepseek-official", @@ -175,7 +201,7 @@ }, { "type": "session/title", - "seq": 12, + "seq": 13, "time": 0, "data": { "title": "Run the advanced packaged-runtime snapsh", @@ -187,19 +213,95 @@ } } }, + { + "type": "assistant/attempt", + "seq": 14, + "time": 0, + "data": { + "turn": 1, + "step": 1, + "stream": [ + { + "type": "chunk", + "time": 0, + "chunk": { + "type": "finish", + "reason": { + "kind": "error", + "failure": { + "code": "IMAGE_OFFLOAD_REQUIRED", + "message": "authored image budget failure", + "offloadImages": 1 + } + } + } + } + ] + } + }, + { + "type": "image/offload", + "seq": 15, + "time": 0, + "data": { + "targets": [ + { + "seq": 10, + "imageIndexes": [ + 0 + ] + } + ] + } + }, + { + "type": "request/header", + "seq": 16, + "time": 0, + "data": { + "header": { + "config": { + "provider": "deepseek-official", + "model": "smoke-model", + "maxTokens": 256000, + "reasoningEffort": "high" + }, + "adapterDefaults": { + "reasoningEffort": true, + "maxTokens": true + }, + "tools": [ + "cordis_define", + "cordis_inspect_list", + "cordis_inspect_query", + "cordis_inspect_self", + "cordis_run", + "cordis_stop", + "cordis_undefine", + "job_kill", + "job_list", + "job_output", + "run_code", + "subagent", + "workflow" + ] + }, + "reason": "series" + } + }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 13, + "seq": 17, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 12 + "throughSeq": 16 } }, { "type": "assistant/message", - "seq": 14, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -323,7 +425,7 @@ }, { "type": "tool/call", - "seq": 15, + "seq": 19, "time": 0, "data": { "turn": 1, @@ -335,7 +437,7 @@ }, { "type": "tool/result", - "seq": 16, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -367,13 +469,13 @@ } }, "sourceEventSeqs": [ - 15 + 19 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 17, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -382,7 +484,7 @@ }, { "type": "step/start", - "seq": 18, + "seq": 22, "time": 0, "data": { "turn": 1, @@ -391,17 +493,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 19, + "seq": 23, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 18 + "throughSeq": 22 } }, { "type": "assistant/message", - "seq": 20, + "seq": 24, "time": 0, "data": { "turn": 1, @@ -525,7 +627,7 @@ }, { "type": "tool/call", - "seq": 21, + "seq": 25, "time": 0, "data": { "turn": 1, @@ -537,7 +639,7 @@ }, { "type": "tool/result", - "seq": 22, + "seq": 26, "time": 0, "data": { "turn": 1, @@ -570,13 +672,13 @@ } }, "sourceEventSeqs": [ - 21 + 25 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 23, + "seq": 27, "time": 0, "data": { "turn": 1, @@ -585,7 +687,7 @@ }, { "type": "step/start", - "seq": 24, + "seq": 28, "time": 0, "data": { "turn": 1, @@ -594,7 +696,7 @@ }, { "type": "system/message", - "seq": 25, + "seq": 29, "time": 0, "data": { "turn": 1, @@ -625,7 +727,7 @@ }, { "type": "request/header", - "seq": 26, + "seq": 30, "time": 0, "data": { "header": { @@ -662,17 +764,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 27, + "seq": 31, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 26 + "throughSeq": 30 } }, { "type": "assistant/message", - "seq": 28, + "seq": 32, "time": 0, "data": { "turn": 1, @@ -796,7 +898,7 @@ }, { "type": "tool/call", - "seq": 29, + "seq": 33, "time": 0, "data": { "turn": 1, @@ -808,7 +910,7 @@ }, { "type": "tool/ptc-dispatch-start", - "seq": 30, + "seq": 34, "time": 0, "data": { "rootCallId": "advanced-code", @@ -822,7 +924,7 @@ }, { "type": "tool/ptc-dispatch", - "seq": 31, + "seq": 35, "time": 0, "data": { "rootCallId": "advanced-code", @@ -843,7 +945,7 @@ }, { "type": "tool/result", - "seq": 32, + "seq": 36, "time": 0, "data": { "turn": 1, @@ -871,13 +973,13 @@ } }, "sourceEventSeqs": [ - 29 + 33 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 33, + "seq": 37, "time": 0, "data": { "turn": 1, @@ -886,7 +988,7 @@ }, { "type": "step/start", - "seq": 34, + "seq": 38, "time": 0, "data": { "turn": 1, @@ -895,17 +997,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 35, + "seq": 39, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 34 + "throughSeq": 38 } }, { "type": "assistant/message", - "seq": 36, + "seq": 40, "time": 0, "data": { "turn": 1, @@ -1029,7 +1131,7 @@ }, { "type": "tool/call", - "seq": 37, + "seq": 41, "time": 0, "data": { "turn": 1, @@ -1041,7 +1143,7 @@ }, { "type": "tool/result", - "seq": 38, + "seq": 42, "time": 0, "data": { "turn": 1, @@ -1074,13 +1176,13 @@ } }, "sourceEventSeqs": [ - 37 + 41 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 39, + "seq": 43, "time": 0, "data": { "turn": 1, @@ -1089,7 +1191,7 @@ }, { "type": "step/start", - "seq": 40, + "seq": 44, "time": 0, "data": { "turn": 1, @@ -1098,17 +1200,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 41, + "seq": 45, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 40 + "throughSeq": 44 } }, { "type": "assistant/message", - "seq": 42, + "seq": 46, "time": 0, "data": { "turn": 1, @@ -1232,7 +1334,7 @@ }, { "type": "tool/call", - "seq": 43, + "seq": 47, "time": 0, "data": { "turn": 1, @@ -1244,7 +1346,7 @@ }, { "type": "tool/ptc-dispatch-start", - "seq": 44, + "seq": 48, "time": 0, "data": { "rootCallId": "advanced-denied-ptc", @@ -1258,7 +1360,7 @@ }, { "type": "tool/ptc-dispatch", - "seq": 45, + "seq": 49, "time": 0, "data": { "rootCallId": "advanced-denied-ptc", @@ -1284,7 +1386,7 @@ }, { "type": "tool/result", - "seq": 46, + "seq": 50, "time": 0, "data": { "turn": 1, @@ -1312,13 +1414,13 @@ } }, "sourceEventSeqs": [ - 43 + 47 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 47, + "seq": 51, "time": 0, "data": { "turn": 1, @@ -1327,7 +1429,7 @@ }, { "type": "step/start", - "seq": 48, + "seq": 52, "time": 0, "data": { "turn": 1, @@ -1336,17 +1438,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 49, + "seq": 53, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 48 + "throughSeq": 52 } }, { "type": "assistant/message", - "seq": 50, + "seq": 54, "time": 0, "data": { "turn": 1, @@ -1470,7 +1572,7 @@ }, { "type": "tool/call", - "seq": 51, + "seq": 55, "time": 0, "data": { "turn": 1, @@ -1482,7 +1584,7 @@ }, { "type": "subagent/catalog", - "seq": 52, + "seq": 56, "time": 0, "data": { "version": 0, @@ -1494,7 +1596,7 @@ }, { "type": "tool/result", - "seq": 53, + "seq": 57, "time": 0, "data": { "turn": 1, @@ -1522,13 +1624,13 @@ } }, "sourceEventSeqs": [ - 51 + 55 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 54, + "seq": 58, "time": 0, "data": { "turn": 1, @@ -1537,7 +1639,7 @@ }, { "type": "step/start", - "seq": 55, + "seq": 59, "time": 0, "data": { "turn": 1, @@ -1546,17 +1648,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 56, + "seq": 60, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 55 + "throughSeq": 59 } }, { "type": "assistant/message", - "seq": 57, + "seq": 61, "time": 0, "data": { "turn": 1, @@ -1680,7 +1782,7 @@ }, { "type": "tool/call", - "seq": 58, + "seq": 62, "time": 0, "data": { "turn": 1, @@ -1692,7 +1794,7 @@ }, { "type": "tool-workflow/run-start", - "seq": 59, + "seq": 63, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1701,7 +1803,7 @@ }, { "type": "subagent/catalog", - "seq": 60, + "seq": 64, "time": 0, "data": { "version": 0, @@ -1712,7 +1814,7 @@ }, { "type": "tool-workflow/agent-start", - "seq": 61, + "seq": 65, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1724,7 +1826,7 @@ }, { "type": "tool-workflow/agent-end", - "seq": 62, + "seq": 66, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1734,7 +1836,7 @@ }, { "type": "tool-workflow/run-end", - "seq": 63, + "seq": 67, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1743,7 +1845,7 @@ }, { "type": "tool/result", - "seq": 64, + "seq": 68, "time": 0, "data": { "turn": 1, @@ -1771,13 +1873,13 @@ } }, "sourceEventSeqs": [ - 58 + 62 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 65, + "seq": 69, "time": 0, "data": { "turn": 1, @@ -1786,7 +1888,7 @@ }, { "type": "step/start", - "seq": 66, + "seq": 70, "time": 0, "data": { "turn": 1, @@ -1795,17 +1897,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 67, + "seq": 71, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 66 + "throughSeq": 70 } }, { "type": "assistant/message", - "seq": 68, + "seq": 72, "time": 0, "data": { "turn": 1, @@ -1929,7 +2031,7 @@ }, { "type": "tool/call", - "seq": 69, + "seq": 73, "time": 0, "data": { "turn": 1, @@ -1941,7 +2043,7 @@ }, { "type": "tool/result", - "seq": 70, + "seq": 74, "time": 0, "data": { "turn": 1, @@ -1969,13 +2071,13 @@ } }, "sourceEventSeqs": [ - 69 + 73 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 71, + "seq": 75, "time": 0, "data": { "turn": 1, @@ -1984,7 +2086,7 @@ }, { "type": "step/start", - "seq": 72, + "seq": 76, "time": 0, "data": { "turn": 1, @@ -1993,7 +2095,7 @@ }, { "type": "system/message", - "seq": 73, + "seq": 77, "time": 0, "data": { "turn": 1, @@ -2014,17 +2116,17 @@ } }, "sourceEventSeqs": [ - 25 + 29 ], "surfaceOp": { "op": "replace", - "startSeq": 25, - "endSeq": 25 + "startSeq": 29, + "endSeq": 29 } }, { "type": "request/header", - "seq": 74, + "seq": 78, "time": 0, "data": { "header": { @@ -2060,17 +2162,17 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 75, + "seq": 79, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 74 + "throughSeq": 78 } }, { "type": "assistant/message", - "seq": 76, + "seq": 80, "time": 0, "data": { "turn": 1, @@ -2178,7 +2280,7 @@ }, { "type": "step/end", - "seq": 77, + "seq": 81, "time": 0, "data": { "turn": 1, @@ -2187,7 +2289,7 @@ }, { "type": "browser-use/stagehand-llm-request", - "seq": 78, + "seq": 82, "time": 0, "data": { "request": { @@ -2260,10 +2362,10 @@ }, { "type": "browser-use/stagehand-llm-result", - "seq": 79, + "seq": 83, "time": 0, "data": { - "requestSeq": 78, + "requestSeq": 82, "blocks": [ { "type": "tool-call", @@ -2283,7 +2385,7 @@ }, { "type": "command/run", - "seq": 80, + "seq": 84, "time": 0, "data": { "commandId": "{{command:1}}", @@ -2295,7 +2397,7 @@ }, { "type": "feedback/record", - "seq": 81, + "seq": 85, "time": 0, "data": { "text": "The session needs a clearer explanation." @@ -2303,7 +2405,7 @@ }, { "type": "command/done", - "seq": 82, + "seq": 86, "time": 0, "data": { "commandId": "{{command:1}}", @@ -2313,7 +2415,7 @@ }, { "type": "feedback/record", - "seq": 83, + "seq": 87, "time": 0, "data": { "category": "other" @@ -2321,7 +2423,7 @@ }, { "type": "feedback/message-put", - "seq": 84, + "seq": 88, "time": 0, "data": { "sessionId": "{{parent}}", @@ -2338,7 +2440,7 @@ }, { "type": "feedback/message-put", - "seq": 85, + "seq": 89, "time": 0, "data": { "sessionId": "{{parent}}", @@ -2354,7 +2456,7 @@ }, { "type": "feedback/message-delete", - "seq": 86, + "seq": 90, "time": 0, "data": { "sessionId": "{{parent}}", @@ -2363,7 +2465,7 @@ }, { "type": "turn/end", - "seq": 87, + "seq": 91, "time": 0, "data": { "turn": 1, @@ -2554,9 +2656,41 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "request/header", + "type": "user/message", "seq": 10, "time": 0, + "data": { + "id": "{{messageId}}", + "role": "user", + "content": [ + { + "type": "image", + "attachment": { + "attachmentId": "sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640", + "mediaType": "image/png", + "width": 1, + "height": 1, + "bytes": 69 + } + } + ], + "source": { + "kind": "plugin", + "plugin": "python-snapshot-image-offload" + } + }, + "surfaceOp": "append" + } + } + }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "request/header", + "seq": 11, + "time": 0, "data": { "header": { "config": { @@ -2596,7 +2730,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/context", - "seq": 11, + "seq": 12, "time": 0, "data": { "provider": "deepseek-official", @@ -2612,7 +2746,7 @@ "sessionId": "{{parent}}", "event": { "type": "session/title", - "seq": 12, + "seq": 13, "time": 0, "data": { "title": "Run the advanced packaged-runtime snapsh", @@ -2626,18 +2760,112 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "assistant/attempt", + "seq": 14, + "time": 0, + "data": { + "turn": 1, + "step": 1, + "stream": [ + { + "type": "chunk", + "time": 0, + "chunk": { + "type": "finish", + "reason": { + "kind": "error", + "failure": { + "code": "IMAGE_OFFLOAD_REQUIRED", + "message": "authored image budget failure", + "offloadImages": 1 + } + } + } + } + ] + } + } + } + }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "image/offload", + "seq": 15, + "time": 0, + "data": { + "targets": [ + { + "seq": 10, + "imageIndexes": [ + 0 + ] + } + ] + } + } + } + }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "request/header", + "seq": 16, + "time": 0, + "data": { + "header": { + "config": { + "provider": "deepseek-official", + "model": "smoke-model", + "maxTokens": 256000, + "reasoningEffort": "high" + }, + "adapterDefaults": { + "reasoningEffort": true, + "maxTokens": true + }, + "tools": [ + "cordis_define", + "cordis_inspect_list", + "cordis_inspect_query", + "cordis_inspect_self", + "cordis_run", + "cordis_stop", + "cordis_undefine", + "job_kill", + "job_list", + "job_output", + "run_code", + "subagent", + "workflow" + ] + }, + "reason": "series" + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 13, + "seq": 17, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 12 + "throughSeq": 16 } } } @@ -2648,7 +2876,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 14, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -2778,7 +3006,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 15, + "seq": 19, "time": 0, "data": { "turn": 1, @@ -2796,7 +3024,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 16, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -2828,7 +3056,7 @@ } }, "sourceEventSeqs": [ - 15 + 19 ], "surfaceOp": "append" } @@ -2840,7 +3068,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 17, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -2855,7 +3083,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 18, + "seq": 22, "time": 0, "data": { "turn": 1, @@ -2870,12 +3098,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 19, + "seq": 23, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 18 + "throughSeq": 22 } } } @@ -2886,7 +3114,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 20, + "seq": 24, "time": 0, "data": { "turn": 1, @@ -3016,7 +3244,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 21, + "seq": 25, "time": 0, "data": { "turn": 1, @@ -3034,7 +3262,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 22, + "seq": 26, "time": 0, "data": { "turn": 1, @@ -3067,7 +3295,7 @@ } }, "sourceEventSeqs": [ - 21 + 25 ], "surfaceOp": "append" } @@ -3079,7 +3307,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 23, + "seq": 27, "time": 0, "data": { "turn": 1, @@ -3094,7 +3322,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 24, + "seq": 28, "time": 0, "data": { "turn": 1, @@ -3109,7 +3337,7 @@ "sessionId": "{{parent}}", "event": { "type": "system/message", - "seq": 25, + "seq": 29, "time": 0, "data": { "turn": 1, @@ -3146,7 +3374,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 26, + "seq": 30, "time": 0, "data": { "header": { @@ -3189,12 +3417,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 27, + "seq": 31, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 26 + "throughSeq": 30 } } } @@ -3205,7 +3433,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 28, + "seq": 32, "time": 0, "data": { "turn": 1, @@ -3335,7 +3563,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 29, + "seq": 33, "time": 0, "data": { "turn": 1, @@ -3353,7 +3581,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/ptc-dispatch-start", - "seq": 30, + "seq": 34, "time": 0, "data": { "rootCallId": "advanced-code", @@ -3373,7 +3601,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/ptc-dispatch", - "seq": 31, + "seq": 35, "time": 0, "data": { "rootCallId": "advanced-code", @@ -3400,7 +3628,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 32, + "seq": 36, "time": 0, "data": { "turn": 1, @@ -3428,7 +3656,7 @@ } }, "sourceEventSeqs": [ - 29 + 33 ], "surfaceOp": "append" } @@ -3440,7 +3668,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 33, + "seq": 37, "time": 0, "data": { "turn": 1, @@ -3455,7 +3683,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 34, + "seq": 38, "time": 0, "data": { "turn": 1, @@ -3470,12 +3698,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 35, + "seq": 39, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 34 + "throughSeq": 38 } } } @@ -3486,7 +3714,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 36, + "seq": 40, "time": 0, "data": { "turn": 1, @@ -3616,7 +3844,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 37, + "seq": 41, "time": 0, "data": { "turn": 1, @@ -3634,7 +3862,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 38, + "seq": 42, "time": 0, "data": { "turn": 1, @@ -3667,7 +3895,7 @@ } }, "sourceEventSeqs": [ - 37 + 41 ], "surfaceOp": "append" } @@ -3679,7 +3907,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 39, + "seq": 43, "time": 0, "data": { "turn": 1, @@ -3694,7 +3922,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 40, + "seq": 44, "time": 0, "data": { "turn": 1, @@ -3709,12 +3937,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 41, + "seq": 45, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 40 + "throughSeq": 44 } } } @@ -3725,7 +3953,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 42, + "seq": 46, "time": 0, "data": { "turn": 1, @@ -3855,7 +4083,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 43, + "seq": 47, "time": 0, "data": { "turn": 1, @@ -3873,7 +4101,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/ptc-dispatch-start", - "seq": 44, + "seq": 48, "time": 0, "data": { "rootCallId": "advanced-denied-ptc", @@ -3893,7 +4121,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/ptc-dispatch", - "seq": 45, + "seq": 49, "time": 0, "data": { "rootCallId": "advanced-denied-ptc", @@ -3925,7 +4153,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 46, + "seq": 50, "time": 0, "data": { "turn": 1, @@ -3953,7 +4181,7 @@ } }, "sourceEventSeqs": [ - 43 + 47 ], "surfaceOp": "append" } @@ -3965,7 +4193,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 47, + "seq": 51, "time": 0, "data": { "turn": 1, @@ -3980,7 +4208,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 48, + "seq": 52, "time": 0, "data": { "turn": 1, @@ -3995,12 +4223,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 49, + "seq": 53, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 48 + "throughSeq": 52 } } } @@ -4011,7 +4239,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 50, + "seq": 54, "time": 0, "data": { "turn": 1, @@ -4141,7 +4369,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 51, + "seq": 55, "time": 0, "data": { "turn": 1, @@ -4234,7 +4462,7 @@ "sessionId": "{{parent}}", "event": { "type": "subagent/catalog", - "seq": 52, + "seq": 56, "time": 0, "data": { "version": 0, @@ -4643,7 +4871,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 53, + "seq": 57, "time": 0, "data": { "turn": 1, @@ -4671,7 +4899,7 @@ } }, "sourceEventSeqs": [ - 51 + 55 ], "surfaceOp": "append" } @@ -4683,7 +4911,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 54, + "seq": 58, "time": 0, "data": { "turn": 1, @@ -4698,7 +4926,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 55, + "seq": 59, "time": 0, "data": { "turn": 1, @@ -4713,12 +4941,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 56, + "seq": 60, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 55 + "throughSeq": 59 } } } @@ -4729,7 +4957,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 57, + "seq": 61, "time": 0, "data": { "turn": 1, @@ -4859,7 +5087,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 58, + "seq": 62, "time": 0, "data": { "turn": 1, @@ -4877,7 +5105,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/run-start", - "seq": 59, + "seq": 63, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -4967,7 +5195,7 @@ "sessionId": "{{parent}}", "event": { "type": "subagent/catalog", - "seq": 60, + "seq": 64, "time": 0, "data": { "version": 0, @@ -4984,7 +5212,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/agent-start", - "seq": 61, + "seq": 65, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -5392,7 +5620,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/agent-end", - "seq": 62, + "seq": 66, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -5408,7 +5636,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/run-end", - "seq": 63, + "seq": 67, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -5423,7 +5651,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 64, + "seq": 68, "time": 0, "data": { "turn": 1, @@ -5451,7 +5679,7 @@ } }, "sourceEventSeqs": [ - 58 + 62 ], "surfaceOp": "append" } @@ -5463,7 +5691,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 65, + "seq": 69, "time": 0, "data": { "turn": 1, @@ -5478,7 +5706,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 66, + "seq": 70, "time": 0, "data": { "turn": 1, @@ -5493,12 +5721,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 67, + "seq": 71, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 66 + "throughSeq": 70 } } } @@ -5509,7 +5737,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 68, + "seq": 72, "time": 0, "data": { "turn": 1, @@ -5639,7 +5867,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 69, + "seq": 73, "time": 0, "data": { "turn": 1, @@ -5657,7 +5885,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 70, + "seq": 74, "time": 0, "data": { "turn": 1, @@ -5685,7 +5913,7 @@ } }, "sourceEventSeqs": [ - 69 + 73 ], "surfaceOp": "append" } @@ -5697,7 +5925,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 71, + "seq": 75, "time": 0, "data": { "turn": 1, @@ -5712,7 +5940,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 72, + "seq": 76, "time": 0, "data": { "turn": 1, @@ -5727,7 +5955,7 @@ "sessionId": "{{parent}}", "event": { "type": "system/message", - "seq": 73, + "seq": 77, "time": 0, "data": { "turn": 1, @@ -5748,12 +5976,12 @@ } }, "sourceEventSeqs": [ - 25 + 29 ], "surfaceOp": { "op": "replace", - "startSeq": 25, - "endSeq": 25 + "startSeq": 29, + "endSeq": 29 } } } @@ -5764,7 +5992,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 74, + "seq": 78, "time": 0, "data": { "header": { @@ -5806,12 +6034,12 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 75, + "seq": 79, "time": 0, "data": { "sessionId": "{{parent}}", "sessionFormatVersion": 3, - "throughSeq": 74 + "throughSeq": 78 } } } @@ -5822,7 +6050,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 76, + "seq": 80, "time": 0, "data": { "turn": 1, @@ -5936,7 +6164,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 77, + "seq": 81, "time": 0, "data": { "turn": 1, @@ -5951,7 +6179,7 @@ "sessionId": "{{parent}}", "event": { "type": "browser-use/stagehand-llm-request", - "seq": 78, + "seq": 82, "time": 0, "data": { "request": { @@ -6030,10 +6258,10 @@ "sessionId": "{{parent}}", "event": { "type": "browser-use/stagehand-llm-result", - "seq": 79, + "seq": 83, "time": 0, "data": { - "requestSeq": 78, + "requestSeq": 82, "blocks": [ { "type": "tool-call", @@ -6059,7 +6287,7 @@ "sessionId": "{{parent}}", "event": { "type": "command/run", - "seq": 80, + "seq": 84, "time": 0, "data": { "commandId": "{{command:1}}", @@ -6077,7 +6305,7 @@ "sessionId": "{{parent}}", "event": { "type": "feedback/record", - "seq": 81, + "seq": 85, "time": 0, "data": { "text": "The session needs a clearer explanation." @@ -6091,7 +6319,7 @@ "sessionId": "{{parent}}", "event": { "type": "command/done", - "seq": 82, + "seq": 86, "time": 0, "data": { "commandId": "{{command:1}}", @@ -6107,7 +6335,7 @@ "sessionId": "{{parent}}", "event": { "type": "feedback/record", - "seq": 83, + "seq": 87, "time": 0, "data": { "category": "other" @@ -6121,7 +6349,7 @@ "sessionId": "{{parent}}", "event": { "type": "feedback/message-put", - "seq": 84, + "seq": 88, "time": 0, "data": { "sessionId": "{{parent}}", @@ -6144,7 +6372,7 @@ "sessionId": "{{parent}}", "event": { "type": "feedback/message-put", - "seq": 85, + "seq": 89, "time": 0, "data": { "sessionId": "{{parent}}", @@ -6166,7 +6394,7 @@ "sessionId": "{{parent}}", "event": { "type": "feedback/message-delete", - "seq": 86, + "seq": 90, "time": 0, "data": { "sessionId": "{{parent}}", @@ -6181,7 +6409,7 @@ "sessionId": "{{parent}}", "event": { "type": "turn/end", - "seq": 87, + "seq": 91, "time": 0, "data": { "turn": 1, diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl index 5227c6e5b8..7ce90f6331 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl @@ -9,53 +9,57 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}}],"source":{"kind":"plugin","plugin":"python-snapshot-image-offload"}},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":12}} +{"type":"assistant/attempt","data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"code":"IMAGE_OFFLOAD_REQUIRED","message":"authored image budget failure","offloadImages":1}}}}]}} +{"type":"image/offload","data":{"targets":[{"seq":10,"imageIndexes":[0]}]}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"series"}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":16}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n ctx.on('tools/pre-execute', (exec, next) => {\\n if (exec.name !== 'snapshot_double' || exec.arguments.value !== -1) return next()\\n return {\\n kind: 'deny',\\n reason: 'Auto review rejected tool \\\"snapshot_double\\\"; its body was not executed',\\n info: {\\n name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED',\\n reason: ' transport raw\\\\r\\\\nreason ',\\n },\\n }\\n })\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-define","args":["{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n ctx.on('tools/pre-execute', (exec, next) => {\\n if (exec.name !== 'snapshot_double' || exec.arguments.value !== -1) return next()\\n return {\\n kind: 'deny',\\n reason: 'Auto review rejected tool \\\"snapshot_double\\\"; its body was not executed',\\n info: {\\n name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED',\\n reason: ' transport raw\\\\r\\\\nreason ',\\n },\\n }\\n })\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n ctx.on('tools/pre-execute', (exec, next) => {\\n if (exec.name !== 'snapshot_double' || exec.arguments.value !== -1) return next()\\n return {\\n kind: 'deny',\\n reason: 'Auto review rejected tool \\\"snapshot_double\\\"; its body was not executed',\\n info: {\\n name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED',\\n reason: ' transport raw\\\\r\\\\nreason ',\\n },\\n }\\n })\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n ctx.on('tools/pre-execute', (exec, next) => {\\n if (exec.name !== 'snapshot_double' || exec.arguments.value !== -1) return next()\\n return {\\n kind: 'deny',\\n reason: 'Auto review rejected tool \\\"snapshot_double\\\"; its body was not executed',\\n info: {\\n name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED',\\n reason: ' transport raw\\\\r\\\\nreason ',\\n },\\n }\\n })\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[19],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":18}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":22}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-run","name":"cordis_run","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-run","args":["{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[21],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[25],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"system/message","data":{"turn":1,"step":3,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"change","startsSeries":true}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":26}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":30}} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-code","args":["{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"snapshot_double","arguments":{"value":21}}} {"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[29],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[33],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":34}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":38}} {"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-denied-native","name":"snapshot_double","arguments":"{\"value\": -1}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-native","name":"snapshot_double","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-native","args":["{\"value\": -1}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-denied-native","name":"snapshot_double","arguments":"{\"value\": -1}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-denied-native","name":"snapshot_double","arguments":"{\"value\": -1}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-denied-native"},"content":[{"type":"tool-result","toolCallId":"advanced-denied-native","content":[{"type":"text","text":"Error: Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":true}],"role":"user","id":"{{messageId}}"},"error":{"name":"AutoReviewDeniedError","code":"AUTO_REVIEW_DENIED","reason":" transport raw\r\nreason "}},"sourceEventSeqs":[37],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-denied-native"},"content":[{"type":"tool-result","toolCallId":"advanced-denied-native","content":[{"type":"text","text":"Error: Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":true}],"role":"user","id":"{{messageId}}"},"error":{"name":"AutoReviewDeniedError","code":"AUTO_REVIEW_DENIED","reason":" transport raw\r\nreason "}},"sourceEventSeqs":[41],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":40}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":44}} {"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-ptc","name":"run_code","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-ptc","args":["{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-denied-ptc","parentCallId":"advanced-denied-ptc","subCallId":"advanced-denied-ptc:ptc:1","name":"snapshot_double","arguments":{"value":-1}}} {"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-denied-ptc","parentCallId":"advanced-denied-ptc","subCallId":"advanced-denied-ptc:ptc:1","name":"snapshot_double","arguments":{"value":-1},"isError":true,"error":{"name":"AutoReviewDeniedError","code":"AUTO_REVIEW_DENIED","reason":" transport raw\r\nreason "},"content":[{"type":"text","text":"Error: Auto review rejected tool \"snapshot_double\"; its body was not executed"}]}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-denied-ptc"},"content":[{"type":"tool-result","toolCallId":"advanced-denied-ptc","content":[{"type":"text","text":"Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[43],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-denied-ptc"},"content":[{"type":"tool-result","toolCallId":"advanced-denied-ptc","content":[{"type":"text","text":"Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[47],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":48}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":52}} {"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","args":["{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":6,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}} {"type":"subagent/catalog","data":{"version":0,"childId":"{{child-1}}","childCreatedAt":0,"mode":"one-shot","label":"Check direct child"}} -{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[51],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"step/start","data":{"turn":1,"step":7}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":55}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":59}} {"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","args":["{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":7,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}} @@ -63,22 +67,22 @@ {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":7,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[58],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":7,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[62],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":7}} {"type":"step/start","data":{"turn":1,"step":8}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":66}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":70}} {"type":"assistant/message","data":{"turn":1,"step":8,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-undefine","args":["{\"pluginId\": \"snap-1\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":8,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":8,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[69],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":8,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[73],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":8}} {"type":"step/start","data":{"turn":1,"step":9}} -{"type":"system/message","data":{"turn":1,"step":9,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"sourceEventSeqs":[25],"surfaceOp":{"op":"replace","startSeq":25,"endSeq":25}} +{"type":"system/message","data":{"turn":1,"step":9,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"sourceEventSeqs":[29],"surfaceOp":{"op":"replace","startSeq":29,"endSeq":29}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change","startsSeries":true}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":74}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":78}} {"type":"assistant/message","data":{"turn":1,"step":9,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}},"id":"{{message:1}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["ADVANCED_EXECUTABLE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":9}} {"type":"browser-use/stagehand-llm-request","data":{"request":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp","maxTokens":4096,"sessionId":"{{parent}}","system":"Extract the heading\n\nReturn the requested structured result by calling stagehand_result exactly once. Put the result in its result argument. Do not emit a text answer or call another tool.","messages":[{"role":"user","source":{"kind":"plugin","plugin":"browser-use-stagehand-native"},"content":[{"type":"text","text":"Page heading: Fixture heading"}],"id":"{{messageId}}"}],"tools":[{"name":"stagehand_result","description":"Return the structured result requested by Stagehand.","parameters":{"type":"object","properties":{"result":{"type":"object","properties":{"extraction":{"type":"string"}},"required":["extraction"],"additionalProperties":false}},"required":["result"],"additionalProperties":false}}]},"responseFormat":{"type":"json_schema","name":"extract","schema":{"type":"object","properties":{"extraction":{"type":"string"}},"required":["extraction"],"additionalProperties":false}}}} -{"type":"browser-use/stagehand-llm-result","data":{"requestSeq":78,"blocks":[{"type":"tool-call","id":"structured-result","name":"stagehand_result","arguments":"{\"result\":{\"extraction\":\"Fixture heading\"}}"}],"finish":{"kind":"tool-calls"},"usage":{"inputTokens":3,"outputTokens":3}}} +{"type":"browser-use/stagehand-llm-result","data":{"requestSeq":82,"blocks":[{"type":"tool-call","id":"structured-result","name":"stagehand_result","arguments":"{\"result\":{\"extraction\":\"Fixture heading\"}}"}],"finish":{"kind":"tool-calls"},"usage":{"inputTokens":3,"outputTokens":3}}} {"type":"command/run","data":{"commandId":"{{command:1}}","name":"feedback","source":{"kind":"user"}}} {"type":"feedback/record","data":{"text":"The session needs a clearer explanation."}} {"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"Feedback recorded for session {{parent}}\nAnonymous user: {{anonymous-user}}."}} diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 607d6b8d9d..6b0d56a403 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -1,6 +1,11 @@ { "comment": "Maps each primary ` ```ts type-equiv ` or ` ```ts public-api ` block (by doc + declared symbol + projection) to the source declaration and original JSDoc it must match. Paired `.zh.md` blocks are byte-identical derivatives checked through their unsuffixed sibling and have no duplicate entry. Omit projection for the complete declaration; use public-api with a ` ```ts public-api ` block for a body-stripped public class declaration. verify-type-equiv.ts enforces a 1:1 correspondence between primary blocks and entries. Add an entry when you add a primary source-equivalence block; remove it when you remove the block.", "entries": [ + { + "doc": "docs/subsystems/compaction.md", + "symbol": "ImageOffloadTarget", + "source": "packages/compaction/compaction-image-offload/src/projection.ts" + }, { "doc": "docs/subsystems/core.md", "symbol": "Branded", @@ -2116,6 +2121,16 @@ "symbol": "ComposerChainProps", "source": "packages/client/ui-conversation/src/client/contract/slots.ts" }, + { + "doc": "docs/subsystems/session.md", + "symbol": "SessionMessageProjection", + "source": "packages/core/session/src/surface.ts" + }, + { + "doc": "docs/subsystems/session.md", + "symbol": "SessionMessageProjectionContext", + "source": "packages/core/session/src/surface.ts" + }, { "doc": "docs/subsystems/ptc-runtime.md", "symbol": "PtcRunSpec", diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 9dc1149edc..24f8cdea60 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -44,6 +44,8 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly> = { * blocks. A package moves on or off this list with its context behavior. */ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { + 'packages/api/terminal-controller': { kind: 'none', reason: 'User-owned terminal processes and screen streams never enter model requests or Session events.' }, + 'packages/client/ui-sidebar-terminal': { kind: 'none', reason: 'The browser renders user terminal screens without exposing them to the model.' }, 'packages/ssh/ssh': { kind: 'none', reason: 'The connection owner transports private provider operations; consumers own all model-facing content.' }, 'packages/ssh/fs-ssh': { kind: 'indirect', reason: 'The remote filesystem delegates model rendering to the existing filesystem consumers.' }, 'packages/ssh/subprocess-ssh': { kind: 'indirect', reason: 'The remote process provider delegates model rendering to Bash, terminal, LSP and ptc-runtime consumers.' }, diff --git a/snapshots/acp/image-compaction/cordis.snapshot.yml b/snapshots/acp/image-compaction/cordis.snapshot.yml index 8503d79a16..213a65ac8b 100644 --- a/snapshots/acp/image-compaction/cordis.snapshot.yml +++ b/snapshots/acp/image-compaction/cordis.snapshot.yml @@ -62,3 +62,6 @@ name: '@deepseek-ai/dsh-compaction-basic' config: retainTokens: 100 + +- insert: + - name: './summary-image-budget.mjs' diff --git a/snapshots/acp/image-compaction/cordis.yml b/snapshots/acp/image-compaction/cordis.yml index bbe7d3b08f..ee11ff4f15 100644 --- a/snapshots/acp/image-compaction/cordis.yml +++ b/snapshots/acp/image-compaction/cordis.yml @@ -40,3 +40,6 @@ name: '@deepseek-ai/dsh-compaction-basic' config: retainTokens: 100 + +- insert: + - name: './summary-image-budget.mjs' diff --git a/snapshots/acp/image-compaction/session.v3.jsonl b/snapshots/acp/image-compaction/session.v3.jsonl index 81b568bfbe..da7df556fb 100644 --- a/snapshots/acp/image-compaction/session.v3.jsonl +++ b/snapshots/acp/image-compaction/session.v3.jsonl @@ -12,19 +12,20 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp","contextWindow":11600}} {"type":"session/title","data":{"title":"Here are six reference screenshots","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"Received six dashboard frames; ready to discuss."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699393857,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788699393857,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Received six dashboard frames; ready to discuss."}}},{"type":"chunk","time":1788699393857,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699393857,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"Received six dashboard frames; ready to discuss."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1789095569556,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1789095569556,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Received six dashboard frames; ready to discuss."}}},{"type":"chunk","time":1789095569556,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1789095569556,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:5}}"}]}} {"type":"turn/start","data":{"turn":2}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"compaction/start","data":{"compactionId":"{{id:1}}","turn":2}} +{"type":"image/offload","data":{"targets":[{"seq":8,"imageIndexes":[0]}]}} {"type":"compaction/summary","data":{"compactionId":"{{id:1}}","summary":[{"type":"text","text":"Six identical 1x1 dashboard reference screenshots were shared and acknowledged."}],"rawOutput":[{"type":"text","text":"Six identical 1x1 dashboard reference screenshots were shared and acknowledged."}],"llmStreamCall":true,"shadowedRange":{"start":8,"end":8},"shadowedSeqs":[8],"shadowedTokenCount":366,"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp","maxTokens":8192,"usage":{"inputTokens":20,"outputTokens":16}}} -{"type":"user/message","data":{"content":[{"type":"text","text":"This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.\n\n"},{"type":"text","text":"Six identical 1x1 dashboard reference screenshots were shared and acknowledged."},{"type":"text","text":""}],"source":{"kind":"plugin","plugin":"compact","compactionId":"{{id:1}}"},"role":"user","id":"{{message:6}}"},"sourceEventSeqs":[19,20,8],"surfaceOp":{"op":"replace","startSeq":8,"endSeq":8}} +{"type":"user/message","data":{"content":[{"type":"text","text":"This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.\n\n"},{"type":"text","text":"Six identical 1x1 dashboard reference screenshots were shared and acknowledged."},{"type":"text","text":""}],"source":{"kind":"plugin","plugin":"compact","compactionId":"{{id:1}}"},"role":"user","id":"{{message:6}}"},"sourceEventSeqs":[19,21,8],"surfaceOp":{"op":"replace","startSeq":8,"endSeq":8}} {"type":"compaction/end","data":{"compactionId":"{{id:1}}","turn":2}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Now reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:5}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"tools":"{{tools}}"},"reason":"series"}} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699393888,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788699393888,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788699393888,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699393888,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1789095569580,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1789095569580,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1789095569580,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1789095569580,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/acp/image-compaction/summary-image-budget.mjs b/snapshots/acp/image-compaction/summary-image-budget.mjs new file mode 100644 index 0000000000..f74dcd3afa --- /dev/null +++ b/snapshots/acp/image-compaction/summary-image-budget.mjs @@ -0,0 +1,23 @@ +/** Authored summary-route rejection for the image-compaction snapshot. */ +export const name = 'summary-image-budget' +export const inject = ['llm'] + +/** + * @param {import('@deepseek-ai/cordis').Context} ctx - Scenario-local context. + */ +export function apply(ctx) { + let failed = false + ctx.on('llm/stream', async function* (options, next) { + if (options.purpose !== 'compaction' || failed) { + yield* next() + return + } + failed = true + yield { + type: 'finish', + reason: { kind: 'error', failure: { + code: 'IMAGE_OFFLOAD_REQUIRED', message: 'authored summary image budget failure', offloadImages: 1, + } }, + } + }) +} diff --git a/snapshots/sdk/inline-image-prompt/session.v3.jsonl b/snapshots/sdk/inline-image-prompt/session.v3.jsonl index aa27d4a471..287efd94a6 100644 --- a/snapshots/sdk/inline-image-prompt/session.v3.jsonl +++ b/snapshots/sdk/inline-image-prompt/session.v3.jsonl @@ -2,16 +2,19 @@ {"type":"permission/preset","data":{"preset":"danger-full-access"}} {"type":"sandbox/mode","data":{"mode":"danger-full-access"}} {"type":"approval/policy","data":{"policy":"never"}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Inspect this image, then reply with exactly "},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"text","text":"the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Inspect these images, then reply with exactly "},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"text","text":"the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Inspect this image, then reply with exactly "},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"text","text":"the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Inspect these images, then reply with exactly "},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}},{"type":"text","text":"the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"}} -{"type":"session/title","data":{"title":"Inspect this image, then reply","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699454715,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788699454715,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788699454715,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699454715,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Inspect these images, then reply","messageSeqs":[8],"source":{"kind":"fallback"}}} +{"type":"assistant/attempt","data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":1789029408176,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"DeepSeek base64 request images exceed the route budget; 3 more oldest occurrence(s) must be offloaded.","code":"IMAGE_OFFLOAD_REQUIRED","offloadImages":3}}}}]}} +{"type":"image/offload","data":{"targets":[{"seq":8,"imageIndexes":[0,1,2]}]}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"tools":"{{tools}}"},"reason":"series"}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1789029408184,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1789029408184,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1789029408184,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1789029408184,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/cordis-inspect-jsdoc/session.v3.jsonl b/snapshots/session/cordis-inspect-jsdoc/session.v3.jsonl index 0c58c12c6e..a184b87d82 100644 --- a/snapshots/session/cordis-inspect-jsdoc/session.v3.jsonl +++ b/snapshots/session/cordis-inspect-jsdoc/session.v3.jsonl @@ -19,7 +19,7 @@ {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-subprocess-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778146017,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788778146017,"index":0,"dt":[],"id":"inspect-subprocess-api","name":"cordis_inspect_query","args":["{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"]},{"type":"chunk","time":1788778146017,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"inspect-subprocess-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"inspect-subprocess-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"inspect-subprocess-api"},"content":[{"type":"tool-result","toolCallId":"inspect-subprocess-api","content":[{"type":"text","text":"{\n \"platform\": \"host\",\n \"provider\": \"Service\",\n \"method\": \"listService\",\n \"data\": {\n \"mode\": \"service\",\n \"service\": {\n \"key\": \"subprocess\",\n \"description\": \"Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).\\n\\nImplementations must honor these semantics:\\n\\n- Executable paths belong to one execution world shared with the mounted filesystem provider.\\n- spawn returns a live handle synchronously. Target identity remains provider-private; `done` resolves with the spawned command's exit facts and may reject for spawn or provider failures.\\n- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here.\\n- SubprocessHandle.terminate (and the spec's abort signal) starts the provider's documented procedure against its managed range. SubprocessHandle.waitForExit observes that same range so a consumer-owned teardown ladder can hold each tier on real quiescence; each provider documents its signalling and observability limits.\\n- Disposal of the service terminates all still-running managed processes and awaits their exit.\\n- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits.\",\n \"access\": {\n \"optional\": {\n \"expression\": \"ctx.get(\\\"subprocess\\\")\",\n \"requiresUndefinedCheck\": true\n },\n \"hardDependency\": {\n \"inject\": [\n \"subprocess\"\n ],\n \"expression\": \"ctx.subprocess\"\n }\n },\n \"methods\": [\n {\n \"signature\": \"abstract resolveExecutable( command: string, env?: Readonly>, signal?: AbortSignal, ): Promise\",\n \"description\": \"Resolve one configured executable in this provider's execution world. Absolute paths are verified; bare names use the provider's scrubbed PATH plus explicit environment overrides. Relative paths containing separators are rejected: the resolution base is undefined, so providers fail loud instead of guessing.\",\n \"parameters\": [\n {\n \"name\": \"command\",\n \"description\": \"absolute executable path or bare PATH name.\"\n },\n {\n \"name\": \"env\",\n \"description\": \"explicit environment entries used for lookup.\"\n },\n {\n \"name\": \"signal\",\n \"description\": \"aborts remote or local lookup.\"\n }\n ],\n \"returns\": \"a canonical executable path.\"\n },\n {\n \"signature\": \"abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle\",\n \"description\": \"Start one managed child process from a fully-specified spec; this seam applies no defaults.\",\n \"parameters\": [\n {\n \"name\": \"spec\",\n \"description\": \"argv, directory, stdio dispositions, grace, cancellation, and environment.\"\n }\n ],\n \"returns\": \"the live process handle (streams/readers, signalling, outcome promise).\",\n \"throws\": [\n \"synchronously when pre-aborted or when argv, cwd, environment, or grace is invalid before handle creation.\"\n ]\n },\n {\n \"signature\": \"abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise\",\n \"description\": \"Allocate a real terminal and start one owned process session. This is the only non-pipe process primitive: implementations own terminal byte I/O, foreground groups, signals, and whole-session quiescence.\",\n \"parameters\": [\n {\n \"name\": \"spec\",\n \"description\": \"fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation.\"\n }\n ],\n \"returns\": \"the live terminal handle after allocation succeeds.\"\n }\n ]\n },\n \"referencedTypes\": [\n {\n \"name\": \"SubprocessCollect\",\n \"declaration\": \"export interface SubprocessCollect {\\n maxBytes: number;\\n spill?: {\\n maxBytes: number;\\n };\\n}\"\n },\n {\n \"name\": \"SubprocessCollectedOutputs\",\n \"declaration\": \"export interface SubprocessCollectedOutputs {\\n readonly stdout?: SubprocessOutputReader;\\n readonly stderr?: SubprocessOutputReader;\\n}\"\n },\n {\n \"name\": \"SubprocessHandle\",\n \"declaration\": \"export interface SubprocessHandle {\\n readonly stdin: Writable | undefined;\\n readonly stdout: Readable | undefined;\\n readonly stderr: Readable | undefined;\\n readonly control: Duplex | undefined;\\n readonly collected: SubprocessCollectedOutputs;\\n readonly done: Promise;\\n terminate(): void;\\n waitForExit(signal?: AbortSignal): Promise;\\n}\"\n },\n {\n \"name\": \"SubprocessOutcome\",\n \"declaration\": \"export interface SubprocessOutcome {\\n exitCode: number | null;\\n signal: NodeJS.Signals | null;\\n}\"\n },\n {\n \"name\": \"SubprocessOutputMode\",\n \"declaration\": \"export type SubprocessOutputMode = 'pipe' | 'inherit' | SubprocessCollect;\"\n },\n {\n \"name\": \"SubprocessOutputRead\",\n \"declaration\": \"export interface SubprocessOutputRead {\\n text: string;\\n nextOffset: number;\\n lossy: boolean;\\n spillPath?: string;\\n}\"\n },\n {\n \"name\": \"SubprocessOutputReader\",\n \"declaration\": \"export interface SubprocessOutputReader {\\n readFrom(fromByte: number): SubprocessOutputRead;\\n}\"\n },\n {\n \"name\": \"SubprocessSpawnSpec\",\n \"declaration\": \"export interface SubprocessSpawnSpec {\\n argv: readonly string[];\\n cwd: string;\\n stdio: SubprocessStdio;\\n graceMs: number;\\n signal?: AbortSignal | undefined;\\n env?: NodeJS.ProcessEnv | undefined;\\n}\"\n },\n {\n \"name\": \"SubprocessStdinMode\",\n \"declaration\": \"export type SubprocessStdinMode = 'ignore' | 'pipe' | {\\n readonly data: string;\\n};\"\n },\n {\n \"name\": \"SubprocessStdio\",\n \"declaration\": \"export interface SubprocessStdio {\\n stdin: SubprocessStdinMode;\\n stdout: SubprocessOutputMode;\\n stderr: SubprocessOutputMode;\\n control?: 'pipe';\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalForeground\",\n \"declaration\": \"export interface SubprocessTerminalForeground {\\n processGroupId: number;\\n inputWaiting: boolean;\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalHandle\",\n \"declaration\": \"export interface SubprocessTerminalHandle {\\n readonly pid: number;\\n readonly output: Readable;\\n readonly done: Promise;\\n write(data: string): Promise;\\n inspectForeground(): Promise;\\n signalForeground(signal: SubprocessTerminalSignal): Promise;\\n terminate(): Promise;\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalSignal\",\n \"declaration\": \"export type SubprocessTerminalSignal = 'SIGINT' | 'SIGTERM' | 'SIGKILL' | 'SIGTSTP' | 'SIGHUP';\"\n },\n {\n \"name\": \"SubprocessTerminalSpawnSpec\",\n \"declaration\": \"export interface SubprocessTerminalSpawnSpec {\\n argv: readonly string[];\\n cwd: string;\\n env?: Record | undefined;\\n rows: number;\\n cols: number;\\n graceMs: number;\\n signal?: AbortSignal | undefined;\\n}\"\n }\n ]\n }\n}"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"inspect-subprocess-api"},"content":[{"type":"tool-result","toolCallId":"inspect-subprocess-api","content":[{"type":"text","text":"{\n \"platform\": \"host\",\n \"provider\": \"Service\",\n \"method\": \"listService\",\n \"data\": {\n \"mode\": \"service\",\n \"service\": {\n \"key\": \"subprocess\",\n \"description\": \"Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).\\n\\nImplementations must honor these semantics:\\n\\n- Executable paths belong to one execution world shared with the mounted filesystem provider.\\n- spawn returns a live handle synchronously. Target identity remains provider-private; `done` resolves with the spawned command's exit facts and may reject for spawn or provider failures.\\n- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here.\\n- SubprocessHandle.terminate (and the spec's abort signal) starts the provider's documented procedure against its managed range. SubprocessHandle.waitForExit observes that same range so a consumer-owned teardown ladder can hold each tier on real quiescence; each provider documents its signalling and observability limits.\\n- Disposal of the service terminates all still-running managed processes and awaits their exit.\\n- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits.\",\n \"access\": {\n \"optional\": {\n \"expression\": \"ctx.get(\\\"subprocess\\\")\",\n \"requiresUndefinedCheck\": true\n },\n \"hardDependency\": {\n \"inject\": [\n \"subprocess\"\n ],\n \"expression\": \"ctx.subprocess\"\n }\n },\n \"methods\": [\n {\n \"signature\": \"abstract resolveExecutable( command: string, env?: Readonly>, signal?: AbortSignal, ): Promise\",\n \"description\": \"Resolve one configured executable in this provider's execution world. Absolute paths are verified; bare names use the provider's scrubbed PATH plus explicit environment overrides. Relative paths containing separators are rejected: the resolution base is undefined, so providers fail loud instead of guessing.\",\n \"parameters\": [\n {\n \"name\": \"command\",\n \"description\": \"absolute executable path or bare PATH name.\"\n },\n {\n \"name\": \"env\",\n \"description\": \"explicit environment entries used for lookup.\"\n },\n {\n \"name\": \"signal\",\n \"description\": \"aborts remote or local lookup.\"\n }\n ],\n \"returns\": \"a canonical executable path.\"\n },\n {\n \"signature\": \"abstract terminalEnvironment(signal?: AbortSignal): Promise\",\n \"description\": \"Inspect shell-selection facts in the provider's execution environment.\",\n \"parameters\": [\n {\n \"name\": \"signal\",\n \"description\": \"cancellation of remote environment inspection.\"\n }\n ],\n \"returns\": \"platform and preferred shell; executable lookup and allocation remain separate operations.\"\n },\n {\n \"signature\": \"abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle\",\n \"description\": \"Start one managed child process from a fully-specified spec; this seam applies no defaults.\",\n \"parameters\": [\n {\n \"name\": \"spec\",\n \"description\": \"argv, directory, stdio dispositions, grace, cancellation, and environment.\"\n }\n ],\n \"returns\": \"the live process handle (streams/readers, signalling, outcome promise).\",\n \"throws\": [\n \"synchronously when pre-aborted or when argv, cwd, environment, or grace is invalid before handle creation.\"\n ]\n },\n {\n \"signature\": \"abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise\",\n \"description\": \"Allocate a real terminal and start one owned process session. This is the only non-pipe process primitive: implementations own terminal byte I/O, foreground groups, signals, and whole-session quiescence.\",\n \"parameters\": [\n {\n \"name\": \"spec\",\n \"description\": \"fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation.\"\n }\n ],\n \"returns\": \"the live terminal handle after allocation succeeds.\"\n }\n ]\n },\n \"referencedTypes\": [\n {\n \"name\": \"SubprocessCollect\",\n \"declaration\": \"export interface SubprocessCollect {\\n maxBytes: number;\\n spill?: {\\n maxBytes: number;\\n };\\n}\"\n },\n {\n \"name\": \"SubprocessCollectedOutputs\",\n \"declaration\": \"export interface SubprocessCollectedOutputs {\\n readonly stdout?: SubprocessOutputReader;\\n readonly stderr?: SubprocessOutputReader;\\n}\"\n },\n {\n \"name\": \"SubprocessHandle\",\n \"declaration\": \"export interface SubprocessHandle {\\n readonly stdin: Writable | undefined;\\n readonly stdout: Readable | undefined;\\n readonly stderr: Readable | undefined;\\n readonly control: Duplex | undefined;\\n readonly collected: SubprocessCollectedOutputs;\\n readonly done: Promise;\\n terminate(): void;\\n waitForExit(signal?: AbortSignal): Promise;\\n}\"\n },\n {\n \"name\": \"SubprocessOutcome\",\n \"declaration\": \"export interface SubprocessOutcome {\\n exitCode: number | null;\\n signal: NodeJS.Signals | null;\\n}\"\n },\n {\n \"name\": \"SubprocessOutputMode\",\n \"declaration\": \"export type SubprocessOutputMode = 'pipe' | 'inherit' | SubprocessCollect;\"\n },\n {\n \"name\": \"SubprocessOutputRead\",\n \"declaration\": \"export interface SubprocessOutputRead {\\n text: string;\\n nextOffset: number;\\n lossy: boolean;\\n spillPath?: string;\\n}\"\n },\n {\n \"name\": \"SubprocessOutputReader\",\n \"declaration\": \"export interface SubprocessOutputReader {\\n readFrom(fromByte: number): SubprocessOutputRead;\\n}\"\n },\n {\n \"name\": \"SubprocessSpawnSpec\",\n \"declaration\": \"export interface SubprocessSpawnSpec {\\n argv: readonly string[];\\n cwd: string;\\n stdio: SubprocessStdio;\\n graceMs: number;\\n signal?: AbortSignal | undefined;\\n env?: NodeJS.ProcessEnv | undefined;\\n}\"\n },\n {\n \"name\": \"SubprocessStdinMode\",\n \"declaration\": \"export type SubprocessStdinMode = 'ignore' | 'pipe' | {\\n readonly data: string;\\n};\"\n },\n {\n \"name\": \"SubprocessStdio\",\n \"declaration\": \"export interface SubprocessStdio {\\n stdin: SubprocessStdinMode;\\n stdout: SubprocessOutputMode;\\n stderr: SubprocessOutputMode;\\n control?: 'pipe';\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalEnvironment\",\n \"declaration\": \"export interface SubprocessTerminalEnvironment {\\n platform: 'posix' | 'windows';\\n defaultShell?: string;\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalForeground\",\n \"declaration\": \"export interface SubprocessTerminalForeground {\\n processGroupId: number;\\n inputWaiting: boolean;\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalHandle\",\n \"declaration\": \"export interface SubprocessTerminalHandle {\\n readonly pid: number;\\n readonly output: Readable;\\n readonly done: Promise;\\n write(data: string): Promise;\\n resize(cols: number, rows: number): Promise;\\n inspectForeground(): Promise;\\n signalForeground(signal: SubprocessTerminalSignal): Promise;\\n terminate(): Promise;\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalSignal\",\n \"declaration\": \"export type SubprocessTerminalSignal = 'SIGINT' | 'SIGTERM' | 'SIGKILL' | 'SIGTSTP' | 'SIGHUP';\"\n },\n {\n \"name\": \"SubprocessTerminalSpawnSpec\",\n \"declaration\": \"export interface SubprocessTerminalSpawnSpec {\\n argv: readonly string[];\\n cwd: string;\\n env?: Record | undefined;\\n rows: number;\\n cols: number;\\n terminalType: string;\\n graceMs: number;\\n signal?: AbortSignal | undefined;\\n}\"\n }\n ]\n }\n}"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"CORDIS_INSPECT_JSDOC_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778146037,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788778146037,"index":0,"dt":[],"texts":["CORDIS_INSPECT_JSDOC_OK"]},{"type":"chunk","time":1788778146037,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORDIS_INSPECT_JSDOC_OK"}}},{"type":"chunk","time":1788778146037,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778146037,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} diff --git a/snapshots/web/auto-review-denial/ui.expected.md b/snapshots/web/auto-review-denial/ui.expected.md index c430cf3f33..b849cc3a7b 100644 --- a/snapshots/web/auto-review-denial/ui.expected.md +++ b/snapshots/web/auto-review-denial/ui.expected.md @@ -121,9 +121,11 @@ - 'button "Request #2"' - text: ASSISTANT - cell "(tool call only)" - - 'row "TOOL, run_code {\"code\":\"try { await tools.bash({command: \"echo hidden-input-ptc\"}) } catch (error) { return \"denial caught\" }\",\"description\":\"Catch an Auto-denied inner call\"}"': + - row "TOOL, run_code Catch an Auto-denied inner call": - cell "TOOL" - - 'cell "run_code{\"code\":\"try { await tools.bash({command: \"echo hidden-input-ptc\"}) } catch (error) { return \"denial caught\" }\",\"description\":\"Catch an Auto-denied inner call\"} → denial caught"' + - cell "run_codeCatch an Auto-denied inner call → denial caught": + - img + - text: run_codeCatch an Auto-denied inner call → denial caught - 'row "SUBTOOL, bash {\"command\":\"echo hidden-input-ptc\"}" [selected]': - cell "SUBTOOL" - 'cell "bash{\"command\":\"echo hidden-input-ptc\"} → AUTO_REVIEW_DENIED"' diff --git a/snapshots/web/navigation-panes/timing.expected.md b/snapshots/web/navigation-panes/timing.expected.md new file mode 100644 index 0000000000..0a7a17c389 --- /dev/null +++ b/snapshots/web/navigation-panes/timing.expected.md @@ -0,0 +1,12 @@ +- tabpanel "Timing": + - term: Started + - definition: + - button "2026-01-01 {{clock}}" + - term: Total duration + - definition: {{duration}} + - term: TTFT + - definition: {{duration}} + - term: Generation + - definition: {{duration}} + - term: Throughput + - definition: {{throughput}} tok/s diff --git a/snapshots/web/ptc-round/code.expected.md b/snapshots/web/ptc-round/code.expected.md new file mode 100644 index 0000000000..5e1d42ae59 --- /dev/null +++ b/snapshots/web/ptc-round/code.expected.md @@ -0,0 +1,7 @@ +- tabpanel "Code": + - text: Code typescript + - button "Wrap lines" [pressed] + - button "Original JSON" + - button "Copy code": + - img + - code: "const bashResult = await tools.bash({ command: \"echo CODE_ROUND_OK\", description: \"Echo CODE_ROUND_OK\" }); let readError = null; try { await tools.read({ file_path: \"missing.txt\" }); } catch (e) { readError = { toolName: e.toolName, message: e.message }; } return { bash: bashResult.stdout.text.trim(), readError };" diff --git a/snapshots/web/ptc-round/trajectory.expected.md b/snapshots/web/ptc-round/trajectory.expected.md new file mode 100644 index 0000000000..ed88d3b743 --- /dev/null +++ b/snapshots/web/ptc-round/trajectory.expected.md @@ -0,0 +1,22 @@ +- heading "Code typescript Wrap lines Copy code" [level=3]: + - button "Code": + - text: Code + - img + - text: typescript + - button "Wrap lines" + - button "Copy code": + - img +- code: "const bashResult = await tools.bash({ command: \"echo CODE_ROUND_OK\", description: \"Echo CODE_ROUND_OK\" }); let readError = null; try { await tools.read({ file_path: \"missing.txt\" }); } catch (e) { readError = { toolName: e.toolName, message: e.message }; } return { bash: bashResult.stdout.text.trim(), readError };" +- heading "Output Copy output" [level=3]: + - button "Output": + - text: Output + - img + - button "Copy output": + - img +- text: "{" +- tree "Result JSON": + - treeitem "bash:\"CODE_ROUND_OK\"," + - 'treeitem "Expand readError:{toolName: \"read\", message: \"cannot read \\\"{{cwd}}/workspace/missing.txt\\\": not found\"}"': + - button "Expand" + - text: "readError:{toolName: \"read\", message: \"cannot read \\\"{{cwd}}/workspace/missing.txt\\\": not found\"}" +- text: "}" diff --git a/tsconfig.base.json b/tsconfig.base.json index 9ebf546f48..643de045a9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -36,6 +36,11 @@ "@deepseek-ai/dsh-experimental-browser-use-stagehand-native": ["./packages/experimental/browser-use-stagehand-native/src/index.ts"], "@deepseek-ai/dsh-experimental-browser-use-runtime/mcp": ["./packages/experimental/browser-use-runtime/src/mcp.ts"], + "@deepseek-ai/dsh-api-terminal-controller": ["./packages/api/terminal-controller/src/index.ts"], + "@deepseek-ai/dsh-api-terminal-controller/types": ["./packages/api/terminal-controller/src/types.ts"], + "@deepseek-ai/dsh-api-terminal-controller/client": ["./packages/api/terminal-controller/src/client/index.ts"], + "@deepseek-ai/dsh-client-ui-sidebar-terminal": ["./packages/client/ui-sidebar-terminal/src/index.ts"], + "@deepseek-ai/dsh-client-ui-sidebar-terminal/client": ["./packages/client/ui-sidebar-terminal/src/client/index.ts"], "@deepseek-ai/dsh-ssh/protocol": ["./packages/ssh/ssh/src/protocol.ts"], "@deepseek-ai/dsh-ssh/schemas": ["./packages/ssh/ssh/src/schemas.ts"], "@deepseek-ai/dsh-subprocess-local/output": ["./packages/subprocess/subprocess-local/src/output.ts"], @@ -270,6 +275,8 @@ // session-log-export lives under session-query/, so the generated aliases map only its // bare name; the browser half needs this explicit subpath for source-mode dynamic imports. "@deepseek-ai/dsh-session-log-export/client": ["./packages/session-query/session-log-export/src/client"], + "@deepseek-ai/dsh-compaction-image-offload/projection": ["./packages/compaction/compaction-image-offload/src/projection.ts"], + "@deepseek-ai/dsh-session-format-catalog/message-projections": ["./packages/session/session-format-catalog/src/message-projections.ts"], // BEGIN generated package aliases — pnpm run gen-tsconfig-paths "@deepseek-ai/dsh-acp": ["./packages/acp/acp/src"], "@deepseek-ai/dsh-acp-app": ["./packages/bundle/acp-app/src"], @@ -302,6 +309,7 @@ "@deepseek-ai/dsh-compaction": ["./packages/compaction/compaction/src"], "@deepseek-ai/dsh-compaction/invariant": ["./packages/compaction/compaction/src/invariant.ts"], "@deepseek-ai/dsh-compaction-basic": ["./packages/compaction/compaction-basic/src"], + "@deepseek-ai/dsh-compaction-image-offload": ["./packages/compaction/compaction-image-offload/src"], "@deepseek-ai/dsh-compaction-tool-result-pruner": ["./packages/compaction/compaction-tool-result-pruner/src"], "@deepseek-ai/dsh-credentials": ["./packages/credentials/credentials/src"], "@deepseek-ai/dsh-credentials/invariant": ["./packages/credentials/credentials/src/invariant.ts"], diff --git a/tsconfig.client.json b/tsconfig.client.json index a5be2366cd..0ab7e0edcb 100644 --- a/tsconfig.client.json +++ b/tsconfig.client.json @@ -42,6 +42,8 @@ "packages/client/*/tests/**/*.host.spec.ts" ], "references": [ + { "path": "./packages/client/ui-sidebar-terminal" }, + { "path": "./packages/api/terminal-controller/tsconfig.client.json" }, // Producer-to-UI spill tests use the ptc-runtime contract with the real tool registry. { "path": "./packages/ptc-runtime/ptc-runtime" }, // Shared leaf: web e2e boots the real host webserver (fixture + real-host diff --git a/tsconfig.host.json b/tsconfig.host.json index e28dddc3f7..6f93d6095c 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -105,6 +105,7 @@ "apps/web/tests/goal-multi-turn-actions.e2e.ts", "apps/web/tests/agent-team-panel.e2e.ts", "apps/web/tests/sidebar-right.e2e.ts", + "apps/web/tests/sidebar-terminal.e2e.ts", "apps/web/tests/chat-scroll-fixture.ts", "apps/web/tests/chat-scroll-contract.e2e.ts", "apps/web/tests/chat-long-interactions.e2e.ts", @@ -147,6 +148,7 @@ { "path": "./packages/experimental/browser-use-playwright-mcp" }, { "path": "./packages/experimental/browser-use-chrome-devtools-mcp" }, { "path": "./packages/experimental/browser-use-stagehand-native" }, + { "path": "./packages/api/terminal-controller/tsconfig.host.json" }, { "path": "./packages/computer-use/computer-use" }, { "path": "./packages/experimental/computer-use-cua-driver-mcp" }, { "path": "./packages/experimental/computer-use-cua-driver-native" }, @@ -298,6 +300,7 @@ { "path": "./packages/compaction/compaction-basic" }, { "path": "./packages/compaction/command-compact" }, { "path": "./packages/compaction/compaction-tool-result-pruner" }, + { "path": "./packages/compaction/compaction-image-offload" }, { "path": "./packages/web/web" }, { "path": "./packages/web/web-search-exa" }, { "path": "./packages/web/web-search-perplexity" }, diff --git a/vitest.config.ts b/vitest.config.ts index 20c6e149ad..cd6752424c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -305,7 +305,6 @@ export default defineConfig({ 'packages/experimental/client-ui-agent-team/src/client/index.ts', // Slash/command/input round: per-file gaps deferred with the same // client-lane debt. TODO(gui): cover and remove with the lane above. - 'packages/client/connection/src/client/fixture.ts', 'packages/client/ui-commands/src/index.ts', 'packages/client/ui-skill/src/index.ts', 'packages/client/ui-input-trigger/src/index.ts', diff --git a/vitest.web-stress.config.ts b/vitest.web-stress.config.ts index 94bda7e117..6da0bd1315 100644 --- a/vitest.web-stress.config.ts +++ b/vitest.web-stress.config.ts @@ -1,10 +1,10 @@ import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'vitest/config' -import { vitestExecArgv } from './vitest.shared.ts' +import { standardDecoratorPlugin, vitestExecArgv } from './vitest.shared.ts' /** Opt-in browser performance lane; no default Vitest config includes *.stress.ts. */ export default defineConfig({ - plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] })], + plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] }), standardDecoratorPlugin()], test: { execArgv: vitestExecArgv, include: ['apps/web/stress-tests/**/*.stress.ts'],