Merge remote-tracking branch 'origin/master' into fix/continuable-subagent-policy-inheritance
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
|
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
|
||||||
2026-07-05-reconstructable-requests.md: c13e40b5b22e5d6fe19f6b5bd137db3a48350390
|
2026-07-05-reconstructable-requests.md: d7b867d6e17c2dfe0e49b70bd0f5e7ff173fb572
|
||||||
2026-07-05-reconstructable-requests.zh.md: 746bcd37dabada949873d394776c1ca43eff4ea2
|
2026-07-05-reconstructable-requests.zh.md: c41e5f89558a9c7b09ccd2bf4fd5b7d9a6cde419
|
||||||
@@ -14,7 +14,7 @@ The reference shape for the happy path is MiniCode's `LLMClient`: a stateful con
|
|||||||
|
|
||||||
### The principle
|
### The principle
|
||||||
|
|
||||||
**Model-visible ⟺ logged.** Anything that reaches a model request must be recorded in the session log. The checkable consequence: **every conversation request the loop sends is a pure function of the session log** — anyone holding the log reconstructs it byte-for-byte. Scope, stated precisely: the guarantee covers the loop-built `GenerateOptions`; provider wire bytes follow from it because both adapters' serialization is a pure per-message function at a pinned code version; direct one-shots (compaction's summarize call) log their envelope scalars (`compact/summary.{provider, model, maxTokens}`) and their input is deterministic code over the logged region — reconstructable from log + code, outside the invariant because only the loop marks request ownership.
|
**Model-visible ⟺ durably referenced.** Anything that reaches a model request must be reconstructable from the session log and the immutable content-addressed objects it references. The checkable consequence: anyone holding the log, its referenced attachment objects, and the pinned code version reconstructs every loop request byte-for-byte. Text-only `GenerateOptions` remain a pure function of the log; image-bearing requests additionally resolve `ImageAttachmentRef` bytes through `ctx.attachments` during adapter serialization, where digest and recorded metadata verification make the object lookup deterministic and fail loud on missing or corrupt data. Direct one-shots (compaction's summarize call) log their envelope scalars (`compact/summary.{provider, model, maxTokens}`), and their input is deterministic code over the logged region plus those referenced objects — outside the invariant because only the loop marks request ownership.
|
||||||
|
|
||||||
Prefix-cache stability is corollary #1, not the headline: an append-only log projected by a per-node pure function yields requests that are append-extensions of their predecessors whenever the header is unchanged — stability is emergent, not managed. Byte-exact audit/replay is corollary #2; resume and fork with *attributable* drift is corollary #3.
|
Prefix-cache stability is corollary #1, not the headline: an append-only log projected by a per-node pure function yields requests that are append-extensions of their predecessors whenever the header is unchanged — stability is emergent, not managed. Byte-exact audit/replay is corollary #2; resume and fork with *attributable* drift is corollary #3.
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Status: implemented
|
|||||||
|
|
||||||
### 原则
|
### 原则
|
||||||
|
|
||||||
**模型可见 ⟺ 已记录。** 凡到达模型请求的内容都必须记录在会话日志中。可检查的推论:**循环发出的每个对话请求都是会话日志的纯函数**——任何人持有日志即可逐字节重建请求。精确的范围声明:该保证覆盖循环构建的 `GenerateOptions`;提供方协议格式(wire format)字节由此推导而来,因为两个适配器的序列化在固定代码版本下都是逐消息的纯函数;直接的一次性调用(压缩的 summarize 调用)记录其信封标量(`compact/summary.{provider, model, maxTokens}`),其输入是对日志区域的确定性代码运算——可从日志加代码重建,因为只有循环会标记请求归属,所以它们不在不变式内。
|
**模型可见 ⟺ 已持久引用。** 凡到达模型请求的内容都必须能从会话日志及其引用的不可变内容寻址对象中重建。可检查的推论:任何人持有日志、日志引用的附件对象和固定代码版本,即可逐字节重建循环的每个请求。纯文本 `GenerateOptions` 仍是日志的纯函数;含图片请求还会在适配器序列化期间通过 `ctx.attachments` 解析 `ImageAttachmentRef` 字节,其中对内容摘要及已记录元数据的校验使对象查找具有确定性,并在数据缺失或损坏时明确失败。直接的一次性调用(压缩的 summarize 调用)记录其信封标量(`compact/summary.{provider, model, maxTokens}`),其输入是对日志区域及这些引用对象的确定性代码运算——由于只有循环会标记请求归属,因此它们不在不变式内。
|
||||||
|
|
||||||
前缀缓存稳定性是推论 #1,而非标题:一个仅追加的日志经逐节点纯函数投影,在 header 不变时自然产出前一请求的追加扩展——稳定性是涌现的,不是管理出来的。字节精确的审计/回放是推论 #2;带*可归因*漂移的恢复与 fork 是推论 #3。
|
前缀缓存稳定性是推论 #1,而非标题:一个仅追加的日志经逐节点纯函数投影,在 header 不变时自然产出前一请求的追加扩展——稳定性是涌现的,不是管理出来的。字节精确的审计/回放是推论 #2;带*可归因*漂移的恢复与 fork 是推论 #3。
|
||||||
|
|
||||||
|
|||||||
@@ -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/bug-fix/2026-07-29-atomic-web-image-admission.md
|
||||||
|
2026-07-29-atomic-web-image-admission.md: c09d376f101a41994df3a10c22c06da4e59f06f6
|
||||||
|
2026-07-29-atomic-web-image-admission.zh.md: 8785f7489b0c433cba43a1747533b1d38aada3d3
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Agent Note: Atomic Web image admission
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
English | [中文](2026-07-29-atomic-web-image-admission.zh.md)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Image prompt admission and `session.selectModel` each read session modality state across asynchronous model and attachment lookups. Without one ordering boundary, an image prompt could validate an image-capable target while a concurrent selection installed a text-only target, or selection could miss a prompt after inbox dequeue but before its durable message event. Scanning the immutable event log avoided the second race but permanently blocked a text-only selection even after compaction removed the image from current model history.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Each live Web agent has one private promise chain shared by image-bearing prompt admission and model selection. A failed operation settles its caller normally and leaves the chain usable. Text-only prompts bypass the chain because they cannot change the modality constraint.
|
||||||
|
|
||||||
|
The pending-publication set records a queued occurrence at dequeue and a steering occurrence already at enqueue (steering items never enter the queued UI mirror), and retains each until its matching `user/message` or `steering/message` event publishes. If admission ends without publishing, the transition to idle retires the entries; inbox discard retires the listed work, and session disposal retires every remaining entry. Model selection checks that set, the queued UI mirror, and `Session.deriveMessages()`, which is the current model-visible history after compaction.
|
||||||
|
|
||||||
|
Provider adapters remain the final enforcement boundary. The host ordering only prevents its mutable route and pending image state from contradicting each other before request assembly.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
**Scan every immutable session event.** This catches published images but treats compacted-away content as permanently model-visible, preventing a valid later switch to a text-only route.
|
||||||
|
|
||||||
|
**Retire the pending mirror at inbox dequeue.** Dequeue precedes the durable message append and leaves the exact interval in which model selection can miss both pending and published state.
|
||||||
|
|
||||||
|
**Serialize every prompt and session mutation.** Text-only prompts and unrelated session operations cannot introduce an image requirement. A broader lock would add latency and ownership without closing another modality race.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
An image prompt and a concurrent model selection have deterministic order, and a text-only target cannot strand an image that has been admitted but not yet published. Selection may wait for an in-flight image admission, while unrelated prompts retain their existing concurrency. Compaction can make a text-only target valid once no pending or derived image remains.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Agent Note: Web 图片准入的原子性
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
[English](2026-07-29-atomic-web-image-admission.md) | 中文
|
||||||
|
|
||||||
|
## 问题
|
||||||
|
|
||||||
|
包含图片的提示词准入与 `session.selectModel` 都会在跨越异步模型查询与附件查询的过程中读取会话模态状态。如果没有统一的顺序边界,包含图片的提示词可能在支持图片的目标上通过校验,并发的选择操作却设置了纯文本目标;选择操作也可能在提示词已从 inbox 出队、但其持久消息事件尚未发布时漏掉该提示词。扫描不可变事件日志可以避免第二种竞态,但即使压缩(compaction)已经从当前模型历史中移除图片,仍会永久阻止选择纯文本目标。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
每个活跃 Web agent(智能体)都有一条私有 promise 链,由包含图片的提示词准入与模型选择共享。操作失败会照常传递给调用方,且不会使该链失效。纯文本提示词绕过该链,因为它们不会改变模态约束。
|
||||||
|
|
||||||
|
待发布集合会在排队条目出队时记录它,而 steering 条目在入队时即被记录(steering 条目从不进入排队 UI 镜像),并各自保留到匹配的 `user/message` 或 `steering/message` 事件发布。若准入结束时未发布事件,转为空闲状态会移除这些条目;inbox 丢弃会移除列出的工作项,会话 dispose(资源释放)则会移除所有剩余条目。模型选择会检查该集合、排队 UI 镜像以及 `Session.deriveMessages()`;后者表示压缩后模型当前可见的历史。
|
||||||
|
|
||||||
|
提供方适配器仍是最终的强制检查边界。宿主的顺序控制仅用于避免其可变路由与待发布图片状态在请求组装前彼此矛盾。
|
||||||
|
|
||||||
|
## 曾考虑的替代方案
|
||||||
|
|
||||||
|
**扫描每个不可变会话事件。** 这能捕获已发布的图片,但会把经压缩移除的内容视为永久对模型可见,从而阻止之后合法切换到纯文本路由。
|
||||||
|
|
||||||
|
**在 inbox 出队时退役待处理镜像。** 出队早于持久消息追加,因此恰好会留下一个时间区间,让模型选择既看不到待处理状态,也看不到已发布状态。
|
||||||
|
|
||||||
|
**序列化每个提示词和会话变更。** 纯文本提示词和无关的会话操作无法引入图片要求。更宽的锁会增加延迟与所有权复杂度,却不会再消除任何模态竞态。
|
||||||
|
|
||||||
|
## 后果
|
||||||
|
|
||||||
|
包含图片的提示词准入与并发模型选择之间具有确定的先后顺序,纯文本目标无法使已获准入但尚未发布的图片搁浅。模型选择可能等待正在进行的图片准入完成,而无关提示词仍按现有方式并发处理。当没有图片等待发布,且派生历史经过压缩后也不再含图片时,纯文本目标可以变得有效。
|
||||||
+6
@@ -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-07-22-web-multimodal-image-input-and-durable-attachments.md
|
||||||
|
2026-07-22-web-multimodal-image-input-and-durable-attachments.md: 2a3925d8b779fed7f8b3330d079fdb3059685fd5
|
||||||
|
2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md: be478ece73039201a820d804435610faea4cb3fe
|
||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
# Agent Note: Web multimodal image input and durable attachments
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
English | [中文](2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Before this change, the Web composer accepted only text: `InputBar` received a string draft, `ConversationService.send()` created text content, and the host forwarded that content to the agent. Users could not paste an image, inspect it before sending, submit an image-only prompt, or recover sent images from history.
|
||||||
|
|
||||||
|
This is not only a composer gap. Core needs a durable image content block, providers need explicit modality handling, and the session log must reconstruct everything visible to a model. [The previous image-block removal](../../implemented/simplification/2026-07-04-drop-image-content-block.md) rejected a partial design that could silently lose or flatten images. A browser object URL, local path, provider URL, or base64 payload cannot be canonical session content.
|
||||||
|
|
||||||
|
The [Web client architecture](../../implemented/architecture/2026-07-19-gui-web-client-architecture.md) keeps components pure and per-session composer state in `ctx.conversation`; the [GUI layering and RPC protocol](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md) makes durable events the source of truth for both live rendering and history replay. Image intake, persistence, provider conversion, and rendering therefore need one explicit lifecycle.
|
||||||
|
|
||||||
|
Peer products converge on an attachment rail above the editor, but their storage choices differ. Codex-style paths such as `/var/folders/.../codex-clipboard-*.png` are reasonable intake staging locations, not durable message identities: the operating system may delete them, another host cannot read them, and a resumed session cannot rely on them.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Pasted or dropped raster images are the Web composer's first consumer of a durable attachment capability. Unsent files remain temporary client-owned draft state. The host validates and durably commits every accepted user image before appending its message event. A provider adapter that produces structured image output must durably commit the output before appending its assistant block. Canonical user and assistant content contains only role-neutral `ImageBlock` references.
|
||||||
|
|
||||||
|
Version one supports PNG, JPEG, WebP, and GIF paste and drag-and-drop, image-only or mixed prompts, historical user and assistant image rendering, and original-image preview on double-click. File picking, generic files, PDF, audio, video, image copying, and a custom context menu remain separate follow-ups.
|
||||||
|
|
||||||
|
### Product behavior
|
||||||
|
|
||||||
|
- Pasting or dropping one or more supported images adds ordered thumbnails above the textarea without inserting placeholder text. Dragging files over the composer highlights the drop target.
|
||||||
|
- The same resident `InputBar` renders the rail in both blank-session Hero and active-session layouts. The rail is hidden when empty and scrolls horizontally instead of widening the composer.
|
||||||
|
- Each approximately 72-by-72-pixel thumbnail has a remove action and opens its original draft image on double-click.
|
||||||
|
- A prompt may contain text and images or images only. Pure text paste remains native browser behavior; mixed clipboard content inserts its text normally while adding its files to the rail, and file-only paste prevents default browser handling. File drops on the composer always prevent browser navigation and report unsupported files locally.
|
||||||
|
- A failed send restores the complete text and image draft without clobbering text or images added while the request was in flight. Removal, successful send, session-scope disposal, rendered-history disposal, and application disposal revoke the object URLs they own.
|
||||||
|
- Historical user and assistant images use one `MessageImage` control. Inline images preserve intrinsic aspect ratio, do not upscale, and stay within a 240-by-240-pixel box.
|
||||||
|
- Double-clicking a message image opens the stored original in a viewport-bounded modal. Escape, the close control, and backdrop activation close it and restore focus.
|
||||||
|
- Version one does not override the browser context menu and provides no explicit image-copy action.
|
||||||
|
|
||||||
|
### Storage lifecycle and ownership
|
||||||
|
|
||||||
|
The persistence boundary is message acceptance, not paste:
|
||||||
|
|
||||||
|
| State | Allowed representation | Durability and ordering |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Unsent user draft | Browser `File` plus object URL; a native client may use an OS temporary file such as `/var/...` | Temporary and client-owned. It may disappear on reload or process exit and never appears in a session event. |
|
||||||
|
| Accepted user image | Immutable object below `DSH_HOME` plus `ImageAttachmentRef` | The host commits every image before `agent.send()` or `agent.steer()` can append the owning user event. |
|
||||||
|
| Structured model image output | Immutable object below `DSH_HOME` plus `ImageAttachmentRef` | The provider adapter commits the bytes before it emits a completed image block or assistant message event. Temporary URLs, paths, and base64 are forbidden in the event. |
|
||||||
|
|
||||||
|
Each session's `InputMachine` state keeps the ordered runtime-only attachment identifiers alongside the live draft. The framework-owned chat store receives only the draft's plain-text persistence mirror, while `ConversationService` owns the corresponding browser-only `File` and object-URL registry:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||||
|
|
||||||
|
type DraftAttachmentId = Branded<'DraftAttachmentId'>
|
||||||
|
|
||||||
|
interface ChatStoreState {
|
||||||
|
selection: object | null
|
||||||
|
draft: string
|
||||||
|
view: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InputState {
|
||||||
|
draft: string
|
||||||
|
imageIds: readonly DraftAttachmentId[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComposerAttachment {
|
||||||
|
kind: 'image'
|
||||||
|
id: DraftAttachmentId
|
||||||
|
file: File
|
||||||
|
previewUrl: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This split uses the session provide channel's input hook and actions as the single subscription path for live composer state while keeping non-serializable browser objects out of persisted JSON. Only the plain-text draft mirror uses `localStorage`; attachment identifiers, browser `File` objects, and object URLs remain scoped to the live session input shell. Unsent images therefore do not survive reload or session-scope disposal. A Workspace switch moves a mixed text-and-image draft only when the destination shell accepts the complete image batch; refusal leaves both parts with the source. A native client may stage input in an OS temporary directory, but it must treat that path exactly like the browser object URL: delete it when no longer needed and copy the bytes into the durable store before message acceptance.
|
||||||
|
|
||||||
|
The local attachment backend resolves an explicit `dshHome`, then `$DSH_HOME`, then `~/.dsh`. It stores content-addressed objects below `$DSH_HOME/attachments/v1/objects/<prefix>/<sha256>` with owner-only directory and file permissions. On each process's first save for one home, it creates that home and synchronizes every ancestor entry to the filesystem root; existence is not treated as durability because another process may still be between `mkdir` and parent `fsync`. A temporary file is then written, synchronized, atomically published, and made durable with directory syncs on the publication path (POSIX; Windows relies on filesystem metadata journaling) before the service returns a reference. The content digest is encoded in the opaque `sha256:<digest>` identifier. Admission and reads fully decode supported rasters before accepting their format and dimensions, and every read also verifies the digest, byte length, and logged metadata.
|
||||||
|
|
||||||
|
The store performs no automatic deletion in version one. Sent user images and model-generated images remain reachable for history, resume, and fork. Reference-aware garbage collection needs a separate design because an age-only rule can delete data still referenced by a durable session. Deployment byte and pixel limits are admission policy on writes; reads verify the digest and recorded metadata without reapplying current admission limits, so lowering policy does not invalidate older history.
|
||||||
|
|
||||||
|
### Durable content and prompt wire
|
||||||
|
|
||||||
|
The attachment seam exposes immutable image write and verified read operations. The canonical metadata is deliberately narrower than a generic file record:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||||
|
|
||||||
|
type AttachmentId = Branded<'AttachmentId'>
|
||||||
|
|
||||||
|
interface ImageAttachmentRef {
|
||||||
|
attachmentId: AttachmentId
|
||||||
|
mediaType: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
|
||||||
|
bytes: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageBlock {
|
||||||
|
type: 'image'
|
||||||
|
attachment: ImageAttachmentRef
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ImageBlock` joins the merge-extensible core `ContentBlockMap` and is valid in either user or assistant content. It never carries base64, an object URL, a filesystem path, or a provider-owned locator. This keeps the session event plus immutable object store sufficient to reconstruct the exact model-visible image. The LLM vocabulary therefore has a type-only dependency on the attachment seam; provider runtime dependencies remain adapter-specific.
|
||||||
|
|
||||||
|
The browser cannot mint a durable reference, so `session.prompt` accepts a narrow intake union rather than canonical `ContentBlock[]`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export {}
|
||||||
|
|
||||||
|
type PromptInputPart =
|
||||||
|
| { type: 'text'; text: string }
|
||||||
|
| {
|
||||||
|
type: 'image'
|
||||||
|
mediaType: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
|
||||||
|
data: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Base64 crosses JSON-RPC once and is discarded after persistence. The host validates canonical base64, image count, aggregate bytes, individual bytes, the declared MIME against a fully decoded raster, intrinsic dimensions, and decoded-pixel count. It awaits the seam's storage-free `validateImage` for every batch member before saving any member, so one malformed image cannot strand the batch's valid members as unreferenced objects. Storage commits then run in submission order to bound full-raster decoder memory. If a later storage I/O operation fails, the host appends no user event, but an earlier immutable content-addressed object may remain unreferenced; version one leaves cleanup to future reference-aware garbage collection instead of adding destructive rollback to the deduplicated store. Only after every image succeeds does it call the agent with normalized text and durable image blocks in the submitted order. A failure exposes no attachment path or raw bytes.
|
||||||
|
|
||||||
|
`session.attachment` is a read-only, session-scoped endpoint. The host serves bytes only when a durable event in that session references the requested attachment identifier. The client deduplicates loads by session and attachment identifier while that session is rendered, revokes resolved URLs on rendered-session disposal, and rejects invalidated late loads before allocating an object URL so an unmounted session or disposed service cannot repopulate the cache.
|
||||||
|
|
||||||
|
### Model capabilities and provider behavior
|
||||||
|
|
||||||
|
Model catalog entries gain optional merge-extensible input modality declarations. A missing declaration means unknown; a present list without `image` is an explicit negative capability.
|
||||||
|
|
||||||
|
The host is the authoritative preflight boundary. It resolves the session's latest routed provider/model, falling back through agent options to host defaults; if that model explicitly excludes image input, it rejects the prompt before writing any attachment or event, and the client restores the draft. Image-bearing prompt admission and model selection share one per-agent serial boundary, and a dequeued prompt remains pending until its durable message event publishes ([ordering decision](../bug-fix/2026-07-29-atomic-web-image-admission.md)); a steering carrier gates from its enqueue until its `steering/message` event publishes, closing the outbox hop that never enters the queued mirror. Selection rejects a text-only target while an image is pending publication or remains in the session's current derived history. Compaction can remove old images and make a later text-only selection valid; idle without publication releases a claimed queued carrier, while steering retained in the outbox stays gated until publication or discard. `session.updateQueue` edits accept text content only, so a queue edit cannot inject an image past this admission boundary. Unknown capability proceeds to the adapter guard so uncatalogued model identifiers remain usable. The browser rejects unsupported declared image media types before allocating preview URLs, but it does not snapshot deployment limits or model capability: a handshake snapshot cannot represent a session's current target after `session.selectModel`, and deployment policy may change independently. The host validates the complete batch against current byte, count, aggregate, media, dimension, pixel, and routed-model policy before writing any attachment or event; its rejection renders through the composer error strip.
|
||||||
|
|
||||||
|
The Pi-AI adapter is the first visual-input route: it resolves `ctx.attachments` at request time, recursively converts each durable image reference including references nested inside tool results, and emits native image content only for models that declare image input. The shipped composition registers Pi-AI OpenAI and Anthropic routes alongside the text-only default DeepSeek route; selecting the active provider/model remains a host composition or profile concern rather than an image-input CLI feature. Request-time service resolution keeps Cordis load order from freezing optional attachment availability. The hand-written DeepSeek adapter throws typed `UNSUPPORTED_CONTENT` for an image anywhere in the request, including nested tool results. No adapter may flatten or skip an image.
|
||||||
|
|
||||||
|
Core supports structured assistant image blocks, but no current production provider route is certified for image output. Any future output-capable adapter must retrieve provider bytes under bounded size and time policy, validate them through the same attachment service, persist them, and only then publish the atomic `ImageBlock`. A URL in assistant Markdown remains text and is never downloaded automatically.
|
||||||
|
|
||||||
|
Provider-neutral token estimation does not guess visual pricing from image dimensions; provider-reported usage remains authoritative. ACP renders an explicit image marker until that protocol surface gains native image support rather than silently omitting the block.
|
||||||
|
|
||||||
|
Compaction replays the selected conversation prefix, including image references, into the configured summarization route. A visual-capable route resolves those references through its adapter; a text-only route fails explicitly instead of silently dropping the visual context. The synthesized checkpoint remains text-only, and `compact-basic` rejects image summary output with `UNSUPPORTED_CONTENT`.
|
||||||
|
|
||||||
|
### History rendering and original preview
|
||||||
|
|
||||||
|
History folding preserves `ImageBlock` in both user and assistant messages. User images align to the trailing edge above their text; assistant images remain in their original content-block position in the leading narration flow. `MessageImage` derives a stable inline box from recorded dimensions, resolves bytes through the session-authorized loader, uses `object-fit: contain`, and turns a missing or corrupt object into a retryable error control.
|
||||||
|
|
||||||
|
Composer thumbnails and each `MessageImage` own ephemeral original-preview state and invoke the same pure `ImageLightbox`. The modal uses the already resolved original object URL, constrains only display size, focuses its close control, and restores the previous focus target when closed.
|
||||||
|
|
||||||
|
### Limits and trust boundaries
|
||||||
|
|
||||||
|
Version one accepts PNG, JPEG, WebP, and GIF only. SVG and remote URLs are excluded. Default limits are 5 MiB per image, 10 images and 20 MiB aggregate image bytes per message, and 40 million intrinsic pixels per image. These deployment-varying limits are validated backend configuration and enforced by the host before persistence. The client connection carrier has an independent configurable `maxRequestBodyBytes` cap (32 MiB by default) for every API request and fails load if it cannot hold the attachment service's aggregate image limit after base64 and envelope expansion; lowering image policy therefore never silently lowers the carrier limit for valid text or other RPCs. A body without a declared length is rejected the moment it crosses the cap rather than drained to its end.
|
||||||
|
|
||||||
|
Malformed base64, unsupported or mismatched media, truncated image payloads, excess bytes, excess image count, excess pixels, missing objects, and integrity mismatches return stable structured failures. Original filenames are reduced to a display basename, control characters are removed, and no local path is logged or returned to the browser.
|
||||||
|
|
||||||
|
### Package and surface changes
|
||||||
|
|
||||||
|
| Surface | Responsibility |
|
||||||
|
| --- | --- |
|
||||||
|
| `packages/attachment/attachment` | Opaque attachment identifier, image reference, limits, failures, and `ctx.attachments` service. |
|
||||||
|
| `packages/attachment/attachment-local` | Private content-addressed storage, complete raster decoding, integrity verification, and configuration. |
|
||||||
|
| `packages/llm/llm` | Role-neutral `ImageBlock` and input-modality metadata. |
|
||||||
|
| `packages/llm/llm-pi-ai` | Resolve durable supported image input into native provider content. |
|
||||||
|
| `packages/llm/llm-deepseek` | Reject image content explicitly. |
|
||||||
|
| `packages/compact/compact-basic` | Preserve images in summary input and reject non-text checkpoint output explicitly. |
|
||||||
|
| `packages/host/apiproxy` and `packages/bundle/base` | Narrow upload wire, persist-before-event ordering, session-authorized reads, limits and model preflight, plus default profile composition. |
|
||||||
|
| `packages/client/connection` and `packages/client/runtime` | Bounded request buffering, wire types, fixture images, prompt uploads, attachment reads, and durable-reference folding. |
|
||||||
|
| `packages/client/ui-conversation` | Per-session draft images, attachment rail, user and assistant image controls, and original preview. |
|
||||||
|
| `packages/acp/acp` | Explicit fallback rendering for image blocks. |
|
||||||
|
|
||||||
|
The attachment packages form the interface/implementation side of one capability seam. Composer behavior stays in the conversation object layer, provider conversion stays in adapters, and no change is required in `agent-loop`.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
The implemented slice includes the attachment seam, role-neutral image block, Pi-AI input conversion, DeepSeek rejection, durable host ordering, Web upload/read protocol, current image-limit enforcement, bounded Web request bodies, in-memory draft images, paste/drop rail, user and assistant history rendering, double-click preview, compaction handling, and keyless assembled Web coverage.
|
||||||
|
|
||||||
|
No compatibility shim is required for the pre-release prompt wire; all call sites and fixtures change with the introducing slice.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
### Keep every intake image in `/var` or another temporary directory
|
||||||
|
|
||||||
|
Temporary storage is appropriate before send, including for a native client that receives clipboard files through the operating system. It is not appropriate after acceptance: cleanup is outside the harness's control, paths are host-specific, and resume or fork can outlive the file. The proposal permits temporary staging but copies accepted bytes into `DSH_HOME` before the event.
|
||||||
|
|
||||||
|
### Persist immediately on paste or drop
|
||||||
|
|
||||||
|
Immediate persistence makes drafts reload-resistant but creates durable objects before a session or message owns them, which requires quota, orphan lifetime, and cleanup policy. Version one keeps the unsent draft temporary and makes send acceptance the durability boundary.
|
||||||
|
|
||||||
|
### Inline base64 in messages and session logs
|
||||||
|
|
||||||
|
This duplicates binary data across RPC, events, history pages, forks, compaction, and browser storage, and invites token accounting to treat encoding text as model text. One immutable object plus small references keeps the durable representation bounded.
|
||||||
|
|
||||||
|
### Use browser object URLs, local paths, or provider URLs as canonical content
|
||||||
|
|
||||||
|
Object URLs expire with the document, local paths are not portable, and provider URLs may expire, track viewers, or expose credentials. They remain temporary transport or preview details only.
|
||||||
|
|
||||||
|
### Use one generic `AttachmentBlock` for images, files, audio, and video
|
||||||
|
|
||||||
|
Composer presentation can use a generic attachment rail, but provider semantics are modality-specific. Images are native multimodal input; PDFs may be provider files or extracted text; video may be native, sampled, or unsupported. A specific `ImageBlock` forces every consumer to handle or reject the modality explicitly.
|
||||||
|
|
||||||
|
### Rely on UI capability checks or silently filter images
|
||||||
|
|
||||||
|
UI state can be stale and does not protect direct SDK, ACP, replay, or uncatalogued model paths. Silent filtering changes user intent. Provider enforcement remains mandatory, while UI checks are optional earlier feedback.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Storage tests cover content-addressed deduplication, private permissions, admission failures, corruption/missing-object failures, and reading history after deployment limits are lowered.
|
||||||
|
- Host and protocol tests cover persist-before-event ordering, absence of base64 in logs, session-scoped authorization, capability rejection, upload limits, bounded HTTP request bodies, image-admission/model-selection races (queued and steering placements), pending publication, idle release without publication, text-only queue edits, and selection against current derived history after compaction.
|
||||||
|
- Client unit tests cover paste and drop, mixed clipboard text, image-only send, draft restoration, ordering, draft/session-scope/application object-URL cleanup, and a deferred historical read that completes after disposal; the keyless assembled built-client lane (`apps/web/tests/image-display.snapshot.ts`, `DSH_EXAMPLE_MODE=lib pnpm run test:snapshot`) covers the historical user and assistant galleries over the authorized attachment route, the original-size lightbox, and the composer paste rail.
|
||||||
|
- Adapter and compaction tests cover native Pi-AI image conversion, late attachment-service composition, text-only rejection, recursively nested tool-result images, preserved summary input, and explicit image-output rejection.
|
||||||
|
- A credentialed real-API test sends a PNG through the Anthropic `claude-opus-4-8` route and requires the model to identify its QR code.
|
||||||
|
- The current production adapter set has no certified image-output route; output-provider certification remains outside version one.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Durable storage grows without garbage collection. Version one chooses replay safety over premature deletion.
|
||||||
|
- A missing or corrupt object makes exact model reconstruction fail. Failing loud preserves integrity but may prevent that session from continuing until repaired.
|
||||||
|
- JSON-RPC base64 adds upload memory and roughly one-third encoding overhead. Version-one limits bound it; larger media needs streaming or a binary transport.
|
||||||
|
- Unsent images do not survive reload. Durable drafts need quota and orphan cleanup rather than reusing message storage implicitly.
|
||||||
|
- Original preview decodes more pixels than the inline control displays. Pixel limits, one clicked preview, and object-URL disposal bound but do not eliminate transient browser memory.
|
||||||
|
- Capability metadata may be missing or stale. Host preflight improves feedback, while adapter enforcement remains authoritative.
|
||||||
|
- A future output provider may require authenticated retrieval before an assistant image can complete, adding latency and a new failure point. Persist-before-event ordering favors replay integrity.
|
||||||
|
- File picking, generic files/PDF, audio/video, durable draft staging, image copying, custom context menus, output-provider certification, and reference-aware garbage collection remain independent designs.
|
||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
# Agent Note: Web 多模态图片输入与持久附件
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
[English](2026-07-22-web-multimodal-image-input-and-durable-attachments.md) | 中文
|
||||||
|
|
||||||
|
## 问题
|
||||||
|
|
||||||
|
在此变更之前,Web 输入区仅接受文本:`InputBar` 接收字符串草稿,`ConversationService.send()` 创建文本内容,宿主再把该内容转发给 agent(智能体)。用户无法粘贴图片、在发送前查看图片、提交仅含图片的提示词,也无法从历史记录中恢复已发送图片。
|
||||||
|
|
||||||
|
这不只是输入区功能缺失。核心层需要持久图片内容块,提供方需要明确处理模态,会话日志则必须重建模型可见的全部内容。[此前移除图片块的决策](../../implemented/simplification/2026-07-04-drop-image-content-block.md)否决了可能静默丢失图片或将其展平的不完整设计。浏览器对象 URL、本地路径、提供方 URL 或 base64 数据都不能成为规范会话内容。
|
||||||
|
|
||||||
|
[Web 客户端架构](../../implemented/architecture/2026-07-19-gui-web-client-architecture.md)要求组件保持纯粹,并将每个会话的输入区状态放在 `ctx.conversation` 中;[GUI 分层与 RPC 协议](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)则要求持久事件成为实时渲染与历史回放的共同真源。因此,图片接收、持久化、提供方转换和渲染需要遵循同一个明确的生命周期。
|
||||||
|
|
||||||
|
同类产品普遍在编辑器上方设置附件栏,但存储方案各不相同。诸如 `/var/folders/.../codex-clipboard-*.png` 的 Codex 式路径适合作为接收输入时的暂存位置,却不能作为持久消息身份:操作系统可能删除文件,另一台宿主无法读取文件,恢复后的会话也不能依赖文件仍然存在。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
粘贴或拖放的光栅图片是 Web 输入区对持久附件能力的首个应用场景。未发送文件仍是由客户端持有的临时草稿状态。宿主在追加相应消息事件前,校验并持久提交每张已接受的用户图片。生成结构化图片输出的提供方适配器在追加相应助手块前,也必须持久提交输出。规范用户内容与助手内容只包含角色无关的 `ImageBlock` 引用。
|
||||||
|
|
||||||
|
第一版支持粘贴和拖放 PNG、JPEG、WebP 与 GIF,支持仅图片或混合提示词,支持渲染历史用户图片与助手图片,并支持双击预览原图。文件选择、通用文件、PDF、音频、视频、图片复制和自定义上下文菜单仍分别作为后续工作。
|
||||||
|
|
||||||
|
### 产品行为
|
||||||
|
|
||||||
|
- 粘贴或拖放一张或多张受支持的图片后,文本框上方会按顺序显示缩略图,但不会插入占位文本。文件拖入输入区时会高亮放置目标。
|
||||||
|
- 同一个常驻 `InputBar` 会在空白会话 Hero 和活跃会话布局中渲染附件栏。附件栏为空时隐藏,通过横向滚动避免撑宽输入区。
|
||||||
|
- 每个缩略图约为 72 × 72 像素,带有移除操作;双击时打开草稿原图。
|
||||||
|
- 提示词可同时包含文本与图片,也可仅包含图片。粘贴纯文本时保持浏览器原生行为;粘贴混合的剪贴板内容时,文本会正常插入,文件则同时添加到附件栏;仅粘贴文件时才阻止浏览器的默认处理。在输入区放置文件时总会阻止浏览器导航,并在本地报告不受支持的文件。
|
||||||
|
- 发送失败时恢复完整的文本与图片草稿,但不会覆盖请求飞行期间新增的文本或图片。移除、发送成功、会话 scope 释放、已渲染历史记录释放和应用释放都会撤销各自持有的对象 URL。
|
||||||
|
- 历史用户图片与助手图片共用一个 `MessageImage` 控件。行内图片保持固有宽高比、不放大,并限制在 240 × 240 像素的边界框内。
|
||||||
|
- 双击消息图片会在不超出视口的模态框中打开存储的原图。按 Escape、激活关闭控件或激活背景区域都会关闭模态框并恢复焦点。
|
||||||
|
- 第一版不覆盖浏览器上下文菜单,也不提供明确的图片复制操作。
|
||||||
|
|
||||||
|
### 存储生命周期与归属
|
||||||
|
|
||||||
|
持久化边界是消息被接受,而不是图片被粘贴:
|
||||||
|
|
||||||
|
| 状态 | 允许的表示 | 持久性与顺序 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 未发送的用户草稿 | 浏览器 `File` 加对象 URL;原生客户端可以使用 `/var/...` 等操作系统临时文件 | 临时且由客户端持有。它可能在重载或进程退出后消失,绝不出现在会话事件中。 |
|
||||||
|
| 已接受的用户图片 | `DSH_HOME` 下的不可变对象加 `ImageAttachmentRef` | 在 `agent.send()` 或 `agent.steer()` 能够追加所属用户事件前,宿主提交每张图片。 |
|
||||||
|
| 结构化模型图片输出 | `DSH_HOME` 下的不可变对象加 `ImageAttachmentRef` | 提供方适配器在发出已完成的图片块或助手消息事件前提交字节。事件中禁止出现临时 URL、路径和 base64。 |
|
||||||
|
|
||||||
|
每个会话的 `InputMachine` 状态在实时草稿旁保存仅限运行时的有序附件标识符。框架持有的 chat store 只接收草稿的纯文本持久化镜像,`ConversationService` 则持有相应的浏览器专用 `File` 与对象 URL 注册表:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||||
|
|
||||||
|
type DraftAttachmentId = Branded<'DraftAttachmentId'>
|
||||||
|
|
||||||
|
interface ChatStoreState {
|
||||||
|
selection: object | null
|
||||||
|
draft: string
|
||||||
|
view: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InputState {
|
||||||
|
draft: string
|
||||||
|
imageIds: readonly DraftAttachmentId[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComposerAttachment {
|
||||||
|
kind: 'image'
|
||||||
|
id: DraftAttachmentId
|
||||||
|
file: File
|
||||||
|
previewUrl: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这一拆分把会话 provide 通道的输入 hook 与 actions 用作实时输入区状态的唯一订阅路径,同时避免把不可序列化的浏览器对象写进持久 JSON。只有纯文本草稿镜像使用 `localStorage`;附件标识符、浏览器 `File` 对象和对象 URL 都限定在实时会话输入外壳的 scope 内。未发送图片因此无法跨重载或会话 scope 释放保留。切换 Workspace 时,只有目标外壳接受完整图片批次,图文混合草稿才会移动;拒绝时,文本和图片都留在来源外壳。原生客户端可以在操作系统临时目录中暂存输入,但必须像对待浏览器对象 URL 一样对待该路径:不再需要时删除,并在消息被接受前把字节复制进持久存储。
|
||||||
|
|
||||||
|
本地附件后端依次解析显式 `dshHome`、`$DSH_HOME` 和 `~/.dsh`。它把内容寻址对象存储在 `$DSH_HOME/attachments/v1/objects/<prefix>/<sha256>` 下,并为目录和文件设置仅所有者可访问的权限。每个进程首次为某个 home 保存对象时,都会创建该 home,并逐级同步每个祖先目录项直至文件系统根目录;不能把存在视为持久性,因为另一个进程可能仍处于 `mkdir` 与父目录 `fsync` 之间。随后,服务写入并同步临时文件,再以原子方式发布,并对发布路径执行目录同步使其持久(POSIX;Windows 依赖文件系统元数据日志),之后才返回引用。内容摘要编码在不透明的 `sha256:<digest>` 标识符中。写入准入与读取都会完整解码受支持的光栅图片,之后才接受其格式和尺寸;每次读取还会校验摘要、字节长度和已记录的元数据。
|
||||||
|
|
||||||
|
第一版不对存储执行自动删除。已发送的用户图片和模型生成图片会一直保留,以供历史记录、恢复和 fork 使用。按引用感知的垃圾回收需要单独设计,因为仅按时间清理可能删除仍被持久会话引用的数据。部署的字节和像素限制是写入时的准入策略;读取时会校验摘要和已记录的元数据,但不重新应用当前准入限制,因此收紧策略不会导致旧历史记录失效。
|
||||||
|
|
||||||
|
### 持久内容与提示词协议
|
||||||
|
|
||||||
|
附件服务边界公开不可变图片写入和经过校验的读取操作。规范元数据刻意比通用文件记录更窄:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||||
|
|
||||||
|
type AttachmentId = Branded<'AttachmentId'>
|
||||||
|
|
||||||
|
interface ImageAttachmentRef {
|
||||||
|
attachmentId: AttachmentId
|
||||||
|
mediaType: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
|
||||||
|
bytes: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageBlock {
|
||||||
|
type: 'image'
|
||||||
|
attachment: ImageAttachmentRef
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ImageBlock` 加入可合并扩展的核心 `ContentBlockMap`,在用户内容和助手内容中都有效。它绝不携带 base64、对象 URL、文件系统路径或提供方持有的定位符。因此,会话事件与不可变对象存储足以共同重建模型可见的确切图片。LLM 词汇由此仅在类型层面依赖附件服务边界;提供方运行时依赖仍由各适配器持有。
|
||||||
|
|
||||||
|
浏览器无法生成持久引用,因此 `session.prompt` 接受范围狭窄的接收联合类型,而不是规范 `ContentBlock[]`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export {}
|
||||||
|
|
||||||
|
type PromptInputPart =
|
||||||
|
| { type: 'text'; text: string }
|
||||||
|
| {
|
||||||
|
type: 'image'
|
||||||
|
mediaType: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
|
||||||
|
data: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Base64 只跨越一次 JSON-RPC,并在持久化后丢弃。宿主会校验规范 base64、图片数量、总字节数、单张图片字节数、声明的 MIME 与完整解码后的光栅图片是否一致、固有尺寸和解码像素数。它会在保存任何成员之前,等待服务边界上不触碰存储的 `validateImage` 完成对每个批次成员的校验,因此一张畸形图片不会把批次中的有效成员留成无引用对象。随后按提交顺序执行存储提交,以限制完整光栅解码器的内存占用。如果后续存储 I/O 操作失败,宿主不会追加用户事件,但先前的不可变内容寻址对象可能保持无引用状态;第一版将清理留给未来按引用感知的垃圾回收,而不向去重存储添加破坏性回滚。只有每张图片都成功后,宿主才会用规范化文本和按提交顺序排列的持久图片块调用 agent。失败时不公开任何附件路径或原始字节。
|
||||||
|
|
||||||
|
`session.attachment` 是只读且限定于会话作用域的端点。只有该会话中的持久事件引用了所请求的附件标识符,宿主才提供字节。会话处于渲染状态时,客户端会按会话和附件标识符对加载操作去重;已渲染会话释放时会撤销已解析的 URL,并在分配对象 URL 前拒绝已失效的延迟加载,以免已卸载的会话或已释放的服务重新写入缓存。
|
||||||
|
|
||||||
|
### 模型能力与提供方行为
|
||||||
|
|
||||||
|
模型目录项增加可选且可合并扩展的输入模态声明。缺少声明表示未知;声明存在但不含 `image`,则明确表示不支持图片。
|
||||||
|
|
||||||
|
宿主是权威的前置检查边界。它会解析会话最新路由到的提供方和模型,并在缺失时依次回退到 agent 选项和宿主默认值;如果该模型明确排除图片输入,宿主会在写入任何附件或事件前拒绝提示词,客户端则恢复草稿。包含图片的提示词准入与模型选择共用一个逐 agent 的串行边界,而且已经出队的提示词在其持久消息事件发布前仍保持待发布状态([顺序决策](../bug-fix/2026-07-29-atomic-web-image-admission.md));steering 载体则从入队起就参与门槛,直到其 `steering/message` 事件发布为止,堵住了从不进入排队镜像的 outbox 窗口。当图片正等待发布或仍存在于会话当前的派生历史中时,模型选择会拒绝纯文本目标。压缩(compaction)可以移除旧图片,使之后选择纯文本目标变得有效;未发布任何事件即转入空闲时,已认领的 queued 载体会被释放,而保留在 outbox 中的 steering 在发布或丢弃前始终受门槛约束。`session.updateQueue` 的编辑只接受文本内容,因此队列编辑无法绕过该准入边界注入图片。能力未知时继续进入适配器强制检查,使未收录的模型标识符仍然可用。浏览器会在分配预览 URL 前拒绝声明不支持的图片媒体类型,但不会为部署限制或模型能力保留快照:握手快照无法表达 `session.selectModel` 之后会话的当前目标,部署策略也可能独立变化。宿主会根据当前的单张字节数、图片数量、总字节数、媒体类型、尺寸、像素数和路由模型策略校验整个批次,再写入任何附件或事件;其拒绝通过 composer 错误条呈现。
|
||||||
|
|
||||||
|
Pi-AI 适配器是首条视觉输入路径:它在请求时解析 `ctx.attachments`,递归转换每个持久图片引用,包括嵌套在工具结果中的引用,并且仅为声明支持图片输入的模型生成提供方原生图片内容。交付的组合会同时注册 Pi-AI OpenAI、Anthropic 路由和仅支持文本的默认 DeepSeek 路由;选择当前提供方/模型仍由宿主组合或配置承担,而不是图片输入 CLI(命令行界面)的功能。在请求时解析服务,可避免 Cordis 加载顺序将可选附件服务的可用性固化。手写 DeepSeek 适配器遇到请求中任何位置的图片时都会抛出类型化的 `UNSUPPORTED_CONTENT` 错误,包括嵌套工具结果中的图片。任何适配器都不得将图片展平或跳过。
|
||||||
|
|
||||||
|
核心层支持结构化助手图片块,但当前没有任何生产提供方路径通过图片输出认证。未来任何支持输出的适配器都必须在有界的大小和时间策略下获取提供方字节,通过同一个附件服务校验并持久化字节,之后才能以原子方式发布 `ImageBlock`。助手 Markdown 中的 URL 仍是文本,绝不自动下载。
|
||||||
|
|
||||||
|
提供方无关的 token 估算不会根据图片尺寸猜测视觉定价;提供方返回的用量仍是权威值。在 ACP(Agent Client Protocol)接口原生支持图片前,ACP 会渲染明确的图片标记,而不是静默省略该块。
|
||||||
|
|
||||||
|
压缩会把选定的会话前缀(包含图片引用)回放到已配置的摘要生成路径中。支持视觉的路径会通过适配器解析这些引用;仅文本路径会明确失败,而不是静默丢弃视觉上下文。合成的检查点仍仅包含文本,`compact-basic` 会以 `UNSUPPORTED_CONTENT` 拒绝包含图片的摘要输出。
|
||||||
|
|
||||||
|
### 历史渲染与原图预览
|
||||||
|
|
||||||
|
历史记录折叠会在用户消息和助手消息中保留 `ImageBlock`。用户图片在文本上方靠尾端对齐;助手图片则保留在靠前端叙述流中的原内容块位置。`MessageImage` 根据记录的尺寸派生稳定的行内边界框,通过会话授权加载器解析字节,使用 `object-fit: contain`,并将对象缺失或损坏转换为可重试的错误控件。
|
||||||
|
|
||||||
|
输入区缩略图和每个 `MessageImage` 各自持有临时原图预览状态,并调用同一个纯 `ImageLightbox`。模态框使用已经解析的原始对象 URL,只限制显示尺寸;它会聚焦关闭控件,并在关闭时把焦点恢复到先前的目标。
|
||||||
|
|
||||||
|
### 限制与信任边界
|
||||||
|
|
||||||
|
第一版仅接受 PNG、JPEG、WebP 和 GIF。不接受 SVG 和远程 URL。默认限制为每张图片 5 MiB、每条消息 10 张图片和 20 MiB 图片总字节数,以及每张图片 4,000 万个固有像素。这些随部署变化的限制属于经过校验的后端配置,并由宿主在持久化前强制执行。客户端连接载体为每个 API 请求设置独立且可配置的 `maxRequestBodyBytes` 上限(默认 32 MiB);如果该上限无法容纳附件服务的图片总量限制经 base64 和请求封装膨胀后的大小,加载就会失败。因此,降低图片策略绝不会静默降低有效文本或其他 RPC 的载体上限。未声明长度的请求体在越过上限的瞬间即被拒绝,而不是先读完再拒。
|
||||||
|
|
||||||
|
格式错误的 base64、不支持或不匹配的媒体、截断的图片数据、超出字节限制、超出图片数量、超出像素限制、对象缺失和完整性不匹配都会返回稳定的结构化错误。原始文件名只保留用于显示的末段,控制字符会被移除,并且任何本地路径都不会写入日志或返回浏览器。
|
||||||
|
|
||||||
|
### 包与接口变更
|
||||||
|
|
||||||
|
| 接口 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `packages/attachment/attachment` | 不透明附件标识符、图片引用、限制、错误和 `ctx.attachments` 服务。 |
|
||||||
|
| `packages/attachment/attachment-local` | 私有内容寻址存储、完整光栅解码、完整性校验和配置。 |
|
||||||
|
| `packages/llm/llm` | 角色无关的 `ImageBlock` 和输入模态元数据。 |
|
||||||
|
| `packages/llm/llm-pi-ai` | 将持久且受支持的图片输入解析为提供方原生内容。 |
|
||||||
|
| `packages/llm/llm-deepseek` | 明确拒绝图片内容。 |
|
||||||
|
| `packages/compact/compact-basic` | 在摘要输入中保留图片,并明确拒绝非文本检查点输出。 |
|
||||||
|
| `packages/host/apiproxy` 和 `packages/bundle/base` | 范围狭窄的上传协议、先持久化再追加事件的顺序、会话授权读取、限制和模型前置检查,以及默认 profile 组合。 |
|
||||||
|
| `packages/client/connection` 和 `packages/client/runtime` | 有界请求缓冲、协议类型、fixture(测试前置数据)图片、提示词上传、附件读取和持久引用折叠。 |
|
||||||
|
| `packages/client/ui-conversation` | 每个会话的草稿图片、附件栏、用户与助手图片控件和原图预览。 |
|
||||||
|
| `packages/acp/acp` | 图片块的明确兜底渲染。 |
|
||||||
|
|
||||||
|
附件包(package)构成一个能力服务边界的接口与实现侧。输入区行为留在会话对象层,提供方转换留在适配器中,无需修改 `agent-loop`。
|
||||||
|
|
||||||
|
### 实现
|
||||||
|
|
||||||
|
已实现的范围包括附件服务边界、角色无关的图片块、Pi-AI 输入转换、DeepSeek 拒绝、宿主持久化顺序、Web 上传与读取协议、当前图片限制执行、大小受限的 Web 请求体、内存草稿图片、粘贴与拖放附件栏、用户与助手历史图片渲染、双击预览、压缩处理,以及组装后无需密钥的 Web 覆盖。
|
||||||
|
|
||||||
|
预发布提示词协议不需要兼容包装层;引入相应切片时会同时修改所有调用点和 fixture。
|
||||||
|
|
||||||
|
## 曾考虑的替代方案
|
||||||
|
|
||||||
|
### 将每张粘贴图片保留在 `/var` 或其他临时目录中
|
||||||
|
|
||||||
|
临时存储适合在发送前使用,也适合通过操作系统接收剪贴板文件的原生客户端。但它不适合在消息被接受后继续使用:清理不在 harness 的控制范围内,路径因宿主而异,恢复或 fork 后的会话也可能比文件存在得更久。提案允许临时暂存,但会在追加事件前将已接受的字节复制进 `DSH_HOME`。
|
||||||
|
|
||||||
|
### 粘贴或拖放后立即持久化
|
||||||
|
|
||||||
|
立即持久化可以让草稿在重载后继续存在,但会在会话或消息持有对象前就创建持久对象,因此必须定义配额、遗留对象生命周期和清理策略。第一版保持未发送草稿为临时状态,并把发送被接受作为持久性边界。
|
||||||
|
|
||||||
|
### 在消息与会话日志中内联 base64
|
||||||
|
|
||||||
|
这种方式会在 RPC、事件、历史分页、fork、压缩和浏览器存储中复制二进制数据,还会诱使 token 计量把编码文本当成模型文本。单一不可变对象配合小型引用,可以让持久表示保持有界。
|
||||||
|
|
||||||
|
### 使用浏览器对象 URL、本地路径或提供方 URL 作为规范内容
|
||||||
|
|
||||||
|
对象 URL 会随文档失效,本地路径不可移植,提供方 URL 则可能过期、跟踪查看者或暴露凭据。它们只能作为临时传输或预览细节存在。
|
||||||
|
|
||||||
|
### 用一个通用 `AttachmentBlock` 表示图片、文件、音频和视频
|
||||||
|
|
||||||
|
输入区展示可以使用通用附件栏,但提供方语义取决于具体模态。图片是原生多模态输入;PDF 可能是提供方文件或提取后的文本;视频可能由模型原生支持、抽帧处理或不受支持。特定的 `ImageBlock` 会迫使每个消费方明确处理或拒绝该模态。
|
||||||
|
|
||||||
|
### 依赖 UI 能力检查或静默过滤图片
|
||||||
|
|
||||||
|
UI 状态可能陈旧,也无法保护直接 SDK、ACP、回放或未收录模型的路径。静默过滤会改变用户意图。提供方强制检查仍是必需项,UI 检查则是可选的提前反馈。
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
- 存储测试覆盖内容寻址去重、私有权限、准入失败、对象损坏或缺失时的失败,以及收紧部署限制后读取历史数据。
|
||||||
|
- 宿主与协议测试覆盖先持久化再追加事件的顺序、日志中不含 base64、会话作用域授权、能力拒绝、上传限制、大小受限的 HTTP 请求体、图片准入与模型选择的竞态(排队与 steering 两种放置)、待发布状态、未发布即空闲时的门槛释放、仅文本的队列编辑,以及压缩后依据当前派生历史进行的选择。
|
||||||
|
- 客户端单元测试覆盖粘贴与拖放、混合剪贴板文本、仅图片发送、草稿恢复、顺序、草稿、会话作用域和应用层级的对象 URL 清理,以及一项在释放后才完成的延迟历史读取;keyless 的组装后构建产物通道(`apps/web/tests/image-display.snapshot.ts`,`DSH_EXAMPLE_MODE=lib pnpm run test:snapshot`)覆盖经授权附件路由渲染的历史用户与助手图片画廊、原图 lightbox,以及 composer 粘贴缩略图条。
|
||||||
|
- 适配器与压缩测试覆盖 Pi-AI 原生图片转换、后置附件服务组合、仅文本拒绝、递归嵌套在工具结果中的图片、保留摘要输入,以及明确拒绝图片输出。
|
||||||
|
- 需要凭据的实际 API 测试会通过 Anthropic `claude-opus-4-8` 路径发送一张 PNG,并要求模型识别其中的二维码。
|
||||||
|
- 当前生产适配器集合没有经过认证的图片输出路由;输出提供方认证仍不在第一版范围内。
|
||||||
|
|
||||||
|
## 后果
|
||||||
|
|
||||||
|
- 持久存储会在没有垃圾回收时持续增长。第一版选择回放安全,而不是过早删除。
|
||||||
|
- 对象缺失或损坏会让模型请求无法精确重建。明确失败可以保持完整性,但在修复前可能阻止该会话继续运行。
|
||||||
|
- JSON-RPC base64 会增加上传内存,并带来约三分之一的编码开销。第一版的限制可以约束开销;更大的媒体需要流式传输或二进制传输协议。
|
||||||
|
- 未发送图片无法跨重载保留。持久草稿需要配额和遗留对象清理,而不是隐式复用消息存储。
|
||||||
|
- 原图预览解码的像素多于行内控件显示的像素。像素限制、一次只打开一个预览和对象 URL 释放可以约束但无法消除浏览器瞬时内存占用。
|
||||||
|
- 能力元数据可能缺失或陈旧。宿主前置检查可以改善反馈,适配器强制检查仍是权威结果。
|
||||||
|
- 未来输出提供方可能需要经过身份认证的下载,助手图片才能完成,这会增加延迟与新的故障点。先持久化再追加事件的顺序优先保障回放完整性。
|
||||||
|
- 文件选择、通用文件与 PDF、音频与视频、持久草稿暂存、图片复制、自定义上下文菜单、输出提供方认证和按引用感知的垃圾回收仍是相互独立的设计。
|
||||||
+6
@@ -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/simplification/2026-07-29-simplify-web-image-input-v1.md
|
||||||
|
2026-07-29-simplify-web-image-input-v1.md: e7847dc2ae18f48146cb2b686dcda64beebf2350
|
||||||
|
2026-07-29-simplify-web-image-input-v1.zh.md: c9e0a3cc5dc07b70be4b0c1244921ccac567d9af
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Agent Note: Simplify Web image input version one
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
English | [中文](2026-07-29-simplify-web-image-input-v1.zh.md)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The first durable Web image-input slice introduced required ordered multi-image intake alongside speculative surfaces for arbitrary CLI provider mounting, output-modality discovery, alternative text, provider-neutral visual token pricing, and browser lifecycle APIs with no cross-package consumer. Keeping the speculative surfaces would turn unchosen future behavior into public contracts and make the initial capability harder to review and maintain.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Version one accepts ordered image batches bounded by configurable per-message count and aggregate-byte limits plus per-image byte and pixel limits. The browser rejects unsupported declared formats before preview allocation, while the host authoritatively decodes the complete batch, checks current deployment bounds, validates every image without storage writes, and only then saves every image while preserving submitted order in the resulting durable blocks. Request buffering derives directly from the attachment service's aggregate image-byte limit. This preserves validation atomicity without adding a batch transaction, rollback protocol, or policy snapshot in `host.describe`.
|
||||||
|
|
||||||
|
Provider/model selection remains configuration and profile state. The boot composition registers the shipped DeepSeek, OpenAI, and Anthropic routes; the CLI does not add image-specific selection flags, inspect the yml provider roster, or dynamically mount an adapter.
|
||||||
|
|
||||||
|
Exact-model metadata carries only the input modalities that current admission decisions consume. `ImageBlock` carries the durable attachment reference; its optional display name supplies accessible UI text, so the core block has no separate alternative-text field. Provider-neutral token estimation does not apply one provider's visual pricing formula to other routes.
|
||||||
|
|
||||||
|
The attachment seam exposes its limits plus storage-free `validateImage`, `saveImage`, and `readImage`. The host depends on that seam rather than implementation re-exports. Browser draft and historical-image operations remain concrete conversation-plugin internals; the public `IConversation` face contains only the input registry and the scoped send, cancel, and history verbs used across package boundaries.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
**Accept only one image.** Comparing or combining several images is a current product requirement. Count and aggregate-byte bounds keep that path finite without reducing it to a single image.
|
||||||
|
|
||||||
|
**Add a storage transaction or rollback protocol.** Storage-free validation prevents malformed later members from leaving earlier valid members unreferenced. Stronger all-or-nothing storage across independent content-addressed objects would require ownership or reclamation semantics that the current product path does not need.
|
||||||
|
|
||||||
|
**Keep future-facing fields and methods as placeholders.** Output modalities, block alternative text, and active-model handshake data had no current decision consumer. Adding them later with their first consumer preserves freedom to choose the correct contract.
|
||||||
|
|
||||||
|
**Estimate every image with one tile formula.** Visual pricing varies by provider, model, detail mode, and preprocessing. A hard-coded provider-neutral estimate would look authoritative while being wrong; provider usage is the authoritative accounting source.
|
||||||
|
|
||||||
|
**Add CLI provider/model selection or dynamic mounting.** Configuration already owns route selection, plugin composition, and credentials. Duplicating those choices in image-input flags would require parsing or mutating the config tree outside the loader.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
The feature retains the two batch limits and one storage-free validation method required by multi-image prompts, while removing unrelated public fields, lifecycle operations, policy snapshots, and route-assembly branches. Provider/model selection remains composition or profile configuration. Pre-request token pressure may undercount visual input until a provider-aware estimator is designed, while reported usage remains exact.
|
||||||
|
|
||||||
|
Reintroducing any removed surface requires a concrete consumer and its failure, lifecycle, replay, and testing contract rather than compatibility with this pre-release shape.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Agent Note: 简化 Web 图片输入第一版
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
[English](2026-07-29-simplify-web-image-input-v1.md) | 中文
|
||||||
|
|
||||||
|
## 问题
|
||||||
|
|
||||||
|
首个持久化 Web 图片输入切片在引入按顺序接收多张图片的必需能力时,也引入了由 CLI(命令行界面)挂载任意提供方、输出模态发现、替代文本、提供方无关的视觉 token 定价,以及没有跨包(package)消费方的浏览器生命周期 API 等推测性表面。保留这些推测性表面会把尚未选择的未来行为变成公共契约,并使初始能力更难评审和维护。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
第一版接受有序图片批次,并以可配置的每条消息图片数量与图片总字节数上限,以及单张图片字节数与像素上限约束批次。浏览器会在分配预览前拒绝声明不支持的格式,而宿主会以权威方式解码完整批次、检查当前部署上限、在不写入存储的情况下校验每张图片,之后才保存每张图片,并在生成的持久图片块中保留提交顺序。请求缓冲上限直接由附件服务的图片总字节数上限派生。这样无需添加批次事务、回滚协议或 `host.describe` 中的策略快照,就能保持校验的原子性。
|
||||||
|
|
||||||
|
提供方/模型选择仍属于配置和 profile 状态。启动组合会注册交付的 DeepSeek、OpenAI 和 Anthropic 路由;CLI 不会添加图片专用选择标志、检查 yml 提供方清单或动态挂载适配器。
|
||||||
|
|
||||||
|
确切模型元数据只携带当前准入决策会消费的输入模态。`ImageBlock` 携带持久附件引用;其可选显示名称提供无障碍 UI 文本,因此核心块没有单独的替代文本字段。与提供方无关的 token 估算不会把某一提供方的视觉定价公式应用于其他路由。
|
||||||
|
|
||||||
|
附件服务边界公开其限制、不触碰存储的 `validateImage`,以及 `saveImage` 和 `readImage`。宿主依赖该服务边界,而不是实现层重新导出的内容。浏览器草稿和历史图片操作仍是具体会话插件的内部实现;公开的 `IConversation` 表面只包含输入注册表,以及跨包边界使用的按作用域发送、取消和历史记录操作。
|
||||||
|
|
||||||
|
## 曾考虑的替代方案
|
||||||
|
|
||||||
|
**只接受一张图片。** 比较或组合多张图片是当前产品要求。图片数量和总字节数上限使这条路径保持有界,而无需将其缩减为单张图片。
|
||||||
|
|
||||||
|
**添加存储事务或回滚协议。** 不触碰存储的校验能防止后面的畸形成员使前面有效的成员成为无引用对象。若要在独立的内容寻址对象之间实现更强的全有或全无存储保证,就需要当前产品路径并不需要的所有权或回收语义。
|
||||||
|
|
||||||
|
**将面向未来的字段和方法保留为占位符。** 输出模态、块替代文本和活跃模型握手数据目前都没有决策消费方。等到第一个消费方出现时再加入这些内容,可以保留选择正确契约的自由。
|
||||||
|
|
||||||
|
**使用一种图块公式估算每张图片。** 视觉定价因提供方、模型、细节模式和预处理而异。一项硬编码且与提供方无关的估算会看似权威,实际却是错误的;提供方用量才是权威核算来源。
|
||||||
|
|
||||||
|
**添加 CLI 提供方/模型选择或动态挂载。** 配置已经负责路由选择、插件组合和凭据。在图片输入标志中重复这些选择,会要求在 loader 之外解析或修改配置树。
|
||||||
|
|
||||||
|
## 后果
|
||||||
|
|
||||||
|
该功能保留了多图片提示词所需的两个批次上限和一个不触碰存储的校验方法,同时移除了无关的公开字段、生命周期操作、策略快照和路由组装分支。提供方/模型选择仍属于组合或 profile 配置。在设计出提供方感知型估算器之前,请求前的 token 压力计算可能少计视觉输入,而上报的用量仍保持精确。
|
||||||
|
|
||||||
|
重新引入任何已移除表面时,都必须有具体消费方,并为其定义失败、生命周期、回放和测试契约,而不是为了兼容这一预发布形态。
|
||||||
@@ -83,6 +83,7 @@ External packages that a workspace package resolves at runtime. `scripts/install
|
|||||||
| [`pnpm`](https://github.com/pnpm/pnpm) | MIT |
|
| [`pnpm`](https://github.com/pnpm/pnpm) | MIT |
|
||||||
| [`react`](https://github.com/facebook/react) | MIT |
|
| [`react`](https://github.com/facebook/react) | MIT |
|
||||||
| [`react-dom`](https://github.com/facebook/react) | MIT |
|
| [`react-dom`](https://github.com/facebook/react) | MIT |
|
||||||
|
| [`sharp`](https://github.com/lovell/sharp) | Apache-2.0 |
|
||||||
| [`shiki`](https://github.com/shikijs/shiki) | MIT |
|
| [`shiki`](https://github.com/shikijs/shiki) | MIT |
|
||||||
| [`supports-color`](https://github.com/chalk/supports-color) | MIT |
|
| [`supports-color`](https://github.com/chalk/supports-color) | MIT |
|
||||||
| [`tsx`](https://github.com/privatenumber/tsx) | MIT |
|
| [`tsx`](https://github.com/privatenumber/tsx) | MIT |
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ flowchart LR
|
|||||||
cfg --> plugin_dsh_base_llm_pi_ai
|
cfg --> plugin_dsh_base_llm_pi_ai
|
||||||
plugin_dsh_base_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
|
plugin_dsh_base_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
|
||||||
cfg --> plugin_dsh_base_session_persistence_jsonl
|
cfg --> plugin_dsh_base_session_persistence_jsonl
|
||||||
|
plugin_dsh_base_attachment_local["attachment-local<br/>@deepseek-ai/dsh-attachment-local"]
|
||||||
|
cfg --> plugin_dsh_base_attachment_local
|
||||||
plugin_dsh_base_session_query_sqlite["session-query-sqlite<br/>@deepseek-ai/dsh-session-query-sqlite"]
|
plugin_dsh_base_session_query_sqlite["session-query-sqlite<br/>@deepseek-ai/dsh-session-query-sqlite"]
|
||||||
cfg --> plugin_dsh_base_session_query_sqlite
|
cfg --> plugin_dsh_base_session_query_sqlite
|
||||||
plugin_dsh_base_session_projection["session-projection<br/>@deepseek-ai/dsh-session-projection"]
|
plugin_dsh_base_session_projection["session-projection<br/>@deepseek-ai/dsh-session-projection"]
|
||||||
@@ -183,6 +185,7 @@ flowchart LR
|
|||||||
| `credentials` | `@deepseek-ai/dsh-credentials-local` |
|
| `credentials` | `@deepseek-ai/dsh-credentials-local` |
|
||||||
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
|
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
|
||||||
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
|
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
|
||||||
|
| `attachment-local` | `@deepseek-ai/dsh-attachment-local` |
|
||||||
| `session-query-sqlite` | `@deepseek-ai/dsh-session-query-sqlite` |
|
| `session-query-sqlite` | `@deepseek-ai/dsh-session-query-sqlite` |
|
||||||
| `session-projection` | `@deepseek-ai/dsh-session-projection` |
|
| `session-projection` | `@deepseek-ai/dsh-session-projection` |
|
||||||
| `telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` |
|
| `telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` |
|
||||||
|
|||||||
@@ -44,9 +44,15 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
|
|||||||
return page.getByRole('dialog', { name: '设置' })
|
return page.getByRole('dialog', { name: '设置' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tokenize the lane-owned preset root the way the scaffold tokenizes cwd. */
|
/** Tokenize the lane-owned preset root after general aria normalization. */
|
||||||
function withPresetRoot(snapshot: string): string {
|
function withPresetRoot(snapshot: string): string {
|
||||||
return snapshot.split(userRoot).join('{{presetRoot}}')
|
const rootSuffix = `/${userRoot.split('/').pop()!}`
|
||||||
|
return snapshot.split('\n').map((line) => {
|
||||||
|
const rootStart = line.indexOf(rootSuffix)
|
||||||
|
if (rootStart === -1) return line
|
||||||
|
const pathStart = line.lastIndexOf(' ', rootStart) + 1
|
||||||
|
return `${line.slice(0, pathStart)}{{presetRoot}}${line.slice(rootStart + rootSuffix.length)}`
|
||||||
|
}).join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
// Multimodal image surfaces over the BUILT client graph (the code-mode-fixture
|
||||||
|
// idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
|
||||||
|
// Opens the fixture history session whose turn 72 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
|
||||||
|
// sessions.attachment route, the double-click ImageLightbox, and the composer
|
||||||
|
// intake chain (paste → ordered thumbnail rail → image-only send enablement → remove).
|
||||||
|
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||||
|
import { expect, it } from 'vitest'
|
||||||
|
import { installAssembledBootEnv, mountAssembledApp } from './assembled-boot.ts'
|
||||||
|
|
||||||
|
installAssembledBootEnv()
|
||||||
|
|
||||||
|
/** Open the fixture history session (the alpha log carrying the turn-72 image pair) and wait for its gallery. */
|
||||||
|
async function openFixtureSession(): Promise<void> {
|
||||||
|
const tree = await screen.findByRole('tree', { name: '会话' }, { timeout: 10_000 })
|
||||||
|
const group = (await within(tree).findAllByText('fixture'))
|
||||||
|
.map(el => el.closest<HTMLElement>('[role="treeitem"]'))
|
||||||
|
.find(el => el?.getAttribute('aria-expanded') !== null)
|
||||||
|
if (group === null || group === undefined) throw new Error('fixture Workspace group missing')
|
||||||
|
if (group.getAttribute('aria-expanded') === 'false') {
|
||||||
|
fireEvent.click(within(group).getByText('fixture'))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(group.getAttribute('aria-expanded')).toBe('true')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const session = await within(tree).findByText('Fixture 历史会话')
|
||||||
|
fireEvent.click(session)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(document.querySelectorAll('[data-align] img').length).toBeGreaterThan(0)
|
||||||
|
}, { timeout: 10_000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders the history image pair through the authorized attachment route and opens the lightbox', async () => {
|
||||||
|
localStorage.setItem('dsh.locale', 'zh')
|
||||||
|
mountAssembledApp()
|
||||||
|
await openFixtureSession()
|
||||||
|
|
||||||
|
// Both the user-side (align=end) and assistant-side (align=start) galleries
|
||||||
|
// load real fixture bytes over sessions.attachment. jsdom provides
|
||||||
|
// createObjectURL, so this environment MUST take the object-URL path — a
|
||||||
|
// data: src here would mean the fallback ran where it should not.
|
||||||
|
await waitFor(() => {
|
||||||
|
if (document.querySelector('[data-align="end"] img') === null
|
||||||
|
|| document.querySelector('[data-align="start"] img') === null) {
|
||||||
|
throw new Error('history image galleries missing')
|
||||||
|
}
|
||||||
|
}, { timeout: 10_000 })
|
||||||
|
const galleryShape = (align: string) => [...document.querySelectorAll(`[data-align="${align}"] img`)]
|
||||||
|
.map(img => ({ alt: img.getAttribute('alt'), scheme: img.getAttribute('src')?.split(':')[0] }))
|
||||||
|
expect({ user: galleryShape('end'), assistant: galleryShape('start') }).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"assistant": [
|
||||||
|
{
|
||||||
|
"alt": "fixture-image.png",
|
||||||
|
"scheme": "blob",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"user": [
|
||||||
|
{
|
||||||
|
"alt": "fixture-image.png",
|
||||||
|
"scheme": "blob",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
const userImage = document.querySelector<HTMLElement>('[data-align="end"] img')!
|
||||||
|
|
||||||
|
// Double-click opens the original-size lightbox; Escape/close dismisses it.
|
||||||
|
const frame = userImage.closest('button')
|
||||||
|
if (frame === null) throw new Error('image frame button missing')
|
||||||
|
fireEvent.doubleClick(frame)
|
||||||
|
const lightbox = await screen.findByRole('dialog')
|
||||||
|
expect(within(lightbox).getByRole('img').getAttribute('src')?.split(':')[0]).toBe('blob')
|
||||||
|
fireEvent.click(within(lightbox).getByRole('button', { name: /关闭/ }))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByRole('dialog')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts pasted images into the composer rail in order and removes them', async () => {
|
||||||
|
localStorage.setItem('dsh.locale', 'zh')
|
||||||
|
mountAssembledApp()
|
||||||
|
|
||||||
|
const tree = await screen.findByRole('tree', { name: '会话' }, { timeout: 10_000 })
|
||||||
|
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="在“fixture”中新建会话"]')
|
||||||
|
if (start === null) throw new Error('fixture Workspace new-session action missing')
|
||||||
|
fireEvent.click(start)
|
||||||
|
|
||||||
|
// Image-only send arming is pinned at package level (input-bar.spec.tsx);
|
||||||
|
// this assembled lane pins the intake chain over the built graph.
|
||||||
|
const textarea = await screen.findByPlaceholderText('描述你想要构建的内容', {}, { timeout: 10_000 })
|
||||||
|
const image = new File([new Uint8Array([137, 80, 78, 71])], 'pasted.png', { type: 'image/png' })
|
||||||
|
fireEvent.paste(textarea, {
|
||||||
|
clipboardData: {
|
||||||
|
items: [{ kind: 'file', type: 'image/png', getAsFile: () => image }],
|
||||||
|
getData: () => '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// The rail is an accessible group holding the draft thumbnail (queried via
|
||||||
|
// DOM: jsdom's a11y-visibility computation hides the composer subtree).
|
||||||
|
const rail = await waitFor(() => {
|
||||||
|
const el = document.querySelector('[role="group"][aria-label="待发送图片"]')
|
||||||
|
if (el === null) throw new Error('attachment rail missing')
|
||||||
|
return el
|
||||||
|
}, { timeout: 5_000 })
|
||||||
|
expect([...rail.querySelectorAll('img')].map(img => ({
|
||||||
|
alt: img.getAttribute('alt'), scheme: img.getAttribute('src')?.split(':')[0],
|
||||||
|
}))).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"alt": "pasted.png",
|
||||||
|
"scheme": "blob",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
|
||||||
|
const second = new File([new Uint8Array([137, 80, 78, 71])], 'second.png', { type: 'image/png' })
|
||||||
|
fireEvent.paste(textarea, {
|
||||||
|
clipboardData: {
|
||||||
|
items: [{ kind: 'file', type: 'image/png', getAsFile: () => second }],
|
||||||
|
getData: () => '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await waitFor(() => {
|
||||||
|
expect([...rail.querySelectorAll('img')].map(img => img.getAttribute('alt')))
|
||||||
|
.toEqual(['pasted.png', 'second.png'])
|
||||||
|
})
|
||||||
|
|
||||||
|
const remove = [...rail.querySelectorAll('button[aria-label^="移除图片"]')]
|
||||||
|
if (remove.length !== 2) throw new Error('remove buttons missing')
|
||||||
|
for (const button of remove) fireEvent.click(button)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(document.querySelector('[role="group"][aria-label="待发送图片"]')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -446,7 +446,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
|||||||
).toBe(3)
|
).toBe(3)
|
||||||
expect(await page.getByText('Ungrouped', { exact: true }).count()).toBe(0)
|
expect(await page.getByText('Ungrouped', { exact: true }).count()).toBe(0)
|
||||||
const hierarchy = page.getByRole('navigation', { name: 'Session hierarchy' })
|
const hierarchy = page.getByRole('navigation', { name: 'Session hierarchy' })
|
||||||
expect(await hierarchy.getByRole('button').count()).toBe(1)
|
await expect.poll(() => hierarchy.getByRole('button').count()).toBe(1)
|
||||||
await compareOrRefreshGolden(
|
await compareOrRefreshGolden(
|
||||||
FORK_EXPECTED,
|
FORK_EXPECTED,
|
||||||
await captureStableAria(page, '[role="tree"][aria-label="Sessions"]', scaffold.workspaceCwd),
|
await captureStableAria(page, '[role="tree"][aria-label="Sessions"]', scaffold.workspaceCwd),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Assembled todo snapshot: boots the real built `packages/client/*/lib/
|
// Assembled todo snapshot: boots the real built `packages/client/*/lib/
|
||||||
// client.js` bundles through AppWebEntry's ModuleLoader path against the
|
// client.js` bundles through AppWebEntry's ModuleLoader path against the
|
||||||
// keyless FixtureApiClient transport, opens the fixture session, and pins the
|
// keyless FixtureApiClient transport, opens the fixture session, and pins the
|
||||||
// two surfaces the fixture's parallel plan (turn 72, two items `in_progress`)
|
// two surfaces the fixture's parallel plan (turn 73, two items `in_progress`)
|
||||||
// reaches — the `todo_write` tool row and the dock's plan strip.
|
// reaches — the `todo_write` tool row and the dock's plan strip.
|
||||||
//
|
//
|
||||||
// The row is pinned as three separate fields on purpose. `summary=` is the
|
// The row is pinned as three separate fields on purpose. `summary=` is the
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/capability-seams.md
|
# pnpm run verify-translation-pairing --write docs/capability-seams.md
|
||||||
capability-seams.md: 345e17c8c28bbe3465abd20639770e6331a70d11
|
capability-seams.md: d5610b6bf6bcb39bfa146ada53ce0734a978b8cd
|
||||||
capability-seams.zh.md: 472aaabf4c992fdcd54fbe0e9a14cf9a82e6b803
|
capability-seams.zh.md: 8a1b38d3600d673a8e0a99941e51d0973101a1c1
|
||||||
@@ -7,10 +7,14 @@ A service can be a core spine service, a swappable capability seam, or a bundle/
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
|
pkg_attachment["attachment"]
|
||||||
|
svc_attachments["ctx.attachments<br/>Durable binary attachment storage"]
|
||||||
|
pkg_attachment_local["attachment-local"]
|
||||||
|
pkg_host_runtime["host-runtime"]
|
||||||
|
pkg_llm_pi_ai["llm-pi-ai"]
|
||||||
pkg_llm["llm"]
|
pkg_llm["llm"]
|
||||||
svc_llm["ctx.llm<br/>LLM adapter registry"]
|
svc_llm["ctx.llm<br/>LLM adapter registry"]
|
||||||
pkg_llm_deepseek["llm-deepseek"]
|
pkg_llm_deepseek["llm-deepseek"]
|
||||||
pkg_llm_pi_ai["llm-pi-ai"]
|
|
||||||
pkg_llm_replay["llm-replay"]
|
pkg_llm_replay["llm-replay"]
|
||||||
pkg_agent_loop["agent-loop"]
|
pkg_agent_loop["agent-loop"]
|
||||||
pkg_compact_basic["compact-basic"]
|
pkg_compact_basic["compact-basic"]
|
||||||
@@ -186,6 +190,8 @@ flowchart LR
|
|||||||
pkg_agent_presets --> svc_agentPresets
|
pkg_agent_presets --> svc_agentPresets
|
||||||
pkg_api_gateway --> svc_typertGateway
|
pkg_api_gateway --> svc_typertGateway
|
||||||
pkg_approval --> svc_approval
|
pkg_approval --> svc_approval
|
||||||
|
pkg_attachment --> svc_attachments
|
||||||
|
pkg_attachment_local --> svc_attachments
|
||||||
pkg_bash --> svc_bash
|
pkg_bash --> svc_bash
|
||||||
pkg_bash_env --> svc_bashEnv
|
pkg_bash_env --> svc_bashEnv
|
||||||
pkg_bash_local --> svc_bash
|
pkg_bash_local --> svc_bash
|
||||||
@@ -280,6 +286,8 @@ flowchart LR
|
|||||||
svc_agents --> pkg_subagent_inprocess
|
svc_agents --> pkg_subagent_inprocess
|
||||||
svc_approval --> pkg_tool_bash
|
svc_approval --> pkg_tool_bash
|
||||||
svc_approval --> pkg_tools
|
svc_approval --> pkg_tools
|
||||||
|
svc_attachments --> pkg_host_runtime
|
||||||
|
svc_attachments --> pkg_llm_pi_ai
|
||||||
svc_bash --> pkg_hooks_claude
|
svc_bash --> pkg_hooks_claude
|
||||||
svc_bash --> pkg_hooks_codex
|
svc_bash --> pkg_hooks_codex
|
||||||
svc_bash --> pkg_tool_bash
|
svc_bash --> pkg_tool_bash
|
||||||
@@ -380,6 +388,7 @@ flowchart LR
|
|||||||
|
|
||||||
| ctx key | Role | Owner | Implementations | Direct consumers | Companion plugins | Note |
|
| ctx key | Role | Owner | Implementations | Direct consumers | Companion plugins | Note |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | `host-runtime`, [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content. |
|
||||||
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
|
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
|
||||||
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
|
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
|
||||||
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
|
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
|
||||||
|
|||||||
@@ -9,10 +9,14 @@
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
|
pkg_attachment["attachment"]
|
||||||
|
svc_attachments["ctx.attachments<br/>Durable binary attachment storage"]
|
||||||
|
pkg_attachment_local["attachment-local"]
|
||||||
|
pkg_host_runtime["host-runtime"]
|
||||||
|
pkg_llm_pi_ai["llm-pi-ai"]
|
||||||
pkg_llm["llm"]
|
pkg_llm["llm"]
|
||||||
svc_llm["ctx.llm<br/>LLM adapter registry"]
|
svc_llm["ctx.llm<br/>LLM adapter registry"]
|
||||||
pkg_llm_deepseek["llm-deepseek"]
|
pkg_llm_deepseek["llm-deepseek"]
|
||||||
pkg_llm_pi_ai["llm-pi-ai"]
|
|
||||||
pkg_llm_replay["llm-replay"]
|
pkg_llm_replay["llm-replay"]
|
||||||
pkg_agent_loop["agent-loop"]
|
pkg_agent_loop["agent-loop"]
|
||||||
pkg_compact_basic["compact-basic"]
|
pkg_compact_basic["compact-basic"]
|
||||||
@@ -188,6 +192,8 @@ flowchart LR
|
|||||||
pkg_agent_presets --> svc_agentPresets
|
pkg_agent_presets --> svc_agentPresets
|
||||||
pkg_api_gateway --> svc_typertGateway
|
pkg_api_gateway --> svc_typertGateway
|
||||||
pkg_approval --> svc_approval
|
pkg_approval --> svc_approval
|
||||||
|
pkg_attachment --> svc_attachments
|
||||||
|
pkg_attachment_local --> svc_attachments
|
||||||
pkg_bash --> svc_bash
|
pkg_bash --> svc_bash
|
||||||
pkg_bash_env --> svc_bashEnv
|
pkg_bash_env --> svc_bashEnv
|
||||||
pkg_bash_local --> svc_bash
|
pkg_bash_local --> svc_bash
|
||||||
@@ -282,6 +288,8 @@ flowchart LR
|
|||||||
svc_agents --> pkg_subagent_inprocess
|
svc_agents --> pkg_subagent_inprocess
|
||||||
svc_approval --> pkg_tool_bash
|
svc_approval --> pkg_tool_bash
|
||||||
svc_approval --> pkg_tools
|
svc_approval --> pkg_tools
|
||||||
|
svc_attachments --> pkg_host_runtime
|
||||||
|
svc_attachments --> pkg_llm_pi_ai
|
||||||
svc_bash --> pkg_hooks_claude
|
svc_bash --> pkg_hooks_claude
|
||||||
svc_bash --> pkg_hooks_codex
|
svc_bash --> pkg_hooks_codex
|
||||||
svc_bash --> pkg_tool_bash
|
svc_bash --> pkg_tool_bash
|
||||||
@@ -382,6 +390,7 @@ flowchart LR
|
|||||||
|
|
||||||
| ctx 键 | 角色 | 所属包 | 实现 | 直接消费方 | 配套插件 | 说明 |
|
| ctx 键 | 角色 | 所属包 | 实现 | 直接消费方 | 配套插件 | 说明 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | `host-runtime`、[`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | 宿主会在会话事件之前提交已接受的图片;提供方适配器将已授权的持久引用解析为提供方原生内容。 |
|
||||||
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、[`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop)、[`compact-basic`](../packages/compact/compact-basic) | - | 适配器注册提供方实现;agent loop(智能体循环)与压缩功能调用提供方无关的流服务。 |
|
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、[`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop)、[`compact-basic`](../packages/compact/compact-basic) | - | 适配器注册提供方实现;agent loop(智能体循环)与压缩功能调用提供方无关的流服务。 |
|
||||||
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 |
|
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 |
|
||||||
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
|
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||||
config-catalog.md: 18980d22c694647374b9fa4e6dfbf245ff2416c4
|
config-catalog.md: 234e07eafdc4a01681ccff4b7ac8290905cd677b
|
||||||
config-catalog.zh.md: a43c561806498ca53a95af815d0cd7686a0100ca
|
config-catalog.zh.md: d092947d31cfe4b24cae5d0ee8570dda39d7a287
|
||||||
+25
-2
@@ -261,6 +261,26 @@ Depends on: [`ToolPresentationMode`](subsystems/tools.md)
|
|||||||
|
|
||||||
Source: [`packages/core/agent-tool-mode/src/index.ts:36`](../packages/core/agent-tool-mode/src/index.ts)
|
Source: [`packages/core/agent-tool-mode/src/index.ts:36`](../packages/core/agent-tool-mode/src/index.ts)
|
||||||
|
|
||||||
|
## `@deepseek-ai/dsh-attachment-local`
|
||||||
|
|
||||||
|
```ts config-catalog
|
||||||
|
/** Local attachment backend configuration. */
|
||||||
|
export interface Config {
|
||||||
|
/** Explicit harness home; omitted follows `DSH_HOME`, then `~/.dsh`. */
|
||||||
|
dshHome?: string
|
||||||
|
/** Maximum encoded bytes accepted for one image. */
|
||||||
|
maxImageBytes?: number
|
||||||
|
/** Maximum image count accepted in one submitted message. */
|
||||||
|
maxImagesPerMessage?: number
|
||||||
|
/** Maximum aggregate encoded image bytes accepted in one submitted message. */
|
||||||
|
maxMessageImageBytes?: number
|
||||||
|
/** Maximum intrinsic width multiplied by height accepted for one image. */
|
||||||
|
maxImagePixels?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Source: [`packages/attachment/attachment-local/src/index.ts:24`](../packages/attachment/attachment-local/src/index.ts)
|
||||||
|
|
||||||
## `@deepseek-ai/dsh-bash-env`
|
## `@deepseek-ai/dsh-bash-env`
|
||||||
|
|
||||||
```ts config-catalog
|
```ts config-catalog
|
||||||
@@ -332,10 +352,12 @@ export interface ConnectionConfig {
|
|||||||
* that is not a bare, canonical authority fails the plugin load.
|
* that is not a bare, canonical authority fails the plugin load.
|
||||||
*/
|
*/
|
||||||
trustedHosts?: string[]
|
trustedHosts?: string[]
|
||||||
|
/** Maximum buffered JSON body for every `/api` request. */
|
||||||
|
maxRequestBodyBytes?: number
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Source: [`packages/client/connection/src/index.ts:32`](../packages/client/connection/src/index.ts)
|
Source: [`packages/client/connection/src/index.ts:52`](../packages/client/connection/src/index.ts)
|
||||||
|
|
||||||
## `@deepseek-ai/dsh-client-hmr`
|
## `@deepseek-ai/dsh-client-hmr`
|
||||||
|
|
||||||
@@ -617,7 +639,7 @@ Source: [`packages/hooks/hooks-codex/src/index.ts:44`](../packages/hooks/hooks-c
|
|||||||
|
|
||||||
## `@deepseek-ai/dsh-host-apiproxy`
|
## `@deepseek-ai/dsh-host-apiproxy`
|
||||||
|
|
||||||
Requires: `agentDefaultModel` · `agents` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userInteraction` · `workspace`
|
Requires: `agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userInteraction` · `workspace`
|
||||||
|
|
||||||
```ts config-catalog
|
```ts config-catalog
|
||||||
/** Gateway plugin config: the Host-only Workspace creation root. */
|
/** Gateway plugin config: the Host-only Workspace creation root. */
|
||||||
@@ -2736,6 +2758,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
|||||||
|
|
||||||
Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)).
|
Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)).
|
||||||
|
|
||||||
|
- `@deepseek-ai/dsh-attachment` — abstract `AttachmentStore` ([`packages/attachment/attachment/src/index.ts`](../packages/attachment/attachment/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-bash` — abstract `BashExecutor` ([`packages/bash/bash/src/index.ts`](../packages/bash/bash/src/index.ts))
|
- `@deepseek-ai/dsh-bash` — abstract `BashExecutor` ([`packages/bash/bash/src/index.ts`](../packages/bash/bash/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
|
- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-compact` — abstract `CompactService` ([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts))
|
- `@deepseek-ai/dsh-compact` — abstract `CompactService` ([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts))
|
||||||
|
|||||||
@@ -263,6 +263,26 @@ export interface Config {
|
|||||||
|
|
||||||
来源:[`packages/core/agent-tool-mode/src/index.ts:36`](../packages/core/agent-tool-mode/src/index.ts)
|
来源:[`packages/core/agent-tool-mode/src/index.ts:36`](../packages/core/agent-tool-mode/src/index.ts)
|
||||||
|
|
||||||
|
## `@deepseek-ai/dsh-attachment-local`
|
||||||
|
|
||||||
|
```ts config-catalog
|
||||||
|
/** Local attachment backend configuration. */
|
||||||
|
export interface Config {
|
||||||
|
/** Explicit harness home; omitted follows `DSH_HOME`, then `~/.dsh`. */
|
||||||
|
dshHome?: string
|
||||||
|
/** Maximum encoded bytes accepted for one image. */
|
||||||
|
maxImageBytes?: number
|
||||||
|
/** Maximum image count accepted in one submitted message. */
|
||||||
|
maxImagesPerMessage?: number
|
||||||
|
/** Maximum aggregate encoded image bytes accepted in one submitted message. */
|
||||||
|
maxMessageImageBytes?: number
|
||||||
|
/** Maximum intrinsic width multiplied by height accepted for one image. */
|
||||||
|
maxImagePixels?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
来源:[`packages/attachment/attachment-local/src/index.ts:24`](../packages/attachment/attachment-local/src/index.ts)
|
||||||
|
|
||||||
## `@deepseek-ai/dsh-bash-env`
|
## `@deepseek-ai/dsh-bash-env`
|
||||||
|
|
||||||
```ts config-catalog
|
```ts config-catalog
|
||||||
@@ -334,10 +354,12 @@ export interface ConnectionConfig {
|
|||||||
* that is not a bare, canonical authority fails the plugin load.
|
* that is not a bare, canonical authority fails the plugin load.
|
||||||
*/
|
*/
|
||||||
trustedHosts?: string[]
|
trustedHosts?: string[]
|
||||||
|
/** Maximum buffered JSON body for every `/api` request. */
|
||||||
|
maxRequestBodyBytes?: number
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
来源:[`packages/client/connection/src/index.ts:32`](../packages/client/connection/src/index.ts)
|
来源:[`packages/client/connection/src/index.ts:52`](../packages/client/connection/src/index.ts)
|
||||||
|
|
||||||
## `@deepseek-ai/dsh-client-hmr`
|
## `@deepseek-ai/dsh-client-hmr`
|
||||||
|
|
||||||
@@ -619,7 +641,7 @@ export interface Config {
|
|||||||
|
|
||||||
## `@deepseek-ai/dsh-host-apiproxy`
|
## `@deepseek-ai/dsh-host-apiproxy`
|
||||||
|
|
||||||
需要:`agentDefaultModel` · `agents` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userInteraction` · `workspace`
|
需要:`agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userInteraction` · `workspace`
|
||||||
|
|
||||||
```ts config-catalog
|
```ts config-catalog
|
||||||
/** Gateway plugin config: the Host-only Workspace creation root. */
|
/** Gateway plugin config: the Host-only Workspace creation root. */
|
||||||
@@ -2737,6 +2759,7 @@ export interface Config {
|
|||||||
|
|
||||||
抽象服务类——部署时应改为加载具体的实现包(参见[能力 seam](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md))。
|
抽象服务类——部署时应改为加载具体的实现包(参见[能力 seam](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md))。
|
||||||
|
|
||||||
|
- `@deepseek-ai/dsh-attachment` — 抽象 `AttachmentStore`([`packages/attachment/attachment/src/index.ts`](../packages/attachment/attachment/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-bash` — 抽象 `BashExecutor`([`packages/bash/bash/src/index.ts`](../packages/bash/bash/src/index.ts))
|
- `@deepseek-ai/dsh-bash` — 抽象 `BashExecutor`([`packages/bash/bash/src/index.ts`](../packages/bash/bash/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-code-runtime` — 抽象 `CodeRuntime`([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
|
- `@deepseek-ai/dsh-code-runtime` — 抽象 `CodeRuntime`([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-compact` — 抽象 `CompactService`([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts))
|
- `@deepseek-ai/dsh-compact` — 抽象 `CompactService`([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts))
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
|
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
|
||||||
event-producer-consumer.md: cc48f7396d77ecbff39449c5e9e4a22ec106ca74
|
event-producer-consumer.md: 6b23e77f0dd5c2a2ace349a0546fa3953aad6646
|
||||||
event-producer-consumer.zh.md: ead7a39abd263064ac9b5080c8d71b47463f3b54
|
event-producer-consumer.zh.md: ca2fed476498a2d5f1c41e9bd30ecee79274bc84
|
||||||
@@ -28,8 +28,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
|||||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-policy`](../packages/fs/fs-policy), [`skill-local`](../packages/skill/skill-local) |
|
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-policy`](../packages/fs/fs-policy), [`skill-local`](../packages/skill/skill-local) |
|
||||||
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
|
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
|
||||||
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
|
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
|
||||||
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:73`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
|
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
|
||||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:62`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
||||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
||||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`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-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
|
| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`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-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
|
||||||
|
|||||||
@@ -30,8 +30,8 @@
|
|||||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-policy`](../packages/fs/fs-policy), [`skill-local`](../packages/skill/skill-local) |
|
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-policy`](../packages/fs/fs-policy), [`skill-local`](../packages/skill/skill-local) |
|
||||||
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
|
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
|
||||||
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
|
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
|
||||||
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:73`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
|
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
|
||||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:62`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
||||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
||||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`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-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
|
| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`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-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/module-graph.md
|
# pnpm run verify-translation-pairing --write docs/module-graph.md
|
||||||
module-graph.md: 58e4ce7c23de28a85bf140b426b4ca43d8d18cf7
|
module-graph.md: 36184038fab93562c564667a771d8e3196bca48b
|
||||||
module-graph.zh.md: f8605d0c94023fcf51c38e6a8d4cca80ae436220
|
module-graph.zh.md: f8b745770635694da2d03770a51040f8ffb29c98
|
||||||
+110
-95
@@ -127,6 +127,10 @@ flowchart TD
|
|||||||
pkg_api_gateway["api-gateway"]
|
pkg_api_gateway["api-gateway"]
|
||||||
pkg_api_remotes["api-remotes"]
|
pkg_api_remotes["api-remotes"]
|
||||||
end
|
end
|
||||||
|
subgraph group_attachment["packages/attachment"]
|
||||||
|
pkg_attachment["attachment"]
|
||||||
|
pkg_attachment_local["attachment-local"]
|
||||||
|
end
|
||||||
subgraph group_boot["packages/boot"]
|
subgraph group_boot["packages/boot"]
|
||||||
pkg_app_boot["app-boot"]
|
pkg_app_boot["app-boot"]
|
||||||
end
|
end
|
||||||
@@ -327,9 +331,8 @@ flowchart TD
|
|||||||
pkg_type_meta --> pkg_invariants
|
pkg_type_meta --> pkg_invariants
|
||||||
pkg_typert_generator --> pkg_invariants
|
pkg_typert_generator --> pkg_invariants
|
||||||
pkg_typert_registry --> pkg_invariants
|
pkg_typert_registry --> pkg_invariants
|
||||||
pkg_llm --> pkg_brand
|
pkg_attachment --> pkg_brand
|
||||||
pkg_llm --> pkg_invariants
|
pkg_attachment --> pkg_invariants
|
||||||
pkg_llm --> pkg_timeout
|
|
||||||
pkg_client_connection --> pkg_host_webserver
|
pkg_client_connection --> pkg_host_webserver
|
||||||
pkg_client_connection --> pkg_invariants
|
pkg_client_connection --> pkg_invariants
|
||||||
pkg_client_hmr --> pkg_client_modules
|
pkg_client_hmr --> pkg_client_modules
|
||||||
@@ -365,34 +368,16 @@ flowchart TD
|
|||||||
pkg_subprocess_local --> pkg_timeout
|
pkg_subprocess_local --> pkg_timeout
|
||||||
pkg_typert_loader --> pkg_invariants
|
pkg_typert_loader --> pkg_invariants
|
||||||
pkg_typert_loader --> pkg_typert_registry
|
pkg_typert_loader --> pkg_typert_registry
|
||||||
pkg_llm_deepseek --> pkg_credentials
|
pkg_llm --> pkg_attachment
|
||||||
pkg_llm_deepseek --> pkg_environment
|
pkg_llm --> pkg_brand
|
||||||
pkg_llm_deepseek --> pkg_invariants
|
pkg_llm --> pkg_invariants
|
||||||
pkg_llm_deepseek --> pkg_llm
|
pkg_llm --> pkg_timeout
|
||||||
pkg_llm_deepseek --> pkg_settings
|
|
||||||
pkg_llm_deepseek --> pkg_timeout
|
|
||||||
pkg_llm_pi_ai --> pkg_credentials
|
|
||||||
pkg_llm_pi_ai --> pkg_environment
|
|
||||||
pkg_llm_pi_ai --> pkg_invariants
|
|
||||||
pkg_llm_pi_ai --> pkg_llm
|
|
||||||
pkg_llm_pi_ai --> pkg_settings
|
|
||||||
pkg_llm_pi_ai --> pkg_timeout
|
|
||||||
pkg_session --> pkg_brand
|
|
||||||
pkg_session --> pkg_invariants
|
|
||||||
pkg_session --> pkg_llm
|
|
||||||
pkg_session --> pkg_scope
|
|
||||||
pkg_session --> pkg_type_meta
|
|
||||||
pkg_system_prompt --> pkg_invariants
|
|
||||||
pkg_system_prompt --> pkg_llm
|
|
||||||
pkg_system_prompt --> pkg_scope
|
|
||||||
pkg_skill --> pkg_invariants
|
|
||||||
pkg_skill --> pkg_llm
|
|
||||||
pkg_skill --> pkg_scope
|
|
||||||
pkg_web --> pkg_invariants
|
|
||||||
pkg_web --> pkg_llm
|
|
||||||
pkg_api_gateway --> pkg_client_connection
|
pkg_api_gateway --> pkg_client_connection
|
||||||
pkg_api_gateway --> pkg_invariants
|
pkg_api_gateway --> pkg_invariants
|
||||||
pkg_api_gateway --> pkg_typert_registry
|
pkg_api_gateway --> pkg_typert_registry
|
||||||
|
pkg_attachment_local --> pkg_attachment
|
||||||
|
pkg_attachment_local --> pkg_invariants
|
||||||
|
pkg_attachment_local --> pkg_paths
|
||||||
pkg_client_locale --> pkg_client_runtime
|
pkg_client_locale --> pkg_client_runtime
|
||||||
pkg_client_locale --> pkg_client_ui_primitives
|
pkg_client_locale --> pkg_client_ui_primitives
|
||||||
pkg_client_locale --> pkg_client_ui_slots
|
pkg_client_locale --> pkg_client_ui_slots
|
||||||
@@ -416,40 +401,36 @@ flowchart TD
|
|||||||
pkg_credentials_local --> pkg_environment
|
pkg_credentials_local --> pkg_environment
|
||||||
pkg_credentials_local --> pkg_invariants
|
pkg_credentials_local --> pkg_invariants
|
||||||
pkg_credentials_local --> pkg_paths
|
pkg_credentials_local --> pkg_paths
|
||||||
pkg_lsp --> pkg_brand
|
|
||||||
pkg_lsp --> pkg_invariants
|
|
||||||
pkg_lsp --> pkg_llm
|
|
||||||
pkg_settings_local --> pkg_atomic_write
|
pkg_settings_local --> pkg_atomic_write
|
||||||
pkg_settings_local --> pkg_invariants
|
pkg_settings_local --> pkg_invariants
|
||||||
pkg_settings_local --> pkg_paths
|
pkg_settings_local --> pkg_paths
|
||||||
pkg_settings_local --> pkg_settings
|
pkg_settings_local --> pkg_settings
|
||||||
pkg_agent --> pkg_invariants
|
pkg_llm_deepseek --> pkg_credentials
|
||||||
pkg_agent --> pkg_llm
|
pkg_llm_deepseek --> pkg_environment
|
||||||
pkg_agent --> pkg_scope
|
pkg_llm_deepseek --> pkg_invariants
|
||||||
pkg_agent --> pkg_session
|
pkg_llm_deepseek --> pkg_llm
|
||||||
pkg_agent --> pkg_system_prompt
|
pkg_llm_deepseek --> pkg_settings
|
||||||
pkg_agent --> pkg_type_meta
|
pkg_llm_deepseek --> pkg_timeout
|
||||||
pkg_skill_badge --> pkg_invariants
|
pkg_llm_pi_ai --> pkg_attachment
|
||||||
pkg_skill_badge --> pkg_skill
|
pkg_llm_pi_ai --> pkg_credentials
|
||||||
pkg_web_fetch_local --> pkg_invariants
|
pkg_llm_pi_ai --> pkg_environment
|
||||||
pkg_web_fetch_local --> pkg_timeout
|
pkg_llm_pi_ai --> pkg_invariants
|
||||||
pkg_web_fetch_local --> pkg_web
|
pkg_llm_pi_ai --> pkg_llm
|
||||||
pkg_web_search_exa --> pkg_environment
|
pkg_llm_pi_ai --> pkg_settings
|
||||||
pkg_web_search_exa --> pkg_invariants
|
pkg_llm_pi_ai --> pkg_timeout
|
||||||
pkg_web_search_exa --> pkg_web
|
pkg_session --> pkg_brand
|
||||||
pkg_web_search_perplexity --> pkg_environment
|
pkg_session --> pkg_invariants
|
||||||
pkg_web_search_perplexity --> pkg_invariants
|
pkg_session --> pkg_llm
|
||||||
pkg_web_search_perplexity --> pkg_web
|
pkg_session --> pkg_scope
|
||||||
pkg_spill --> pkg_brand
|
pkg_session --> pkg_type_meta
|
||||||
pkg_spill --> pkg_invariants
|
pkg_system_prompt --> pkg_invariants
|
||||||
pkg_spill --> pkg_llm
|
pkg_system_prompt --> pkg_llm
|
||||||
pkg_spill --> pkg_session
|
pkg_system_prompt --> pkg_scope
|
||||||
pkg_acp_snapshot --> pkg_invariants
|
pkg_skill --> pkg_invariants
|
||||||
pkg_acp_snapshot --> pkg_session
|
pkg_skill --> pkg_llm
|
||||||
pkg_app_boot --> pkg_environment
|
pkg_skill --> pkg_scope
|
||||||
pkg_app_boot --> pkg_invariants
|
pkg_web --> pkg_invariants
|
||||||
pkg_app_boot --> pkg_paths
|
pkg_web --> pkg_llm
|
||||||
pkg_app_boot --> pkg_system_prompt
|
|
||||||
pkg_client_ui_question --> pkg_client_locale
|
pkg_client_ui_question --> pkg_client_locale
|
||||||
pkg_client_ui_question --> pkg_invariants
|
pkg_client_ui_question --> pkg_invariants
|
||||||
pkg_client_ui_settings_general --> pkg_client_connection
|
pkg_client_ui_settings_general --> pkg_client_connection
|
||||||
@@ -480,10 +461,54 @@ flowchart TD
|
|||||||
pkg_client_ui_workspace --> pkg_client_ui_primitives
|
pkg_client_ui_workspace --> pkg_client_ui_primitives
|
||||||
pkg_client_ui_workspace --> pkg_client_ui_slots
|
pkg_client_ui_workspace --> pkg_client_ui_slots
|
||||||
pkg_client_ui_workspace --> pkg_invariants
|
pkg_client_ui_workspace --> pkg_invariants
|
||||||
|
pkg_lsp --> pkg_brand
|
||||||
|
pkg_lsp --> pkg_invariants
|
||||||
|
pkg_lsp --> pkg_llm
|
||||||
|
pkg_agent --> pkg_invariants
|
||||||
|
pkg_agent --> pkg_llm
|
||||||
|
pkg_agent --> pkg_scope
|
||||||
|
pkg_agent --> pkg_session
|
||||||
|
pkg_agent --> pkg_system_prompt
|
||||||
|
pkg_agent --> pkg_type_meta
|
||||||
|
pkg_skill_badge --> pkg_invariants
|
||||||
|
pkg_skill_badge --> pkg_skill
|
||||||
|
pkg_web_fetch_local --> pkg_invariants
|
||||||
|
pkg_web_fetch_local --> pkg_timeout
|
||||||
|
pkg_web_fetch_local --> pkg_web
|
||||||
|
pkg_web_search_exa --> pkg_environment
|
||||||
|
pkg_web_search_exa --> pkg_invariants
|
||||||
|
pkg_web_search_exa --> pkg_web
|
||||||
|
pkg_web_search_perplexity --> pkg_environment
|
||||||
|
pkg_web_search_perplexity --> pkg_invariants
|
||||||
|
pkg_web_search_perplexity --> pkg_web
|
||||||
|
pkg_spill --> pkg_brand
|
||||||
|
pkg_spill --> pkg_invariants
|
||||||
|
pkg_spill --> pkg_llm
|
||||||
|
pkg_spill --> pkg_session
|
||||||
|
pkg_acp_snapshot --> pkg_invariants
|
||||||
|
pkg_acp_snapshot --> pkg_session
|
||||||
|
pkg_app_boot --> pkg_environment
|
||||||
|
pkg_app_boot --> pkg_invariants
|
||||||
|
pkg_app_boot --> pkg_paths
|
||||||
|
pkg_app_boot --> pkg_system_prompt
|
||||||
|
pkg_client_ui_layout --> pkg_client_runtime
|
||||||
|
pkg_client_ui_layout --> pkg_client_ui_slots
|
||||||
|
pkg_client_ui_layout --> pkg_client_ui_theme
|
||||||
|
pkg_client_ui_layout --> pkg_invariants
|
||||||
pkg_code_runtime_worker --> pkg_code_runtime
|
pkg_code_runtime_worker --> pkg_code_runtime
|
||||||
pkg_code_runtime_worker --> pkg_invariants
|
pkg_code_runtime_worker --> pkg_invariants
|
||||||
pkg_code_runtime_worker --> pkg_session
|
pkg_code_runtime_worker --> pkg_session
|
||||||
pkg_code_runtime_worker --> pkg_timeout
|
pkg_code_runtime_worker --> pkg_timeout
|
||||||
|
pkg_host_directory_picker_browse --> pkg_client_locale
|
||||||
|
pkg_host_directory_picker_browse --> pkg_client_runtime
|
||||||
|
pkg_host_directory_picker_browse --> pkg_client_ui_primitives
|
||||||
|
pkg_host_directory_picker_browse --> pkg_client_ui_slots
|
||||||
|
pkg_host_directory_picker_browse --> pkg_client_ui_workspace
|
||||||
|
pkg_host_directory_picker_browse --> pkg_invariants
|
||||||
|
pkg_host_directory_picker_native --> pkg_client_runtime
|
||||||
|
pkg_host_directory_picker_native --> pkg_client_ui_slots
|
||||||
|
pkg_host_directory_picker_native --> pkg_client_ui_workspace
|
||||||
|
pkg_host_directory_picker_native --> pkg_invariants
|
||||||
pkg_agent_presets --> pkg_atomic_write
|
pkg_agent_presets --> pkg_atomic_write
|
||||||
pkg_agent_presets --> pkg_invariants
|
pkg_agent_presets --> pkg_invariants
|
||||||
pkg_agent_presets --> pkg_paths
|
pkg_agent_presets --> pkg_paths
|
||||||
@@ -538,25 +563,15 @@ flowchart TD
|
|||||||
pkg_loader_smoke --> pkg_invariants
|
pkg_loader_smoke --> pkg_invariants
|
||||||
pkg_loader_smoke --> pkg_llm
|
pkg_loader_smoke --> pkg_llm
|
||||||
pkg_loader_smoke --> pkg_session
|
pkg_loader_smoke --> pkg_session
|
||||||
pkg_client_ui_layout --> pkg_client_runtime
|
|
||||||
pkg_client_ui_layout --> pkg_client_ui_slots
|
|
||||||
pkg_client_ui_layout --> pkg_client_ui_theme
|
|
||||||
pkg_client_ui_layout --> pkg_invariants
|
|
||||||
pkg_time_context --> pkg_agent
|
pkg_time_context --> pkg_agent
|
||||||
pkg_time_context --> pkg_invariants
|
pkg_time_context --> pkg_invariants
|
||||||
pkg_time_context --> pkg_session
|
pkg_time_context --> pkg_session
|
||||||
pkg_host_apiproxy --> pkg_agent_presets
|
pkg_host_apiproxy --> pkg_agent_presets
|
||||||
pkg_host_apiproxy --> pkg_invariants
|
pkg_host_apiproxy --> pkg_invariants
|
||||||
pkg_host_directory_picker_browse --> pkg_client_locale
|
pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
|
||||||
pkg_host_directory_picker_browse --> pkg_client_runtime
|
pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
|
||||||
pkg_host_directory_picker_browse --> pkg_client_ui_primitives
|
pkg_host_directory_picker_auto --> pkg_host_webserver
|
||||||
pkg_host_directory_picker_browse --> pkg_client_ui_slots
|
pkg_host_directory_picker_auto --> pkg_invariants
|
||||||
pkg_host_directory_picker_browse --> pkg_client_ui_workspace
|
|
||||||
pkg_host_directory_picker_browse --> pkg_invariants
|
|
||||||
pkg_host_directory_picker_native --> pkg_client_runtime
|
|
||||||
pkg_host_directory_picker_native --> pkg_client_ui_slots
|
|
||||||
pkg_host_directory_picker_native --> pkg_client_ui_workspace
|
|
||||||
pkg_host_directory_picker_native --> pkg_invariants
|
|
||||||
pkg_commands --> pkg_agent
|
pkg_commands --> pkg_agent
|
||||||
pkg_commands --> pkg_brand
|
pkg_commands --> pkg_brand
|
||||||
pkg_commands --> pkg_invariants
|
pkg_commands --> pkg_invariants
|
||||||
@@ -696,10 +711,6 @@ flowchart TD
|
|||||||
pkg_command_feedback --> pkg_commands
|
pkg_command_feedback --> pkg_commands
|
||||||
pkg_command_feedback --> pkg_invariants
|
pkg_command_feedback --> pkg_invariants
|
||||||
pkg_command_feedback --> pkg_session
|
pkg_command_feedback --> pkg_session
|
||||||
pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
|
|
||||||
pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
|
|
||||||
pkg_host_directory_picker_auto --> pkg_host_webserver
|
|
||||||
pkg_host_directory_picker_auto --> pkg_invariants
|
|
||||||
pkg_permission --> pkg_bash
|
pkg_permission --> pkg_bash
|
||||||
pkg_permission --> pkg_commands
|
pkg_permission --> pkg_commands
|
||||||
pkg_permission --> pkg_invariants
|
pkg_permission --> pkg_invariants
|
||||||
@@ -1033,6 +1044,8 @@ flowchart TD
|
|||||||
pkg_web_app --> pkg_invariants
|
pkg_web_app --> pkg_invariants
|
||||||
pkg_web_app --> pkg_system_prompt
|
pkg_web_app --> pkg_system_prompt
|
||||||
pkg_client_ui_conversation --> pkg_agent
|
pkg_client_ui_conversation --> pkg_agent
|
||||||
|
pkg_client_ui_conversation --> pkg_attachment
|
||||||
|
pkg_client_ui_conversation --> pkg_brand
|
||||||
pkg_client_ui_conversation --> pkg_client_locale
|
pkg_client_ui_conversation --> pkg_client_locale
|
||||||
pkg_client_ui_conversation --> pkg_client_runtime
|
pkg_client_ui_conversation --> pkg_client_runtime
|
||||||
pkg_client_ui_conversation --> pkg_client_ui_primitives
|
pkg_client_ui_conversation --> pkg_client_ui_primitives
|
||||||
@@ -1253,7 +1266,7 @@ flowchart TD
|
|||||||
| [`type-meta`](../packages/typert/type-meta) | `typert` | [`invariants`](../packages/support/invariants) |
|
| [`type-meta`](../packages/typert/type-meta) | `typert` | [`invariants`](../packages/support/invariants) |
|
||||||
| [`typert-generator`](../packages/typert/generator) | `typert` | [`invariants`](../packages/support/invariants) |
|
| [`typert-generator`](../packages/typert/generator) | `typert` | [`invariants`](../packages/support/invariants) |
|
||||||
| [`typert-registry`](../packages/typert/registry) | `typert` | [`invariants`](../packages/support/invariants) |
|
| [`typert-registry`](../packages/typert/registry) | `typert` | [`invariants`](../packages/support/invariants) |
|
||||||
| [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) |
|
| [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
| [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta), [`typert-registry`](../packages/typert/registry) |
|
| [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta), [`typert-registry`](../packages/typert/registry) |
|
||||||
@@ -1268,20 +1281,28 @@ flowchart TD
|
|||||||
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
|
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
|
||||||
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||||
| [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
|
| [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
|
||||||
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
| [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) |
|
||||||
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
|
||||||
| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`type-meta`](../packages/typert/type-meta) |
|
|
||||||
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
|
||||||
| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
|
||||||
| [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
|
||||||
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
|
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
|
||||||
|
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
|
||||||
| [`client-locale`](../packages/client/locale) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
| [`client-locale`](../packages/client/locale) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
|
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
|
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
|
||||||
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
|
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
|
||||||
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
|
||||||
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
|
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
|
||||||
|
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
||||||
|
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
||||||
|
| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`type-meta`](../packages/typert/type-meta) |
|
||||||
|
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||||
|
| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||||
|
| [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
||||||
|
| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
||||||
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) |
|
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) |
|
||||||
| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/support/invariants), [`skill`](../packages/skill/skill) |
|
| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/support/invariants), [`skill`](../packages/skill/skill) |
|
||||||
| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
|
| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
|
||||||
@@ -1290,13 +1311,10 @@ flowchart TD
|
|||||||
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||||
| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
||||||
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) |
|
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) |
|
||||||
| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) |
|
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
||||||
|
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
|
||||||
| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) |
|
| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) |
|
||||||
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
|
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
|
||||||
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||||
@@ -1310,11 +1328,9 @@ flowchart TD
|
|||||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||||
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) |
|
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) |
|
||||||
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||||
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
||||||
| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
|
| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
|
||||||
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
|
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
||||||
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
|
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
|
||||||
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
|
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
|
||||||
| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
||||||
@@ -1348,7 +1364,6 @@ flowchart TD
|
|||||||
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
||||||
| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
|
| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
|
||||||
| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
||||||
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) |
|
| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) |
|
||||||
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||||
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
|
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
|
||||||
@@ -1404,7 +1419,7 @@ flowchart TD
|
|||||||
| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||||
| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||||
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
|
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
|
||||||
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
|
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
|
||||||
| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||||
| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
|
| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
|
||||||
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||||
|
|||||||
+111
-96
@@ -129,6 +129,10 @@ flowchart TD
|
|||||||
pkg_api_gateway["api-gateway"]
|
pkg_api_gateway["api-gateway"]
|
||||||
pkg_api_remotes["api-remotes"]
|
pkg_api_remotes["api-remotes"]
|
||||||
end
|
end
|
||||||
|
subgraph group_attachment["packages/attachment"]
|
||||||
|
pkg_attachment["attachment"]
|
||||||
|
pkg_attachment_local["attachment-local"]
|
||||||
|
end
|
||||||
subgraph group_boot["packages/boot"]
|
subgraph group_boot["packages/boot"]
|
||||||
pkg_app_boot["app-boot"]
|
pkg_app_boot["app-boot"]
|
||||||
end
|
end
|
||||||
@@ -329,9 +333,8 @@ flowchart TD
|
|||||||
pkg_type_meta --> pkg_invariants
|
pkg_type_meta --> pkg_invariants
|
||||||
pkg_typert_generator --> pkg_invariants
|
pkg_typert_generator --> pkg_invariants
|
||||||
pkg_typert_registry --> pkg_invariants
|
pkg_typert_registry --> pkg_invariants
|
||||||
pkg_llm --> pkg_brand
|
pkg_attachment --> pkg_brand
|
||||||
pkg_llm --> pkg_invariants
|
pkg_attachment --> pkg_invariants
|
||||||
pkg_llm --> pkg_timeout
|
|
||||||
pkg_client_connection --> pkg_host_webserver
|
pkg_client_connection --> pkg_host_webserver
|
||||||
pkg_client_connection --> pkg_invariants
|
pkg_client_connection --> pkg_invariants
|
||||||
pkg_client_hmr --> pkg_client_modules
|
pkg_client_hmr --> pkg_client_modules
|
||||||
@@ -367,34 +370,16 @@ flowchart TD
|
|||||||
pkg_subprocess_local --> pkg_timeout
|
pkg_subprocess_local --> pkg_timeout
|
||||||
pkg_typert_loader --> pkg_invariants
|
pkg_typert_loader --> pkg_invariants
|
||||||
pkg_typert_loader --> pkg_typert_registry
|
pkg_typert_loader --> pkg_typert_registry
|
||||||
pkg_llm_deepseek --> pkg_credentials
|
pkg_llm --> pkg_attachment
|
||||||
pkg_llm_deepseek --> pkg_environment
|
pkg_llm --> pkg_brand
|
||||||
pkg_llm_deepseek --> pkg_invariants
|
pkg_llm --> pkg_invariants
|
||||||
pkg_llm_deepseek --> pkg_llm
|
pkg_llm --> pkg_timeout
|
||||||
pkg_llm_deepseek --> pkg_settings
|
|
||||||
pkg_llm_deepseek --> pkg_timeout
|
|
||||||
pkg_llm_pi_ai --> pkg_credentials
|
|
||||||
pkg_llm_pi_ai --> pkg_environment
|
|
||||||
pkg_llm_pi_ai --> pkg_invariants
|
|
||||||
pkg_llm_pi_ai --> pkg_llm
|
|
||||||
pkg_llm_pi_ai --> pkg_settings
|
|
||||||
pkg_llm_pi_ai --> pkg_timeout
|
|
||||||
pkg_session --> pkg_brand
|
|
||||||
pkg_session --> pkg_invariants
|
|
||||||
pkg_session --> pkg_llm
|
|
||||||
pkg_session --> pkg_scope
|
|
||||||
pkg_session --> pkg_type_meta
|
|
||||||
pkg_system_prompt --> pkg_invariants
|
|
||||||
pkg_system_prompt --> pkg_llm
|
|
||||||
pkg_system_prompt --> pkg_scope
|
|
||||||
pkg_skill --> pkg_invariants
|
|
||||||
pkg_skill --> pkg_llm
|
|
||||||
pkg_skill --> pkg_scope
|
|
||||||
pkg_web --> pkg_invariants
|
|
||||||
pkg_web --> pkg_llm
|
|
||||||
pkg_api_gateway --> pkg_client_connection
|
pkg_api_gateway --> pkg_client_connection
|
||||||
pkg_api_gateway --> pkg_invariants
|
pkg_api_gateway --> pkg_invariants
|
||||||
pkg_api_gateway --> pkg_typert_registry
|
pkg_api_gateway --> pkg_typert_registry
|
||||||
|
pkg_attachment_local --> pkg_attachment
|
||||||
|
pkg_attachment_local --> pkg_invariants
|
||||||
|
pkg_attachment_local --> pkg_paths
|
||||||
pkg_client_locale --> pkg_client_runtime
|
pkg_client_locale --> pkg_client_runtime
|
||||||
pkg_client_locale --> pkg_client_ui_primitives
|
pkg_client_locale --> pkg_client_ui_primitives
|
||||||
pkg_client_locale --> pkg_client_ui_slots
|
pkg_client_locale --> pkg_client_ui_slots
|
||||||
@@ -418,40 +403,36 @@ flowchart TD
|
|||||||
pkg_credentials_local --> pkg_environment
|
pkg_credentials_local --> pkg_environment
|
||||||
pkg_credentials_local --> pkg_invariants
|
pkg_credentials_local --> pkg_invariants
|
||||||
pkg_credentials_local --> pkg_paths
|
pkg_credentials_local --> pkg_paths
|
||||||
pkg_lsp --> pkg_brand
|
|
||||||
pkg_lsp --> pkg_invariants
|
|
||||||
pkg_lsp --> pkg_llm
|
|
||||||
pkg_settings_local --> pkg_atomic_write
|
pkg_settings_local --> pkg_atomic_write
|
||||||
pkg_settings_local --> pkg_invariants
|
pkg_settings_local --> pkg_invariants
|
||||||
pkg_settings_local --> pkg_paths
|
pkg_settings_local --> pkg_paths
|
||||||
pkg_settings_local --> pkg_settings
|
pkg_settings_local --> pkg_settings
|
||||||
pkg_agent --> pkg_invariants
|
pkg_llm_deepseek --> pkg_credentials
|
||||||
pkg_agent --> pkg_llm
|
pkg_llm_deepseek --> pkg_environment
|
||||||
pkg_agent --> pkg_scope
|
pkg_llm_deepseek --> pkg_invariants
|
||||||
pkg_agent --> pkg_session
|
pkg_llm_deepseek --> pkg_llm
|
||||||
pkg_agent --> pkg_system_prompt
|
pkg_llm_deepseek --> pkg_settings
|
||||||
pkg_agent --> pkg_type_meta
|
pkg_llm_deepseek --> pkg_timeout
|
||||||
pkg_skill_badge --> pkg_invariants
|
pkg_llm_pi_ai --> pkg_attachment
|
||||||
pkg_skill_badge --> pkg_skill
|
pkg_llm_pi_ai --> pkg_credentials
|
||||||
pkg_web_fetch_local --> pkg_invariants
|
pkg_llm_pi_ai --> pkg_environment
|
||||||
pkg_web_fetch_local --> pkg_timeout
|
pkg_llm_pi_ai --> pkg_invariants
|
||||||
pkg_web_fetch_local --> pkg_web
|
pkg_llm_pi_ai --> pkg_llm
|
||||||
pkg_web_search_exa --> pkg_environment
|
pkg_llm_pi_ai --> pkg_settings
|
||||||
pkg_web_search_exa --> pkg_invariants
|
pkg_llm_pi_ai --> pkg_timeout
|
||||||
pkg_web_search_exa --> pkg_web
|
pkg_session --> pkg_brand
|
||||||
pkg_web_search_perplexity --> pkg_environment
|
pkg_session --> pkg_invariants
|
||||||
pkg_web_search_perplexity --> pkg_invariants
|
pkg_session --> pkg_llm
|
||||||
pkg_web_search_perplexity --> pkg_web
|
pkg_session --> pkg_scope
|
||||||
pkg_spill --> pkg_brand
|
pkg_session --> pkg_type_meta
|
||||||
pkg_spill --> pkg_invariants
|
pkg_system_prompt --> pkg_invariants
|
||||||
pkg_spill --> pkg_llm
|
pkg_system_prompt --> pkg_llm
|
||||||
pkg_spill --> pkg_session
|
pkg_system_prompt --> pkg_scope
|
||||||
pkg_acp_snapshot --> pkg_invariants
|
pkg_skill --> pkg_invariants
|
||||||
pkg_acp_snapshot --> pkg_session
|
pkg_skill --> pkg_llm
|
||||||
pkg_app_boot --> pkg_environment
|
pkg_skill --> pkg_scope
|
||||||
pkg_app_boot --> pkg_invariants
|
pkg_web --> pkg_invariants
|
||||||
pkg_app_boot --> pkg_paths
|
pkg_web --> pkg_llm
|
||||||
pkg_app_boot --> pkg_system_prompt
|
|
||||||
pkg_client_ui_question --> pkg_client_locale
|
pkg_client_ui_question --> pkg_client_locale
|
||||||
pkg_client_ui_question --> pkg_invariants
|
pkg_client_ui_question --> pkg_invariants
|
||||||
pkg_client_ui_settings_general --> pkg_client_connection
|
pkg_client_ui_settings_general --> pkg_client_connection
|
||||||
@@ -482,10 +463,54 @@ flowchart TD
|
|||||||
pkg_client_ui_workspace --> pkg_client_ui_primitives
|
pkg_client_ui_workspace --> pkg_client_ui_primitives
|
||||||
pkg_client_ui_workspace --> pkg_client_ui_slots
|
pkg_client_ui_workspace --> pkg_client_ui_slots
|
||||||
pkg_client_ui_workspace --> pkg_invariants
|
pkg_client_ui_workspace --> pkg_invariants
|
||||||
|
pkg_lsp --> pkg_brand
|
||||||
|
pkg_lsp --> pkg_invariants
|
||||||
|
pkg_lsp --> pkg_llm
|
||||||
|
pkg_agent --> pkg_invariants
|
||||||
|
pkg_agent --> pkg_llm
|
||||||
|
pkg_agent --> pkg_scope
|
||||||
|
pkg_agent --> pkg_session
|
||||||
|
pkg_agent --> pkg_system_prompt
|
||||||
|
pkg_agent --> pkg_type_meta
|
||||||
|
pkg_skill_badge --> pkg_invariants
|
||||||
|
pkg_skill_badge --> pkg_skill
|
||||||
|
pkg_web_fetch_local --> pkg_invariants
|
||||||
|
pkg_web_fetch_local --> pkg_timeout
|
||||||
|
pkg_web_fetch_local --> pkg_web
|
||||||
|
pkg_web_search_exa --> pkg_environment
|
||||||
|
pkg_web_search_exa --> pkg_invariants
|
||||||
|
pkg_web_search_exa --> pkg_web
|
||||||
|
pkg_web_search_perplexity --> pkg_environment
|
||||||
|
pkg_web_search_perplexity --> pkg_invariants
|
||||||
|
pkg_web_search_perplexity --> pkg_web
|
||||||
|
pkg_spill --> pkg_brand
|
||||||
|
pkg_spill --> pkg_invariants
|
||||||
|
pkg_spill --> pkg_llm
|
||||||
|
pkg_spill --> pkg_session
|
||||||
|
pkg_acp_snapshot --> pkg_invariants
|
||||||
|
pkg_acp_snapshot --> pkg_session
|
||||||
|
pkg_app_boot --> pkg_environment
|
||||||
|
pkg_app_boot --> pkg_invariants
|
||||||
|
pkg_app_boot --> pkg_paths
|
||||||
|
pkg_app_boot --> pkg_system_prompt
|
||||||
|
pkg_client_ui_layout --> pkg_client_runtime
|
||||||
|
pkg_client_ui_layout --> pkg_client_ui_slots
|
||||||
|
pkg_client_ui_layout --> pkg_client_ui_theme
|
||||||
|
pkg_client_ui_layout --> pkg_invariants
|
||||||
pkg_code_runtime_worker --> pkg_code_runtime
|
pkg_code_runtime_worker --> pkg_code_runtime
|
||||||
pkg_code_runtime_worker --> pkg_invariants
|
pkg_code_runtime_worker --> pkg_invariants
|
||||||
pkg_code_runtime_worker --> pkg_session
|
pkg_code_runtime_worker --> pkg_session
|
||||||
pkg_code_runtime_worker --> pkg_timeout
|
pkg_code_runtime_worker --> pkg_timeout
|
||||||
|
pkg_host_directory_picker_browse --> pkg_client_locale
|
||||||
|
pkg_host_directory_picker_browse --> pkg_client_runtime
|
||||||
|
pkg_host_directory_picker_browse --> pkg_client_ui_primitives
|
||||||
|
pkg_host_directory_picker_browse --> pkg_client_ui_slots
|
||||||
|
pkg_host_directory_picker_browse --> pkg_client_ui_workspace
|
||||||
|
pkg_host_directory_picker_browse --> pkg_invariants
|
||||||
|
pkg_host_directory_picker_native --> pkg_client_runtime
|
||||||
|
pkg_host_directory_picker_native --> pkg_client_ui_slots
|
||||||
|
pkg_host_directory_picker_native --> pkg_client_ui_workspace
|
||||||
|
pkg_host_directory_picker_native --> pkg_invariants
|
||||||
pkg_agent_presets --> pkg_atomic_write
|
pkg_agent_presets --> pkg_atomic_write
|
||||||
pkg_agent_presets --> pkg_invariants
|
pkg_agent_presets --> pkg_invariants
|
||||||
pkg_agent_presets --> pkg_paths
|
pkg_agent_presets --> pkg_paths
|
||||||
@@ -540,25 +565,15 @@ flowchart TD
|
|||||||
pkg_loader_smoke --> pkg_invariants
|
pkg_loader_smoke --> pkg_invariants
|
||||||
pkg_loader_smoke --> pkg_llm
|
pkg_loader_smoke --> pkg_llm
|
||||||
pkg_loader_smoke --> pkg_session
|
pkg_loader_smoke --> pkg_session
|
||||||
pkg_client_ui_layout --> pkg_client_runtime
|
|
||||||
pkg_client_ui_layout --> pkg_client_ui_slots
|
|
||||||
pkg_client_ui_layout --> pkg_client_ui_theme
|
|
||||||
pkg_client_ui_layout --> pkg_invariants
|
|
||||||
pkg_time_context --> pkg_agent
|
pkg_time_context --> pkg_agent
|
||||||
pkg_time_context --> pkg_invariants
|
pkg_time_context --> pkg_invariants
|
||||||
pkg_time_context --> pkg_session
|
pkg_time_context --> pkg_session
|
||||||
pkg_host_apiproxy --> pkg_agent_presets
|
pkg_host_apiproxy --> pkg_agent_presets
|
||||||
pkg_host_apiproxy --> pkg_invariants
|
pkg_host_apiproxy --> pkg_invariants
|
||||||
pkg_host_directory_picker_browse --> pkg_client_locale
|
pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
|
||||||
pkg_host_directory_picker_browse --> pkg_client_runtime
|
pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
|
||||||
pkg_host_directory_picker_browse --> pkg_client_ui_primitives
|
pkg_host_directory_picker_auto --> pkg_host_webserver
|
||||||
pkg_host_directory_picker_browse --> pkg_client_ui_slots
|
pkg_host_directory_picker_auto --> pkg_invariants
|
||||||
pkg_host_directory_picker_browse --> pkg_client_ui_workspace
|
|
||||||
pkg_host_directory_picker_browse --> pkg_invariants
|
|
||||||
pkg_host_directory_picker_native --> pkg_client_runtime
|
|
||||||
pkg_host_directory_picker_native --> pkg_client_ui_slots
|
|
||||||
pkg_host_directory_picker_native --> pkg_client_ui_workspace
|
|
||||||
pkg_host_directory_picker_native --> pkg_invariants
|
|
||||||
pkg_commands --> pkg_agent
|
pkg_commands --> pkg_agent
|
||||||
pkg_commands --> pkg_brand
|
pkg_commands --> pkg_brand
|
||||||
pkg_commands --> pkg_invariants
|
pkg_commands --> pkg_invariants
|
||||||
@@ -698,10 +713,6 @@ flowchart TD
|
|||||||
pkg_command_feedback --> pkg_commands
|
pkg_command_feedback --> pkg_commands
|
||||||
pkg_command_feedback --> pkg_invariants
|
pkg_command_feedback --> pkg_invariants
|
||||||
pkg_command_feedback --> pkg_session
|
pkg_command_feedback --> pkg_session
|
||||||
pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
|
|
||||||
pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
|
|
||||||
pkg_host_directory_picker_auto --> pkg_host_webserver
|
|
||||||
pkg_host_directory_picker_auto --> pkg_invariants
|
|
||||||
pkg_permission --> pkg_bash
|
pkg_permission --> pkg_bash
|
||||||
pkg_permission --> pkg_commands
|
pkg_permission --> pkg_commands
|
||||||
pkg_permission --> pkg_invariants
|
pkg_permission --> pkg_invariants
|
||||||
@@ -1035,6 +1046,8 @@ flowchart TD
|
|||||||
pkg_web_app --> pkg_invariants
|
pkg_web_app --> pkg_invariants
|
||||||
pkg_web_app --> pkg_system_prompt
|
pkg_web_app --> pkg_system_prompt
|
||||||
pkg_client_ui_conversation --> pkg_agent
|
pkg_client_ui_conversation --> pkg_agent
|
||||||
|
pkg_client_ui_conversation --> pkg_attachment
|
||||||
|
pkg_client_ui_conversation --> pkg_brand
|
||||||
pkg_client_ui_conversation --> pkg_client_locale
|
pkg_client_ui_conversation --> pkg_client_locale
|
||||||
pkg_client_ui_conversation --> pkg_client_runtime
|
pkg_client_ui_conversation --> pkg_client_runtime
|
||||||
pkg_client_ui_conversation --> pkg_client_ui_primitives
|
pkg_client_ui_conversation --> pkg_client_ui_primitives
|
||||||
@@ -1225,7 +1238,7 @@ flowchart TD
|
|||||||
pkg_acp_demo --> pkg_workspace_context
|
pkg_acp_demo --> pkg_workspace_context
|
||||||
```
|
```
|
||||||
|
|
||||||
| Package | Group | Depends on |
|
| 包 | 分组 | 依赖项 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [`invariants`](../packages/support/invariants) | `support` | — |
|
| [`invariants`](../packages/support/invariants) | `support` | — |
|
||||||
| [`atomic-write`](../packages/util/atomic-write) | `util` | [`invariants`](../packages/support/invariants) |
|
| [`atomic-write`](../packages/util/atomic-write) | `util` | [`invariants`](../packages/support/invariants) |
|
||||||
@@ -1255,7 +1268,7 @@ flowchart TD
|
|||||||
| [`type-meta`](../packages/typert/type-meta) | `typert` | [`invariants`](../packages/support/invariants) |
|
| [`type-meta`](../packages/typert/type-meta) | `typert` | [`invariants`](../packages/support/invariants) |
|
||||||
| [`typert-generator`](../packages/typert/generator) | `typert` | [`invariants`](../packages/support/invariants) |
|
| [`typert-generator`](../packages/typert/generator) | `typert` | [`invariants`](../packages/support/invariants) |
|
||||||
| [`typert-registry`](../packages/typert/registry) | `typert` | [`invariants`](../packages/support/invariants) |
|
| [`typert-registry`](../packages/typert/registry) | `typert` | [`invariants`](../packages/support/invariants) |
|
||||||
| [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) |
|
| [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
| [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta), [`typert-registry`](../packages/typert/registry) |
|
| [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta), [`typert-registry`](../packages/typert/registry) |
|
||||||
@@ -1270,20 +1283,28 @@ flowchart TD
|
|||||||
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
|
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
|
||||||
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||||
| [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
|
| [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
|
||||||
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
| [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) |
|
||||||
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
|
||||||
| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`type-meta`](../packages/typert/type-meta) |
|
|
||||||
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
|
||||||
| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
|
||||||
| [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
|
||||||
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
|
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
|
||||||
|
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
|
||||||
| [`client-locale`](../packages/client/locale) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
| [`client-locale`](../packages/client/locale) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
|
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
|
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
|
||||||
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
|
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
|
||||||
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
|
||||||
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
|
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
|
||||||
|
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
||||||
|
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
||||||
|
| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`type-meta`](../packages/typert/type-meta) |
|
||||||
|
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||||
|
| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||||
|
| [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
||||||
|
| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
||||||
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) |
|
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) |
|
||||||
| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/support/invariants), [`skill`](../packages/skill/skill) |
|
| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/support/invariants), [`skill`](../packages/skill/skill) |
|
||||||
| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
|
| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
|
||||||
@@ -1292,13 +1313,10 @@ flowchart TD
|
|||||||
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||||
| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
||||||
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) |
|
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) |
|
||||||
| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) |
|
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) |
|
||||||
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
||||||
|
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
|
||||||
|
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
|
||||||
| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) |
|
| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) |
|
||||||
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
|
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
|
||||||
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||||
@@ -1312,11 +1330,9 @@ flowchart TD
|
|||||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||||
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) |
|
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) |
|
||||||
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||||
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
||||||
| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
|
| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
|
||||||
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
|
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
||||||
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
|
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
|
||||||
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
|
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
|
||||||
| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
|
||||||
@@ -1350,7 +1366,6 @@ flowchart TD
|
|||||||
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
||||||
| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
|
| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
|
||||||
| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
|
||||||
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
|
|
||||||
| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) |
|
| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) |
|
||||||
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||||
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
|
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
|
||||||
@@ -1406,7 +1421,7 @@ flowchart TD
|
|||||||
| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||||
| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||||
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
|
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
|
||||||
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
|
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
|
||||||
| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||||
| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
|
| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
|
||||||
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/subsystems/README.md
|
# pnpm run verify-translation-pairing --write docs/subsystems/README.md
|
||||||
README.md: 096fd7de4d2644dac664fac940b6487052115258
|
README.md: b1b57466feeee7625b0b5de78e5f9ff220ddef32
|
||||||
README.zh.md: 6b6758db8f7118a09f6b0998231d3944f44f5052
|
README.zh.md: 492051f013def0f196902d1105f84a5644057f3a
|
||||||
@@ -24,6 +24,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
|
|||||||
| [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, and the guarded execution pipeline |
|
| [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, and the guarded execution pipeline |
|
||||||
| [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy |
|
| [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy |
|
||||||
| [approval.md](approval.md) | the one-shot user-approval seam: `ApprovalRequest`, `ApprovalOutcome`, per-session policy, audit and answerer contracts |
|
| [approval.md](approval.md) | the one-shot user-approval seam: `ApprovalRequest`, `ApprovalOutcome`, per-session policy, audit and answerer contracts |
|
||||||
|
| [attachment.md](attachment.md) | durable image identity and metadata, validation inputs, verified reads, and the `AttachmentStore` seam |
|
||||||
| [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashProcess` handles |
|
| [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashProcess` handles |
|
||||||
| [subprocess.md](subprocess.md) | the subprocess seam: fully-explicit `SubprocessSpawnSpec`, offset-based output readers, unclassified `SubprocessOutcome`, and the managed `DSH_*` environment vocabulary |
|
| [subprocess.md](subprocess.md) | the subprocess seam: fully-explicit `SubprocessSpawnSpec`, offset-based output readers, unclassified `SubprocessOutcome`, and the managed `DSH_*` environment vocabulary |
|
||||||
| [pty.md](pty.md) | persistent terminal ids, backend/session contracts, send readiness, bounded reads, and owner-visible snapshots |
|
| [pty.md](pty.md) | persistent terminal ids, backend/session contracts, send readiness, bounded reads, and owner-visible snapshots |
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
| [tools.md](tools.md) | `ToolDefinition` 完整字段、schema DSL、`ToolExecution`/`ToolResult`、工具展示 UI 类型,以及受保护的执行流水线 |
|
| [tools.md](tools.md) | `ToolDefinition` 完整字段、schema DSL、`ToolExecution`/`ToolResult`、工具展示 UI 类型,以及受保护的执行流水线 |
|
||||||
| [user-interaction.md](user-interaction.md) | UI 支持的人工问答 seam:`AskUserQuestionRequest`、answer/options 词汇、提供方 API、错误分类体系 |
|
| [user-interaction.md](user-interaction.md) | UI 支持的人工问答 seam:`AskUserQuestionRequest`、answer/options 词汇、提供方 API、错误分类体系 |
|
||||||
| [approval.md](approval.md) | 一次性用户审批 seam:`ApprovalRequest`、`ApprovalOutcome`、逐会话策略、审计与 answerer 约定 |
|
| [approval.md](approval.md) | 一次性用户审批 seam:`ApprovalRequest`、`ApprovalOutcome`、逐会话策略、审计与 answerer 约定 |
|
||||||
|
| [attachment.md](attachment.md) | 持久图片标识与元数据、校验输入、经校验读取,以及 `AttachmentStore` seam |
|
||||||
| [bash.md](bash.md) | bash 执行器 seam:`BashExecRequest`/`Spec`、`BashRunResult`、后台 `BashProcess` 句柄 |
|
| [bash.md](bash.md) | bash 执行器 seam:`BashExecRequest`/`Spec`、`BashRunResult`、后台 `BashProcess` 句柄 |
|
||||||
| [subprocess.md](subprocess.md) | 子进程 seam:完全显式的 `SubprocessSpawnSpec`、基于偏移的输出读取器、不含分类的 `SubprocessOutcome`,以及受管 `DSH_*` 环境词汇 |
|
| [subprocess.md](subprocess.md) | 子进程 seam:完全显式的 `SubprocessSpawnSpec`、基于偏移的输出读取器、不含分类的 `SubprocessOutcome`,以及受管 `DSH_*` 环境词汇 |
|
||||||
| [pty.md](pty.md) | 持久化终端 ID、后端/会话约定、发送就绪状态、有界读取与 owner 可见快照 |
|
| [pty.md](pty.md) | 持久化终端 ID、后端/会话约定、发送就绪状态、有界读取与 owner 可见快照 |
|
||||||
|
|||||||
@@ -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/subsystems/attachment.md
|
||||||
|
attachment.md: 51556c3a0391a571656f407ed22edb8b93eb5326
|
||||||
|
attachment.zh.md: 2a6079845ae6f19884b9fb3312e32187161d57d1
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Durable Image Attachments
|
||||||
|
|
||||||
|
English | [中文](attachment.zh.md)
|
||||||
|
|
||||||
|
The attachment seam separates binary image ownership from the session log. A producer gives validated encoded bytes to [`ctx.attachments`](#ctxattachments--attachmentstore-abstract-seam); the service publishes an immutable content-addressed reference only after the object is durable. Session events and model-visible `ImageBlock`s contain that reference and metadata, never a browser object URL, host temporary path, provider URL, or base64 payload.
|
||||||
|
|
||||||
|
Unsent browser drafts may stay in memory and native clients may stage them in operating-system temporary storage. Once the host accepts a user message, its images move below `<DSH_HOME>/attachments/v1` before the user event is appended. Structured model image output follows the same persist-before-event rule.
|
||||||
|
|
||||||
|
Source: [`packages/attachment/attachment/src/types.ts`](../../packages/attachment/attachment/src/types.ts)
|
||||||
|
|
||||||
|
## Identity and verified metadata
|
||||||
|
|
||||||
|
`AttachmentId` is a branded opaque string. The local backend currently emits `sha256:<digest>`, but consumers must neither parse that representation nor derive a filesystem path from it.
|
||||||
|
|
||||||
|
```ts type-equiv
|
||||||
|
/** Raster image formats accepted by the version-one attachment path. */
|
||||||
|
type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts type-equiv
|
||||||
|
/** Durable, serializable metadata for one immutable image object. */
|
||||||
|
interface ImageAttachmentRef {
|
||||||
|
/** Opaque storage identifier; never a filesystem path or bearer URL. */
|
||||||
|
attachmentId: AttachmentId
|
||||||
|
/** Media type verified from the stored bytes. */
|
||||||
|
mediaType: ImageMediaType
|
||||||
|
/** Exact encoded byte length. */
|
||||||
|
bytes: number
|
||||||
|
/** Intrinsic encoded width in pixels. */
|
||||||
|
width: number
|
||||||
|
/** Intrinsic encoded height in pixels. */
|
||||||
|
height: number
|
||||||
|
/** Optional display name stripped of local path information. */
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts type-equiv
|
||||||
|
/** Deployment-resolved limits used by upload admission and request buffering. */
|
||||||
|
interface ImageAttachmentLimits {
|
||||||
|
maxImageBytes: number
|
||||||
|
maxImagesPerMessage: number
|
||||||
|
maxMessageImageBytes: number
|
||||||
|
maxImagePixels: number
|
||||||
|
mediaTypes: readonly ImageMediaType[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The reference records intrinsic dimensions and encoded length so clients can lay out history without decoding first, while every authoritative read still re-checks digest, media signature, dimensions, and metadata against the object.
|
||||||
|
|
||||||
|
## Commit and verified-read payloads
|
||||||
|
|
||||||
|
```ts type-equiv
|
||||||
|
/** Request to validate and durably commit one image. */
|
||||||
|
interface SaveImageAttachment {
|
||||||
|
data: Uint8Array
|
||||||
|
/** Caller-declared media type, checked against fully decoded bytes. */
|
||||||
|
mediaType: ImageMediaType
|
||||||
|
/** Optional browser/provider display name; it is never interpreted as a path. */
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts type-equiv
|
||||||
|
/** Stored image bytes returned after reference and digest verification. */
|
||||||
|
interface StoredImageAttachment {
|
||||||
|
ref: ImageAttachmentRef
|
||||||
|
data: Uint8Array
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`saveImage()` validates bytes and atomically commits one object before returning its reference. `validateImage()` runs the same admission checks without persisting anything; batch callers validate every member through it before saving any member, so validation rejection leaves no partial objects behind. `readImage()` accepts a reference from an authorized session path and returns bytes only after integrity verification. The service is deliberately retention-neutral: resumed and forked sessions may share objects, so reference-aware garbage collection is deferred rather than tied to any one session's deletion.
|
||||||
|
|
||||||
|
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||||
|
|
||||||
|
<a id="cordis-surface"></a>
|
||||||
|
|
||||||
|
## Cordis surface
|
||||||
|
|
||||||
|
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||||
|
|
||||||
|
<a id="ctxattachments--attachmentstore-abstract-seam"></a>
|
||||||
|
|
||||||
|
### `ctx.attachments` — `AttachmentStore` (abstract seam)
|
||||||
|
|
||||||
|
Immutable binary attachment service. Implementations validate bytes before publishing a reference.
|
||||||
|
|
||||||
|
```ts cordis-catalog
|
||||||
|
/**
|
||||||
|
* Validate one image without persisting it.
|
||||||
|
* Batch callers validate every member before saving any member.
|
||||||
|
* @param input - encoded bytes, declared media type, and optional display name.
|
||||||
|
* @returns completion after the encoded raster has been fully decoded.
|
||||||
|
*/
|
||||||
|
abstract validateImage(input: SaveImageAttachment): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and durably commit one image before its owning session event is appended.
|
||||||
|
* @param input - encoded bytes, declared media type, and optional display name.
|
||||||
|
* @returns a durable content-addressed reference.
|
||||||
|
*/
|
||||||
|
abstract saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read one image and verify that bytes still match the recorded reference.
|
||||||
|
* @param ref - durable reference from the session log.
|
||||||
|
* @returns the verified bytes and canonical reference.
|
||||||
|
*/
|
||||||
|
abstract readImage(ref: ImageAttachmentRef): Promise<StoredImageAttachment>
|
||||||
|
```
|
||||||
|
|
||||||
|
Source: [`packages/attachment/attachment/src/index.ts:29`](../../packages/attachment/attachment/src/index.ts)
|
||||||
|
<!-- END GENERATED cordis-surface -->
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# 持久图片附件
|
||||||
|
|
||||||
|
[English](attachment.md) | 中文
|
||||||
|
|
||||||
|
附件 seam 将二进制图片的所有权与会话日志分离。生成方把经过校验的编码字节交给 [`ctx.attachments`](#ctxattachments--attachmentstore-abstract-seam);只有对象完成持久化后,该服务才会发布不可变的内容寻址引用。会话事件和模型可见的 `ImageBlock` 包含该引用及其元数据,绝不包含浏览器对象 URL、宿主临时路径、提供方 URL 或 base64 数据。
|
||||||
|
|
||||||
|
未发送的浏览器草稿可以保留在内存中,原生客户端也可以将其暂存于操作系统临时存储。宿主接受用户消息后,会先把消息中的图片移到 `<DSH_HOME>/attachments/v1` 下,再追加用户事件。结构化模型图片输出遵循同样的先持久化、后追加事件规则。
|
||||||
|
|
||||||
|
来源:[`packages/attachment/attachment/src/types.ts`](../../packages/attachment/attachment/src/types.ts)
|
||||||
|
|
||||||
|
## 标识与经过校验的元数据
|
||||||
|
|
||||||
|
`AttachmentId` 是带类型标记的不透明字符串。本地后端目前生成 `sha256:<digest>`,但消费方既不能解析这种表示,也不能据此派生文件系统路径。
|
||||||
|
|
||||||
|
```ts type-equiv
|
||||||
|
/** Raster image formats accepted by the version-one attachment path. */
|
||||||
|
type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts type-equiv
|
||||||
|
/** Durable, serializable metadata for one immutable image object. */
|
||||||
|
interface ImageAttachmentRef {
|
||||||
|
/** Opaque storage identifier; never a filesystem path or bearer URL. */
|
||||||
|
attachmentId: AttachmentId
|
||||||
|
/** Media type verified from the stored bytes. */
|
||||||
|
mediaType: ImageMediaType
|
||||||
|
/** Exact encoded byte length. */
|
||||||
|
bytes: number
|
||||||
|
/** Intrinsic encoded width in pixels. */
|
||||||
|
width: number
|
||||||
|
/** Intrinsic encoded height in pixels. */
|
||||||
|
height: number
|
||||||
|
/** Optional display name stripped of local path information. */
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts type-equiv
|
||||||
|
/** Deployment-resolved limits used by upload admission and request buffering. */
|
||||||
|
interface ImageAttachmentLimits {
|
||||||
|
maxImageBytes: number
|
||||||
|
maxImagesPerMessage: number
|
||||||
|
maxMessageImageBytes: number
|
||||||
|
maxImagePixels: number
|
||||||
|
mediaTypes: readonly ImageMediaType[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
引用记录固有尺寸和编码长度,使客户端无需先解码即可排布历史记录;每次权威读取仍会根据对象重新校验摘要、媒体签名、尺寸和元数据。
|
||||||
|
|
||||||
|
## 提交与校验读取的数据
|
||||||
|
|
||||||
|
```ts type-equiv
|
||||||
|
/** Request to validate and durably commit one image. */
|
||||||
|
interface SaveImageAttachment {
|
||||||
|
data: Uint8Array
|
||||||
|
/** Caller-declared media type, checked against fully decoded bytes. */
|
||||||
|
mediaType: ImageMediaType
|
||||||
|
/** Optional browser/provider display name; it is never interpreted as a path. */
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts type-equiv
|
||||||
|
/** Stored image bytes returned after reference and digest verification. */
|
||||||
|
interface StoredImageAttachment {
|
||||||
|
ref: ImageAttachmentRef
|
||||||
|
data: Uint8Array
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`saveImage()` 校验字节并以原子方式提交一个对象,之后才返回其引用。`validateImage()` 执行相同的准入检查,但不持久化任何内容;批量调用方会在保存任何成员前通过它校验所有成员,因此校验拒绝不会留下部分对象。`readImage()` 接受来自已授权会话路径的引用,只在完整性校验通过后返回字节。该服务刻意不规定保留策略:恢复和 fork 后的会话可能共享对象,因此基于引用的垃圾回收会延期实现,而不是与任何一个会话的删除绑定。
|
||||||
|
|
||||||
|
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||||
|
|
||||||
|
<a id="cordis-surface"></a>
|
||||||
|
|
||||||
|
## Cordis surface
|
||||||
|
|
||||||
|
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||||
|
|
||||||
|
<a id="ctxattachments--attachmentstore-abstract-seam"></a>
|
||||||
|
|
||||||
|
### `ctx.attachments` — `AttachmentStore` (abstract seam)
|
||||||
|
|
||||||
|
Immutable binary attachment service. Implementations validate bytes before publishing a reference.
|
||||||
|
|
||||||
|
```ts cordis-catalog
|
||||||
|
/**
|
||||||
|
* Validate one image without persisting it.
|
||||||
|
* Batch callers validate every member before saving any member.
|
||||||
|
* @param input - encoded bytes, declared media type, and optional display name.
|
||||||
|
* @returns completion after the encoded raster has been fully decoded.
|
||||||
|
*/
|
||||||
|
abstract validateImage(input: SaveImageAttachment): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and durably commit one image before its owning session event is appended.
|
||||||
|
* @param input - encoded bytes, declared media type, and optional display name.
|
||||||
|
* @returns a durable content-addressed reference.
|
||||||
|
*/
|
||||||
|
abstract saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read one image and verify that bytes still match the recorded reference.
|
||||||
|
* @param ref - durable reference from the session log.
|
||||||
|
* @returns the verified bytes and canonical reference.
|
||||||
|
*/
|
||||||
|
abstract readImage(ref: ImageAttachmentRef): Promise<StoredImageAttachment>
|
||||||
|
```
|
||||||
|
|
||||||
|
Source: [`packages/attachment/attachment/src/index.ts:29`](../../packages/attachment/attachment/src/index.ts)
|
||||||
|
<!-- END GENERATED cordis-surface -->
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md
|
# pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md
|
||||||
llm-streaming.md: 4d450c19ec2bbfacabcefc83466e67c8a6c82bd6
|
llm-streaming.md: 9f92052d411e3bd4256db63df54eba7f4e313b18
|
||||||
llm-streaming.zh.md: 777c44eaff2b1ee6e5939e04e580d6ad34b1ad1a
|
llm-streaming.zh.md: ab5540c9e7adce70f0462fb478e8f674d1f6bba4
|
||||||
@@ -22,12 +22,13 @@ Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)
|
|||||||
interface ContentBlockMap {
|
interface ContentBlockMap {
|
||||||
'text': TextBlock
|
'text': TextBlock
|
||||||
'reasoning': ReasoningBlock
|
'reasoning': ReasoningBlock
|
||||||
|
'image': ImageBlock
|
||||||
'tool-call': ToolCallBlock
|
'tool-call': ToolCallBlock
|
||||||
'tool-result': ToolResultBlock
|
'tool-result': ToolResultBlock
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The block interfaces (full fields in source): `TextBlock` (`text`), `ReasoningBlock` (thinking, distinct from visible text), `ToolCallBlock` (`id: CallId`, `name`, raw-JSON `arguments`), `ToolResultBlock` (`toolCallId`, nested `content: ContentBlock[]`, `isError?`). `ContentBlock = ContentBlockMap[ContentBlockType]`. The core set is limited to blocks every shipping path honors — multimodal content (images, audio, …) has no core block type; a feature that needs one adds it via the merge-extensible map together with the adapter/UI/compaction support that honors it.
|
The block interfaces (full fields in source): `TextBlock` (`text`), `ReasoningBlock` (thinking, distinct from visible text), `ImageBlock` (a durable [image attachment](attachment.md)), `ToolCallBlock` (`id: CallId`, `name`, raw-JSON `arguments`), and `ToolResultBlock` (`toolCallId`, nested `content: ContentBlock[]`, `isError?`). `ContentBlock = ContentBlockMap[ContentBlockType]`. A new modality belongs in the merge-extensible map only when its adapter, UI, compaction, and durable replay paths honor it.
|
||||||
|
|
||||||
Source: [`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts)
|
Source: [`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts)
|
||||||
|
|
||||||
@@ -396,6 +397,8 @@ interface LlmModelInfo {
|
|||||||
name: string
|
name: string
|
||||||
/** Optional user-facing distinction from otherwise similar models. */
|
/** Optional user-facing distinction from otherwise similar models. */
|
||||||
description?: string
|
description?: string
|
||||||
|
/** Accepted request modalities; absent means unknown, while an explicit omission is negative capability. */
|
||||||
|
inputModalities?: readonly ModelModality[]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -830,7 +833,7 @@ async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<Prepared
|
|||||||
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
||||||
```
|
```
|
||||||
|
|
||||||
Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts)
|
Source: [`packages/llm/llm/src/index.ts:294`](../../packages/llm/llm/src/index.ts)
|
||||||
|
|
||||||
<a id="llm-events"></a>
|
<a id="llm-events"></a>
|
||||||
|
|
||||||
@@ -855,7 +858,7 @@ The provider topology changed: an adapter registered or unregistered routes, or
|
|||||||
'llm/adapters-updated'(): void
|
'llm/adapters-updated'(): void
|
||||||
```
|
```
|
||||||
|
|
||||||
Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts)
|
Source: [`packages/llm/llm/src/index.ts:75`](../../packages/llm/llm/src/index.ts)
|
||||||
|
|
||||||
<a id="llmstream--waterfall"></a>
|
<a id="llmstream--waterfall"></a>
|
||||||
|
|
||||||
@@ -879,5 +882,5 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
|
|||||||
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
|
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
|
||||||
```
|
```
|
||||||
|
|
||||||
Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts)
|
Source: [`packages/llm/llm/src/index.ts:64`](../../packages/llm/llm/src/index.ts)
|
||||||
<!-- END GENERATED cordis-surface -->
|
<!-- END GENERATED cordis-surface -->
|
||||||
@@ -22,12 +22,13 @@
|
|||||||
interface ContentBlockMap {
|
interface ContentBlockMap {
|
||||||
'text': TextBlock
|
'text': TextBlock
|
||||||
'reasoning': ReasoningBlock
|
'reasoning': ReasoningBlock
|
||||||
|
'image': ImageBlock
|
||||||
'tool-call': ToolCallBlock
|
'tool-call': ToolCallBlock
|
||||||
'tool-result': ToolResultBlock
|
'tool-result': ToolResultBlock
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
各块接口(完整字段见源码):`TextBlock`(`text`)、`ReasoningBlock`(thinking,区别于可见文本)、`ToolCallBlock`(`id: CallId`、`name`、原始 JSON `arguments`)、`ToolResultBlock`(`toolCallId`、嵌套 `content: ContentBlock[]`、`isError?`)。`ContentBlock = ContentBlockMap[ContentBlockType]`。核心集仅限于每条交付路径都尊重的块——多模态内容(图像、音频等)没有核心块类型;需要的功能通过可合并扩展的 map 添加,同时提供适配器/UI/压缩支持。
|
各块接口(完整字段见源码):`TextBlock`(`text`)、`ReasoningBlock`(thinking,区别于可见文本)、`ImageBlock`(一个持久的[图片附件](attachment.md))、`ToolCallBlock`(`id: CallId`、`name`、原始 JSON `arguments`),以及 `ToolResultBlock`(`toolCallId`、嵌套 `content: ContentBlock[]`、`isError?`)。`ContentBlock = ContentBlockMap[ContentBlockType]`。仅当适配器、UI、压缩和持久回放路径均支持某种新模态时,才将其纳入可合并扩展的 map。
|
||||||
|
|
||||||
源码:[`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts)
|
源码:[`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts)
|
||||||
|
|
||||||
@@ -404,6 +405,8 @@ interface LlmModelInfo {
|
|||||||
name: string
|
name: string
|
||||||
/** Optional user-facing distinction from otherwise similar models. */
|
/** Optional user-facing distinction from otherwise similar models. */
|
||||||
description?: string
|
description?: string
|
||||||
|
/** Accepted request modalities; absent means unknown, while an explicit omission is negative capability. */
|
||||||
|
inputModalities?: readonly ModelModality[]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -838,7 +841,7 @@ async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<Prepared
|
|||||||
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
||||||
```
|
```
|
||||||
|
|
||||||
Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts)
|
Source: [`packages/llm/llm/src/index.ts:294`](../../packages/llm/llm/src/index.ts)
|
||||||
|
|
||||||
<a id="llm-events"></a>
|
<a id="llm-events"></a>
|
||||||
|
|
||||||
@@ -863,7 +866,7 @@ The provider topology changed: an adapter registered or unregistered routes, or
|
|||||||
'llm/adapters-updated'(): void
|
'llm/adapters-updated'(): void
|
||||||
```
|
```
|
||||||
|
|
||||||
Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts)
|
Source: [`packages/llm/llm/src/index.ts:75`](../../packages/llm/llm/src/index.ts)
|
||||||
|
|
||||||
<a id="llmstream--waterfall"></a>
|
<a id="llmstream--waterfall"></a>
|
||||||
|
|
||||||
@@ -887,5 +890,5 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
|
|||||||
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
|
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
|
||||||
```
|
```
|
||||||
|
|
||||||
Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts)
|
Source: [`packages/llm/llm/src/index.ts:64`](../../packages/llm/llm/src/index.ts)
|
||||||
<!-- END GENERATED cordis-surface -->
|
<!-- END GENERATED cordis-surface -->
|
||||||
File diff suppressed because one or more lines are too long
@@ -311,6 +311,11 @@
|
|||||||
"src/**/*.ts"
|
"src/**/*.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"packages/attachment/attachment": {
|
||||||
|
"project": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
"packages/util/timeout": {
|
"packages/util/timeout": {
|
||||||
"entry": [
|
"entry": [
|
||||||
"tests/**/*.spec.ts"
|
"tests/**/*.spec.ts"
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/README.md
|
# pnpm run verify-translation-pairing --write packages/README.md
|
||||||
README.md: c18a46b7131f7782be68f3c96fa99b89615de471
|
README.md: c246534a26cd7297f2ba8885099c8b517a5dc2b0
|
||||||
README.zh.md: 3cd766ed70b7847bef8229a48b65873540365851
|
README.zh.md: 4139874d3ebbf82fad8680a3721a1cf35a553706
|
||||||
+3
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
English | [中文](README.zh.md)
|
English | [中文](README.zh.md)
|
||||||
|
|
||||||
Packages use the `@deepseek-ai/dsh-*` scope. Cordis `Service` subclasses and function plugins contribute through `ctx.effect()`, `ctx.on()`, or `ctx.waterfall()`. Authoring rules: [package](AGENTS.md) and [root](../AGENTS.md#conventions).
|
npm scope: `@deepseek-ai/dsh-*`; Cordis `Service` subclasses and function plugins contribute through `ctx.effect()`, `ctx.on()`, or `ctx.waterfall()`. Rules: [package](AGENTS.md), [root](../AGENTS.md#conventions).
|
||||||
|
|
||||||
## Hierarchy
|
## Hierarchy
|
||||||
|
|
||||||
@@ -31,13 +31,14 @@ Groups hold `packages/<group>/<pkg>/`; names stay `@deepseek-ai/dsh-<pkg>`. **Gr
|
|||||||
| [`tasks/`](tasks/README.md) | Generic background-task runtime and model-facing `task_*` control tools | Product — stable surface |
|
| [`tasks/`](tasks/README.md) | Generic background-task runtime and model-facing `task_*` control tools | Product — stable surface |
|
||||||
| [`workflow/`](workflow/README.md) | Workflow seam, worker-thread engine, and model-facing `workflow`/`ralph` tools | Product — stable surface |
|
| [`workflow/`](workflow/README.md) | Workflow seam, worker-thread engine, and model-facing `workflow`/`ralph` tools | Product — stable surface |
|
||||||
| [`web/`](web/README.md) | Web capability family: seam, search/fetch provider impls, and the model-facing web tools | Product — stable surface |
|
| [`web/`](web/README.md) | Web capability family: seam, search/fetch provider impls, and the model-facing web tools | Product — stable surface |
|
||||||
|
| [`attachment/`](attachment/README.md) | Durable attachment identity, validation, local content-addressed storage | Product — stable surface |
|
||||||
| [`spill/`](spill/README.md) | Spill capability family: storage seam, local impl, tool-result spill policy | Product — stable surface |
|
| [`spill/`](spill/README.md) | Spill capability family: storage seam, local impl, tool-result spill policy | Product — stable surface |
|
||||||
| [`todo/`](todo/README.md) | The model-facing `todo_write` tool | Product — stable surface |
|
| [`todo/`](todo/README.md) | The model-facing `todo_write` tool | Product — stable surface |
|
||||||
| [`plan/`](plan/README.md) | Plan collaboration state with a direct entry command and reviewed exit | Product — stable surface |
|
| [`plan/`](plan/README.md) | Plan collaboration state with a direct entry command and reviewed exit | Product — stable surface |
|
||||||
| [`preset/`](preset/README.md) | Per-session agent composition from preset `cordis.yml` files | Product — stable surface |
|
| [`preset/`](preset/README.md) | Per-session agent composition from preset `cordis.yml` files | Product — stable surface |
|
||||||
| [`guard/`](guard/README.md) | Loop-hygiene guards: advisory repeat-call reminders + the `tools/execute` deadline enforcer | Product — stable surface |
|
| [`guard/`](guard/README.md) | Loop-hygiene guards: advisory repeat-call reminders + the `tools/execute` deadline enforcer | Product — stable surface |
|
||||||
| [`bundle/`](bundle/README.md) | Installable `dsh --profile` patch layers | Product — stable surface |
|
| [`bundle/`](bundle/README.md) | Installable `dsh --profile` patch layers | Product — stable surface |
|
||||||
| [`self-modification/`](self-modification/README.md) | The agent modifies its own runtime: inspect the live runtime's plugins and services, mount/unmount model-written plugins ([design](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)) and restricted repository Plugin loading | Product — stable surface |
|
| [`self-modification/`](self-modification/README.md) | Agent runtime self-modification: live plugin/service inspection, model-written plugin mount/unmount ([design](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)), restricted repository Plugin loading | Product — stable surface |
|
||||||
| [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable surface |
|
| [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable surface |
|
||||||
| [`session/`](session/README.md) | Durable session data plane: persistence seam + JSONL/SQLite backends, projection seam, log-backed titles, session reporting | Product — stable surface |
|
| [`session/`](session/README.md) | Durable session data plane: persistence seam + JSONL/SQLite backends, projection seam, log-backed titles, session reporting | Product — stable surface |
|
||||||
| [`session-query/`](session-query/README.md) | Session retrieval family: logical corpus, bounded reads, lineage, event relationships, semantic filtering, and SQLite full-text search | Product — stable surface |
|
| [`session-query/`](session-query/README.md) | Session retrieval family: logical corpus, bounded reads, lineage, event relationships, semantic filtering, and SQLite full-text search | Product — stable surface |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[English](README.md) | 中文
|
[English](README.md) | 中文
|
||||||
|
|
||||||
所有包都使用 `@deepseek-ai/dsh-*` scope。Cordis `Service` 子类和函数插件的贡献通过 `ctx.effect()`、`ctx.on()` 或 `ctx.waterfall()` 注册。编写规则见[包](AGENTS.md)与[根规则](../AGENTS.md#conventions)。
|
npm scope 为 `@deepseek-ai/dsh-*`;Cordis `Service` 子类和函数插件通过 `ctx.effect()`、`ctx.on()` 或 `ctx.waterfall()` 注册。规则见[包](AGENTS.md)与[根规则](../AGENTS.md#conventions)。
|
||||||
|
|
||||||
## 层级结构
|
## 层级结构
|
||||||
|
|
||||||
@@ -31,13 +31,14 @@
|
|||||||
| [`tasks/`](tasks/README.md) | 通用后台任务运行时和面向模型的 `task_*` 控制工具 | 产品:稳定接口 |
|
| [`tasks/`](tasks/README.md) | 通用后台任务运行时和面向模型的 `task_*` 控制工具 | 产品:稳定接口 |
|
||||||
| [`workflow/`](workflow/README.md) | 工作流 seam、worker 线程引擎和面向模型的 `workflow`/`ralph` 工具 | 产品:稳定接口 |
|
| [`workflow/`](workflow/README.md) | 工作流 seam、worker 线程引擎和面向模型的 `workflow`/`ralph` 工具 | 产品:稳定接口 |
|
||||||
| [`web/`](web/README.md) | Web 能力系列:seam、搜索/获取提供方实现和面向模型的 Web 工具 | 产品:稳定接口 |
|
| [`web/`](web/README.md) | Web 能力系列:seam、搜索/获取提供方实现和面向模型的 Web 工具 | 产品:稳定接口 |
|
||||||
|
| [`attachment/`](attachment/README.md) | 持久附件标识、校验、本地内容寻址存储 | 产品:稳定接口 |
|
||||||
| [`spill/`](spill/README.md) | spill 能力系列:存储 seam、本地实现、工具结果 spill 策略 | 产品:稳定接口 |
|
| [`spill/`](spill/README.md) | spill 能力系列:存储 seam、本地实现、工具结果 spill 策略 | 产品:稳定接口 |
|
||||||
| [`todo/`](todo/README.md) | 面向模型的 `todo_write` 工具 | 产品:稳定接口 |
|
| [`todo/`](todo/README.md) | 面向模型的 `todo_write` 工具 | 产品:稳定接口 |
|
||||||
| [`plan/`](plan/README.md) | Plan 协作状态,提供直接进入命令与经评审的退出 | 产品:稳定接口 |
|
| [`plan/`](plan/README.md) | Plan 协作状态,提供直接进入命令与经评审的退出 | 产品:稳定接口 |
|
||||||
| [`preset/`](preset/README.md) | 由 preset `cordis.yml` 按会话组装 agent | 产品:稳定接口 |
|
| [`preset/`](preset/README.md) | 由 preset `cordis.yml` 按会话组装 agent | 产品:稳定接口 |
|
||||||
| [`guard/`](guard/README.md) | 循环卫生守卫:建议性重复调用提醒 + `tools/execute` 截止时间强制执行器 | 产品:稳定接口 |
|
| [`guard/`](guard/README.md) | 循环卫生守卫:建议性重复调用提醒 + `tools/execute` 截止时间强制执行器 | 产品:稳定接口 |
|
||||||
| [`bundle/`](bundle/README.md) | 可安装的 `dsh --profile` 补丁层 | 产品:稳定接口 |
|
| [`bundle/`](bundle/README.md) | 可安装的 `dsh --profile` 补丁层 | 产品:稳定接口 |
|
||||||
| [`self-modification/`](self-modification/README.md) | agent 修改自身运行时:检查实时运行时的插件与服务,挂载/卸载模型所写插件([设计](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)),以及受限仓库插件加载 | 产品:稳定接口 |
|
| [`self-modification/`](self-modification/README.md) | agent 运行时自修改:实时插件/服务检查、模型所写插件挂载/卸载([设计](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md))、受限仓库插件加载 | 产品:稳定接口 |
|
||||||
| [`hooks/`](hooks/README.md) | 钩子桥接 + 共享 Claude Code/Codex 协议格式库 | 产品:稳定接口 |
|
| [`hooks/`](hooks/README.md) | 钩子桥接 + 共享 Claude Code/Codex 协议格式库 | 产品:稳定接口 |
|
||||||
| [`session/`](session/README.md) | 持久会话数据平面:持久化 seam + JSONL/SQLite 后端、投影 seam、日志支持的标题、会话上报 | 产品:稳定接口 |
|
| [`session/`](session/README.md) | 持久会话数据平面:持久化 seam + JSONL/SQLite 后端、投影 seam、日志支持的标题、会话上报 | 产品:稳定接口 |
|
||||||
| [`session-query/`](session-query/README.md) | 会话检索系列:逻辑语料库、有界读取、血缘、事件关系、语义过滤和 SQLite 全文搜索 | 产品:稳定接口 |
|
| [`session-query/`](session-query/README.md) | 会话检索系列:逻辑语料库、有界读取、血缘、事件关系、语义过滤和 SQLite 全文搜索 | 产品:稳定接口 |
|
||||||
|
|||||||
@@ -166,6 +166,17 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
|||||||
content: { type: 'text', text: block.text },
|
content: { type: 'text', text: block.text },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else if (block.type === 'image') {
|
||||||
|
notify({
|
||||||
|
sessionId: record.agent.session.id,
|
||||||
|
update: {
|
||||||
|
sessionUpdate: 'agent_message_chunk',
|
||||||
|
content: {
|
||||||
|
type: 'text',
|
||||||
|
text: `[image attachment ${block.attachment.attachmentId}]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,35 @@ describe('ACP prompt lifecycle', () => {
|
|||||||
await vi.waitFor(() => { expect(messageText(harness!)).toBe('cut off') })
|
await vi.waitFor(() => { expect(messageText(harness!)).toBe('cut off') })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders an assistant image as an explicit attachment placeholder', async () => {
|
||||||
|
const attachmentId = `sha256:${'a'.repeat(64)}` as never
|
||||||
|
harness = await makeBridgeHarness({
|
||||||
|
script: [[
|
||||||
|
{ type: 'block-start', index: 0, blockType: 'image' },
|
||||||
|
{
|
||||||
|
type: 'block-end',
|
||||||
|
index: 0,
|
||||||
|
block: {
|
||||||
|
type: 'image',
|
||||||
|
attachment: {
|
||||||
|
attachmentId,
|
||||||
|
mediaType: 'image/png',
|
||||||
|
bytes: 1,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'finish', reason: { kind: 'stop' } },
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
const sessionId = await newSession(harness)
|
||||||
|
await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'show it' }] })
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(messageText(harness!)).toBe(`[image attachment ${String(attachmentId)}]`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('rejects a failed turn and never publishes its partial chunks', async () => {
|
it('rejects a failed turn and never publishes its partial chunks', async () => {
|
||||||
harness = await makeBridgeHarness({ script: [errorResponse('provider boom')] })
|
harness = await makeBridgeHarness({ script: [errorResponse('provider boom')] })
|
||||||
const sessionId = await newSession(harness)
|
const sessionId = await newSession(harness)
|
||||||
|
|||||||
@@ -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/attachment/README.md
|
||||||
|
README.md: 61b4e5c602f475f85bbe859b8483e30b518e06c8
|
||||||
|
README.zh.md: b4a80917e166a3b0836e6e9c79d536d9dd8e1a41
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# attachment/ - durable attachment capability family
|
||||||
|
|
||||||
|
English | [中文](README.zh.md)
|
||||||
|
|
||||||
|
The durable binary attachment seam and its local filesystem implementation. Both are product packages.
|
||||||
|
|
||||||
|
| Package | Role | ctx key |
|
||||||
|
|---|---|---|
|
||||||
|
| `attachment/` | Immutable attachment references, image limits, and storage service | `ctx.attachments` |
|
||||||
|
| `attachment-local/` | Content-addressed private storage below `DSH_HOME` | (registers on `ctx.attachments`) |
|
||||||
|
|
||||||
|
Unsent browser drafts are intentionally outside this capability. Bytes enter durable storage only when a user prompt is submitted or when a provider adapter commits structured model output.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# attachment/:持久附件能力族
|
||||||
|
|
||||||
|
[English](README.md) | 中文
|
||||||
|
|
||||||
|
持久二进制附件服务边界及其本地文件系统实现。两者均为产品包(package)。
|
||||||
|
|
||||||
|
| 包 | 角色 | ctx 键 |
|
||||||
|
|---|---|---|
|
||||||
|
| `attachment/` | 不可变附件引用、图片限制和存储服务 | `ctx.attachments` |
|
||||||
|
| `attachment-local/` | `DSH_HOME` 下的私有内容寻址存储 | (注册至 `ctx.attachments`) |
|
||||||
|
|
||||||
|
未发送的浏览器草稿刻意位于这项能力之外。只有用户提交提示词,或提供方适配器提交结构化模型输出时,字节才进入持久存储。
|
||||||
@@ -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/attachment/attachment-local/README.md
|
||||||
|
README.md: 80001b29b392fe1c8b663f46d47f1ec0726e6d0f
|
||||||
|
README.zh.md: c3b95ace06b9f5ada156f20f33a1740a235400aa
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# @deepseek-ai/dsh-attachment-local
|
||||||
|
|
||||||
|
English | [中文](README.zh.md)
|
||||||
|
|
||||||
|
The private local implementation of [`@deepseek-ai/dsh-attachment`](../attachment). Objects land at `<DSH_HOME>/attachments/v1/objects/<sha256-prefix>/<sha256>` and are addressed by an opaque `sha256:` id. Each process proves a home durable once by syncing every ancestor entry to the filesystem root, so a directory another process created but has not yet synced is never mistaken for a safe boundary. Writes then use a private staging directory, owner-only files, a synced temporary file, an atomic exclusive hard-link publish, and directory syncs on the publication path (POSIX; Windows relies on filesystem metadata journaling) so the reported reference survives a crash. Write admission and reads fully decode the raster before accepting its format and dimensions; reads also re-check the digest and logged metadata. Byte and pixel limits are write-time admission policy, so a later policy reduction does not make already-admitted history unreadable.
|
||||||
|
|
||||||
|
`DSH_HOME` resolves through the shared path policy: explicit config, `$DSH_HOME`, then `~/.dsh`. Session logs contain only the reference and verified metadata, never this host path.
|
||||||
|
|
||||||
|
## Model Experience
|
||||||
|
|
||||||
|
Indirectly, through durable replay of historical user images and structured model image output after restart and fork.
|
||||||
|
|
||||||
|
#### KV Cache effect
|
||||||
|
|
||||||
|
None beyond the image block owned by the requesting adapter.
|
||||||
|
|
||||||
|
## Known Limitations and Deferred Work
|
||||||
|
|
||||||
|
- Objects are retained indefinitely; reference-aware garbage collection is deferred.
|
||||||
|
- The local backend assumes the host and provider adapter share this filesystem service.
|
||||||
|
- Animated GIF metadata is validated from the logical screen; frame-level decoding policy is provider-owned.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# @deepseek-ai/dsh-attachment-local
|
||||||
|
|
||||||
|
[English](README.md) | 中文
|
||||||
|
|
||||||
|
这是 [`@deepseek-ai/dsh-attachment`](../attachment) 的私有本地实现。对象存放在 `<DSH_HOME>/attachments/v1/objects/<sha256-prefix>/<sha256>`,并通过不透明的 `sha256:` 标识符寻址。每个进程都会通过将每个祖先目录项逐级同步到文件系统根目录,为某个 home 一次性证明其持久性,因此绝不会把另一个进程已经创建但尚未同步的目录误认为安全边界。随后,写入过程使用私有暂存目录、仅所有者可访问的文件、经过同步的临时文件、原子且排他的硬链接发布,并对发布路径执行目录同步(适用于 POSIX;Windows 依赖文件系统元数据日志),确保已报告的引用能够在崩溃后继续存在。写入准入与读取都会完整解码光栅图片,之后才接受其格式和尺寸;读取还会重新校验摘要和已记录的元数据。字节和像素限制属于写入时的准入策略,因此后续收紧限制不会导致已经接纳的历史记录变得不可读。
|
||||||
|
|
||||||
|
`DSH_HOME` 按共享路径策略解析:显式配置、`$DSH_HOME`,最后是 `~/.dsh`。会话日志只包含引用和经过校验的元数据,绝不包含这个宿主路径。
|
||||||
|
|
||||||
|
## 模型体验
|
||||||
|
|
||||||
|
该包通过重启和 fork 后对历史用户图片与结构化模型图片输出的持久回放间接影响模型。
|
||||||
|
|
||||||
|
#### KV 缓存影响
|
||||||
|
|
||||||
|
除发起请求的适配器所持有的图片块外,不产生其他影响。
|
||||||
|
|
||||||
|
## 已知限制与待完成工作
|
||||||
|
|
||||||
|
- 对象会无限期保留;基于引用的垃圾回收尚未实现。
|
||||||
|
- 本地后端假定宿主与提供方适配器共享同一个文件系统服务。
|
||||||
|
- 动态 GIF 的元数据根据逻辑屏幕进行校验;逐帧解码策略由提供方持有。
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@deepseek-ai/dsh-attachment-local",
|
||||||
|
"description": "Private content-addressed DSH_HOME attachment storage",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/types/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
|
||||||
|
"./invariant": { "types": "./lib/types/invariant.d.ts", "default": "./lib/invariant.js" },
|
||||||
|
"./src/*": "./src/*",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"files": ["lib/index.js", "lib/invariant.js", "lib/types/**/*.d.ts"],
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@deepseek-ai/dsh-attachment": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-paths": "^0.0.1",
|
||||||
|
"cordis": "^4.0.0-rc.7"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"schemastery": "^3.18.0",
|
||||||
|
"sharp": "^0.35.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-paths": "workspace:^",
|
||||||
|
"cordis": "^4.0.0-rc.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/** Raster inspection: full decode at admission, header-only probe on verified reads. */
|
||||||
|
|
||||||
|
import sharp, { type Sharp } from 'sharp'
|
||||||
|
import { AttachmentError } from '@deepseek-ai/dsh-attachment'
|
||||||
|
import type { ImageMediaType } from '@deepseek-ai/dsh-attachment'
|
||||||
|
|
||||||
|
/** Decoded metadata from a supported image. */
|
||||||
|
export interface DetectedImage {
|
||||||
|
mediaType: ImageMediaType
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const MEDIA_TYPES: Readonly<Record<string, ImageMediaType>> = {
|
||||||
|
png: 'image/png',
|
||||||
|
jpeg: 'image/jpeg',
|
||||||
|
webp: 'image/webp',
|
||||||
|
gif: 'image/gif',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function imageMetadata(image: Sharp): Promise<DetectedImage> {
|
||||||
|
const metadata = await image.metadata()
|
||||||
|
const mediaType = MEDIA_TYPES[metadata.format as string]
|
||||||
|
if (mediaType === undefined) {
|
||||||
|
throw new AttachmentError('Unsupported or malformed image data.', 'INVALID_IMAGE')
|
||||||
|
}
|
||||||
|
return { mediaType, width: metadata.width, height: metadata.height }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a supported raster's header and return its intrinsic metadata without
|
||||||
|
* decoding pixels. Digest-verified reads use this: admission already proved
|
||||||
|
* that these exact bytes decode completely, so the read path only re-derives
|
||||||
|
* the reference fields instead of paying the full-raster decode again.
|
||||||
|
* @param data - complete encoded image bytes.
|
||||||
|
* @returns verified format and dimensions.
|
||||||
|
*/
|
||||||
|
export async function probeImage(data: Uint8Array): Promise<DetectedImage> {
|
||||||
|
try {
|
||||||
|
return await imageMetadata(sharp(data, { failOn: 'error', limitInputPixels: false }))
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AttachmentError) throw error
|
||||||
|
throw new AttachmentError('Unsupported or malformed image data.', 'INVALID_IMAGE', { cause: error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully decode a supported raster and return its intrinsic metadata.
|
||||||
|
* @param data - complete encoded image bytes.
|
||||||
|
* @param maxPixels - decoded-pixel admission limit.
|
||||||
|
* @returns verified format and dimensions.
|
||||||
|
*/
|
||||||
|
export async function detectImage(data: Uint8Array, maxPixels?: number): Promise<DetectedImage> {
|
||||||
|
try {
|
||||||
|
const image = sharp(data, { failOn: 'error', limitInputPixels: false })
|
||||||
|
const detected = await imageMetadata(image)
|
||||||
|
if (maxPixels !== undefined && detected.width * detected.height > maxPixels) {
|
||||||
|
throw new AttachmentError('Image exceeds the configured decoded-pixel limit.', 'IMAGE_TOO_MANY_PIXELS')
|
||||||
|
}
|
||||||
|
await image.raw().toBuffer()
|
||||||
|
return detected
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AttachmentError) throw error
|
||||||
|
throw new AttachmentError('Unsupported or malformed image data.', 'INVALID_IMAGE', { cause: error })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/** Local durable attachment backend rooted below `DSH_HOME`. @module @deepseek-ai/dsh-attachment-local */
|
||||||
|
|
||||||
|
import { join, resolve } from 'node:path'
|
||||||
|
import { Context } from 'cordis'
|
||||||
|
import z from 'schemastery'
|
||||||
|
import { AttachmentStore } from '@deepseek-ai/dsh-attachment'
|
||||||
|
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@deepseek-ai/dsh-attachment'
|
||||||
|
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||||
|
import { readImageFile, saveImageFile, validateImageFile } from './store.ts'
|
||||||
|
|
||||||
|
export { detectImage } from './image.ts'
|
||||||
|
export { readImageFile, saveImageFile, validateImageFile } from './store.ts'
|
||||||
|
|
||||||
|
/** Default maximum encoded bytes for one image. */
|
||||||
|
export const DEFAULT_MAX_IMAGE_BYTES = 5 * 1024 * 1024
|
||||||
|
/** Default maximum images in one prompt. */
|
||||||
|
export const DEFAULT_MAX_IMAGES_PER_MESSAGE = 10
|
||||||
|
/** Default maximum aggregate image bytes in one prompt. */
|
||||||
|
export const DEFAULT_MAX_MESSAGE_IMAGE_BYTES = 20 * 1024 * 1024
|
||||||
|
/** Default maximum intrinsic pixels for one image. */
|
||||||
|
export const DEFAULT_MAX_IMAGE_PIXELS = 40_000_000
|
||||||
|
|
||||||
|
/** Local attachment backend configuration. */
|
||||||
|
export interface Config {
|
||||||
|
/** Explicit harness home; omitted follows `DSH_HOME`, then `~/.dsh`. */
|
||||||
|
dshHome?: string
|
||||||
|
/** Maximum encoded bytes accepted for one image. */
|
||||||
|
maxImageBytes?: number
|
||||||
|
/** Maximum image count accepted in one submitted message. */
|
||||||
|
maxImagesPerMessage?: number
|
||||||
|
/** Maximum aggregate encoded image bytes accepted in one submitted message. */
|
||||||
|
maxMessageImageBytes?: number
|
||||||
|
/** Maximum intrinsic width multiplied by height accepted for one image. */
|
||||||
|
maxImagePixels?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Persistent content-addressed local attachment store. */
|
||||||
|
export class LocalAttachmentStore extends AttachmentStore {
|
||||||
|
static Config: z<Config> = z.object({
|
||||||
|
dshHome: z.string(),
|
||||||
|
maxImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_IMAGE_BYTES),
|
||||||
|
maxImagesPerMessage: z.number().step(1).min(1).default(DEFAULT_MAX_IMAGES_PER_MESSAGE),
|
||||||
|
maxMessageImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_MESSAGE_IMAGE_BYTES),
|
||||||
|
maxImagePixels: z.number().step(1).min(1).default(DEFAULT_MAX_IMAGE_PIXELS),
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Absolute versioned storage root. */
|
||||||
|
readonly root: string
|
||||||
|
readonly imageLimits: ImageAttachmentLimits
|
||||||
|
|
||||||
|
constructor(ctx: Context, config: Config) {
|
||||||
|
super(ctx)
|
||||||
|
this.root = resolve(join(resolveDshHome(config.dshHome), 'attachments', 'v1'))
|
||||||
|
this.imageLimits = Object.freeze({
|
||||||
|
maxImageBytes: config.maxImageBytes ?? DEFAULT_MAX_IMAGE_BYTES,
|
||||||
|
maxImagesPerMessage: config.maxImagesPerMessage ?? DEFAULT_MAX_IMAGES_PER_MESSAGE,
|
||||||
|
maxMessageImageBytes: config.maxMessageImageBytes ?? DEFAULT_MAX_MESSAGE_IMAGE_BYTES,
|
||||||
|
maxImagePixels: config.maxImagePixels ?? DEFAULT_MAX_IMAGE_PIXELS,
|
||||||
|
mediaTypes: Object.freeze(['image/png', 'image/jpeg', 'image/webp', 'image/gif'] as const),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateImage(input: SaveImageAttachment): Promise<void> {
|
||||||
|
await validateImageFile(input, this.imageLimits)
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef> {
|
||||||
|
return saveImageFile(this.root, input, this.imageLimits)
|
||||||
|
}
|
||||||
|
|
||||||
|
async readImage(ref: ImageAttachmentRef): Promise<StoredImageAttachment> {
|
||||||
|
return readImageFile(this.root, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LocalAttachmentStore
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/** Package-owned invariant companion for `@deepseek-ai/dsh-attachment-local`. @module @deepseek-ai/dsh-attachment-local/invariant */
|
||||||
|
|
||||||
|
/* jscpd:ignore-start */
|
||||||
|
import type { Context } from 'cordis'
|
||||||
|
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||||
|
|
||||||
|
const PACKAGE_NAME = '@deepseek-ai/dsh-attachment-local'
|
||||||
|
/** Cordis companion plugin name. */
|
||||||
|
export const name = 'attachment-local-invariant'
|
||||||
|
/** Services required before package ownership can be reserved. */
|
||||||
|
export const inject = ['invariants', 'attachments']
|
||||||
|
/** No runtime invariant: immutable writes and verified reads are enforced directly at the backend boundary. */
|
||||||
|
const install: InvariantInstaller = () => {}
|
||||||
|
/**
|
||||||
|
* Register the package invariant companion.
|
||||||
|
* @param ctx - Cordis context carrying the invariant service.
|
||||||
|
* @returns the registration disposer.
|
||||||
|
*/
|
||||||
|
export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||||
|
/* jscpd:ignore-end */
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/** Content-addressed, owner-private local attachment storage. */
|
||||||
|
|
||||||
|
import { createHash, randomUUID } from 'node:crypto'
|
||||||
|
import { constants } from 'node:fs'
|
||||||
|
import { chmod, link, mkdir, open, readFile, unlink } from 'node:fs/promises'
|
||||||
|
import { dirname, join, parse, resolve } from 'node:path'
|
||||||
|
import {
|
||||||
|
AttachmentError,
|
||||||
|
AttachmentId,
|
||||||
|
} from '@deepseek-ai/dsh-attachment'
|
||||||
|
import type {
|
||||||
|
ImageAttachmentLimits,
|
||||||
|
ImageAttachmentRef,
|
||||||
|
SaveImageAttachment,
|
||||||
|
StoredImageAttachment,
|
||||||
|
} from '@deepseek-ai/dsh-attachment'
|
||||||
|
import { detectImage, probeImage } from './image.ts'
|
||||||
|
|
||||||
|
const ID_PATTERN = /^sha256:([a-f0-9]{64})$/
|
||||||
|
const durableHomes = new Set<string>()
|
||||||
|
|
||||||
|
function digest(data: Uint8Array): string {
|
||||||
|
return createHash('sha256').update(data).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayName(value: string | undefined): string | undefined {
|
||||||
|
if (value === undefined) return undefined
|
||||||
|
// Strip both separator styles by hand: a POSIX host treats `\` as an
|
||||||
|
// ordinary character, so path.basename would keep a Windows client's full
|
||||||
|
// local path and leak it into the reference and the session log.
|
||||||
|
const leaf = value.slice(Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\')) + 1)
|
||||||
|
const clean = leaf.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 255)
|
||||||
|
return clean === '' ? undefined : clean
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectPath(root: string, sha256: string): string {
|
||||||
|
return join(root, 'objects', sha256.slice(0, 2), sha256)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureReference(ref: ImageAttachmentRef): string {
|
||||||
|
const match = ID_PATTERN.exec(String(ref.attachmentId))
|
||||||
|
if (match?.[1] === undefined) throw new AttachmentError('Attachment reference is invalid.', 'INVALID_ATTACHMENT_REF')
|
||||||
|
return match[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inspectMetadata(
|
||||||
|
data: Uint8Array,
|
||||||
|
declaredMediaType: ImageAttachmentRef['mediaType'],
|
||||||
|
maxPixels?: number,
|
||||||
|
): Promise<Omit<ImageAttachmentRef, 'attachmentId' | 'name'>> {
|
||||||
|
if (data.byteLength === 0) throw new AttachmentError('Image is empty.', 'INVALID_IMAGE')
|
||||||
|
const detected = await detectImage(data, maxPixels)
|
||||||
|
if (detected.mediaType !== declaredMediaType) throw new AttachmentError('Declared image type does not match its bytes.', 'IMAGE_TYPE_MISMATCH')
|
||||||
|
return { ...detected, bytes: data.byteLength }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the full admission policy for one image without touching storage.
|
||||||
|
* @param input - encoded bytes and declared metadata.
|
||||||
|
* @param limits - resolved storage policy.
|
||||||
|
* @returns completion after the encoded raster has been fully decoded.
|
||||||
|
*/
|
||||||
|
export async function validateImageFile(input: SaveImageAttachment, limits: ImageAttachmentLimits): Promise<void> {
|
||||||
|
if (input.data.byteLength > limits.maxImageBytes) {
|
||||||
|
throw new AttachmentError('Image exceeds the configured byte limit.', 'IMAGE_TOO_LARGE')
|
||||||
|
}
|
||||||
|
await inspectMetadata(input.data, input.mediaType, limits.maxImagePixels)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a directory's entries durable (fsync on a read-only directory handle).
|
||||||
|
* A synced file alone does not survive a crash when its directory entry never
|
||||||
|
* reached storage, so the publication directory is synced before a durable
|
||||||
|
* reference is reported.
|
||||||
|
*/
|
||||||
|
async function syncDirectory(path: string): Promise<void> {
|
||||||
|
/* v8 ignore next -- Windows cannot open directory handles; NTFS metadata journaling owns entry durability there. */
|
||||||
|
if (process.platform === 'win32') return
|
||||||
|
/* v8 ignore start -- Windows cannot exercise directory fsync; POSIX behavior tests enforce this peer. */
|
||||||
|
const handle = await open(path, constants.O_RDONLY)
|
||||||
|
try {
|
||||||
|
await handle.sync()
|
||||||
|
} finally {
|
||||||
|
await handle.close()
|
||||||
|
}
|
||||||
|
/* v8 ignore stop */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create one private directory tree and persist every ancestor entry up to a
|
||||||
|
* caller-vouched durable boundary. The walk deliberately ignores what mkdir
|
||||||
|
* reports as newly created: a concurrent first save can create a level this
|
||||||
|
* process then merely observes, so "already existed" is not "already durable"
|
||||||
|
* — the entry may still be unsynced in the creator, and a crash would drop a
|
||||||
|
* directory the session checkpoint already references. Re-syncing a durable
|
||||||
|
* entry is harmless; skipping an unsynced one is not.
|
||||||
|
* @param path - absolute directory to create.
|
||||||
|
* @param boundary - absolute ancestor the caller vouches is already durable.
|
||||||
|
*/
|
||||||
|
async function ensureDurableDirectory(path: string, boundary: string): Promise<void> {
|
||||||
|
const target = resolve(path)
|
||||||
|
const stop = resolve(boundary)
|
||||||
|
await mkdir(target, { recursive: true, mode: 0o700 })
|
||||||
|
await chmod(target, 0o700)
|
||||||
|
let level = target
|
||||||
|
while (level !== stop) {
|
||||||
|
const parent = dirname(level)
|
||||||
|
await syncDirectory(parent)
|
||||||
|
/* v8 ignore next -- filesystem-root guard: callers pass a boundary that is an ancestor of path, so the walk reaches it first. */
|
||||||
|
if (parent === level) return
|
||||||
|
level = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establish this process's proof that one DSH_HOME entry and every ancestor
|
||||||
|
* below the filesystem root are durable. Mere existence is insufficient: a
|
||||||
|
* concurrent process may have created the directory but not synced its parent.
|
||||||
|
*/
|
||||||
|
async function ensureDurableHome(path: string): Promise<string> {
|
||||||
|
const home = resolve(path)
|
||||||
|
if (!durableHomes.has(home)) {
|
||||||
|
await ensureDurableDirectory(home, parse(home).root)
|
||||||
|
durableHomes.add(home)
|
||||||
|
}
|
||||||
|
return home
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save and verify immutable image bytes below a versioned attachment root.
|
||||||
|
* @param root - absolute `DSH_HOME/attachments/v1` root.
|
||||||
|
* @param input - encoded bytes and declared metadata.
|
||||||
|
* @param limits - resolved storage policy.
|
||||||
|
* @returns durable content-addressed reference.
|
||||||
|
*/
|
||||||
|
export async function saveImageFile(root: string, input: SaveImageAttachment, limits: ImageAttachmentLimits): Promise<ImageAttachmentRef> {
|
||||||
|
if (input.data.byteLength > limits.maxImageBytes) throw new AttachmentError('Image exceeds the configured byte limit.', 'IMAGE_TOO_LARGE')
|
||||||
|
const metadata = await inspectMetadata(input.data, input.mediaType, limits.maxImagePixels)
|
||||||
|
const sha256 = digest(input.data)
|
||||||
|
const bucket = join(root, 'objects', sha256.slice(0, 2))
|
||||||
|
const staging = join(root, 'tmp')
|
||||||
|
// Establish DSH_HOME itself against the filesystem root once per process.
|
||||||
|
// Every process performs that proof independently, so observing a directory
|
||||||
|
// another process created can never be mistaken for durable publication.
|
||||||
|
const boundary = await ensureDurableHome(dirname(dirname(resolve(root))))
|
||||||
|
await ensureDurableDirectory(bucket, boundary)
|
||||||
|
await ensureDurableDirectory(staging, boundary)
|
||||||
|
const temporary = join(staging, randomUUID())
|
||||||
|
const target = objectPath(root, sha256)
|
||||||
|
let handle
|
||||||
|
try {
|
||||||
|
handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600)
|
||||||
|
await handle.writeFile(input.data)
|
||||||
|
await handle.sync()
|
||||||
|
await handle.close()
|
||||||
|
handle = undefined
|
||||||
|
try {
|
||||||
|
await link(temporary, target)
|
||||||
|
} catch (error) {
|
||||||
|
/* v8 ignore next -- Private same-filesystem directories make EEXIST the only recoverable link race. */
|
||||||
|
if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST')) throw error
|
||||||
|
const existing = new Uint8Array(await readFile(target))
|
||||||
|
if (digest(existing) !== sha256) throw new AttachmentError('Stored attachment failed integrity verification.', 'ATTACHMENT_CORRUPT')
|
||||||
|
}
|
||||||
|
// Persist the target entry and close a concurrent bucket-creation window
|
||||||
|
// before the reference can reach a session checkpoint. The dedup path
|
||||||
|
// repeats both syncs because it may observe another writer's link before
|
||||||
|
// that writer reaches its own durability boundary.
|
||||||
|
await syncDirectory(bucket)
|
||||||
|
await syncDirectory(join(root, 'objects'))
|
||||||
|
await unlink(temporary)
|
||||||
|
} catch (error) {
|
||||||
|
/* v8 ignore next -- A descriptor can remain open only when the underlying write/sync/close operation fails. */
|
||||||
|
if (handle !== undefined) await handle.close().catch(
|
||||||
|
/* v8 ignore next -- Close failure is superseded by the storage operation that entered cleanup. */
|
||||||
|
() => {},
|
||||||
|
)
|
||||||
|
await unlink(temporary).catch(
|
||||||
|
/* v8 ignore next -- The callback requires a second independent staging-unlink failure. */
|
||||||
|
(cleanupError: unknown) => {
|
||||||
|
/* v8 ignore next -- Cleanup is best-effort only for a staging file already removed by a failed operation. */
|
||||||
|
if (!(cleanupError instanceof Error && 'code' in cleanupError && cleanupError.code === 'ENOENT')) throw cleanupError
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (error instanceof AttachmentError) throw error
|
||||||
|
throw new AttachmentError('Unable to persist image attachment.', 'ATTACHMENT_WRITE_FAILED', { cause: error })
|
||||||
|
}
|
||||||
|
const name = displayName(input.name)
|
||||||
|
return {
|
||||||
|
attachmentId: AttachmentId(`sha256:${sha256}`),
|
||||||
|
...metadata,
|
||||||
|
...(name !== undefined ? { name } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read and verify one content-addressed image.
|
||||||
|
* @param root - absolute `DSH_HOME/attachments/v1` root.
|
||||||
|
* @param ref - reference recorded in the session log.
|
||||||
|
* @returns verified bytes and reference.
|
||||||
|
*/
|
||||||
|
export async function readImageFile(root: string, ref: ImageAttachmentRef): Promise<StoredImageAttachment> {
|
||||||
|
const sha256 = ensureReference(ref)
|
||||||
|
let data: Uint8Array
|
||||||
|
try {
|
||||||
|
data = new Uint8Array(await readFile(objectPath(root, sha256)))
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') throw new AttachmentError('Attachment object is missing.', 'ATTACHMENT_NOT_FOUND')
|
||||||
|
throw new AttachmentError('Unable to read image attachment.', 'ATTACHMENT_READ_FAILED', { cause: error })
|
||||||
|
}
|
||||||
|
if (digest(data) !== sha256) throw new AttachmentError('Stored attachment failed integrity verification.', 'ATTACHMENT_CORRUPT')
|
||||||
|
// The digest proves these are the exact bytes admission fully decoded, so
|
||||||
|
// the read path only re-derives the header fields (no raster decode, no
|
||||||
|
// per-request pixel amplification on history replay).
|
||||||
|
const metadata = await probeImage(data)
|
||||||
|
if (metadata.mediaType !== ref.mediaType || data.byteLength !== ref.bytes
|
||||||
|
|| metadata.width !== ref.width || metadata.height !== ref.height) {
|
||||||
|
throw new AttachmentError('Stored attachment metadata does not match its reference.', 'ATTACHMENT_CORRUPT')
|
||||||
|
}
|
||||||
|
return { ref, data }
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import sharp from 'sharp'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { detectImage, probeImage } from '../src/image.ts'
|
||||||
|
|
||||||
|
async function raster(format: 'png' | 'jpeg' | 'webp' | 'gif'): Promise<Uint8Array> {
|
||||||
|
const image = sharp({
|
||||||
|
create: { width: 3, height: 2, channels: 4, background: { r: 1, g: 2, b: 3, alpha: 1 } },
|
||||||
|
})
|
||||||
|
return new Uint8Array(await image.toFormat(format).toBuffer())
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('raster decoding', () => {
|
||||||
|
it('decodes every supported format and its intrinsic dimensions', async () => {
|
||||||
|
for (const [format, mediaType] of [
|
||||||
|
['png', 'image/png'],
|
||||||
|
['jpeg', 'image/jpeg'],
|
||||||
|
['webp', 'image/webp'],
|
||||||
|
['gif', 'image/gif'],
|
||||||
|
] as const) {
|
||||||
|
await expect(detectImage(await raster(format)))
|
||||||
|
.resolves.toEqual({ mediaType, width: 3, height: 2 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects excess decoded pixels before decoding', async () => {
|
||||||
|
await expect(detectImage(await raster('png'), 5))
|
||||||
|
.rejects.toMatchObject({ code: 'IMAGE_TOO_MANY_PIXELS' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects malformed bytes and truncated payloads with readable headers', async () => {
|
||||||
|
await expect(detectImage(Uint8Array.of(1, 2, 3)))
|
||||||
|
.rejects.toMatchObject({ code: 'INVALID_IMAGE' })
|
||||||
|
const unsupported = await sharp({
|
||||||
|
create: { width: 1, height: 1, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 1 } },
|
||||||
|
}).tiff().toBuffer()
|
||||||
|
await expect(detectImage(unsupported)).rejects.toMatchObject({ code: 'INVALID_IMAGE' })
|
||||||
|
const complete = await raster('png')
|
||||||
|
const truncated = complete.subarray(0, 62)
|
||||||
|
await expect(sharp(truncated).metadata()).resolves.toMatchObject({ width: 3, height: 2 })
|
||||||
|
await expect(detectImage(truncated)).rejects.toMatchObject({ code: 'INVALID_IMAGE' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('probes malformed bytes and unsupported formats into the same stable error', async () => {
|
||||||
|
await expect(probeImage(Uint8Array.of(1, 2, 3)))
|
||||||
|
.rejects.toMatchObject({ code: 'INVALID_IMAGE' })
|
||||||
|
const unsupported = await sharp({
|
||||||
|
create: { width: 1, height: 1, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 1 } },
|
||||||
|
}).tiff().toBuffer()
|
||||||
|
await expect(probeImage(unsupported)).rejects.toMatchObject({ code: 'INVALID_IMAGE' })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Context } from 'cordis'
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
|
import { mkdtemp, rm } from 'node:fs/promises'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import LocalAttachmentStore, {
|
||||||
|
DEFAULT_MAX_IMAGE_BYTES,
|
||||||
|
DEFAULT_MAX_IMAGE_PIXELS,
|
||||||
|
DEFAULT_MAX_IMAGES_PER_MESSAGE,
|
||||||
|
DEFAULT_MAX_MESSAGE_IMAGE_BYTES,
|
||||||
|
} from '../src/index.ts'
|
||||||
|
|
||||||
|
describe('local attachment service', () => {
|
||||||
|
it('resolves every omitted admission limit explicitly', () => {
|
||||||
|
const service = new LocalAttachmentStore(new Context(), {})
|
||||||
|
expect(service.imageLimits).toEqual({
|
||||||
|
maxImageBytes: DEFAULT_MAX_IMAGE_BYTES,
|
||||||
|
maxImagesPerMessage: DEFAULT_MAX_IMAGES_PER_MESSAGE,
|
||||||
|
maxMessageImageBytes: DEFAULT_MAX_MESSAGE_IMAGE_BYTES,
|
||||||
|
maxImagePixels: DEFAULT_MAX_IMAGE_PIXELS,
|
||||||
|
mediaTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saves and reads through the service boundary', async () => {
|
||||||
|
const dshHome = await mkdtemp(join(tmpdir(), 'dsh-attachment-service-'))
|
||||||
|
try {
|
||||||
|
const service = new LocalAttachmentStore(new Context(), { dshHome })
|
||||||
|
const data = Uint8Array.from(Buffer.from(
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||||
|
'base64',
|
||||||
|
))
|
||||||
|
const ref = await service.saveImage({ data, mediaType: 'image/png' })
|
||||||
|
await expect(service.readImage(ref)).resolves.toEqual({ ref, data })
|
||||||
|
} finally {
|
||||||
|
await rm(dshHome, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validates without persisting: a rejected image leaves no storage root behind', async () => {
|
||||||
|
const dshHome = await mkdtemp(join(tmpdir(), 'dsh-attachment-validate-'))
|
||||||
|
try {
|
||||||
|
const service = new LocalAttachmentStore(new Context(), { dshHome })
|
||||||
|
await expect(service.validateImage({ data: Uint8Array.of(1, 2, 3), mediaType: 'image/png' }))
|
||||||
|
.rejects.toThrow(/Unsupported or malformed image data/)
|
||||||
|
const valid = Uint8Array.from(Buffer.from(
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||||
|
'base64',
|
||||||
|
))
|
||||||
|
const limited = new LocalAttachmentStore(new Context(), { dshHome, maxImageBytes: 1 })
|
||||||
|
await expect(limited.validateImage({ data: valid, mediaType: 'image/png' }))
|
||||||
|
.rejects.toMatchObject({ code: 'IMAGE_TOO_LARGE' })
|
||||||
|
await expect(service.validateImage({ data: valid, mediaType: 'image/png' })).resolves.toBeUndefined()
|
||||||
|
expect(existsSync(service.root)).toBe(false)
|
||||||
|
} finally {
|
||||||
|
await rm(dshHome, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { constants } from 'node:fs'
|
||||||
|
import { chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { dirname, join, parse, resolve } from 'node:path'
|
||||||
|
import { mkdtemp, rm } from 'node:fs/promises'
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
import type { ImageAttachmentLimits } from '@deepseek-ai/dsh-attachment'
|
||||||
|
import { readImageFile, saveImageFile } from '../src/store.ts'
|
||||||
|
|
||||||
|
const fsControl = vi.hoisted(() => ({ syncedDirectories: [] as string[] }))
|
||||||
|
|
||||||
|
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('node:fs/promises')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
async open(...args: Parameters<typeof actual.open>): ReturnType<typeof actual.open> {
|
||||||
|
if (args[1] === constants.O_RDONLY) fsControl.syncedDirectories.push(String(args[0]))
|
||||||
|
return actual.open(...args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const PNG = Uint8Array.from(Buffer.from(
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||||
|
'base64',
|
||||||
|
))
|
||||||
|
|
||||||
|
const LIMITS: ImageAttachmentLimits = {
|
||||||
|
maxImageBytes: 1024,
|
||||||
|
maxImagesPerMessage: 2,
|
||||||
|
maxMessageImageBytes: 2048,
|
||||||
|
maxImagePixels: 16,
|
||||||
|
mediaTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const roots: string[] = []
|
||||||
|
|
||||||
|
async function root(): Promise<string> {
|
||||||
|
const value = await mkdtemp(join(tmpdir(), 'dsh-attachment-'))
|
||||||
|
roots.push(value)
|
||||||
|
return join(value, 'attachments', 'v1')
|
||||||
|
}
|
||||||
|
|
||||||
|
function parentChainToRoot(path: string): string[] {
|
||||||
|
const parents: string[] = []
|
||||||
|
let level = resolve(path)
|
||||||
|
const root = parse(level).root
|
||||||
|
while (level !== root) {
|
||||||
|
level = dirname(level)
|
||||||
|
parents.push(level)
|
||||||
|
}
|
||||||
|
return parents
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all(roots.splice(0).map(path => rm(path, { recursive: true, force: true })))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('local attachment store', () => {
|
||||||
|
it.skipIf(process.platform === 'win32')('syncs every object ancestor up to the durable boundary before returning', async () => {
|
||||||
|
const storageRoot = await root()
|
||||||
|
const base = join(storageRoot, '..', '..')
|
||||||
|
const sha256 = createHash('sha256').update(PNG).digest('hex')
|
||||||
|
const objects = join(storageRoot, 'objects')
|
||||||
|
const bucket = join(objects, sha256.slice(0, 2))
|
||||||
|
fsControl.syncedDirectories.length = 0
|
||||||
|
|
||||||
|
await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
|
||||||
|
|
||||||
|
// Each process first proves DSH_HOME durable all the way to the filesystem
|
||||||
|
// root; existence alone cannot vouch for a concurrent creator's fsync.
|
||||||
|
// Later directory creation can then stop at that process-proven boundary.
|
||||||
|
expect(fsControl.syncedDirectories).toEqual([
|
||||||
|
...parentChainToRoot(base),
|
||||||
|
// bucket chain: every parent entry between the bucket and the boundary.
|
||||||
|
objects,
|
||||||
|
storageRoot,
|
||||||
|
join(storageRoot, '..'),
|
||||||
|
base,
|
||||||
|
// staging chain re-walks the shared ancestors after creating tmp.
|
||||||
|
storageRoot,
|
||||||
|
join(storageRoot, '..'),
|
||||||
|
base,
|
||||||
|
// publication: the settled object's bucket and its parent for the rename.
|
||||||
|
bucket,
|
||||||
|
objects,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates and persists a missing nested home directory against the filesystem root', async () => {
|
||||||
|
const storageRoot = join(await root(), 'home', 'attachments', 'v1')
|
||||||
|
|
||||||
|
const ref = await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
|
||||||
|
|
||||||
|
await expect(readImageFile(storageRoot, ref)).resolves.toEqual({ ref, data: PNG })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('publishes one private content-addressed object and deduplicates equal bytes', async () => {
|
||||||
|
const storageRoot = await root()
|
||||||
|
const first = await saveImageFile(storageRoot, {
|
||||||
|
data: PNG, mediaType: 'image/png', name: '/private/tmp/pixel.png',
|
||||||
|
}, LIMITS)
|
||||||
|
const second = await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
|
||||||
|
const sha256 = createHash('sha256').update(PNG).digest('hex')
|
||||||
|
const object = join(storageRoot, 'objects', sha256.slice(0, 2), sha256)
|
||||||
|
|
||||||
|
expect(first).toEqual({
|
||||||
|
attachmentId: `sha256:${sha256}`,
|
||||||
|
mediaType: 'image/png',
|
||||||
|
bytes: PNG.byteLength,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
name: 'pixel.png',
|
||||||
|
})
|
||||||
|
expect(second.attachmentId).toBe(first.attachmentId)
|
||||||
|
expect(new Uint8Array(await readFile(object))).toEqual(PNG)
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
expect((await stat(object)).mode & 0o777).toBe(0o600)
|
||||||
|
expect((await stat(join(storageRoot, 'objects', sha256.slice(0, 2)))).mode & 0o777).toBe(0o700)
|
||||||
|
}
|
||||||
|
await expect(readImageFile(storageRoot, first)).resolves.toEqual({ ref: first, data: PNG })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps admitted history readable after deployment limits become stricter', async () => {
|
||||||
|
const storageRoot = await root()
|
||||||
|
const ref = await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
|
||||||
|
|
||||||
|
await expect(readImageFile(storageRoot, ref)).resolves.toEqual({ ref, data: PNG })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects malformed bytes, mismatched declarations, byte limits, and decoded-pixel limits', async () => {
|
||||||
|
const storageRoot = await root()
|
||||||
|
await expect(saveImageFile(storageRoot, {
|
||||||
|
data: new Uint8Array(0), mediaType: 'image/png',
|
||||||
|
}, LIMITS)).rejects.toMatchObject({ code: 'INVALID_IMAGE' })
|
||||||
|
await expect(saveImageFile(storageRoot, {
|
||||||
|
data: Uint8Array.of(1, 2, 3), mediaType: 'image/png',
|
||||||
|
}, LIMITS)).rejects.toMatchObject({ code: 'INVALID_IMAGE' })
|
||||||
|
await expect(saveImageFile(storageRoot, {
|
||||||
|
data: PNG, mediaType: 'image/jpeg',
|
||||||
|
}, LIMITS)).rejects.toMatchObject({ code: 'IMAGE_TYPE_MISMATCH' })
|
||||||
|
await expect(saveImageFile(storageRoot, {
|
||||||
|
data: PNG, mediaType: 'image/png',
|
||||||
|
}, { ...LIMITS, maxImageBytes: 1 })).rejects.toMatchObject({ code: 'IMAGE_TOO_LARGE' })
|
||||||
|
|
||||||
|
const wide = new Uint8Array(await sharp({
|
||||||
|
create: { width: 5, height: 5, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 1 } },
|
||||||
|
}).png().toBuffer())
|
||||||
|
await expect(saveImageFile(storageRoot, {
|
||||||
|
data: wide, mediaType: 'image/png',
|
||||||
|
}, LIMITS)).rejects.toMatchObject({ code: 'IMAGE_TOO_MANY_PIXELS' })
|
||||||
|
const unnamed = await saveImageFile(storageRoot, {
|
||||||
|
data: PNG, mediaType: 'image/png', name: '\u0000',
|
||||||
|
}, LIMITS)
|
||||||
|
expect(unnamed).not.toHaveProperty('name')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails closed when an object is missing, corrupted, or addressed by an invalid reference', async () => {
|
||||||
|
const storageRoot = await root()
|
||||||
|
const ref = await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
|
||||||
|
const sha256 = String(ref.attachmentId).slice('sha256:'.length)
|
||||||
|
const object = join(storageRoot, 'objects', sha256.slice(0, 2), sha256)
|
||||||
|
await chmod(object, 0o600)
|
||||||
|
await writeFile(object, Uint8Array.of(1, 2, 3))
|
||||||
|
await expect(readImageFile(storageRoot, ref))
|
||||||
|
.rejects.toMatchObject({ code: 'ATTACHMENT_CORRUPT' })
|
||||||
|
await expect(readImageFile(storageRoot, { ...ref, attachmentId: 'bad' as never }))
|
||||||
|
.rejects.toMatchObject({ code: 'INVALID_ATTACHMENT_REF' })
|
||||||
|
|
||||||
|
const missingRoot = await root()
|
||||||
|
await mkdir(missingRoot, { recursive: true })
|
||||||
|
await expect(readImageFile(missingRoot, ref))
|
||||||
|
.rejects.toMatchObject({ code: 'ATTACHMENT_NOT_FOUND' })
|
||||||
|
|
||||||
|
const unreadableRoot = await root()
|
||||||
|
const target = join(unreadableRoot, 'objects', sha256.slice(0, 2), sha256)
|
||||||
|
await mkdir(target, { recursive: true })
|
||||||
|
await expect(readImageFile(unreadableRoot, ref))
|
||||||
|
.rejects.toMatchObject({ code: 'ATTACHMENT_READ_FAILED' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects conflicting existing objects and reference metadata mismatches', async () => {
|
||||||
|
const storageRoot = await root()
|
||||||
|
const sha256 = createHash('sha256').update(PNG).digest('hex')
|
||||||
|
const target = join(storageRoot, 'objects', sha256.slice(0, 2), sha256)
|
||||||
|
await mkdir(join(storageRoot, 'objects', sha256.slice(0, 2)), { recursive: true })
|
||||||
|
await writeFile(target, Uint8Array.of(1, 2, 3))
|
||||||
|
await expect(saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS))
|
||||||
|
.rejects.toMatchObject({ code: 'ATTACHMENT_CORRUPT' })
|
||||||
|
|
||||||
|
await writeFile(target, PNG)
|
||||||
|
const ref = await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
|
||||||
|
await expect(readImageFile(storageRoot, { ...ref, width: ref.width + 1 }))
|
||||||
|
.rejects.toMatchObject({ code: 'ATTACHMENT_CORRUPT' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('maps unexpected publication failures to a stable storage error', async () => {
|
||||||
|
const storageRoot = await root()
|
||||||
|
const sha256 = createHash('sha256').update(PNG).digest('hex')
|
||||||
|
const target = join(storageRoot, 'objects', sha256.slice(0, 2), sha256)
|
||||||
|
await mkdir(target, { recursive: true })
|
||||||
|
|
||||||
|
await expect(saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS))
|
||||||
|
.rejects.toMatchObject({ code: 'ATTACHMENT_WRITE_FAILED' })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": { "rootDir": "src", "outDir": "lib/types" },
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../../../vendor/cosmokit" },
|
||||||
|
{ "path": "../../../vendor/cordis" },
|
||||||
|
{ "path": "../attachment" },
|
||||||
|
{ "path": "../../util/paths" },
|
||||||
|
{ "path": "../../support/invariants" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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/attachment/attachment/README.md
|
||||||
|
README.md: 4f450316294e554396adb9a8454051a08d9befd3
|
||||||
|
README.zh.md: fe51b0003cdf1659c7c56106b97c6f3139ebe890
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# @deepseek-ai/dsh-attachment
|
||||||
|
|
||||||
|
English | [中文](README.zh.md)
|
||||||
|
|
||||||
|
The durable attachment seam. `ctx.attachments` validates and atomically commits immutable image bytes, then returns a serializable `ImageAttachmentRef`; consumers never persist browser paths, object URLs, provider URLs, or base64 in session events.
|
||||||
|
|
||||||
|
Unsent composer images remain browser-owned temporary drafts. `validateImage` runs the same admission policy without persisting; batch writers validate every member first so a malformed member cannot strand earlier members as unreferenced objects. `saveImage` commits each accepted image before any model-visible session event is published, and `readImage` verifies the content-addressed object against its logged metadata.
|
||||||
|
|
||||||
|
## Model Experience
|
||||||
|
|
||||||
|
Indirectly, through the role-neutral core `ImageBlock` and provider adapters that resolve its durable reference.
|
||||||
|
|
||||||
|
#### KV Cache effect
|
||||||
|
|
||||||
|
Adding an image changes the provider request and therefore invalidates the affected request suffix.
|
||||||
|
|
||||||
|
## Known Limitations and Deferred Work
|
||||||
|
|
||||||
|
- Version one accepts PNG, JPEG, WebP, and GIF only.
|
||||||
|
- Retention and garbage collection are deferred because resumed and forked sessions may share immutable objects.
|
||||||
|
- Generic files, audio, video, and persistent unsent drafts require separate lifecycle and provider contracts.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# @deepseek-ai/dsh-attachment
|
||||||
|
|
||||||
|
[English](README.md) | 中文
|
||||||
|
|
||||||
|
持久附件服务边界。`ctx.attachments` 校验并以原子方式提交不可变图片字节,随后返回可序列化的 `ImageAttachmentRef`;消费方绝不会在会话事件中持久保存浏览器路径、对象 URL、提供方 URL 或 base64。
|
||||||
|
|
||||||
|
未发送的输入区图片仍是由浏览器持有的临时草稿。`validateImage` 运行相同的准入策略,但不执行持久化;批量写入方会先校验每个成员,避免某个格式错误的成员使较早的成员成为无引用对象。`saveImage` 会在发布任何模型可见的会话事件前提交每张已接受的图片,`readImage` 则根据已记录的元数据校验内容寻址对象。
|
||||||
|
|
||||||
|
## 模型体验
|
||||||
|
|
||||||
|
该包通过角色无关的核心 `ImageBlock`,以及解析其持久引用的提供方适配器,间接影响模型。
|
||||||
|
|
||||||
|
#### KV 缓存影响
|
||||||
|
|
||||||
|
添加图片会改变提供方请求,因此会使受影响的请求后缀失效。
|
||||||
|
|
||||||
|
## 已知限制与待完成工作
|
||||||
|
|
||||||
|
- 第一版仅接受 PNG、JPEG、WebP 和 GIF。
|
||||||
|
- 保留策略与垃圾回收尚未实现,因为恢复和 fork 后的会话可能共享不可变对象。
|
||||||
|
- 通用文件、音频、视频和持久的未发送草稿需要单独的生命周期与提供方契约。
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@deepseek-ai/dsh-attachment",
|
||||||
|
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/types/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
|
||||||
|
"./invariant": { "types": "./lib/types/invariant.d.ts", "default": "./lib/invariant.js" },
|
||||||
|
"./src/*": "./src/*",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"files": ["lib/index.js", "lib/invariant.js", "lib/types/**/*.d.ts"],
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@deepseek-ai/dsh-brand": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||||
|
"cordis": "^4.0.0-rc.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||||
|
"cordis": "^4.0.0-rc.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/** Attachment identifier brand. @module @deepseek-ai/dsh-attachment/brand */
|
||||||
|
|
||||||
|
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||||
|
|
||||||
|
/** Opaque content-addressed identifier for one immutable attachment object. */
|
||||||
|
export type AttachmentId = Branded<'AttachmentId'>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Brand a validated storage identifier.
|
||||||
|
* @param value - backend-produced opaque identifier.
|
||||||
|
* @returns the branded identifier.
|
||||||
|
*/
|
||||||
|
export function AttachmentId(value: string): AttachmentId {
|
||||||
|
return value as AttachmentId
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/** Attachment failure class. @module @deepseek-ai/dsh-attachment/error */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stable failures suitable for host RPC error mapping.
|
||||||
|
*
|
||||||
|
* Deliberately re-implements the `HarnessError` shape instead of extending it:
|
||||||
|
* the base lives in `@deepseek-ai/dsh-llm`, which itself depends on this
|
||||||
|
* package (`ImageBlock` references `ImageAttachmentRef`), so sharing the base
|
||||||
|
* would create a dependency cycle. Consumers route on `code`, never on the
|
||||||
|
* prototype chain, so the shapes stay interchangeable at the wire boundary.
|
||||||
|
*/
|
||||||
|
export class AttachmentError extends Error {
|
||||||
|
/** Stable machine-routing failure code. */
|
||||||
|
readonly code: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param message - human-readable failure description without raw bytes or host paths.
|
||||||
|
* @param code - stable machine-routing code.
|
||||||
|
* @param options - optional chained cause.
|
||||||
|
*/
|
||||||
|
constructor(message: string, code: string, options?: ErrorOptions) {
|
||||||
|
super(message, options)
|
||||||
|
this.name = 'AttachmentError'
|
||||||
|
this.code = code
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/** Durable attachment storage seam (`ctx.attachments`). @module @deepseek-ai/dsh-attachment */
|
||||||
|
|
||||||
|
import { Context, Service } from 'cordis'
|
||||||
|
import type {
|
||||||
|
ImageAttachmentLimits,
|
||||||
|
ImageAttachmentRef,
|
||||||
|
SaveImageAttachment,
|
||||||
|
StoredImageAttachment,
|
||||||
|
} from './types.ts'
|
||||||
|
|
||||||
|
export { AttachmentId } from './brand.ts'
|
||||||
|
export { AttachmentError } from './error.ts'
|
||||||
|
export type {
|
||||||
|
AttachmentId as AttachmentIdType,
|
||||||
|
ImageAttachmentLimits,
|
||||||
|
ImageAttachmentRef,
|
||||||
|
ImageMediaType,
|
||||||
|
SaveImageAttachment,
|
||||||
|
StoredImageAttachment,
|
||||||
|
} from './types.ts'
|
||||||
|
|
||||||
|
declare module 'cordis' {
|
||||||
|
interface Context {
|
||||||
|
attachments: AttachmentStore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Immutable binary attachment service. Implementations validate bytes before publishing a reference. */
|
||||||
|
export abstract class AttachmentStore extends Service {
|
||||||
|
constructor(ctx: Context) {
|
||||||
|
super(ctx, 'attachments')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deployment-resolved image policy used by authoritative and fast-path validation. */
|
||||||
|
abstract readonly imageLimits: ImageAttachmentLimits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate one image without persisting it.
|
||||||
|
* Batch callers validate every member before saving any member.
|
||||||
|
* @param input - encoded bytes, declared media type, and optional display name.
|
||||||
|
* @returns completion after the encoded raster has been fully decoded.
|
||||||
|
*/
|
||||||
|
abstract validateImage(input: SaveImageAttachment): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and durably commit one image before its owning session event is appended.
|
||||||
|
* @param input - encoded bytes, declared media type, and optional display name.
|
||||||
|
* @returns a durable content-addressed reference.
|
||||||
|
*/
|
||||||
|
abstract saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read one image and verify that bytes still match the recorded reference.
|
||||||
|
* @param ref - durable reference from the session log.
|
||||||
|
* @returns the verified bytes and canonical reference.
|
||||||
|
*/
|
||||||
|
abstract readImage(ref: ImageAttachmentRef): Promise<StoredImageAttachment>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttachmentStore
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/** Package-owned invariant companion for `@deepseek-ai/dsh-attachment`. @module @deepseek-ai/dsh-attachment/invariant */
|
||||||
|
|
||||||
|
/* jscpd:ignore-start */
|
||||||
|
import type { Context } from 'cordis'
|
||||||
|
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||||
|
|
||||||
|
const PACKAGE_NAME = '@deepseek-ai/dsh-attachment'
|
||||||
|
/** Cordis companion plugin name. */
|
||||||
|
export const name = 'attachment-invariant'
|
||||||
|
/** Service required before package ownership can be reserved. */
|
||||||
|
export const inject = ['invariants']
|
||||||
|
/** No runtime invariant: this stateless seam owns types while implementations enforce immutable-store checks. */
|
||||||
|
const install: InvariantInstaller = () => {}
|
||||||
|
/**
|
||||||
|
* Register the package invariant companion.
|
||||||
|
* @param ctx - Cordis context carrying the invariant service.
|
||||||
|
* @returns the registration disposer.
|
||||||
|
*/
|
||||||
|
export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||||
|
/* jscpd:ignore-end */
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/** Durable attachment vocabulary. @module @deepseek-ai/dsh-attachment/types */
|
||||||
|
|
||||||
|
import type { AttachmentId } from './brand.ts'
|
||||||
|
|
||||||
|
export type { AttachmentId } from './brand.ts'
|
||||||
|
|
||||||
|
/** Raster image formats accepted by the version-one attachment path. */
|
||||||
|
export type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
|
||||||
|
|
||||||
|
/** Durable, serializable metadata for one immutable image object. */
|
||||||
|
export interface ImageAttachmentRef {
|
||||||
|
/** Opaque storage identifier; never a filesystem path or bearer URL. */
|
||||||
|
attachmentId: AttachmentId
|
||||||
|
/** Media type verified from the stored bytes. */
|
||||||
|
mediaType: ImageMediaType
|
||||||
|
/** Exact encoded byte length. */
|
||||||
|
bytes: number
|
||||||
|
/** Intrinsic encoded width in pixels. */
|
||||||
|
width: number
|
||||||
|
/** Intrinsic encoded height in pixels. */
|
||||||
|
height: number
|
||||||
|
/** Optional display name stripped of local path information. */
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deployment-resolved limits used by upload admission and request buffering. */
|
||||||
|
export interface ImageAttachmentLimits {
|
||||||
|
maxImageBytes: number
|
||||||
|
maxImagesPerMessage: number
|
||||||
|
maxMessageImageBytes: number
|
||||||
|
maxImagePixels: number
|
||||||
|
mediaTypes: readonly ImageMediaType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Request to validate and durably commit one image. */
|
||||||
|
export interface SaveImageAttachment {
|
||||||
|
data: Uint8Array
|
||||||
|
/** Caller-declared media type, checked against fully decoded bytes. */
|
||||||
|
mediaType: ImageMediaType
|
||||||
|
/** Optional browser/provider display name; it is never interpreted as a path. */
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stored image bytes returned after reference and digest verification. */
|
||||||
|
export interface StoredImageAttachment {
|
||||||
|
ref: ImageAttachmentRef
|
||||||
|
data: Uint8Array
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": { "rootDir": "src", "outDir": "lib/types" },
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../../../vendor/cosmokit" },
|
||||||
|
{ "path": "../../../vendor/cordis" },
|
||||||
|
{ "path": "../../util/brand" },
|
||||||
|
{ "path": "../../support/invariants" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -107,6 +107,12 @@
|
|||||||
config:
|
config:
|
||||||
root: !!js dshHomePath('sessions')
|
root: !!js dshHomePath('sessions')
|
||||||
|
|
||||||
|
# Durable image bytes live outside the append-only session log. Messages
|
||||||
|
# keep content-addressed references that this shared backend resolves for
|
||||||
|
# provider requests and authorized history reads.
|
||||||
|
- id: attachment-local
|
||||||
|
name: '@deepseek-ai/dsh-attachment-local'
|
||||||
|
|
||||||
# Raw configs can supply a process-local path or disable this shared session
|
# Raw configs can supply a process-local path or disable this shared session
|
||||||
# capability. The neutral default is process-local and opens only when used.
|
# capability. The neutral default is process-local and opens only when used.
|
||||||
- id: session-query-sqlite
|
- id: session-query-sqlite
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||||
"@deepseek-ai/dsh-agent-default-model": "workspace:^",
|
"@deepseek-ai/dsh-agent-default-model": "workspace:^",
|
||||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-attachment-local": "workspace:^",
|
||||||
"@deepseek-ai/dsh-bash-env": "workspace:^",
|
"@deepseek-ai/dsh-bash-env": "workspace:^",
|
||||||
"@deepseek-ai/dsh-bash-sandbox": "workspace:^",
|
"@deepseek-ai/dsh-bash-sandbox": "workspace:^",
|
||||||
"@deepseek-ai/dsh-command-compact": "workspace:^",
|
"@deepseek-ai/dsh-command-compact": "workspace:^",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
},
|
},
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||||
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
|
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
|
||||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
// The ./api and ./client subpath exports are the browser-safe channels.
|
// The ./api and ./client subpath exports are the browser-safe channels.
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, PromptContentPart, HostApi, EventsApi, MuxFrame, HostFrame,
|
||||||
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
||||||
DirectoryEntry, DirectoryListing,
|
DirectoryEntry, DirectoryListing,
|
||||||
WorkspaceApi, WorkspaceId, WorkspaceView,
|
ResponseValue, WorkspaceApi, WorkspaceId, WorkspaceView,
|
||||||
CommandsApi, CommandDescriptor, SkillsApi, SkillEntry,
|
CommandsApi, CommandDescriptor, SkillsApi, SkillEntry,
|
||||||
ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
|
ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
|
||||||
ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels,
|
ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels,
|
||||||
@@ -37,6 +37,9 @@ export type { SessionId, SessionEvent } from '@deepseek-ai/dsh-session/types'
|
|||||||
export type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
export type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
||||||
export type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm/types'
|
export type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm/types'
|
||||||
|
|
||||||
|
/** Successful value returned by the connection-generation host handshake. */
|
||||||
|
export type HostDescription = import('@deepseek-ai/dsh-host-apiproxy/api').ResponseValue<'host.describe'>
|
||||||
|
|
||||||
import type { RpcResponse, RpcResult } from '@deepseek-ai/dsh-host-apiproxy/api'
|
import type { RpcResponse, RpcResult } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -131,11 +131,15 @@ export class ConnectionController {
|
|||||||
// subscribed baseline. The timeout guards against a carrier that never fires onOpen
|
// subscribed baseline. The timeout guards against a carrier that never fires onOpen
|
||||||
// (see ConnectionConfig.streamOpenTimeoutMs).
|
// (see ConnectionConfig.streamOpenTimeoutMs).
|
||||||
const timeout = new AbortController()
|
const timeout = new AbortController()
|
||||||
await Promise.all([
|
const [description] = await Promise.all([
|
||||||
this.api.host.describe({}),
|
this.api.host.describe({}),
|
||||||
Promise.race([streamsOpen, sleep(this.config.streamOpenTimeoutMs, timeout.signal)]),
|
Promise.race([streamsOpen, sleep(this.config.streamOpenTimeoutMs, timeout.signal)]),
|
||||||
])
|
])
|
||||||
timeout.abort()
|
timeout.abort()
|
||||||
|
const descriptionResult = description.result
|
||||||
|
if (!descriptionResult.ok) {
|
||||||
|
throw new Error(`host.describe failed: ${descriptionResult.error.code}: ${descriptionResult.error.message}`)
|
||||||
|
}
|
||||||
if (ac.signal.aborted) throw new Error('generation aborted during readiness handshake')
|
if (ac.signal.aborted) throw new Error('generation aborted during readiness handshake')
|
||||||
this.attempt = 0
|
this.attempt = 0
|
||||||
this.emitState('connected')
|
this.emitState('connected')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// FixtureApi: standalone UI development without a server. Real contract shape: unary takes
|
// FixtureApi: standalone UI development without a server. Real contract shape: unary takes
|
||||||
// RpcRequest<P> and returns RpcResponse<T> (echoing the rpcId); streams yield RpcRequest<frame>
|
// RpcRequest<P> and returns RpcResponse<T> (echoing the rpcId); streams yield RpcRequest<frame>
|
||||||
// (the fixture IS the fake server, so it mints frame rpcIds); root respond takes ClientResponse
|
// (the fixture IS the fake server, so it mints frame rpcIds); root respond takes ClientResponse
|
||||||
// and returns RpcReceipt. fx-alpha carries a hand-built history script (60 turns, pageable);
|
// and returns RpcReceipt. fx-alpha carries a hand-built history script (74 turns, pageable);
|
||||||
// prompt triggers a chunked streaming replay; cancel stops the replay; resident pending
|
// prompt triggers a chunked streaming replay; cancel stops the replay; resident pending
|
||||||
// approval/question requests exercise replay and composer takeover with stable rpcIds.
|
// approval/question requests exercise replay and composer takeover with stable rpcIds.
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
ToolResultMessage,
|
ToolResultMessage,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
} from '@deepseek-ai/dsh-llm'
|
} from '@deepseek-ai/dsh-llm'
|
||||||
|
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||||
import type {
|
import type {
|
||||||
SessionEvent,
|
SessionEvent,
|
||||||
SessionId,
|
SessionId,
|
||||||
@@ -51,10 +52,10 @@ function userMessage(content: ContentBlock[], source: MessageSource = { kind: 'u
|
|||||||
return createUserMessage({ content, source })
|
return createUserMessage({ content, source })
|
||||||
}
|
}
|
||||||
|
|
||||||
function assistantMessage(content: ContentBlock[]): AssistantMessage {
|
function assistantMessage(content: ContentBlock[], model = 'fx-1'): AssistantMessage {
|
||||||
return createAssistantMessage({
|
return createAssistantMessage({
|
||||||
content,
|
content,
|
||||||
source: { provider: 'fixture', model: 'fx-1' },
|
source: { provider: 'fixture', model },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +331,16 @@ function sid(id: string): SessionId {
|
|||||||
return id as 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. */
|
/** Deterministic provider billing attached to fixture assistant messages. */
|
||||||
function fixtureUsage(turn: number, step: number): TokenUsage {
|
function fixtureUsage(turn: number, step: number): TokenUsage {
|
||||||
return {
|
return {
|
||||||
@@ -340,7 +351,7 @@ function fixtureUsage(turn: number, step: number): TokenUsage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** fx-alpha history script: 60 turns (~130+ messages -> 3 pages at PAGE_MESSAGES=50),
|
/** fx-alpha history script: 74 turns (~150+ messages -> 4 pages at PAGE_MESSAGES=50),
|
||||||
* mixing reasoning blocks / tool call+result / context. */
|
* mixing reasoning blocks / tool call+result / context. */
|
||||||
function buildAlphaLog(): SessionEvent[] {
|
function buildAlphaLog(): SessionEvent[] {
|
||||||
const events: Record<string, unknown>[] = []
|
const events: Record<string, unknown>[] = []
|
||||||
@@ -476,7 +487,7 @@ function buildAlphaLog(): SessionEvent[] {
|
|||||||
push({ type: 'step/end', data: { turn, step: 0 } })
|
push({ type: 'step/end', data: { turn, step: 0 } })
|
||||||
push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
|
push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
|
||||||
}
|
}
|
||||||
// Turn 72: todo_write sample — the TodoRow toolview in the flow plus the
|
// Turn 73: todo_write sample — the TodoRow toolview in the flow plus the
|
||||||
// todo/write snapshot event feeding the TodoPanel plan strip. Two items are
|
// todo/write snapshot event feeding the TodoPanel plan strip. Two items are
|
||||||
// in_progress: this fixture chooses the parallel policy, so both surfaces
|
// in_progress: this fixture chooses the parallel policy, so both surfaces
|
||||||
// must render a parallel plan rather than the first active item alone.
|
// must render a parallel plan rather than the first active item alone.
|
||||||
@@ -535,8 +546,32 @@ function buildAlphaLog(): SessionEvent[] {
|
|||||||
toolTurn(70, 'web_search', '{"query":"deepseek harness architecture"}', 'Search results for deepseek harness architecture.')
|
toolTurn(70, 'web_search', '{"query":"deepseek harness architecture"}', 'Search results for deepseek harness architecture.')
|
||||||
toolTurn(71, 'web_fetch', '{"url":"https://www.deepseek.com/blog/harness-architecture"}', '# Harness architecture\n\nEverything is a plugin.')
|
toolTurn(71, 'web_fetch', '{"url":"https://www.deepseek.com/blog/harness-architecture"}', '# Harness architecture\n\nEverything is a plugin.')
|
||||||
|
|
||||||
|
// Turn 72: 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: 72 } })
|
||||||
|
push({
|
||||||
|
type: 'user/message',
|
||||||
|
surfaceOp: 'append',
|
||||||
|
data: userMessage([{ type: 'image', attachment: FIXTURE_IMAGE_REF }, ...text('历史用户图片')]),
|
||||||
|
})
|
||||||
|
push({ type: 'step/start', data: { turn: 72, step: 0 } })
|
||||||
|
push({
|
||||||
|
type: 'assistant/message',
|
||||||
|
surfaceOp: 'append',
|
||||||
|
data: {
|
||||||
|
turn: 72,
|
||||||
|
step: 0,
|
||||||
|
message: assistantMessage(
|
||||||
|
[...text('结构化模型图片:'), { type: 'image', attachment: FIXTURE_IMAGE_REF }],
|
||||||
|
'fx-vision',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
push({ type: 'step/end', data: { turn: 72, step: 0 } })
|
||||||
|
push({ type: 'turn/end', data: { turn: 72, reason: { kind: 'completed' } } })
|
||||||
|
|
||||||
const todoArgs = JSON.stringify({ todos: fixtureTodos })
|
const todoArgs = JSON.stringify({ todos: fixtureTodos })
|
||||||
toolTurn(72, 'todo_write', todoArgs, 'Updated todo list: 1 pending, 2 in progress, 1 completed.')
|
toolTurn(73, '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
|
// The real tool appends the snapshot mid-execution — between tool/call and
|
||||||
// tool/result — so the fixture reproduces that exact ordering (the last
|
// tool/result — so the fixture reproduces that exact ordering (the last
|
||||||
// toolTurn events run ... tool/call, tool/result, step/end, turn/end).
|
// toolTurn events run ... tool/call, tool/result, step/end, turn/end).
|
||||||
@@ -855,7 +890,6 @@ function estimateFixtureContent(blocks: readonly ContentBlock[]): number {
|
|||||||
// ContentBlockMap is merge-extensible: this client graph sees only the
|
// ContentBlockMap is merge-extensible: this client graph sees only the
|
||||||
// base four members, but fixture turns do carry extended blocks at
|
// base four members, but fixture turns do carry extended blocks at
|
||||||
// runtime, so the structural JSON fallback below is live code.
|
// runtime, so the structural JSON fallback below is live code.
|
||||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- the type collapses without the out-of-graph merges (see above).
|
|
||||||
if (block.type === 'tool-result') {
|
if (block.type === 'tool-result') {
|
||||||
return tokens + estimateFixtureContent(block.content) + BLOCK_OVERHEAD
|
return tokens + estimateFixtureContent(block.content) + BLOCK_OVERHEAD
|
||||||
}
|
}
|
||||||
@@ -1057,6 +1091,18 @@ function pageOf(
|
|||||||
return { events, hasMore: start > 0 }
|
return { events, 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<string, unknown>
|
||||||
|
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. */
|
/** Fixture mirror of first-party message extraction used by session-query. */
|
||||||
function searchBlockText(block: ContentBlock): string[] {
|
function searchBlockText(block: ContentBlock): string[] {
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
@@ -1351,6 +1397,10 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
|||||||
session.sessionId,
|
session.sessionId,
|
||||||
{ provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
{ provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||||
]))
|
]))
|
||||||
|
const attachments = new Map<string, { attachment: ImageAttachmentRef; data: string }>([[
|
||||||
|
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. */
|
/** Credential store double: set/unset flip the describe badge, values never read back. */
|
||||||
const fixtureCredentials = new Map<string, true>([
|
const fixtureCredentials = new Map<string, true>([
|
||||||
// The assembled fixture represents an already-configured shipped
|
// The assembled fixture represents an already-configured shipped
|
||||||
@@ -1368,7 +1418,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
|||||||
['my-agent', { trust: 'user', content: "- id: tool-read\n name: '@deepseek-ai/dsh-tool-read'\n" }],
|
['my-agent', { trust: 'user', content: "- id: tool-read\n name: '@deepseek-ai/dsh-tool-read'\n" }],
|
||||||
])
|
])
|
||||||
let fixtureDefaultPreset = 'standard'
|
let fixtureDefaultPreset = 'standard'
|
||||||
const nextTurn = new Map<SessionId, number>([[sid('fx-alpha'), 60]])
|
const nextTurn = new Map<SessionId, number>([[sid('fx-alpha'), 74]])
|
||||||
let nextSession = 1
|
let nextSession = 1
|
||||||
let nextRpc = 1
|
let nextRpc = 1
|
||||||
let attachedSessions = options.empty ? 0 : 1
|
let attachedSessions = options.empty ? 0 : 1
|
||||||
@@ -2145,9 +2195,26 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
|||||||
// First accepted prompt appends events: the summary stops being blank.
|
// First accepted prompt appends events: the summary stops being blank.
|
||||||
summary.blank = false
|
summary.blank = false
|
||||||
const userText = content.map(b => (b.type === 'text' ? b.text : '')).join('')
|
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 }
|
||||||
|
})
|
||||||
if (mode === 'steer' && replays.has(id)) {
|
if (mode === 'steer' && replays.has(id)) {
|
||||||
// Steering: the durable user/message lands inside the current turn; the replay continues.
|
// Steering: the durable user/message lands inside the current turn; the replay continues.
|
||||||
append(id, { type: 'user/message', surfaceOp: 'append', data: userMessage(content) })
|
append(id, { type: 'user/message', surfaceOp: 'append', data: userMessage(durable) })
|
||||||
return ok(request, { accepted: true as const })
|
return ok(request, { accepted: true as const })
|
||||||
}
|
}
|
||||||
const turn = nextTurn.get(id) ?? 0
|
const turn = nextTurn.get(id) ?? 0
|
||||||
@@ -2160,7 +2227,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
|||||||
if (plan.wanted !== null && plan.wanted !== plan.active) {
|
if (plan.wanted !== null && plan.wanted !== plan.active) {
|
||||||
append(id, { type: 'plan/mode', data: { active: plan.wanted } })
|
append(id, { type: 'plan/mode', data: { active: plan.wanted } })
|
||||||
}
|
}
|
||||||
append(id, { type: 'user/message', surfaceOp: 'append', data: userMessage(content) })
|
append(id, { type: 'user/message', surfaceOp: 'append', data: userMessage(durable) })
|
||||||
// Capacity parallel of the host token-meter's request/context record:
|
// Capacity parallel of the host token-meter's request/context record:
|
||||||
// log-only, appended inside the open turn, and deduplicated against the
|
// log-only, appended inside the open turn, and deduplicated against the
|
||||||
// route already recorded (the fixture never varies contextWindow).
|
// route already recorded (the fixture never varies contextWindow).
|
||||||
@@ -2186,6 +2253,27 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
|||||||
)
|
)
|
||||||
return ok(request, { accepted: true as const })
|
return ok(request, { accepted: true as const })
|
||||||
},
|
},
|
||||||
|
attachment: (request) => {
|
||||||
|
const stored = attachments.get(String(request.payload.attachmentId))
|
||||||
|
if (stored === undefined) {
|
||||||
|
return err(request, {
|
||||||
|
code: 'attachment-error',
|
||||||
|
message: 'fixture attachment missing',
|
||||||
|
details: { reason: 'ATTACHMENT_NOT_FOUND' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!logReferencesAttachment(
|
||||||
|
logs.get(request.payload.sessionId) ?? [],
|
||||||
|
String(request.payload.attachmentId),
|
||||||
|
)) {
|
||||||
|
return err(request, {
|
||||||
|
code: 'attachment-error',
|
||||||
|
message: 'fixture attachment is not referenced by this session',
|
||||||
|
details: { reason: 'ATTACHMENT_NOT_REFERENCED' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ok(request, stored)
|
||||||
|
},
|
||||||
updateQueue: request => err(request, {
|
updateQueue: request => err(request, {
|
||||||
code: 'queue-item-not-found',
|
code: 'queue-item-not-found',
|
||||||
message: 'fixture has no pending queue item',
|
message: 'fixture has no pending queue item',
|
||||||
@@ -2837,6 +2925,7 @@ export class FixtureApiClient extends AbstractApiClient {
|
|||||||
case 'session.rename': return this.api.sessions.rename(request)
|
case 'session.rename': return this.api.sessions.rename(request)
|
||||||
case 'session.fork': return this.api.sessions.fork(request)
|
case 'session.fork': return this.api.sessions.fork(request)
|
||||||
case 'session.prompt': return this.api.sessions.prompt(request)
|
case 'session.prompt': return this.api.sessions.prompt(request)
|
||||||
|
case 'session.attachment': return this.api.sessions.attachment(request)
|
||||||
case 'session.updateQueue': return this.api.sessions.updateQueue(request)
|
case 'session.updateQueue': return this.api.sessions.updateQueue(request)
|
||||||
case 'session.cancel': return this.api.sessions.cancel(request)
|
case 'session.cancel': return this.api.sessions.cancel(request)
|
||||||
case 'subagent.list': return this.api.subagents.list(request)
|
case 'subagent.list': return this.api.subagents.list(request)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type { ClientConnectionRpc } from '../rpc.ts'
|
|||||||
|
|
||||||
// ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
|
// ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
|
||||||
export type {
|
export type {
|
||||||
ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, PromptContentPart, HostApi, EventsApi, MuxFrame, HostFrame,
|
||||||
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
||||||
DirectoryEntry, DirectoryListing,
|
DirectoryEntry, DirectoryListing,
|
||||||
ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView,
|
ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView,
|
||||||
@@ -24,7 +24,7 @@ export type {
|
|||||||
SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt,
|
SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt,
|
||||||
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
||||||
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
|
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
|
||||||
IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
|
HostDescription, IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
|
||||||
GoalsApi, GoalRef,
|
GoalsApi, GoalRef,
|
||||||
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
|
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
|
||||||
CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi,
|
CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi,
|
||||||
|
|||||||
@@ -21,8 +21,14 @@ export interface FetchHandler {
|
|||||||
* @param req - incoming node:http request (fully read before dispatch).
|
* @param req - incoming node:http request (fully read before dispatch).
|
||||||
* @param res - node:http response the bridge writes and owns to completion.
|
* @param res - node:http response the bridge writes and owns to completion.
|
||||||
* @param apiHandler - fetch-shaped API carrier the request is dispatched to.
|
* @param apiHandler - fetch-shaped API carrier the request is dispatched to.
|
||||||
|
* @param maxRequestBodyBytes - maximum body bytes buffered before dispatch.
|
||||||
*/
|
*/
|
||||||
export async function bridge(req: IncomingMessage, res: ServerResponse, apiHandler: FetchHandler): Promise<void> {
|
export async function bridge(
|
||||||
|
req: IncomingMessage,
|
||||||
|
res: ServerResponse,
|
||||||
|
apiHandler: FetchHandler,
|
||||||
|
maxRequestBodyBytes = 32 * 1024 * 1024,
|
||||||
|
): Promise<void> {
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
// Client-disconnect detection MUST hang off the response, not the request:
|
// Client-disconnect detection MUST hang off the response, not the request:
|
||||||
// since Node 16, IncomingMessage 'close' fires as soon as the request body is
|
// since Node 16, IncomingMessage 'close' fires as soon as the request body is
|
||||||
@@ -32,8 +38,26 @@ export async function bridge(req: IncomingMessage, res: ServerResponse, apiHandl
|
|||||||
res.on('close', () => {
|
res.on('close', () => {
|
||||||
if (!res.writableEnded) abort.abort()
|
if (!res.writableEnded) abort.abort()
|
||||||
})
|
})
|
||||||
|
const declaredLength = req.headers['content-length']
|
||||||
|
if (declaredLength !== undefined && Number(declaredLength) > maxRequestBodyBytes) {
|
||||||
|
res.writeHead(413, { connection: 'close' })
|
||||||
|
res.end()
|
||||||
|
req.destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
const chunks: Buffer[] = []
|
const chunks: Buffer[] = []
|
||||||
for await (const chunk of req) chunks.push(chunk as Buffer)
|
let received = 0
|
||||||
|
for await (const chunk of req) {
|
||||||
|
const buffer = chunk as Buffer
|
||||||
|
received += buffer.byteLength
|
||||||
|
if (received > maxRequestBodyBytes) {
|
||||||
|
res.writeHead(413, { connection: 'close' })
|
||||||
|
res.end()
|
||||||
|
req.destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chunks.push(buffer)
|
||||||
|
}
|
||||||
/* v8 ignore next 3 -- `??` arms: node:http always sets url/method on server
|
/* v8 ignore next 3 -- `??` arms: node:http always sets url/method on server
|
||||||
requests; the fields are only optional on the client-side IncomingMessage type */
|
requests; the fields are only optional on the client-side IncomingMessage type */
|
||||||
const request = new Request(new URL(req.url ?? '/', 'http://dsh.internal'), {
|
const request = new Request(new URL(req.url ?? '/', 'http://dsh.internal'), {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/** Host HTTP bridge for browser-client RPC. */
|
/** Host HTTP bridge for browser-client RPC. */
|
||||||
import type { Context } from 'cordis'
|
import type { Context } from 'cordis'
|
||||||
import z from 'schemastery'
|
import z from 'schemastery'
|
||||||
|
import type {} from '@deepseek-ai/dsh-attachment'
|
||||||
// Activates the httpServer Context merge used below.
|
// Activates the httpServer Context merge used below.
|
||||||
import type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
|
import type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
|
||||||
import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy'
|
import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy'
|
||||||
@@ -25,6 +26,25 @@ export { API_PATH, HOST_EVENTS_PATH, MUX_EVENTS_PATH } from './api-path.ts'
|
|||||||
/** Stable Cordis plugin name. */
|
/** Stable Cordis plugin name. */
|
||||||
export const name = 'client-connection'
|
export const name = 'client-connection'
|
||||||
|
|
||||||
|
/** Headroom for RPC JSON fields around aggregate base64 image payloads. */
|
||||||
|
const REQUEST_ENVELOPE_HEADROOM_BYTES = 1024 * 1024
|
||||||
|
|
||||||
|
function assertImageBodyCapacity(ctx: Context, maxRequestBodyBytes: number): void {
|
||||||
|
const attachments = ctx.get('attachments')
|
||||||
|
if (attachments === undefined) return
|
||||||
|
const requiredImageBodyBytes = Math.ceil(
|
||||||
|
attachments.imageLimits.maxMessageImageBytes * 4 / 3,
|
||||||
|
) + REQUEST_ENVELOPE_HEADROOM_BYTES
|
||||||
|
if (maxRequestBodyBytes < requiredImageBodyBytes) {
|
||||||
|
throw new Error(
|
||||||
|
`client-connection maxRequestBodyBytes (${String(maxRequestBodyBytes)}) must be at least `
|
||||||
|
+ `${String(requiredImageBodyBytes)} for the configured aggregate image limit`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Default carrier cap for all HTTP RPC bodies. */
|
||||||
|
const DEFAULT_MAX_REQUEST_BODY_BYTES = 32 * 1024 * 1024
|
||||||
|
|
||||||
/** Services required before providing Connection; API Proxy is an optional `/api` fallback. */
|
/** Services required before providing Connection; API Proxy is an optional `/api` fallback. */
|
||||||
export const inject = ['httpServer']
|
export const inject = ['httpServer']
|
||||||
|
|
||||||
@@ -39,10 +59,13 @@ export interface ConnectionConfig {
|
|||||||
* that is not a bare, canonical authority fails the plugin load.
|
* that is not a bare, canonical authority fails the plugin load.
|
||||||
*/
|
*/
|
||||||
trustedHosts?: string[]
|
trustedHosts?: string[]
|
||||||
|
/** Maximum buffered JSON body for every `/api` request. */
|
||||||
|
maxRequestBodyBytes?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Config: z<ConnectionConfig> = z.object({
|
export const Config: z<ConnectionConfig> = z.object({
|
||||||
trustedHosts: z.array(String).default([]),
|
trustedHosts: z.array(String).default([]),
|
||||||
|
maxRequestBodyBytes: z.natural().min(1).default(DEFAULT_MAX_REQUEST_BODY_BYTES),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,9 +132,11 @@ const PRIVILEGED_METHODS = new Set([
|
|||||||
export function apply(ctx: Context, config?: ConnectionConfig): void {
|
export function apply(ctx: Context, config?: ConnectionConfig): void {
|
||||||
// The Loader resolves schema defaults; hand-built test contexts may pass none.
|
// The Loader resolves schema defaults; hand-built test contexts may pass none.
|
||||||
const trustedHosts = config?.trustedHosts ?? []
|
const trustedHosts = config?.trustedHosts ?? []
|
||||||
|
const maxRequestBodyBytes = config?.maxRequestBodyBytes ?? DEFAULT_MAX_REQUEST_BODY_BYTES
|
||||||
// Config boundary: a malformed entry fails the load loudly here rather than
|
// Config boundary: a malformed entry fails the load loudly here rather than
|
||||||
// silently authorizing its hostname prefix at request time.
|
// silently authorizing its hostname prefix at request time.
|
||||||
for (const entry of trustedHosts) assertTrustedAuthority(entry)
|
for (const entry of trustedHosts) assertTrustedAuthority(entry)
|
||||||
|
if (ctx.get('apiProxy') !== undefined) assertImageBodyCapacity(ctx, maxRequestBodyBytes)
|
||||||
const connection = new HostConnectionService(ctx, trustedHosts)
|
const connection = new HostConnectionService(ctx, trustedHosts)
|
||||||
const fetchHandler = connection.createSharedFetchHandler(API_PATH, {
|
const fetchHandler = connection.createSharedFetchHandler(API_PATH, {
|
||||||
async fetch(request) {
|
async fetch(request) {
|
||||||
@@ -144,11 +169,12 @@ export function apply(ctx: Context, config?: ConnectionConfig): void {
|
|||||||
res.end('forbidden')
|
res.end('forbidden')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bridge(req, res, fetchHandler)
|
await bridge(req, res, fetchHandler, maxRequestBodyBytes)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ctx.effect(() => ctx.httpServer.register(route), 'client-connection: /api route')
|
ctx.effect(() => ctx.httpServer.register(route), 'client-connection: /api route')
|
||||||
ctx.inject(['apiProxy'], (apiCtx) => {
|
ctx.inject(['apiProxy'], (apiCtx) => {
|
||||||
|
assertImageBodyCapacity(apiCtx, maxRequestBodyBytes)
|
||||||
const downlinks = new WebSocketDownlinks(apiCtx.apiProxy)
|
const downlinks = new WebSocketDownlinks(apiCtx.apiProxy)
|
||||||
const registerDownlink = (
|
const registerDownlink = (
|
||||||
path: string,
|
path: string,
|
||||||
|
|||||||
@@ -83,6 +83,35 @@ describe('connection lifecycle', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('treats a host.describe business error as generation failure', async () => {
|
||||||
|
const api = new FakeApiClient()
|
||||||
|
let describeCalls = 0
|
||||||
|
api.onDescribe = () => {
|
||||||
|
describeCalls += 1
|
||||||
|
if (describeCalls === 1) {
|
||||||
|
return Promise.resolve({
|
||||||
|
rpcId: 'bad-describe' as never,
|
||||||
|
result: {
|
||||||
|
ok: false as const,
|
||||||
|
error: { code: 'internal' as const, message: 'not ready', details: {} },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Promise.resolve(ok({ version: '0', cwd: '/f', attachedSessions: 0 }))
|
||||||
|
}
|
||||||
|
let connected = 0
|
||||||
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||||
|
const controller = new ConnectionController(api, { onConnected: () => { connected++ } }, FAST)
|
||||||
|
controller.start()
|
||||||
|
try {
|
||||||
|
await vi.waitFor(() => { expect(describeCalls).toBe(2) })
|
||||||
|
await vi.waitFor(() => { expect(connected).toBe(1) })
|
||||||
|
} finally {
|
||||||
|
controller.stop()
|
||||||
|
warnSpy.mockRestore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('converges stream/error frames into reconnect instead of dispatching them', async () => {
|
it('converges stream/error frames into reconnect instead of dispatching them', async () => {
|
||||||
const api = new FakeApiClient()
|
const api = new FakeApiClient()
|
||||||
const muxSeen: string[] = []
|
const muxSeen: string[] = []
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export class FakeApiClient implements IApiClient {
|
|||||||
=> Promise<RpcResponse<{ selected: ModelSelection }>> =
|
=> Promise<RpcResponse<{ selected: ModelSelection }>> =
|
||||||
payload => Promise.resolve(ok({ selected: { provider: payload.provider, model: payload.model } }))
|
payload => Promise.resolve(ok({ selected: { provider: payload.provider, model: payload.model } }))
|
||||||
onPrompt: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
onPrompt: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||||
|
onAttachment: (payload: unknown) => Promise<RpcResponse<{ attachment: { attachmentId: never; mediaType: 'image/png'; bytes: number; width: number; height: number }; data: string }>> =
|
||||||
|
() => Promise.resolve(ok({ attachment: { attachmentId: 'a' as never, mediaType: 'image/png', bytes: 1, width: 1, height: 1 }, data: 'AA==' }))
|
||||||
onUpdateQueue: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
onUpdateQueue: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||||
onCancel: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
onCancel: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||||
onDescribe: (payload: unknown) => Promise<RpcResponse<{ version: string; cwd: string; attachedSessions: number }>> =
|
onDescribe: (payload: unknown) => Promise<RpcResponse<{ version: string; cwd: string; attachedSessions: number }>> =
|
||||||
@@ -110,6 +112,7 @@ export class FakeApiClient implements IApiClient {
|
|||||||
rename: (payload: unknown) => this.record('session.rename', payload, this.onRename(payload)),
|
rename: (payload: unknown) => this.record('session.rename', payload, this.onRename(payload)),
|
||||||
fork: (payload: unknown) => this.record('session.fork', payload, this.onFork(payload)),
|
fork: (payload: unknown) => this.record('session.fork', payload, this.onFork(payload)),
|
||||||
prompt: (payload: unknown) => this.record('session.prompt', payload, this.onPrompt(payload)),
|
prompt: (payload: unknown) => this.record('session.prompt', payload, this.onPrompt(payload)),
|
||||||
|
attachment: (payload: unknown) => this.record('session.attachment', payload, this.onAttachment(payload)),
|
||||||
updateQueue: (payload: unknown) => this.record('session.updateQueue', payload, this.onUpdateQueue(payload)),
|
updateQueue: (payload: unknown) => this.record('session.updateQueue', payload, this.onUpdateQueue(payload)),
|
||||||
cancel: (payload: unknown) => this.record('session.cancel', payload, this.onCancel(payload)),
|
cancel: (payload: unknown) => this.record('session.cancel', payload, this.onCancel(payload)),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,34 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import { bridge } from '../src/http-bridge.ts'
|
import { bridge } from '../src/http-bridge.ts'
|
||||||
|
|
||||||
describe('HTTP bridge abort', () => {
|
describe('HTTP bridge abort', () => {
|
||||||
|
it('destroys a declared-oversize request instead of draining it', async () => {
|
||||||
|
const destroyed: true[] = []
|
||||||
|
const request = Readable.from([]) as unknown as IncomingMessage
|
||||||
|
Object.assign(request, {
|
||||||
|
url: '/api/session.prompt',
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json', 'content-length': '999999' },
|
||||||
|
destroy: () => { destroyed.push(true) },
|
||||||
|
})
|
||||||
|
let status: number | undefined
|
||||||
|
let headers: unknown
|
||||||
|
const response = Object.assign(new EventEmitter(), {
|
||||||
|
writableEnded: false,
|
||||||
|
writeHead(code: number, values?: unknown) { status = code; headers = values; return this },
|
||||||
|
write() { return true },
|
||||||
|
end(this: { writableEnded: boolean }) { this.writableEnded = true; return this },
|
||||||
|
}) as unknown as ServerResponse
|
||||||
|
|
||||||
|
await bridge(request, response, {
|
||||||
|
fetch: () => { throw new Error('a rejected request must never reach the handler') },
|
||||||
|
}, 1000)
|
||||||
|
// The socket must not stay parked draining a body the client can trickle
|
||||||
|
// at will after the rejection — same discipline as the chunked overrun.
|
||||||
|
expect(status).toBe(413)
|
||||||
|
expect(headers).toMatchObject({ connection: 'close' })
|
||||||
|
expect(destroyed).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('aborts a pending native picker request when the browser disconnects', async () => {
|
it('aborts a pending native picker request when the browser disconnects', async () => {
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
type: 'client-request', rpcId: 'picker-1', method: 'host.pickDirectory', payload: {},
|
type: 'client-request', rpcId: 'picker-1', method: 'host.pickDirectory', payload: {},
|
||||||
@@ -38,7 +66,7 @@ describe('HTTP bridge abort', () => {
|
|||||||
}
|
}
|
||||||
return Response.json({ aborted: fetchRequest.signal.aborted })
|
return Response.json({ aborted: fetchRequest.signal.aborted })
|
||||||
},
|
},
|
||||||
})
|
}, Number.MAX_SAFE_INTEGER)
|
||||||
await started
|
await started
|
||||||
response.emit('close')
|
response.emit('close')
|
||||||
await pending
|
await pending
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import type { AddressInfo } from 'node:net'
|
import type { AddressInfo } from 'node:net'
|
||||||
import type { IncomingMessage, ServerResponse } from 'node:http'
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||||
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy/api'
|
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||||
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment'
|
||||||
import { RpcId, type ClientRequest } from '@deepseek-ai/dsh-host-apiproxy/api'
|
import { RpcId, type ClientRequest } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||||
import type { HttpServerService, WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
|
import type { HttpServerService, WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
|
||||||
import { API_PATH, apply, HOST_EVENTS_PATH, inject, MUX_EVENTS_PATH, type HostConnectionHandle } from '../src/index.ts'
|
import { API_PATH, apply, HOST_EVENTS_PATH, inject, MUX_EVENTS_PATH, type HostConnectionHandle } from '../src/index.ts'
|
||||||
@@ -89,6 +90,19 @@ async function mounted(config?: { trustedHosts?: string[] }): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('connection node half', () => {
|
describe('connection node half', () => {
|
||||||
|
it('fails loud when the carrier cap cannot hold the configured image batch', () => {
|
||||||
|
const ctx = new Context()
|
||||||
|
const routes: WebRoute[] = []
|
||||||
|
ctx.provide('httpServer', fakeHttpServer(routes, []) as HttpServerService)
|
||||||
|
ctx.provide('attachments', {
|
||||||
|
imageLimits: { maxMessageImageBytes: 20 * 1024 * 1024 },
|
||||||
|
} as AttachmentStore)
|
||||||
|
ctx.provide('apiProxy', {} as ApiProxy)
|
||||||
|
expect(() => { apply(ctx, { maxRequestBodyBytes: 1024 }) })
|
||||||
|
.toThrow(/must be at least .* aggregate image limit/)
|
||||||
|
expect(routes).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
it('fails the load on a trustedHosts entry that is not a bare authority', async () => {
|
it('fails the load on a trustedHosts entry that is not a bare authority', async () => {
|
||||||
const routes: WebRoute[] = []
|
const routes: WebRoute[] = []
|
||||||
const upgrades: WebUpgradeRoute[] = []
|
const upgrades: WebUpgradeRoute[] = []
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../attachment/attachment"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../../llm/llm"
|
"path": "../../llm/llm"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||||
"@deepseek-ai/dsh-compact": "workspace:^",
|
"@deepseek-ai/dsh-compact": "workspace:^",
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
* must stub); runtime-internal entry points (history staging, wire-frame
|
* must stub); runtime-internal entry points (history staging, wire-frame
|
||||||
* dispatch) stay on the class, invisible out here.
|
* dispatch) stay on the class, invisible out here.
|
||||||
*/
|
*/
|
||||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||||
import type {
|
import type {
|
||||||
MessageId, QueueAction, RpcResult, SessionId,
|
MessageId, PromptContentPart, QueueAction, RpcResult, SessionId,
|
||||||
} from '@deepseek-ai/dsh-client-connection/client'
|
} from '@deepseek-ai/dsh-client-connection/client'
|
||||||
import type { ConversationSnapshot } from '../sessions/conversation.ts'
|
import type { ConversationSnapshot } from '../sessions/conversation.ts'
|
||||||
import type { ObservableSnapshot } from './store.ts'
|
import type { ObservableSnapshot } from './store.ts'
|
||||||
@@ -33,11 +33,19 @@ export interface ISession {
|
|||||||
readonly projections: ProjectionsFace
|
readonly projections: ProjectionsFace
|
||||||
/**
|
/**
|
||||||
* Send a prompt into the session.
|
* Send a prompt into the session.
|
||||||
* @param content - model-facing content blocks.
|
* @param content - text plus browser-owned temporary image uploads.
|
||||||
* @param mode - 'queue' appends a turn; 'steer' interrupts the running one.
|
* @param mode - 'queue' appends a turn; 'steer' interrupts the running one.
|
||||||
* @returns acceptance, or the business error (also mirrored into snapshot.promptError).
|
* @returns acceptance, or the business error (also mirrored into snapshot.promptError).
|
||||||
*/
|
*/
|
||||||
prompt(content: ContentBlock[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>>
|
prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>>
|
||||||
|
/**
|
||||||
|
* Resolve one durable image referenced by this session.
|
||||||
|
* @param attachmentId - opaque id found in the folded session log.
|
||||||
|
* @returns the authenticated reference and decoded bytes.
|
||||||
|
*/
|
||||||
|
readAttachment(
|
||||||
|
attachmentId: AttachmentIdType,
|
||||||
|
): Promise<RpcResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>>
|
||||||
/**
|
/**
|
||||||
* Apply one edit, remove, or strict steer action to a still-pending queue occurrence.
|
* Apply one edit, remove, or strict steer action to a still-pending queue occurrence.
|
||||||
* @param itemId - agent-owned inbox occurrence identity.
|
* @param itemId - agent-owned inbox occurrence identity.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||||
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
||||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
||||||
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||||
import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types'
|
import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types'
|
||||||
import type { TodoItem } from '@deepseek-ai/dsh-session/types'
|
import type { TodoItem } from '@deepseek-ai/dsh-session/types'
|
||||||
import type {
|
import type {
|
||||||
@@ -43,6 +44,7 @@ export interface AssistantProvenanceView {
|
|||||||
export type AssistantBlock =
|
export type AssistantBlock =
|
||||||
| { kind: 'text'; text: string }
|
| { kind: 'text'; text: string }
|
||||||
| { kind: 'reasoning'; text: string }
|
| { kind: 'reasoning'; text: string }
|
||||||
|
| { kind: 'image'; attachment: ImageAttachmentRef }
|
||||||
| { kind: 'tool-call'; callId: string; name: string; argsRaw: string }
|
| { kind: 'tool-call'; callId: string; name: string; argsRaw: string }
|
||||||
| { kind: 'other'; block: unknown }
|
| { kind: 'other'; block: unknown }
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ export function toAssistantBlock(block: ContentBlock): AssistantBlock {
|
|||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case 'text': return { kind: 'text', text: block.text }
|
case 'text': return { kind: 'text', text: block.text }
|
||||||
case 'reasoning': return { kind: 'reasoning', text: block.text }
|
case 'reasoning': return { kind: 'reasoning', text: block.text }
|
||||||
|
case 'image': return { kind: 'image', attachment: block.attachment }
|
||||||
case 'tool-call': return { kind: 'tool-call', callId: String(block.id), name: block.name, argsRaw: block.arguments }
|
case 'tool-call': return { kind: 'tool-call', callId: String(block.id), name: block.name, argsRaw: block.arguments }
|
||||||
default: return { kind: 'other', block }
|
default: return { kind: 'other', block }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Sessions remain resident after creation so they continue consuming mux frames off-screen.
|
// Sessions remain resident after creation so they continue consuming mux frames off-screen.
|
||||||
|
|
||||||
import type { Context } from 'cordis'
|
import type { Context } from 'cordis'
|
||||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||||
import type {
|
import type {
|
||||||
HistoryEntry, IApiClient, MessageId, MuxFrame, QueueAction, RpcError,
|
HistoryEntry, IApiClient, MessageId, MuxFrame, PromptContentPart, QueueAction, RpcError,
|
||||||
RpcId, RpcResponse, RpcResult, SessionId, SubagentAddress, ToolEventView,
|
RpcId, RpcResponse, RpcResult, SessionId, SubagentAddress, ToolEventView,
|
||||||
} from '@deepseek-ai/dsh-client-connection/client'
|
} from '@deepseek-ai/dsh-client-connection/client'
|
||||||
// Value import from the inline-safe wire layer (not the connection plugin):
|
// Value import from the inline-safe wire layer (not the connection plugin):
|
||||||
@@ -178,11 +178,11 @@ export class Session implements SessionFace {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
|
* Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
|
||||||
* @param content - core content blocks verbatim.
|
* @param content - text plus browser-owned temporary image uploads.
|
||||||
* @param mode - queue appends after the current turn; steer interrupts it.
|
* @param mode - queue appends after the current turn; steer interrupts it.
|
||||||
* @returns the prompt result (also mirrored into promptError on failure).
|
* @returns the prompt result (also mirrored into promptError on failure).
|
||||||
*/
|
*/
|
||||||
async prompt(content: ContentBlock[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>> {
|
async prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>> {
|
||||||
this.promptError = null
|
this.promptError = null
|
||||||
this.lastAgentError = null
|
this.lastAgentError = null
|
||||||
// Synchronous, before the first await: the blank → engaging edge must be
|
// Synchronous, before the first await: the blank → engaging edge must be
|
||||||
@@ -205,8 +205,24 @@ export class Session implements SessionFace {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const routed = (await this.api.subagents.prompt({ ...this.address, content })).result
|
if (content.some(part => part.type === 'image')) {
|
||||||
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
|
result = {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
code: 'attachment-error',
|
||||||
|
message: 'Image input is unavailable for subagent continuations.',
|
||||||
|
details: { reason: 'SUBAGENT_IMAGE_UNSUPPORTED' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const routed = (await this.api.subagents.prompt({
|
||||||
|
...this.address,
|
||||||
|
content: content.flatMap(part => part.type === 'text'
|
||||||
|
? [{ type: 'text' as const, text: part.text }]
|
||||||
|
: []),
|
||||||
|
})).result
|
||||||
|
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
result = transportError(error)
|
result = transportError(error)
|
||||||
@@ -232,6 +248,28 @@ export class Session implements SessionFace {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve one image referenced by this session into browser-consumable bytes.
|
||||||
|
* @param attachmentId - opaque id found in the folded session log.
|
||||||
|
* @returns the authenticated reference and decoded bytes.
|
||||||
|
*/
|
||||||
|
async readAttachment(
|
||||||
|
attachmentId: AttachmentIdType,
|
||||||
|
): Promise<RpcResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>> {
|
||||||
|
try {
|
||||||
|
const result = (await this.api.sessions.attachment({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
attachmentId,
|
||||||
|
})).result
|
||||||
|
if (!result.ok) return result
|
||||||
|
const binary = atob(result.value.data)
|
||||||
|
const data = Uint8Array.from(binary, char => char.charCodeAt(0))
|
||||||
|
return { ok: true, value: { attachment: result.value.attachment, data } }
|
||||||
|
} catch (error) {
|
||||||
|
return transportError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Apply one operation to a still-pending queue occurrence. */
|
/** Apply one operation to a still-pending queue occurrence. */
|
||||||
async updateQueue(itemId: MessageId, action: QueueAction): Promise<RpcResult<{ accepted: true }>> {
|
async updateQueue(itemId: MessageId, action: QueueAction): Promise<RpcResult<{ accepted: true }>> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
/** Assistant block classifier (moved here with sessions/conversation.ts). */
|
/** Assistant block classifier (moved here with sessions/conversation.ts). */
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
|
||||||
import type { ContentBlock } from '@deepseek-ai/dsh-client-connection/client'
|
import type { ContentBlock } from '@deepseek-ai/dsh-client-connection/client'
|
||||||
import { toAssistantBlock, toAssistantBlocks } from '../src/client/sessions/conversation.ts'
|
import { toAssistantBlock, toAssistantBlocks } from '../src/client/sessions/conversation.ts'
|
||||||
|
|
||||||
describe('toAssistantBlock', () => {
|
describe('toAssistantBlock', () => {
|
||||||
it('classifies the four block shapes', () => {
|
it('classifies the four block shapes', () => {
|
||||||
|
const attachment = {
|
||||||
|
attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`),
|
||||||
|
mediaType: 'image/png' as const,
|
||||||
|
bytes: 68,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
}
|
||||||
const blocks: ContentBlock[] = [
|
const blocks: ContentBlock[] = [
|
||||||
{ type: 'text', text: '正文' },
|
{ type: 'text', text: '正文' },
|
||||||
{ type: 'reasoning', text: '思考' },
|
{ type: 'reasoning', text: '思考' },
|
||||||
{ type: 'tool-call', id: 'c1', name: 'echo', arguments: '{}' } as ContentBlock,
|
{ type: 'tool-call', id: 'c1', name: 'echo', arguments: '{}' } as ContentBlock,
|
||||||
{ type: 'image', data: 'x' } as unknown as ContentBlock,
|
{ type: 'image', attachment },
|
||||||
]
|
]
|
||||||
expect(toAssistantBlocks(blocks)).toEqual([
|
expect(toAssistantBlocks(blocks)).toEqual([
|
||||||
{ kind: 'text', text: '正文' },
|
{ kind: 'text', text: '正文' },
|
||||||
{ kind: 'reasoning', text: '思考' },
|
{ kind: 'reasoning', text: '思考' },
|
||||||
{ kind: 'tool-call', callId: 'c1', name: 'echo', argsRaw: '{}' },
|
{ kind: 'tool-call', callId: 'c1', name: 'echo', argsRaw: '{}' },
|
||||||
{ kind: 'other', block: blocks[3] },
|
{ kind: 'image', attachment },
|
||||||
])
|
])
|
||||||
expect(toAssistantBlock(blocks[0] as ContentBlock)).toEqual({ kind: 'text', text: '正文' })
|
expect(toAssistantBlock(blocks[0] as ContentBlock)).toEqual({ kind: 'text', text: '正文' })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export class FakeApiClient implements IApiClient {
|
|||||||
Promise<RpcResponse<{ selected: ModelSelection }>> =
|
Promise<RpcResponse<{ selected: ModelSelection }>> =
|
||||||
payload => Promise.resolve(ok({ selected: { provider: payload.provider, model: payload.model } }))
|
payload => Promise.resolve(ok({ selected: { provider: payload.provider, model: payload.model } }))
|
||||||
onPrompt: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
onPrompt: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||||
|
onAttachment: (payload: unknown) => Promise<RpcResponse<{ attachment: { attachmentId: never; mediaType: 'image/png'; bytes: number; width: number; height: number }; data: string }>> =
|
||||||
|
() => Promise.resolve(ok({ attachment: { attachmentId: 'a' as never, mediaType: 'image/png', bytes: 1, width: 1, height: 1 }, data: 'AA==' }))
|
||||||
onUpdateQueue: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
onUpdateQueue: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||||
onCancel: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
onCancel: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||||
|
|
||||||
@@ -129,6 +131,7 @@ export class FakeApiClient implements IApiClient {
|
|||||||
rename: (payload: unknown) => this.record('session.rename', payload, this.onRename(payload)),
|
rename: (payload: unknown) => this.record('session.rename', payload, this.onRename(payload)),
|
||||||
fork: (payload: unknown) => this.record('session.fork', payload, this.onFork(payload)),
|
fork: (payload: unknown) => this.record('session.fork', payload, this.onFork(payload)),
|
||||||
prompt: (payload: unknown) => this.record('session.prompt', payload, this.onPrompt(payload)),
|
prompt: (payload: unknown) => this.record('session.prompt', payload, this.onPrompt(payload)),
|
||||||
|
attachment: (payload: unknown) => this.record('session.attachment', payload, this.onAttachment(payload)),
|
||||||
updateQueue: (payload: unknown) => this.record('session.updateQueue', payload, this.onUpdateQueue(payload)),
|
updateQueue: (payload: unknown) => this.record('session.updateQueue', payload, this.onUpdateQueue(payload)),
|
||||||
cancel: (payload: unknown) => this.record('session.cancel', payload, this.onCancel(payload)),
|
cancel: (payload: unknown) => this.record('session.cancel', payload, this.onCancel(payload)),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -531,6 +531,21 @@ describe('prompt and cancel errors', () => {
|
|||||||
expect(result.ok).toBe(false)
|
expect(result.ok).toBe(false)
|
||||||
expect(session.getSnapshot().promptError).toMatchObject({ op: 'stop', error: { code: 'internal' } })
|
expect(session.getSnapshot().promptError).toMatchObject({ op: 'stop', error: { code: 'internal' } })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('reads session-authorized attachment bytes and keeps the opaque id on the wire', async () => {
|
||||||
|
const { api, session } = makeSession()
|
||||||
|
const result = await session.readAttachment('attachment-1' as never)
|
||||||
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
attachment: { attachmentId: 'a', mediaType: 'image/png', bytes: 1, width: 1, height: 1 },
|
||||||
|
data: Uint8Array.of(0),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(api.callsOf('session.attachment')).toEqual([{
|
||||||
|
sessionId: SID, attachmentId: 'attachment-1',
|
||||||
|
}])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('rename', () => {
|
describe('rename', () => {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../attachment/attachment"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../vendor/cordis"
|
"path": "../../../vendor/cordis"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/** Test-owned sessions face: the SlotsService host contract over declarative fixtures. */
|
/** Test-owned sessions face: the SlotsService host contract over declarative fixtures. */
|
||||||
import type { Context } from 'cordis'
|
import type { Context } from 'cordis'
|
||||||
|
import type { AttachmentIdType } from '@deepseek-ai/dsh-attachment'
|
||||||
import { createScope, scopeOf, SessionProvideChannel } from '@deepseek-ai/dsh-client-runtime/client'
|
import { createScope, scopeOf, SessionProvideChannel } from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import type {
|
import type {
|
||||||
@@ -88,6 +89,15 @@ export class FixtureSession implements SessionFace {
|
|||||||
throw new Error(`test session "${this.sessionId}": prompt is not stubbed — supply it on the fixture's session face`)
|
throw new Error(`test session "${this.sessionId}": prompt is not stubbed — supply it on the fixture's session face`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fail-loud stub; supply `readAttachment` on the fixture's session face to exercise it.
|
||||||
|
* @param _attachmentId - opaque durable attachment id.
|
||||||
|
* @returns never — always throws.
|
||||||
|
*/
|
||||||
|
readAttachment(_attachmentId: AttachmentIdType): never {
|
||||||
|
throw new Error(`test session "${this.sessionId}": readAttachment is not stubbed — supply it on the fixture's session face`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fail-loud stub; supply `updateQueue` on the fixture's session face to exercise it.
|
* Fail-loud stub; supply `updateQueue` on the fixture's session face to exercise it.
|
||||||
* @returns never — always throws.
|
* @returns never — always throws.
|
||||||
|
|||||||
@@ -520,6 +520,7 @@ describe('fixture session face', () => {
|
|||||||
await runtime.sessions.add({ id: 's1' })
|
await runtime.sessions.add({ id: 's1' })
|
||||||
const bare = runtime.sessions.behavior('s1')
|
const bare = runtime.sessions.behavior('s1')
|
||||||
expect(() => bare.prompt()).toThrow(/prompt is not stubbed/)
|
expect(() => bare.prompt()).toThrow(/prompt is not stubbed/)
|
||||||
|
expect(() => bare.readAttachment('att-1' as Parameters<typeof bare.readAttachment>[0])).toThrow(/readAttachment is not stubbed/)
|
||||||
expect(() => bare.updateQueue()).toThrow(/updateQueue is not stubbed/)
|
expect(() => bare.updateQueue()).toThrow(/updateQueue is not stubbed/)
|
||||||
expect(() => bare.cancel()).toThrow(/cancel is not stubbed/)
|
expect(() => bare.cancel()).toThrow(/cancel is not stubbed/)
|
||||||
expect(() => bare.command()).toThrow(/command is not stubbed/)
|
expect(() => bare.command()).toThrow(/command is not stubbed/)
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-attachment": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-brand": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||||
@@ -56,6 +58,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
|
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type {
|
|||||||
} from './contract/slots.ts'
|
} from './contract/slots.ts'
|
||||||
import type { InputNotice } from './input/contract.ts'
|
import type { InputNotice } from './input/contract.ts'
|
||||||
import { createChatStore } from './stores.ts'
|
import { createChatStore } from './stores.ts'
|
||||||
import { ConversationService } from './service.ts'
|
import { ConversationService, UnsupportedImageMediaTypeError } from './service.ts'
|
||||||
import type { IConversation } from './service.ts'
|
import type { IConversation } from './service.ts'
|
||||||
import { ComposerBlockRegistry } from './input/blocks.ts'
|
import { ComposerBlockRegistry } from './input/blocks.ts'
|
||||||
import type { ComposerBlock } from './input/blocks.ts'
|
import type { ComposerBlock } from './input/blocks.ts'
|
||||||
@@ -91,6 +91,13 @@ function scopedConversation(sessions: ISessions, id: SessionId): IConversation {
|
|||||||
return conversation
|
return conversation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolve package-internal attachment operations from the public service registration. */
|
||||||
|
function concreteConversation(ctx: Context): ConversationService {
|
||||||
|
const conversation = ctx.get('conversation') as ConversationService | undefined
|
||||||
|
if (conversation === undefined) throw new Error('ui-conversation: conversation service unavailable')
|
||||||
|
return conversation
|
||||||
|
}
|
||||||
|
|
||||||
/** Chain routing: claim the composer while an approval wait is pending (pure — owner props only). */
|
/** Chain routing: claim the composer while an approval wait is pending (pure — owner props only). */
|
||||||
function selectApproval({ interactions }: ComposerChainProps): ApprovalWait | null {
|
function selectApproval({ interactions }: ComposerChainProps): ApprovalWait | null {
|
||||||
return interactions.find((i): i is ApprovalWait => i.kind === 'approval') ?? null
|
return interactions.find((i): i is ApprovalWait => i.kind === 'approval') ?? null
|
||||||
@@ -201,9 +208,16 @@ export function apply(ctx: Context): void {
|
|||||||
if (sessionId !== undefined && nextId !== sessionId) {
|
if (sessionId !== undefined && nextId !== sessionId) {
|
||||||
const from = inputHub.shell(sessionId)
|
const from = inputHub.shell(sessionId)
|
||||||
const draft = from.snapshot.draft
|
const draft = from.snapshot.draft
|
||||||
if (draft !== '') {
|
const imageIds = from.snapshot.imageIds
|
||||||
inputHub.shell(nextId).setDraft(draft)
|
const next = inputHub.shell(nextId)
|
||||||
from.setDraft('')
|
if (imageIds.length === 0 || next.addImages(imageIds)) {
|
||||||
|
if (draft !== '') {
|
||||||
|
next.setDraft(draft)
|
||||||
|
from.setDraft('')
|
||||||
|
}
|
||||||
|
if (imageIds.length > 0) {
|
||||||
|
for (const id of imageIds) from.removeImage(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sessions.open(nextId)
|
sessions.open(nextId)
|
||||||
@@ -220,10 +234,14 @@ export function apply(ctx: Context): void {
|
|||||||
'conversation.view': { kind: 'list', scope: 'session' },
|
'conversation.view': { kind: 'list', scope: 'session' },
|
||||||
},
|
},
|
||||||
store: chatStore,
|
store: chatStore,
|
||||||
inject: (sessionId: SessionId, _actions: BoundActions<typeof chatStore>): ConversationSessionInjected => ({
|
inject: (sessionId: SessionId, _actions: BoundActions<typeof chatStore>): ConversationSessionInjected => {
|
||||||
views,
|
const conversation = concreteConversation(ctx)
|
||||||
bindDraftMirror: write => inputHub.shell(sessionId).bindMirror(write),
|
return {
|
||||||
}),
|
views,
|
||||||
|
releaseSessionImages: (id) => { conversation.releaseSessionImages(id) },
|
||||||
|
bindDraftMirror: write => inputHub.shell(sessionId).bindMirror(write),
|
||||||
|
}
|
||||||
|
},
|
||||||
}, ConversationSession)
|
}, ConversationSession)
|
||||||
|
|
||||||
// Header chrome sits above the resident scrollport but shares the same
|
// Header chrome sits above the resident scrollport but shares the same
|
||||||
@@ -262,6 +280,9 @@ export function apply(ctx: Context): void {
|
|||||||
if (sessionId === undefined) {
|
if (sessionId === undefined) {
|
||||||
return {
|
return {
|
||||||
keyboard: undefined,
|
keyboard: undefined,
|
||||||
|
addImages: undefined,
|
||||||
|
removeImage: undefined,
|
||||||
|
draftImages: undefined,
|
||||||
resolveSubmitMode: (running, gesture, steeringAvailable) =>
|
resolveSubmitMode: (running, gesture, steeringAvailable) =>
|
||||||
submissionPolicy.resolve(running, gesture, steeringAvailable),
|
submissionPolicy.resolve(running, gesture, steeringAvailable),
|
||||||
toggleCommandMenu: undefined,
|
toggleCommandMenu: undefined,
|
||||||
@@ -270,10 +291,32 @@ export function apply(ctx: Context): void {
|
|||||||
hooks: { notices: ABSENT_NOTICES, lexicon: ABSENT_LEXICON, menuLauncher: ABSENT_MENU_LAUNCHER },
|
hooks: { notices: ABSENT_NOTICES, lexicon: ABSENT_LEXICON, menuLauncher: ABSENT_MENU_LAUNCHER },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const conversation = concreteConversation(ctx)
|
||||||
const shell = inputHub.shell(sessionId)
|
const shell = inputHub.shell(sessionId)
|
||||||
const slash = inputHub.slash(sessionId)
|
const slash = inputHub.slash(sessionId)
|
||||||
return {
|
return {
|
||||||
keyboard: shell,
|
keyboard: shell,
|
||||||
|
addImages: (files) => {
|
||||||
|
try {
|
||||||
|
const images = conversation.createDraftImages(files)
|
||||||
|
if (!shell.addImages(images.map(image => image.id))) {
|
||||||
|
conversation.releaseDraftImages(images)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof UnsupportedImageMediaTypeError) {
|
||||||
|
return t('image.unsupportedType', {
|
||||||
|
type: error.mediaType || t('image.unknownType'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeImage: (id) => {
|
||||||
|
conversation.releaseDraftImage(id)
|
||||||
|
shell.removeImage(id)
|
||||||
|
},
|
||||||
|
draftImages: ids => conversation.draftImages(ids),
|
||||||
resolveSubmitMode: (running, gesture, steeringAvailable) =>
|
resolveSubmitMode: (running, gesture, steeringAvailable) =>
|
||||||
submissionPolicy.resolve(running, gesture, steeringAvailable),
|
submissionPolicy.resolve(running, gesture, steeringAvailable),
|
||||||
toggleCommandMenu: slash === undefined
|
toggleCommandMenu: slash === undefined
|
||||||
@@ -332,6 +375,7 @@ export function apply(ctx: Context): void {
|
|||||||
},
|
},
|
||||||
store: chatStore,
|
store: chatStore,
|
||||||
inject: (sessionId: SessionId, actions: BoundActions<typeof chatStore>): ChatViewInjected => {
|
inject: (sessionId: SessionId, actions: BoundActions<typeof chatStore>): ChatViewInjected => {
|
||||||
|
const conversation = concreteConversation(ctx)
|
||||||
const scoped = scopedConversation(sessions, sessionId)
|
const scoped = scopedConversation(sessions, sessionId)
|
||||||
return {
|
return {
|
||||||
openDetails: (target) => {
|
openDetails: (target) => {
|
||||||
@@ -347,6 +391,7 @@ export function apply(ctx: Context): void {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
loadOlder: () => { void scoped.loadOlder() },
|
loadOlder: () => { void scoped.loadOlder() },
|
||||||
|
loadImage: attachment => conversation.resolveImage(sessionId, attachment),
|
||||||
// Unregistered 'trajectory' id is safe: the tab ring falls back to
|
// Unregistered 'trajectory' id is safe: the tab ring falls back to
|
||||||
// the first view, and the untouched inspect target stays inert.
|
// the first view, and the untouched inspect target stays inert.
|
||||||
inspectCall: (callId) => {
|
inspectCall: (callId) => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type { AssistantBlock } from '@deepseek-ai/dsh-client-runtime/client'
|
|||||||
import { JsonBlock, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
|
import { JsonBlock, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives'
|
import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
import type { ChatViewSlotProps } from '../contract/slots.ts'
|
import type { ChatViewSlotProps } from '../contract/slots.ts'
|
||||||
|
import { ImageGallery, type ImageLoader } from './MessageImage.tsx'
|
||||||
import { ReasoningRow } from './ReasoningRow.tsx'
|
import { ReasoningRow } from './ReasoningRow.tsx'
|
||||||
import css from './AssistantMarkdown.module.css'
|
import css from './AssistantMarkdown.module.css'
|
||||||
|
|
||||||
@@ -22,6 +23,8 @@ export interface AssistantMarkdownProps {
|
|||||||
streaming: boolean
|
streaming: boolean
|
||||||
/** Frozen partial of an aborted turn: rendered with a stopped marker. */
|
/** Frozen partial of an aborted turn: rendered with a stopped marker. */
|
||||||
interrupted?: boolean | undefined
|
interrupted?: boolean | undefined
|
||||||
|
/** Session-authorized durable image loader. */
|
||||||
|
loadImage?: ImageLoader
|
||||||
/** Resolved prose file mentions for this Assistant's closing turn. */
|
/** Resolved prose file mentions for this Assistant's closing turn. */
|
||||||
mentions?: MarkdownFileMentions | undefined
|
mentions?: MarkdownFileMentions | undefined
|
||||||
/** The owning view's locale seat, passed down as a plain prop. */
|
/** The owning view's locale seat, passed down as a plain prop. */
|
||||||
@@ -30,8 +33,9 @@ export interface AssistantMarkdownProps {
|
|||||||
|
|
||||||
/** Reasoning block as the Think variant summary row (figma 39:28304). */
|
/** Reasoning block as the Think variant summary row (figma 39:28304). */
|
||||||
export const AssistantMarkdown = memo(function AssistantMarkdown({
|
export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||||
blocks, streaming, interrupted, mentions, t,
|
blocks, streaming, interrupted, loadImage, mentions, t,
|
||||||
}: AssistantMarkdownProps) {
|
}: AssistantMarkdownProps) {
|
||||||
|
const imageLoader = loadImage ?? (() => Promise.reject(new Error(t('image.serviceUnavailable'))))
|
||||||
// Stable per locale revision (t identity changes on switch): a fresh object
|
// Stable per locale revision (t identity changes on switch): a fresh object
|
||||||
// per render would rebuild MarkdownText's component table every chunk.
|
// per render would rebuild MarkdownText's component table every chunk.
|
||||||
const codeLabels = useMemo(() => ({ copyLabel: t('copy'), copiedLabel: t('copied') }), [t])
|
const codeLabels = useMemo(() => ({ copyLabel: t('copy'), copiedLabel: t('copied') }), [t])
|
||||||
@@ -58,6 +62,7 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'reasoning': return <ReasoningRow key={i} text={block.text} running={streaming && i === last} t={t} />
|
case 'reasoning': return <ReasoningRow key={i} text={block.text} running={streaming && i === last} t={t} />
|
||||||
|
case 'image': return <ImageGallery key={i} images={[block]} load={imageLoader} align="start" t={t} />
|
||||||
// Grouped into tool rows by ChatView; hasVisible above skips an empty shell.
|
// Grouped into tool rows by ChatView; hasVisible above skips an empty shell.
|
||||||
case 'tool-call': return null
|
case 'tool-call': return null
|
||||||
default: return (
|
default: return (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AssistantMarkdown } from './AssistantMarkdown.tsx'
|
|||||||
|
|
||||||
/** Streaming, settled, and interrupted Assistant states share one keyed renderer instance. */
|
/** Streaming, settled, and interrupted Assistant states share one keyed renderer instance. */
|
||||||
export const AssistantNodeView = memo(function AssistantNodeView({
|
export const AssistantNodeView = memo(function AssistantNodeView({
|
||||||
node, useTurnData, openFile, fileMentions, t,
|
node, useTurnData, openFile, loadImage, fileMentions, t,
|
||||||
}: ChatNodeViewProps<'assistant-step'>) {
|
}: ChatNodeViewProps<'assistant-step'>) {
|
||||||
const data = node.data
|
const data = node.data
|
||||||
const turn = node.location.kind === 'turn' || node.location.kind === 'step'
|
const turn = node.location.kind === 'turn' || node.location.kind === 'step'
|
||||||
@@ -25,6 +25,7 @@ export const AssistantNodeView = memo(function AssistantNodeView({
|
|||||||
blocks={data.blocks}
|
blocks={data.blocks}
|
||||||
streaming={data.status === 'running'}
|
streaming={data.status === 'running'}
|
||||||
interrupted={data.status === 'interrupted'}
|
interrupted={data.status === 'interrupted'}
|
||||||
|
loadImage={loadImage}
|
||||||
mentions={mentions}
|
mentions={mentions}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type RoutedChatNodeOwner = {
|
|||||||
/** Subscribe and dispatch one stable Context key without observing sibling Nodes. */
|
/** Subscribe and dispatch one stable Context key without observing sibling Nodes. */
|
||||||
export const ChatNodeSeat = memo(function ChatNodeSeat({
|
export const ChatNodeSeat = memo(function ChatNodeSeat({
|
||||||
nodeKey, selectedCallId, cwd, openFile, inspectCall, forkAt,
|
nodeKey, selectedCallId, cwd, openFile, inspectCall, forkAt,
|
||||||
fileMentions, useSession, renderSlot, t,
|
loadImage, fileMentions, useSession, renderSlot, t,
|
||||||
}: ChatNodeSeatProps) {
|
}: ChatNodeSeatProps) {
|
||||||
const node = useSession(snapshot => snapshot.chat.nodes.get(nodeKey))
|
const node = useSession(snapshot => snapshot.chat.nodes.get(nodeKey))
|
||||||
const routedNode = node as ChatNode | undefined
|
const routedNode = node as ChatNode | undefined
|
||||||
@@ -30,8 +30,9 @@ export const ChatNodeSeat = memo(function ChatNodeSeat({
|
|||||||
openFile,
|
openFile,
|
||||||
inspectCall,
|
inspectCall,
|
||||||
forkAt,
|
forkAt,
|
||||||
|
loadImage,
|
||||||
fileMentions,
|
fileMentions,
|
||||||
}, [node, selectedCallId, cwd, openFile, inspectCall, forkAt, fileMentions])
|
}, [node, selectedCallId, cwd, openFile, inspectCall, forkAt, loadImage, fileMentions])
|
||||||
if (routedNode === undefined || owner === null) return null
|
if (routedNode === undefined || owner === null) return null
|
||||||
// Runtime dispatch owns the correlation: every Node's discriminant is the
|
// Runtime dispatch owns the correlation: every Node's discriminant is the
|
||||||
// keyed-slot entry passed alongside that same Node. TypeScript does not
|
// keyed-slot entry passed alongside that same Node. TypeScript does not
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user