Merge github/master into xtr/session-store-write-lifecycle

This commit is contained in:
_Kerman committed 2026-10-21 15:01:22 +08:00
commit 8f0489dc06
510 files changed
+9208 -3631

No files matched your search

@@ -9,8 +9,8 @@
en: f96bf1ebc0719fb0
zh: e9a1144f0e76572d
/agent-note-background-hover-and-screenshot-coordinates/decision:
en: 0cdd295d5f1872ed
zh: fe03ec9f7df94a4a
en: 0f45528ea6c5988c
zh: 9bde9df417ddb1ce
/agent-note-background-hover-and-screenshot-coordinates/alternatives-considered:
en: 5fc4beaf79761c50
zh: 4948333b0020be28
@@ -12,7 +12,7 @@ Hover-only controls can sometimes be revealed without moving the user's pointer.
Native hover first uses a same-window element or ancestor advertising both `AXShowAlternateUI` and `AXShowDefaultUI`. Entering its revealed descendants preserves the parent UI; leaving restores it. An unavailable route falls back to the real system pointer. An AX action failure propagates because received effects may be uncertain. The result records the route and whether the system pointer moved. System movement requires the selected foreground app and AppKit's mouse hit test, which excludes input-transparent overlays. No route grants [cross-call ownership](2026-10-13-cua-continuous-hover.md).
Model-facing coordinate input defaults to pixels in the returned screenshot. `relative: true` accepts integer thousandths from 0 to 1000 for click, hover, scroll and drag; it does not apply to AX indices. The Host owns the mapping to native window points. An application-wide observation refreshes its inventory; a scoped observation preserves other windows' image mappings. Window resizing invalidates the old image, while movement preserves local coordinates. Relative endpoints remain inside the window and do not depend on screenshot resolution.
Model-facing coordinate input defaults to pixels in the returned screenshot. `relative: true` accepts integer thousandths from 0 to 999 for click, hover, scroll and drag; it does not apply to AX indices. The Host owns the mapping to native window points. An application-wide observation refreshes its inventory; a scoped observation preserves other windows' image mappings. Window resizing invalidates the old image, while movement preserves local coordinates. Relative endpoints remain inside the window and do not depend on screenshot resolution.
`LlmAdapter.imageRequestTarget` exposes the same dimensions and byte target used by image serialization. DeepSeek and pi-ai share their existing projection functions with this method. Other adapters preserve normalized images unless they declare resizing. Screenshot bytes returned to programs match the request image; its durable source is admitted and stored once. The native PNG metadata is checked against decoded source dimensions before any input mapping is published.
@@ -12,7 +12,7 @@ Status: implemented
原生悬停优先寻找同一窗口内同时声明 `AXShowAlternateUI` 和 `AXShowDefaultUI` 的元素或祖先。进入其展开区域的子控件时保持父控件展开,离开后恢复。没有可用路径时回退到真实系统鼠标。AX 动作失败直接返回,因为已产生的效果可能不确定。结果记录实际路径及是否移动系统鼠标。系统移动要求所选应用处于前台,并使用 AppKit 的鼠标命中查询排除不接收输入的覆盖窗口。所有路径都遵守[跨调用不分配所有权](2026-10-13-cua-continuous-hover.zh.md)的决定。
面向模型的坐标默认使用返回截图中的像素。click、hover、scroll 和 drag 传入 `relative: true` 时接受 0~1000 的千分比整数,不适用于 AX 索引。Host 负责换算原生窗口 point。应用级观察刷新窗口清单,单窗口观察保留其他窗口的图片映射。窗口尺寸变化使旧图片失效,移动窗口保留局部坐标。相对坐标的端点保持在窗口内,不随截图分辨率改变。
面向模型的坐标默认使用返回截图中的像素。click、hover、scroll 和 drag 传入 `relative: true` 时接受 0~999 的千分比整数,不适用于 AX 索引。Host 负责换算原生窗口 point。应用级观察刷新窗口清单,单窗口观察保留其他窗口的图片映射。窗口尺寸变化使旧图片失效,移动窗口保留局部坐标。相对坐标的端点保持在窗口内,不随截图分辨率改变。
`LlmAdapter.imageRequestTarget` 提供与图片序列化相同的尺寸和字节目标。DeepSeek 和 pi-ai 在该方法中复用已有投影函数。其他适配器除非声明缩放,否则保留归一化图片。返回程序的截图字节与模型请求图片一致,持久原图只接入并保存一次。发布输入映射前,原生 PNG 元数据必须与解码后的原图尺寸一致。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-08-26-web-rich-text-quotes.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-26-web-rich-text-quotes.md
/agent-note-durable-rich-text-quotes-in-the-web-composer:
en: 49522a0a81db6981
zh: 92e81872377e96e9
/agent-note-durable-rich-text-quotes-in-the-web-composer/problem:
en: 7bd295c086ad9905
zh: 704dc665acfaa5ec
/agent-note-durable-rich-text-quotes-in-the-web-composer/decision:
en: d27134e46ab55ea0
zh: 3f1da063b661a1b3
/agent-note-durable-rich-text-quotes-in-the-web-composer/alternatives-considered:
en: 1999da9f900c9be3
zh: cf6aad990a180851
/agent-note-durable-rich-text-quotes-in-the-web-composer/verification:
en: b320c262d9c08ee4
zh: 6047f864213a2b4c
/agent-note-durable-rich-text-quotes-in-the-web-composer/consequences:
en: bb6f4338aad0ec41
zh: 93a4396b619492b1
@@ -0,0 +1,37 @@
# Agent Note: Durable rich-text quotes in the Web composer
Status: implemented
English | [中文](2026-08-26-web-rich-text-quotes.zh.md)
## Problem
Selected transcript excerpts need atomic editing and reload-safe drafts without depending on the availability of their source messages.
## Decision
`ui-conversation` owns an inline `QuoteNode` beside reference nodes in the existing editor runtime. Its clipboard projection is canonical Markdown, while its detect projection is one atomic character. The existing semantic `DraftSnapshot` persists quote text and clipboard-coordinate spans beside references; raw Lexical JSON and runtime node identities remain outside browser storage. Session switching, Workspace transfer, and failed-send restoration use this same snapshot. Quote controls receive runtime-only locale labels.
`ui-chat` accepts one nonblank browser selection within displayed user content, including pending steering and local submission echoes or finalized Assistant text and reasoning. Streaming and interrupted Assistant output, tools, injected context, and Markdown controls are excluded. Selection extraction follows visible line and block breaks and inserts through the current Session input actions.
Text interaction belongs to the shared primitive and the Chat-owned list Slot. Quote registers only gesture admission and insertion. Local absolute placement keeps actions in the content coordinate system; outside presses and Escape close the same area-owned interaction for every consumer. Dismissal and selection clearing are separate operations so a failed insertion preserves the excerpt. Context-menu consumers cancel their originating browser event synchronously; rendering a Slot cannot retroactively cancel it.
The [text-derived blockquote decision](../simplification/2026-10-13-text-derived-message-quotes.md) owns sent-message recognition and presentation. The [UserText evaluator](../architecture/2026-10-10-user-text-json-nodes.md) renders derived quote nodes through the existing matcher registry.
Quotes display a compact single-line preview; clipped excerpts expose the complete text in a shared inline HoverCard. Their text has no source-navigation action. The editable draft alone exposes whole-node removal. Only selected text survives message persistence: no source Session, message, or selection coordinates are retained.
## Alternatives considered
**Persist Lexical JSON or introduce another draft format.** Editor serialization would become a product storage obligation, while a second draft format would duplicate restoration and transfer ownership. The existing semantic snapshot already carries reference spans and extends to quotes.
**Retain source navigation.** Availability checks would require source coordinates, subscriptions, history pagination, focus routing, and failure states. A future navigation feature must define those durable coordinates and target-owned actions before adding them.
**Store UserText nodes or callbacks.** Text nodes are derived presentation, and callbacks are runtime state. Both can change independently of the logged input and semantic draft.
## Verification
Composer tests cover atomic removal and undo, semantic storage round trips, Markdown serialization, and failed-send restoration. UserText tests cover text recognition, literal excerpt rendering, literal references, and credential spelling. The keyless Web scenario exercises selection, clipped previews, draft switching and reload, persistence, and ordinary Markdown submission through the shipped composition.
## Consequences
The draft and Session retain enough data for atomic editing and replay without retaining source-history dependencies. UserText remains a display projection; its JSON nodes never replace authoritative content. Browser-local storage retains unsent selected text under the existing draft policy.
@@ -0,0 +1,37 @@
# Agent Note:Web composer 中的持久富文本 quote
Status: implemented
[English](2026-08-26-web-rich-text-quotes.md) | 中文
## 问题
所选 transcript 摘录需要原子编辑与可跨刷新恢复的草稿,不依赖来源消息的可用性。
## 决策
`ui-conversation` 在现有编辑器 runtime 中于 reference 节点旁拥有行内 `QuoteNode`。其剪贴板投影是规范 Markdown,检测投影则是一个原子字符。现有语义 `DraftSnapshot` 在 reference 旁持久化 quote 文本与剪贴板坐标范围;原始 Lexical JSON 与 runtime 节点身份不进入浏览器存储。Session 切换、Workspace 转移和发送失败恢复使用同一个 snapshot。Quote 控件接收仅用于 runtime 的 locale 标签。
`ui-chat` 接受已展示的用户内容(包括待接纳 steering 和本地发送回显)、或最终完成的 Assistant 文本与 reasoning 中的一个非空浏览器选区。流式及中断的 Assistant 输出、tool、注入上下文和 Markdown 控件被排除。选区提取遵循可见的行与 block 换行,通过当前 Session 的 input actions 插入。
文本交互属于共享基础能力与 Chat 拥有的 list Slot,quote 只注册手势接管与插入操作。本地绝对定位让操作处于内容坐标系;外部按下与 Escape 对所有使用方关闭同一个区域拥有的交互。关闭展示与清除选区是独立操作,因此插入失败时保留摘录。右键菜单使用方同步取消来源浏览器事件;渲染 Slot 无法事后取消事件。
[文本派生 blockquote 决策](../simplification/2026-10-13-text-derived-message-quotes.zh.md)拥有发送后的消息识别与展示。[UserText 计算器](../architecture/2026-10-10-user-text-json-nodes.zh.md)通过现有 matcher registry 渲染派生的 quote 节点。
Quote 显示紧凑的单行预览;截断的摘录通过共享行内 HoverCard 展示完整文本。文本不提供来源导航操作,只有可编辑草稿提供整节点删除。消息持久化只保留所选文本:不保留来源 Session、消息或选区坐标。
## 考虑过的替代方案
**持久化 Lexical JSON 或引入另一套草稿格式。** 编辑器序列化会成为产品存储义务,第二套草稿格式则会重复拥有恢复与转移逻辑。现有语义 snapshot 已携带 reference 范围,也可扩展到 quote。
**保留来源导航。** 可用性检查需要来源坐标、订阅、历史分页、焦点路由和失败状态。未来若添加导航功能,必须先定义其持久坐标与目标拥有的操作。
**存储 UserText 节点或 callback。** 文本节点是派生的展示,callback 是 runtime 状态;二者都可以独立于记录的输入和语义草稿变化。
## 验证
Composer 测试覆盖原子删除与撤销、语义存储往返、Markdown 序列化和发送失败恢复。UserText 测试覆盖文本识别、字面摘录展示、字面 reference 与凭据原文。无密钥 Web 场景通过已发布组合验证选区、截断预览、草稿切换与刷新、持久化和普通 Markdown 提交。
## 后果
草稿与 Session 保留足以支持原子编辑和 replay 的数据,不保留对来源历史的依赖。UserText 仍是展示投影;其 JSON 节点不会替代权威 content。浏览器本地存储按现有草稿策略保留未发送的所选文本。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-13-text-derived-message-quotes.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-10-13-text-derived-message-quotes.md
/agent-note-recognize-message-quotes-from-markdown-text:
en: a2abd7f693937d51
zh: f9e4bb817536dbbe
/agent-note-recognize-message-quotes-from-markdown-text/problem:
en: d71ebbacbd9bd8a3
zh: 45c1f7fd1d797f61
/agent-note-recognize-message-quotes-from-markdown-text/decision:
en: 0abb09d5ba396f69
zh: dd067b0ca5fd376f
/agent-note-recognize-message-quotes-from-markdown-text/alternatives-considered:
en: 05c6009796927583
zh: 9f08229767c07b31
/agent-note-recognize-message-quotes-from-markdown-text/verification:
en: 790598fd0f3f0c67
zh: 0399e0e322ffca40
/agent-note-recognize-message-quotes-from-markdown-text/consequences:
en: 21f96c04f6edcc1d
zh: 8ea3f42a8e981592
@@ -0,0 +1,29 @@
# Agent Note: Recognize message quotes from Markdown text
Status: implemented
English | [中文](2026-10-13-text-derived-message-quotes.zh.md)
## Problem
Quote-only content indexes duplicate information in Markdown text and require coordinated prompt, source, echo, and queue metadata. The same visible blockquote can acquire different presentation and direct-input behavior depending on how it was created.
## Decision
Messages contain ordinary Markdown text. The shared blockquote matcher recognizes consecutive explicitly marked lines for UserText presentation. Hand-written and composer-created blockquotes have identical sent-message behavior. Quote previews display decoded text without running UserText rules over their contents. Session-reference and skill processing applies normally to quoted text; recognized Session labels retain the same presentation as the surrounding quote text.
The [rich-text quote feature](../feature/2026-08-26-web-rich-text-quotes.md) retains atomic draft nodes and semantic restoration spans. This decision supersedes only its durable quote-index distinction. UserText nodes and recognition ranges remain derived values.
## Alternatives considered
**Keep quote-only indexes.** Indexes preserve a distinction between typed and inserted quotes, but that distinction is unnecessary for sent messages and requires metadata coordination across every producer and renderer.
**Remove atomic draft spans.** Plain text cannot restore whole-node editing, undo, Session switching, or failed-send restoration. Draft spans remain browser-owned editing state.
## Verification
Matcher tests cover original offsets, explicit line markers, blank excerpts, and literal nested quotes. Composer tests assert ordinary text submission. The shipped Web scenario checks text-only persistence and quote display after reload.
## Consequences
Messages need no quote fields or content indexes. Typed blockquotes also display as quotes without changing direct-input processing. Recognition deliberately covers explicit quote lines rather than Markdown lazy continuation or fenced-code parsing. Source navigation still requires a separate durable-coordinate design.
@@ -0,0 +1,29 @@
# Agent Note: 从 Markdown 文本识别消息 quote
Status: implemented
[English](2026-10-13-text-derived-message-quotes.md) | 中文
## 问题
纯 quote content 索引重复了 Markdown 文本中的信息,并要求 prompt、source、回显与队列协同携带元数据。同样可见的 blockquote 会因创建方式不同而获得不同的展示与直接输入行为。
## 决策
消息包含普通 Markdown 文本。共享 blockquote 匹配器识别连续的显式标记行,用于 UserText 展示。手写与输入框创建的 blockquote 在发送后具有相同行为。Quote 预览直接展示还原后的文本,不对正文再次运行 UserText 规则。Session-reference 与 skill 处理照常作用于引用文本;已识别的 Session 标签与周围引用文字保持相同样式。
[富文本 quote 功能](../feature/2026-08-26-web-rich-text-quotes.zh.md)保留原子草稿节点与语义恢复范围。本决策仅取代其持久 quote 索引区分。UserText 节点与识别范围仍是派生值。
## 考虑过的替代方案
**保留纯 quote 索引。** 索引保留手写与插入 quote 的区别,但发送后的消息无需这一区分,且要求每个生产方与渲染器协同处理元数据。
**移除原子草稿范围。** 纯文本无法恢复整节点编辑、撤销、Session 切换或发送失败恢复。草稿范围仍是浏览器拥有的编辑状态。
## 验证
匹配器测试覆盖原始偏移、显式行标记、空白摘录与字面的嵌套 quote。Composer 测试断言普通文本提交。已发布 Web 场景检查仅文本的持久化与刷新后的 quote 展示。
## 后果
消息无需 quote 字段或 content 索引。手写 blockquote 也显示为 quote,不改变直接输入处理。识别有意限定为显式引用行,不涵盖 Markdown 懒惰续行或代码围栏解析。来源导航仍需独立设计持久坐标。
+1 -1
View File
@@ -55,7 +55,7 @@ This is not a one-way shortening pass. Add or restore prose when code, types, an
- **Postmortems:** retain the incident sequence, evidence, causal chain, impact, and prevention. Remove repeated persuasion or implementation detail that does not establish causality.
- **Skills and agent instructions:** state behavioral guardrails and explicit scope limitations such as “guidance, not a script/checklist.” Keep the workflow concise and link its source of truth.
- **Examples and configuration comments:** explain access limits, non-obvious wiring or load order, security stance, replay behavior, exceptions, and likely misuse. Do not narrate entries that the configuration already shows.
- **Prompts and visible strings:** treat wording as behavior. Client UI copy belongs in typed locale dictionaries and reaches Cordis-free primitives as explicit localized props; inspect text, accessibility names, tooltips, placeholders, and format templates together, then run `verify-client-ui-i18n`. Update the owning runnable snapshot for model-visible text and repository-required GUI evidence. If the authorized scope has no owning scenario, leave the wording unchanged and report the deferral; do not silently fold it into a prose-only edit.
- **Prompts and visible strings:** treat wording as behavior. Client UI copy belongs in typed locale dictionaries and reaches Cordis-free primitives as explicit localized props; inspect text, accessibility names, tooltips, placeholders, and format templates together, then run `verify-client-ui-i18n`. English UI copy, including plugin `meta.title` and `meta.description`, uses sentence case: capitalize only the first word, proper nouns, and product names such as `Chrome DevTools`, `Git`, or `MCP` (`Git worktrees`, not `Git Worktrees`). Chinese copy keeps its own conventions. Update the owning runnable snapshot for model-visible text and repository-required GUI evidence. If the authorized scope has no owning scenario, leave the wording unchanged and report the deferral; do not silently fold it into a prose-only edit.
- **Diagnostics:** name the failing subject or path, violated rule, and correction when it is non-obvious. Remove internal execution narration.
Preserve searchable mechanism names and meaningful modal, temporal, or negative emphasis. Normalize decorative emphasis only.
+1 -1
View File
@@ -155,7 +155,7 @@ Real-API tests/demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and
- **Tests describe behavior, not correctness.** Change obsolete behavior with its tests; explain why in the PR.
- **Create Agent Notes only for durable decision rationale;** mechanical/local edits are exempt, including local UI changes ([scope](.agents/notes/README.md#when-to-write-one)). Archived notes are frozen: never edit or treat them as current authority ([archive policy](.agents/notes/README.md#archiving-and-deletion)).
- **Official plugin artwork:** use [dsh-official-plugin-artwork](.agents/skills/dsh-official-plugin-artwork/SKILL.md) for official and official experimental plugin icons; obtain designer approval before landing new glyphs.
- **Client UI copy is locale-owned.** Route product text through typed dictionaries and `t` or localized primitive props; `verify-client-ui-i18n` rejects hardcoded copy ([decision](.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)).
- **Client UI copy is locale-owned.** Route product text through typed dictionaries and `t` or localized primitive props; `verify-client-ui-i18n` rejects hardcoded copy ([decision](.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)). English copy, including plugin titles, uses sentence case ([rule](.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location)).
- **Testing policy** — [docs/testing.md](docs/testing.md). Every non-trivial model- or product-user-visible change updates a keyless recorded-session snapshot; [snapshot ownership](snapshots/AGENTS.md) reserves the top-level tree for session-driven cases and keeps other expected output owner-local. Fixtures replay on macOS/Linux; fix fixtures, not normalizers.
- **Design each tool's UI presentation up front.** Host presenters stay pure; Web cards derive from raw events and persisted result metadata ([cookbook](docs/cookbook/adding-a-tool.md)).
- **Plan unit, e2e, and snapshot coverage** for capability seams, lifecycle paths, and transcript output; include missing snapshot-harness support in the same change.
+1
View File
@@ -78,6 +78,7 @@ External packages installed for runtime use or embedded in distributed Host or b
| [`ajv`](https://github.com/ajv-validator/ajv) | MIT |
| [`anser`](https://github.com/IonicaBizau/anser) | MIT |
| [`big.js`](https://github.com/MikeMcl/big.js) | MIT |
| [`birpc`](https://github.com/antfu-collective/birpc) | MIT |
| [`buffer`](https://github.com/feross/buffer) | MIT |
| [`chokidar`](https://github.com/paulmillr/chokidar) | MIT |
| [`chrome-devtools-frontend`](https://github.com/ChromeDevTools/devtools-frontend) | BSD-3-Clause |
+3 -2
View File
@@ -5,7 +5,7 @@ import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import { SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-skill'
import type {} from '@deepseek-ai/dsh-tools'
import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit'
import { unsupportedModel, unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit'
const overlayPath = process.argv[2]
if (overlayPath === undefined) throw new Error('dsh-badge snapshot requires an overlay path')
@@ -31,6 +31,7 @@ try {
id: agentId,
options: {},
session,
model: unsupportedModel(),
inbox: unsupportedInbox(),
status: 'idle',
send: () => {},
@@ -43,7 +44,7 @@ try {
}
const decision = await agentEvents(ctx, agent).waterfall(
'agent/pre-step',
{ messages: [], turn: 1, step: 1, signal: new AbortController().signal },
{ modelSelection: { provider: 'mock', model: 'mock' }, messages: [], turn: 1, step: 1, signal: new AbortController().signal },
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
)
const catalog = decision.kind === 'enter'
@@ -17,6 +17,7 @@
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"cli-smoke-call","name":"bash","arguments":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}}
{"type":"job/registered","data":{"jobId":"{{id:1}}","owner":"{{sessionId}}","kind":"bash","label":"printf CLI_TOOL_ROUND_TRIP","startedAt":0,"toolCall":{"callId":"cli-smoke-call","rootCallId":"cli-smoke-call"}}}
{"type":"job/settled","data":{"jobId":"{{id:1}}","owner":"{{sessionId}}","kind":"bash","label":"printf CLI_TOOL_ROUND_TRIP","startedAt":0,"toolCall":{"callId":"cli-smoke-call","rootCallId":"cli-smoke-call"},"finishedAt":0,"status":"completed","cause":"producer","producerReported":true,"detail":"exit code: 0"}}
{"type":"model/selection","data":{"provider":"cli-mock","model":"cli-mock","reasoningEffort":"off"}}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"cli-smoke-call"},"toolCallId":"cli-smoke-call","content":[{"type":"text","text":"CLI_TOOL_ROUND_TRIP"}],"isError":false,"id":"{{sessionId}}"},"meta":{"cwd":"{{cwd}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"step/start","data":{"turn":1,"step":2}}
+3
View File
@@ -72,5 +72,8 @@
"@deepseek-ai/dsh-experimental-cot-translation-bundle": "workspace:*",
"@deepseek-ai/dsh-experimental-worktree": "workspace:*",
"@deepseek-ai/dsh-experimental-plugin-market-bundle": "workspace:*"
},
"dependencies": {
"@deepseek-ai/dsh-message-quotes": "workspace:*"
}
}
+8 -8
View File
@@ -85,8 +85,8 @@ describe('web e2e: Agent Teams panel', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-team-panel'))
const projectionReads = vi.spyOn(scaffold.ctx.sessionController, 'projections')
onTestFinished(() => { projectionReads.mockRestore() })
await page.locator('[data-team-action]').getByRole('button', { name: /Agent Team/iu }).click()
const action = page.getByRole('dialog', { name: 'Agent Team', exact: true })
await page.locator('[data-team-action]').getByRole('button', { name: 'Agent team', exact: true }).click()
const action = page.getByRole('dialog', { name: 'Agent team', exact: true })
await action.getByText('No shared tasks yet. Create them through the conversation.').waitFor()
await action.getByText('lead').waitFor()
expect(projectionReads).not.toHaveBeenCalled()
@@ -119,8 +119,8 @@ describe('web e2e: Agent Teams panel', () => {
it('keeps the panel inside the viewport and closes on outside click or Escape', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-team-panel-keyboard'))
const panel = page.getByRole('dialog', { name: 'Agent Team', exact: true })
const trigger = page.locator('[data-team-action]').getByRole('button', { name: /Agent Team/iu })
const panel = page.getByRole('dialog', { name: 'Agent team', exact: true })
const trigger = page.locator('[data-team-action]').getByRole('button', { name: 'Agent team', exact: true })
const viewport = page.viewportSize()!
await trigger.click()
await panel.getByText('lead', { exact: true }).waitFor()
@@ -162,8 +162,8 @@ describe('web e2e: Agent Teams panel', () => {
})
it('keeps the hovered panel open when the pointer returns directly to its trigger', async () => {
const trigger = page.locator('[data-team-action]').getByRole('button', { name: 'Agent Team', exact: true })
const panel = page.getByRole('dialog', { name: 'Agent Team', exact: true })
const trigger = page.locator('[data-team-action]').getByRole('button', { name: 'Agent team', exact: true })
const panel = page.getByRole('dialog', { name: 'Agent team', exact: true })
await trigger.hover()
await panel.waitFor()
await panel.hover()
@@ -182,8 +182,8 @@ describe('web e2e: Agent Teams panel', () => {
writeScopes: [],
})
const viewport = page.viewportSize()!
const trigger = page.locator('[data-team-action]').getByRole('button', { name: 'Agent Team', exact: true })
const panel = page.getByRole('dialog', { name: 'Agent Team', exact: true })
const trigger = page.locator('[data-team-action]').getByRole('button', { name: 'Agent team', exact: true })
const panel = page.getByRole('dialog', { name: 'Agent team', exact: true })
const card = panel.locator('article').filter({ hasText: 'Responsive task' })
await trigger.click()
await card.waitFor()
+7 -1
View File
@@ -26,6 +26,10 @@ it('keeps settings and expanded plugin dialogs clear of the Windows caption acro
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置', exact: true })
const settingsMask = settings.locator('..').locator(':scope > [aria-hidden="true"]')
const permissionText = settings.getByText('权限', { exact: true }).locator('..')
expect(await permissionText.evaluate(el => getComputedStyle(el).paddingRight)).toBe('48px')
const cubes = settings.getByRole('button', { name: /浅色|深色|跟随系统/ })
expect(await cubes.first().evaluate(el => getComputedStyle(el).flexBasis)).toBe('180px')
for (const fullscreen of [false, true, false]) {
await page.evaluate(value => document.documentElement.toggleAttribute('data-fullscreen', value), fullscreen)
const margin = fullscreen ? 24 : 60
@@ -86,8 +90,10 @@ it('keeps settings and expanded plugin dialogs clear of the Windows caption acro
}, platform)
if (await reopenSidebar.isVisible()) await reopenSidebar.click()
await openSettings(page, 'zh')
await expect.poll(async () => (await settings.boundingBox())?.y).toBe(platform === 'darwin' ? 68 : 24)
await expect.poll(async () => (await settings.boundingBox())?.y).toBe(platform === 'darwin' ? 68 : 0)
expect((await settingsMask.boundingBox())?.y).toBe(0)
expect(await permissionText.evaluate(el => getComputedStyle(el).paddingRight)).toBe(platform === 'darwin' ? '48px' : '16px')
expect(await cubes.first().evaluate(el => getComputedStyle(el).flexBasis)).toBe(platform === 'darwin' ? '180px' : '0px')
await page.keyboard.press('Escape')
await settings.waitFor({ state: 'hidden' })
}
+4 -4
View File
@@ -80,15 +80,15 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr
expect(await card.getByRole('button', { name: /Accessibility permission|Screen Recording permission/ }).count()).toBe(0)
await page.setViewportSize({ width: 1680, height: 600 })
const plugins = page.locator('[data-plugin-panel]')
const openDetails = page.getByRole('button', { name: 'View Computer Use', exact: true })
const openDetails = page.getByRole('button', { name: 'View Computer use', exact: true })
await openDetails.scrollIntoViewIfNeeded()
expect(await plugins.evaluate(element => element.scrollTop)).toBeGreaterThan(0)
await openDetails.click()
const panel = page.locator('[data-cua-settings]')
const accessibility = panel.getByRole('group', { name: 'Accessibility', exact: true })
await accessibility.getByRole('button', { name: 'Request permission', exact: true }).waitFor()
expect(await panel.getByRole('region', { name: 'Authorize Computer Use', exact: true }).getByRole('group').count()).toBe(4)
expect(await panel.getByRole('region', { name: 'Manage applications Computer Use can access', exact: true }).count()).toBe(1)
expect(await panel.getByRole('region', { name: 'Authorize computer use', exact: true }).getByRole('group').count()).toBe(4)
expect(await panel.getByRole('region', { name: 'Manage applications computer use can access', exact: true }).count()).toBe(1)
await expect.poll(() => panel.locator('li img').evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(1)
expect(await panel.getByText('/fixture/app', { exact: true }).count()).toBe(0)
expect(await accessibility.getByText('Visual Studio Code.app', { exact: true }).count()).toBe(1)
@@ -186,7 +186,7 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr
return { errors, layouts, documents, stop: () => { observer.disconnect(); cancelAnimationFrame(frame) } }
})
try {
const toggle = page.locator('[data-plugin-panel]').getByRole('switch', { name: 'Enable Computer Use', exact: true })
const toggle = page.locator('[data-plugin-panel]').getByRole('switch', { name: 'Enable Computer use', exact: true })
for (const width of [1680, 390]) {
await page.setViewportSize({ width, height: 1000 })
if (width === 390) await page.locator('[data-sidebar-collapsed]').waitFor({ state: 'attached' })
@@ -95,6 +95,8 @@ describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages sett
onTestFailed(() => saveFailureShot(page, 'web-e2e-deepseek-messages-default'))
await page.keyboard.press('Escape')
await scaffold.ctx.agentDefaultModel.saveSelection({ provider: 'deepseek-official', model: 'deepseek-v4-flash' })
const patchPath = join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml')
expect(await readFile(patchPath, 'utf8')).toContain('model: deepseek-v4-flash')
await page.reload({ waitUntil: 'load' })
const input = page.locator('[data-composer-input]').first()
const trigger = page.getByRole('button', { name: '请选择模型', exact: true })
@@ -109,8 +111,8 @@ describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages sett
await expect.poll(() => scaffold.ctx.agentDefaultModel.currentSelection()).toMatchObject({
provider: 'deepseek-official', model: 'deepseek-flash',
})
const settings = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(settings).toContain('provider: deepseek-official')
const settings = await readFile(patchPath, 'utf8')
expect(settings).not.toContain('id: agent-default-model')
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
})
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,8 +1,8 @@
- text: Waiting for approval
- group "Approval details":
- strong: Fixture
- text: Allow Computer Use to launch, read, capture and control this application?
- paragraph: Session access lasts until DSH restarts or Computer Use is disabled.
- text: Allow computer use to launch, read, capture and control this application?
- paragraph: Session access lasts until DSH restarts or computer use is disabled.
- button "Reject"
- button "Allow for this session"
- button "Always allow"
@@ -1,8 +1,8 @@
- region "Authorize Computer Use":
- heading "Authorize Computer Use" [level=3]
- region "Authorize computer use":
- heading "Authorize computer use" [level=3]
- group "Default mode for new sessions":
- strong: Default mode for new sessions
- paragraph: Computer Use is available only in PTC mode.
- paragraph: Computer use is available only in PTC mode.
- button "Set to PTC"
- group "PTC execution mode":
- strong: PTC execution mode
@@ -17,8 +17,8 @@
- paragraph: In System Settings, allow Visual Studio Code.app to capture application windows.
- button "Request permission"
- paragraph: Manage permissions in System Settings for the application named by macOS.
- region "Manage applications Computer Use can access":
- heading "Manage applications Computer Use can access" [level=3]
- region "Manage applications computer use can access":
- heading "Manage applications computer use can access" [level=3]
- heading "Always-allowed applications" [level=4]
- list:
- listitem:
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "Settings":
- text: Settings
- navigation:
- text: Settings
- button "General"
- button "Models"
- button "Built-in plugins"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,6 +1,6 @@
- dialog "Settings":
- text: Settings
- navigation:
- text: Settings
- button "General"
- button "Models"
- button "Built-in plugins"
@@ -1,6 +1,6 @@
- dialog "Settings":
- text: Settings
- navigation:
- text: Settings
- button "General"
- button "Models"
- button "Built-in plugins"
@@ -1,6 +1,6 @@
- dialog "设置":
- text: 设置
- navigation:
- text: 设置
- button "通用设置"
- button "模型"
- button "内置插件"
@@ -1,9 +1,9 @@
- bundle: Developer Tools
- bundle: Developer tools
description: Inspect and debug raw session data, grouped chat messages, and the NodeJS backend.
- component: NodeJS Inspector
- component: NodeJS inspector
description: Press Ctrl/Cmd+Shift+. to open Chrome DevTools in the bottom panel for development and debugging.
- component: Session Log
- component: Session log
description: Enable analysis of the current session's raw data and grouped chat messages in the session sidebar.
- session sidebar: Session Log
- session sidebar: Session log
description: Analyze raw logs and grouped chat messages for the current session in the sidebar.
- NodeJS bottom: NodeJS Inspector
- NodeJS bottom: NodeJS inspector
+1 -1
View File
@@ -35,7 +35,7 @@ it('opens bottom DevTools after the Developer Tools bundle is enabled on an alre
await page.getByRole('navigation', { name: 'Global panels' }).getByRole('button', { name: 'Plugins', exact: true }).click()
const plugins = page.locator('[data-plugin-panel]')
const toggle = plugins.getByRole('switch', { name: 'Enable Developer Tools', exact: true })
const toggle = plugins.getByRole('switch', { name: 'Enable Developer tools', exact: true })
await toggle.waitFor()
expect(await toggle.getAttribute('aria-checked')).toBe('false')
const panel = page.locator('[data-inspector-panel]')
+4 -4
View File
@@ -923,14 +923,14 @@ describe('web e2e: plugin manager', () => {
expect(await child.getByText('为智能体提供成员协调、消息通信和共享任务管理工具。', { exact: true }).count()).toBe(1)
try {
await setLanguage('en')
await panel.getByRole('heading', { name: 'Agent Teams', exact: true }).waitFor()
await panel.getByRole('heading', { name: 'Agent teams', exact: true }).waitFor()
expect(await packageName.textContent()).toBe('@deepseek-ai/dsh-experimental-agent-team-profile')
expect(await panel.getByText('Enable team collaboration, team tools, the member roster, and the shared task board.').count()).toBe(1)
await child.getByText('Team Tools', { exact: true }).waitFor()
await child.getByText('Team tools', { exact: true }).waitFor()
expect(await child.getByText('Give agents tools to coordinate members, exchange messages, and manage shared tasks.', { exact: true }).count()).toBe(1)
await panel.getByRole('button', { name: 'Back to plugins' }).click()
await panel.getByRole('button', { name: 'View Agent Teams', exact: true }).waitFor()
expect(await panel.getByRole('switch', { name: 'Enable Agent Teams', exact: true }).count()).toBe(1)
await panel.getByRole('button', { name: 'View Agent teams', exact: true }).waitFor()
expect(await panel.getByRole('switch', { name: 'Enable Agent teams', exact: true }).count()).toBe(1)
// Schedule ships in the delivered composition, so no bundle card carries it in either language.
expect(await panel.getByRole('button', { name: 'View Automation tasks', exact: true }).count()).toBe(0)
// The official configuration pages follow the language too, from their own dictionary.
+1 -1
View File
@@ -776,7 +776,7 @@ async function openSessionInspector(page: Page): Promise<void> {
// The showcase opens with a file tab in the right sidebar; a new tab shows the guide entries.
await page.getByRole('button', { name: 'New tab', exact: true }).click()
await page.locator('[data-sidebar-right-guide-entry="session-inspector-log"]').click({ timeout: 30_000 })
await page.getByRole('table', { name: 'Raw Log', exact: true }).waitFor({ timeout: 15_000 })
await page.getByRole('table', { name: 'Raw log', exact: true }).waitFor({ timeout: 15_000 })
expect(await setBundle(page, INSPECTOR_BUNDLE, false)).toBe('applied')
}
+646
View File
@@ -0,0 +1,646 @@
/** Keyless real-Web coverage for quote selection, persistence, and uniform reference presentation. */
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Locator, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay'
import { SessionId } from '@deepseek-ai/dsh-session'
import { formatSessionReferenceMention } from '@deepseek-ai/dsh-session-reference'
import { formatQuoteText } from '@deepseek-ai/dsh-client-ui-primitives'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold,
parseSeedFixture, renderSeedFixture, seedSession, selectedSessionFixture, watchConsole, webSnapshotMode,
type WebScaffold,
} from './scaffold.ts'
import { newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/rich-text-quotes', import.meta.url))
const BORROWED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/session.jsonl', import.meta.url))
const UI_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/rich-text-quotes/ui.expected.md', import.meta.url))
const MODE = webSnapshotMode()
const SESSION_ID = 'rich-text-quotes-web-e2e'
const OTHER_ID = 'rich-text-quotes-other'
const LEGACY_DRAFT_KEY = `dsh.conversation.${SESSION_ID}`
const CURRENT_DRAFT_KEY = `dsh.conversation.${SESSION_ID}`
const LEGACY_DRAFT = JSON.stringify({ draft: 'Imported plain draft', view: 'chat', viewRequest: null })
const SOURCE_TEXT = 'Durable source phrase selected from an earlier message and preserved as structured quote metadata.'
const THINKING_TEXT = 'Settled reasoning source'
const LITERAL_CREDENTIAL = 'sk-1234567890abcdefghijklmnopqrstuvwxyz'
const SESSION_MENTION = formatSessionReferenceMention({ sessionId: SessionId(OTHER_ID), label: 'Quote source' })
const DIRECT_SYNTAX_TEXT = [
`/quoted-demo ${SESSION_MENTION} ${LITERAL_CREDENTIAL} **literal Markdown**`,
...Array.from({ length: 80 }, (_, index) => `Quote line ${String(index + 1)} contains preserved text.`),
'End of long quote.',
].join('\n')
const SELECTED_SYNTAX_TEXT = DIRECT_SYNTAX_TEXT.replaceAll('\n', ' ')
const SENT_SYNTAX_TEXT = SELECTED_SYNTAX_TEXT.replace(SESSION_MENTION, '@Quote source')
const REPLY = 'Quote received.'
async function quoteAppearance(quote: Locator): Promise<Record<string, string>> {
return quote.evaluate((element) => {
const style = getComputedStyle(element)
return {
alignItems: style.alignItems,
color: style.color,
display: style.display,
fontSize: style.fontSize,
gap: style.gap,
lineHeight: style.lineHeight,
margin: style.margin,
padding: style.padding,
}
})
}
const REPLAY: ReplayOverrideDoc = [{
kind: 'chunks',
chunks: [
{ type: 'block-start', index: 0, blockType: 'text' },
{ type: 'text-delta', index: 0, text: REPLY },
{ type: 'block-end', index: 0, block: { type: 'text', text: REPLY } },
{ type: 'usage', usage: { inputTokens: 128, outputTokens: 8 } },
{ type: 'finish', reason: { kind: 'stop' } },
],
}]
function quoteHistoryFixture(raw: string): string {
const { headerLine } = parseSeedFixture(raw)
let seq = 0
let time = 0
const events: Array<Record<string, unknown> & { seq: number; time: number }> = []
const at = (event: Record<string, unknown>): number => {
const assigned = seq++
events.push({ ...event, seq: assigned, time: time++ })
return assigned
}
at({ type: 'turn/start', data: { turn: 1 } })
at({
type: 'user/message',
data: {
id: '{{message:1}}',
role: 'user',
content: [{ type: 'text', text: SOURCE_TEXT }],
source: { kind: 'user', rpcId: '{{rpc:1}}' },
},
surfaceOp: 'append',
})
for (let ordinal = 2; ordinal <= 51; ordinal++) {
at({
type: 'user/message',
data: {
id: `{{message:${String(ordinal)}}}`,
role: 'user',
content: [{
type: 'text',
text: ordinal === 2 ? formatQuoteText(DIRECT_SYNTAX_TEXT) : `Filler message ${String(ordinal)}`,
}],
source: { kind: 'user', rpcId: `{{rpc:${String(ordinal)}}}` },
},
surfaceOp: 'append',
})
}
at({
type: 'user/message',
data: {
id: '{{message:52}}',
role: 'user',
content: [
{ type: 'text', text: `> ${SOURCE_TEXT}` },
{ type: 'text', text: '\n\nUse that excerpt.' },
],
source: {
kind: 'user',
rpcId: '{{rpc:52}}',
},
},
surfaceOp: 'append',
})
at({
type: 'session/title',
data: {
title: 'Quote history',
messageSeqs: Array.from({ length: 52 }, (_value, index) => index + 1),
source: { kind: 'fallback' },
},
})
at({ type: 'step/start', data: { turn: 1, step: 1 } })
at({
type: 'assistant/message',
data: {
turn: 1,
step: 1,
message: {
id: '{{message:53}}',
role: 'assistant',
content: [
{ type: 'reasoning', text: THINKING_TEXT },
{ type: 'text', text: 'Quote fixture ready.' },
],
source: { kind: 'model', provider: 'fixture', model: 'fixture' },
},
stream: [
{ type: 'chunk', time: 0, chunk: { type: 'block-start', index: 0, blockType: 'reasoning' } },
{ type: 'reasoning-chunks', time0: 0, index: 0, dt: [], texts: [THINKING_TEXT] },
{ type: 'chunk', time: 0, chunk: { type: 'block-end', index: 0, block: { type: 'reasoning', text: THINKING_TEXT } } },
{ type: 'chunk', time: 0, chunk: { type: 'block-start', index: 1, blockType: 'text' } },
{ type: 'text-chunks', time0: 0, index: 1, dt: [], texts: ['Quote fixture ready.'] },
{ type: 'chunk', time: 0, chunk: { type: 'block-end', index: 1, block: { type: 'text', text: 'Quote fixture ready.' } } },
{ type: 'chunk', time: 0, chunk: { type: 'finish', reason: { kind: 'stop' } } },
],
},
surfaceOp: 'append',
})
at({ type: 'step/end', data: { turn: 1, step: 1 } })
at({ type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } })
return renderSeedFixture(headerLine, events)
}
function otherSessionFixture(raw: string): string {
const { headerLine } = parseSeedFixture(raw)
let seq = 0
const events: Array<Record<string, unknown> & { seq: number; time: number }> = []
const at = (event: Record<string, unknown>): void => {
events.push({ ...event, seq, time: seq })
seq += 1
}
at({ type: 'turn/start', data: { turn: 1 } })
at({
type: 'user/message', surfaceOp: 'append',
data: {
id: '{{message:1}}', role: 'user',
content: [{ type: 'text', text: 'Other session.' }],
source: { kind: 'user', rpcId: '{{rpc:1}}' },
},
})
at({ type: 'session/title', data: { title: 'Other session', messageSeqs: [1], source: { kind: 'fallback' } } })
at({ type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } })
return renderSeedFixture(headerLine, events)
}
async function selectAllText(locator: Locator): Promise<void> {
await locator.evaluate((element) => {
const range = document.createRange()
range.selectNodeContents(element)
const selection = window.getSelection()
selection?.removeAllRanges()
selection?.addRange(range)
document.dispatchEvent(new Event('selectionchange'))
})
}
async function expectEllipsizedQuote(box: Locator, text: Locator, container: Locator): Promise<number> {
const [quoteBounds, containerBounds, presentation] = await Promise.all([
box.boundingBox(),
container.boundingBox(),
text.evaluate((element) => {
const style = getComputedStyle(element)
return {
clientWidth: element.clientWidth,
scrollWidth: element.scrollWidth,
overflow: style.overflow,
textOverflow: style.textOverflow,
whiteSpace: style.whiteSpace,
}
}),
])
expect(quoteBounds).not.toBeNull()
expect(containerBounds).not.toBeNull()
const expectedMaximum = Math.min(containerBounds!.width * 0.8, 300)
expect(quoteBounds!.width).toBeLessThanOrEqual(expectedMaximum + 1)
expect(presentation.overflow).toBe('hidden')
expect(presentation.textOverflow).toBe('ellipsis')
expect(presentation.whiteSpace).toBe('nowrap')
expect(presentation.scrollWidth).toBeGreaterThan(presentation.clientWidth)
return quoteBounds!.width
}
async function openQuoteSession(page: Page): Promise<void> {
await page.locator(`[data-row-key="session:${SESSION_ID}"]`).click()
}
async function expectScrollableQuote(page: Page, quote: Locator, text: string): Promise<void> {
await quote.hover()
const content = page.locator('[data-quote-popover]')
await expect.poll(() => content.textContent()).toBe(text)
const panel = content.locator('..')
await expect.poll(async () => {
const bounds = await panel.boundingBox()
return bounds !== null && bounds.y >= 8 && bounds.y + bounds.height <= page.viewportSize()!.height - 8
}).toBe(true)
await panel.hover()
await page.mouse.wheel(0, 10_000)
await expect.poll(() => panel.evaluate(element => ({
scrolled: element.scrollTop > 0,
atEnd: element.scrollTop + element.clientHeight >= element.scrollHeight - 1,
}))).toEqual({ scrolled: true, atEnd: true })
expect(await content.evaluate((element) => {
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT)
let last: Node | null = null
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) last = node
if (last === null) return false
const length = last.textContent?.length ?? 0
const range = document.createRange()
range.setStart(last, length - 'End of long quote.'.length)
range.setEnd(last, length)
const end = range.getBoundingClientRect()
const panelBounds = element.parentElement!.getBoundingClientRect()
return end.top >= panelBounds.top && end.bottom <= panelBounds.bottom
})).toBe(true)
await page.mouse.move(0, 0)
await expect.poll(() => content.count()).toBe(0)
}
async function crossQuoteGap(page: Page, quote: Locator, text: string, placement: 'above' | 'below'): Promise<void> {
await quote.hover()
const content = page.locator('[data-quote-popover]')
await expect.poll(() => content.count()).toBe(1)
await expect.poll(() => content.textContent()).toBe(text)
const anchor = (await quote.locator('..').boundingBox())!
const panel = (await content.locator('..').boundingBox())!
expect(panel.width).toBeLessThan(300)
const above = panel.y < anchor.y
expect(above).toBe(placement === 'above')
const x = Math.max(anchor.x, panel.x) + Math.min(anchor.width, panel.width) / 2
const y = above ? (panel.y + panel.height + anchor.y) / 2 : (anchor.y + anchor.height + panel.y) / 2
expect(await page.evaluate(point =>
Boolean(document.elementFromPoint(point.x, point.y)?.closest('[data-hovercard-bridge]')), { x, y })).toBe(true)
await page.mouse.move(x, y, { steps: 8 })
await page.evaluate(() => new Promise<void>((resolve) => { requestAnimationFrame(() => { requestAnimationFrame(() => { resolve() }) }) }))
await expect.poll(() => content.count()).toBe(1)
expect(await content.textContent()).toBe(text)
await content.hover()
const body = (await content.boundingBox())!
await page.mouse.move(body.x + 12, body.y + 15)
await page.mouse.down()
await page.mouse.move(body.x + 60, body.y + 15, { steps: 5 })
await page.mouse.up()
expect(await page.evaluate(() => window.getSelection()?.toString().trim())).not.toBe('')
expect(await content.count()).toBe(1)
await page.keyboard.press('Escape')
await expect.poll(() => content.count()).toBe(0)
}
describe.skipIf(MODE === 'record')('web e2e: rich-text quotes', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let replayDir: string
let tripwire: ReturnType<typeof watchConsole>
beforeAll(async () => {
replayDir = await mkdtemp(join(tmpdir(), 'dsh-rich-text-quotes-replay-'))
const replayOverride = join(replayDir, 'replay.override.json')
await writeFile(replayOverride, JSON.stringify(REPLAY))
scaffold = await launchWebScaffold({
replayFixture: join(replayDir, 'override-only.jsonl'),
replayOverride,
})
const skillDir = join(scaffold.workspaceCwd, '.agents', 'skills', 'quoted-demo')
await mkdir(skillDir, { recursive: true })
await writeFile(join(skillDir, 'SKILL.md'), [
'---',
'name: quoted-demo',
'description: Example user-invoked skill',
'disable-model-invocation: true',
'---',
'',
'An example skill instruction.',
'',
].join('\n'))
const borrowed = await readFile(await selectedSessionFixture(BORROWED), 'utf8')
const createdAt = Date.now() - 60_000
await seedSession(scaffold, quoteHistoryFixture(borrowed), SESSION_ID, undefined, { createdAt })
await seedSession(scaffold, otherSessionFixture(borrowed), OTHER_ID, undefined, { createdAt: createdAt + 1_000 })
browser = await chromium.launch()
page = await newEnglishPage(browser)
await page.addInitScript(({ key, value }) => {
if (localStorage.getItem(key) === null) localStorage.setItem(key, value)
}, { key: LEGACY_DRAFT_KEY, value: LEGACY_DRAFT })
tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
const group = page.getByRole('treeitem', { name: /Ungrouped/ })
await group.waitFor({ timeout: 15_000 })
if (await group.getAttribute('aria-expanded') !== 'true') await group.click()
await openQuoteSession(page)
}, 120_000)
afterAll(async () => {
const failures: unknown[] = []
await browser?.close().catch((error: unknown) => failures.push(error))
await scaffold?.close().catch((error: unknown) => failures.push(error))
if (replayDir !== undefined) {
await rm(replayDir, { recursive: true, force: true })
.catch((error: unknown) => failures.push(error))
}
if (failures.length === 1) throw failures[0]
if (failures.length > 1) throw new AggregateError(failures, 'rich-text-quotes e2e cleanup failed')
})
it('renders and restores rich quotes with uniform styling after reference recognition', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-rich-text-quotes'))
const input = page.locator('[data-composer-input]').first()
await expect.poll(() => input.textContent()).toBe('Imported plain draft')
await input.click()
await input.press('ControlOrMeta+A')
await input.press('Backspace')
await expect.poll(() => input.textContent()).toBe('')
const sentQuote = page.locator('[data-message-bubble] [data-quote-preview]').filter({ hasText: SOURCE_TEXT }).first()
await sentQuote.waitFor({ timeout: 15_000 })
await expect.poll(() => sentQuote.getAttribute('title')).toBeNull()
await expect.poll(() => sentQuote.getAttribute('role')).toBeNull()
await expect.poll(() => sentQuote.getAttribute('tabindex')).toBeNull()
await expect.poll(() => sentQuote.locator('xpath=ancestor::*[@data-message-bubble][1]').textContent())
.toBe(`${SOURCE_TEXT} Use that excerpt.`)
const sentText = sentQuote.locator('[data-quote-preview-text]')
const sentWidth = await expectEllipsizedQuote(
sentQuote,
sentText,
sentQuote.locator('xpath=ancestor::*[@data-message-bubble][1]'),
)
const sentAppearance = await quoteAppearance(sentQuote)
expect(sentAppearance.gap).toBe('4px')
expect(sentAppearance.margin).toBe('0px')
await sentQuote.hover()
const quotePopover = page.locator('[data-quote-popover]')
await expect.poll(() => quotePopover.textContent()).toBe(SOURCE_TEXT)
const hoveredSentBounds = await sentQuote.boundingBox()
expect(hoveredSentBounds).not.toBeNull()
expect(hoveredSentBounds!.width).toBeCloseTo(sentWidth, 0)
await quotePopover.hover()
// Exceed the pointer grace to prove the panel itself keeps the popover open.
await page.waitForTimeout(300)
await expect.poll(() => quotePopover.count()).toBe(1)
await page.mouse.move(0, 0)
await expect.poll(() => quotePopover.count()).toBe(0)
const sourceBlocks = page.locator('[data-text-interaction-source]').filter({ hasText: SOURCE_TEXT })
const olderSource = page.getByRole('button', { name: 'Load earlier' })
if (await sourceBlocks.count() < 2 && await olderSource.isVisible()) await olderSource.click()
await expect.poll(() => sourceBlocks.count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(2)
const source = sourceBlocks.filter({ hasNot: page.locator('[data-quote-preview]') }).first()
await source.waitFor()
const quoteAction = page.getByRole('button', { name: 'Quote text', exact: true })
await source.scrollIntoViewIfNeeded()
await source.dispatchEvent('pointerdown', { pointerId: 1, pointerType: 'mouse', buttons: 1 })
await selectAllText(source)
await expect.poll(() => quoteAction.count()).toBe(0)
await source.dispatchEvent('pointerup', { pointerId: 1, pointerType: 'mouse', buttons: 0 })
await expect.poll(() => quoteAction.count()).toBe(1)
expect(await quoteAction.evaluate(element => element.closest('[data-chat-flow]') !== null)).toBe(true)
expect(await quoteAction.evaluate(element => getComputedStyle(element.closest('[data-text-interaction-surface]')!).position)).toBe('absolute')
await page.keyboard.press('Escape')
await expect.poll(() => quoteAction.count()).toBe(0)
await page.evaluate(() => window.dispatchEvent(new Event('resize')))
await expect.poll(() => quoteAction.count()).toBe(0)
await source.dispatchEvent('pointerdown', { pointerId: 1, pointerType: 'mouse', button: 0, buttons: 1 })
await selectAllText(source)
await source.dispatchEvent('pointerup', { pointerId: 1, pointerType: 'mouse', button: 0, buttons: 0 })
await expect.poll(() => quoteAction.count()).toBe(1)
await input.click()
await source.scrollIntoViewIfNeeded()
await expect.poll(() => quoteAction.count()).toBe(0)
await source.dispatchEvent('pointerdown', { pointerId: 1, pointerType: 'mouse', button: 0, buttons: 1 })
await selectAllText(source)
await source.dispatchEvent('pointerup', { pointerId: 1, pointerType: 'mouse', button: 0, buttons: 0 })
await expect.poll(() => quoteAction.count()).toBe(1)
await page.locator('[data-conversation-scroll]').evaluate((element) => {
element.scrollTop = element.scrollHeight
element.dispatchEvent(new Event('scroll'))
})
await expect.poll(() => quoteAction.isVisible()).toBe(false)
expect(await page.locator('[data-text-interaction-surface] button').count()).toBe(1)
await source.scrollIntoViewIfNeeded()
await expect.poll(() => quoteAction.isVisible()).toBe(true)
await quoteAction.click()
const draftQuote = page.locator('[data-composer-quote]')
const draftPreview = draftQuote.locator('[data-quote-preview]')
const draftPreviewText = draftPreview.locator('[data-quote-preview-text]')
await expect.poll(() => draftQuote.count()).toBe(1)
await expect.poll(() => draftQuote.textContent()).toContain(SOURCE_TEXT)
const draftText = draftPreviewText
await expect.poll(() => draftPreview.getAttribute('role')).toBeNull()
await expect.poll(() => draftPreview.getAttribute('tabindex')).toBeNull()
const draftWidth = await expectEllipsizedQuote(
draftPreview,
draftPreviewText,
input,
)
expect(await quoteAppearance(draftPreview)).toEqual(sentAppearance)
await draftText.hover()
await expect.poll(() => quotePopover.textContent()).toBe(SOURCE_TEXT)
const hoveredDraftBounds = await draftPreview.boundingBox()
expect(hoveredDraftBounds).not.toBeNull()
expect(hoveredDraftBounds!.width).toBeCloseTo(draftWidth, 0)
await page.mouse.move(0, 0)
await expect.poll(() => quotePopover.count()).toBe(0)
await input.press('Backspace')
await expect.poll(() => draftQuote.count()).toBe(0)
await expect.poll(() => input.textContent()).toBe('')
await input.press('ControlOrMeta+z')
await expect.poll(() => draftQuote.count()).toBe(1)
const removeQuote = draftQuote.getByRole('button', { name: 'Remove quote' })
const removeIcons = removeQuote.locator('svg')
await expect.poll(() => removeIcons.first().evaluate(element => getComputedStyle(element).opacity)).toBe('1')
await expect.poll(() => removeIcons.last().evaluate(element => getComputedStyle(element).opacity)).toBe('0')
await input.click()
await page.keyboard.press('ControlOrMeta+A')
await expect.poll(() => page.evaluate(() => window.getSelection()?.toString() ?? '')).toContain(SOURCE_TEXT)
await draftQuote.hover()
await expect.poll(() => removeIcons.first().evaluate(element => getComputedStyle(element).opacity)).toBe('0')
await expect.poll(() => removeIcons.last().evaluate(element => getComputedStyle(element).opacity)).toBe('1')
await removeQuote.focus()
await expect.poll(() => removeIcons.last().evaluate(element => getComputedStyle(element).opacity)).toBe('1')
await expectEllipsizedQuote(draftPreview, draftPreviewText, input)
await removeQuote.click()
await expect.poll(() => draftQuote.count()).toBe(0)
await expect.poll(() => input.textContent()).toBe('')
await expect.poll(() => page.evaluate(() => window.getSelection()?.isCollapsed ?? true)).toBe(true)
await page.keyboard.press('ControlOrMeta+z')
await expect.poll(() => draftQuote.count()).toBe(1)
await input.click()
await page.keyboard.press('ControlOrMeta+A')
await page.keyboard.press('ArrowRight')
await page.keyboard.press('Backspace')
await page.keyboard.press('Backspace')
await expect.poll(() => draftQuote.count()).toBe(0)
await page.keyboard.press('ControlOrMeta+z')
await expect.poll(() => draftQuote.count()).toBe(1)
await page.keyboard.press('ControlOrMeta+A')
await page.keyboard.press('ArrowLeft')
await page.keyboard.press('Delete')
await expect.poll(() => draftQuote.count()).toBe(0)
await page.keyboard.press('ControlOrMeta+z')
await expect.poll(() => draftQuote.count()).toBe(1)
await page.locator(`[data-row-key="session:${OTHER_ID}"]`).click()
await openQuoteSession(page)
await expect.poll(() => draftQuote.count()).toBe(1)
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
const restoredQuote = page.locator('[data-composer-quote]')
await expect.poll(() => restoredQuote.count(), { timeout: 15_000 }).toBe(1)
const restoredPreview = restoredQuote.locator('[data-quote-preview]')
const restoredText = restoredQuote.locator('[data-quote-preview-text]')
await expect.poll(() => restoredPreview.getAttribute('role')).toBeNull()
await restoredText.click()
await restoredQuote.getByRole('button', { name: 'Remove quote' }).click()
await expect.poll(() => input.textContent()).toBe('')
const process = page.locator('[data-turn-process]').first()
if (await process.getAttribute('aria-expanded') === 'false') await process.click()
await page.getByRole('button', { name: 'Analysis completed', exact: true }).click()
const think = page.getByRole('button', { name: /Think/u })
await think.click()
const reasoningSource = page.locator('[data-text-interaction-source]').filter({ hasText: THINKING_TEXT })
await reasoningSource.waitFor()
await selectAllText(reasoningSource)
await expect.poll(() => quoteAction.isVisible()).toBe(true)
const beforeComposerScroll = await reasoningSource.evaluate((element) => {
const scroller = element.closest<HTMLElement>('[data-conversation-scroll]')!
const composer = scroller.querySelector<HTMLElement>('[data-composer-seat]')!
const previous = scroller.scrollTop
scroller.scrollTop -= composer.getBoundingClientRect().top - element.getBoundingClientRect().top + 16
return previous
})
await expect.poll(() => reasoningSource.evaluate((element) => {
const composer = element.closest('[data-conversation-scroll]')!.querySelector('[data-composer-seat]')!
return element.getBoundingClientRect().top >= composer.getBoundingClientRect().top
})).toBe(true)
await expect.poll(() => quoteAction.isVisible()).toBe(false)
expect(await page.locator('[data-text-interaction-surface] button').count()).toBe(1)
await page.locator('[data-conversation-scroll]').evaluate((element, top) => { element.scrollTop = top }, beforeComposerScroll)
await expect.poll(() => quoteAction.isVisible()).toBe(true)
await quoteAction.click()
await expect.poll(() => draftQuote.textContent()).toContain(THINKING_TEXT)
await think.click()
await expect.poll(() => reasoningSource.isVisible()).toBe(false)
const reasoningQuote = draftQuote.locator('[data-quote-preview-text]')
await expect.poll(() => draftQuote.locator('[data-quote-preview]').getAttribute('role')).toBeNull()
await reasoningQuote.click()
await expect.poll(() => reasoningSource.isVisible()).toBe(false)
await expect.poll(() => think.getAttribute('aria-expanded')).toBe('false')
const backToBottom = page.getByRole('button', { name: 'Back to bottom', exact: true })
if (await backToBottom.isVisible()) await backToBottom.click()
await expect.poll(() => backToBottom.isVisible()).toBe(false)
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await draftQuote.getByRole('button', { name: 'Remove quote' }).click()
const loadEarlier = page.getByRole('button', { name: 'Load earlier' })
if (await loadEarlier.isVisible()) await loadEarlier.click()
await page.getByText('Filler message 3', { exact: true }).waitFor({ timeout: 15_000 })
await page.locator('[data-conversation-scroll]').evaluate((element) => {
element.scrollTop = 0
element.dispatchEvent(new Event('scroll'))
})
const syntaxSource = page.locator('[data-text-interaction-source]').filter({ hasText: DIRECT_SYNTAX_TEXT }).first()
await syntaxSource.waitFor({ timeout: 15_000 })
await syntaxSource.scrollIntoViewIfNeeded()
await selectAllText(syntaxSource)
await quoteAction.click()
const viewport = page.viewportSize()!
try {
await expectScrollableQuote(page, draftPreview, SELECTED_SYNTAX_TEXT)
await page.setViewportSize({ width: 420, height: 420 })
await expectScrollableQuote(page, draftPreview, SELECTED_SYNTAX_TEXT)
} finally {
await page.setViewportSize(viewport)
}
const beforeOffset = scaffold.ctx.agents.get(SessionId(SESSION_ID))!.session.seq
const settled = scaffold.whenTurnSettled()
await page.getByRole('button', { name: 'Send message' }).click()
const settledSessionId = await settled
await page.getByText(REPLY, { exact: true }).waitFor({ timeout: 20_000 })
const longSentQuote = page.locator('[data-message-bubble] [data-quote-preview]').filter({ hasText: SENT_SYNTAX_TEXT }).last()
try {
await expectScrollableQuote(page, longSentQuote, SENT_SYNTAX_TEXT)
await page.setViewportSize({ width: 420, height: 420 })
await expectScrollableQuote(page, longSentQuote, SENT_SYNTAX_TEXT)
} finally {
await page.setViewportSize(viewport)
}
expect(await longSentQuote.textContent()).toContain(LITERAL_CREDENTIAL)
const sentSource = longSentQuote.locator('[data-quote-preview-text]')
expect(await sentSource.textContent()).toBe(SENT_SYNTAX_TEXT)
expect(await sentSource.locator('*').count()).toBe(0)
const textAppearance = await quoteAppearance(sentSource)
const quoteStyle = await quoteAppearance(longSentQuote)
expect(textAppearance.color).toBe(quoteStyle.color)
expect(textAppearance.fontSize).toBe(quoteStyle.fontSize)
expect(textAppearance.lineHeight).toBe(quoteStyle.lineHeight)
expect(await longSentQuote.locator('[data-secret], [data-ref-chip], strong').count()).toBe(0)
const events = scaffold.ctx.agents.get(settledSessionId)!.session.snapshotEvents(beforeOffset)
expect(events.some(event => event.type === 'user/message'
&& event.data.source.kind === 'skill-invocation')).toBe(true)
expect(events.some(event => event.type === 'user/message'
&& event.data.source.kind === 'session-reference')).toBe(true)
const submitted = events.find(event => event.type === 'user/message'
&& event.data.source.kind === 'user')
expect(submitted).toMatchObject({
data: {
content: [{ type: 'text', text: formatQuoteText(SENT_SYNTAX_TEXT) }],
},
})
expect(submitted).not.toHaveProperty('data.source.quotes')
await expect.poll(() => input.textContent()).toBe('')
const persisted = await page.evaluate((key): unknown => JSON.parse(localStorage.getItem(key)!), CURRENT_DRAFT_KEY)
expect(persisted).toMatchObject({ draft: { text: '', references: [] } })
await page.reload({ waitUntil: 'load' })
await page.getByText(REPLY, { exact: true }).waitFor({ timeout: 20_000 })
await expectScrollableQuote(page, longSentQuote, SENT_SYNTAX_TEXT)
const literalQuote = await captureStableAria(
page, `[data-message-bubble]:has([data-quote-preview-text]):has-text("${LITERAL_CREDENTIAL}") >> nth=-1`, scaffold.workspaceCwd,
)
await compareOrRefreshGolden(UI_EXPECTED, `${snapshot}\n\n## Literal quote body\n\n${literalQuote}`, MODE)
await expect.poll(() => input.textContent()).toBe('')
expect(await page.locator('[data-slot-error]').count()).toBe(0)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
}, 120_000)
it('keeps adjacent quote previews exclusive while crossing their hover gaps', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-quote-hover-gap'))
const texts = ['First excerpt', 'Second excerpt'].map(line => Array.from({ length: 6 }, () => line).join('\n'))
const quoted = texts.map(formatQuoteText)
const separator = '\nx\n'
const draft = {
text: quoted[0]! + separator + quoted[1]!, references: [],
quotes: texts.map((text, index) => ({
text, offset: index === 0 ? 0 : quoted[0]!.length + separator.length, length: quoted[index]!.length,
})),
}
const viewport = page.viewportSize()!
try {
await page.evaluate(({ key, draft }) => {
localStorage.setItem(key, JSON.stringify({ draft, view: 'chat', viewRequest: null }))
}, { key: CURRENT_DRAFT_KEY, draft })
await page.reload({ waitUntil: 'load' })
const draftQuotes = page.locator('[data-composer-quote] [data-quote-preview]')
await expect.poll(() => draftQuotes.count()).toBe(2)
await crossQuoteGap(page, draftQuotes.last(), texts[1]!, 'above')
const settled = scaffold.whenTurnSettled()
await page.getByRole('button', { name: 'Send message' }).click()
await settled
await page.setViewportSize({ width: 600, height: 1000 })
const sentQuotes = page.locator('[data-message-bubble] [data-quote-preview]').filter({ hasText: 'First excerpt' })
await sentQuotes.last().waitFor()
await sentQuotes.last().evaluate((element) => { element.scrollIntoView({ block: 'center' }) })
await crossQuoteGap(page, sentQuotes.last(), texts[0]!, 'below')
expect(tripwire.pageErrors).toEqual([])
} finally {
await page.keyboard.press('Escape')
await page.setViewportSize(viewport)
}
})
})
+130
View File
@@ -0,0 +1,130 @@
// UI-only phone geometry for the Automation tasks page: no text field it owns
// sits under the 16px touch step, so the page never reads wider than the screen
// — iOS Safari zooms the visual viewport for a focused field below it.
import type { Browser, Locator, Page } from 'playwright'
import { chromium } from 'playwright'
import { expect, it } from 'vitest'
import { SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-schedule'
import { launchWebScaffold, watchConsole } from './scaffold.ts'
const PHONE_TASK_SESSION = SessionId('schedule-phone-web-e2e')
const PHONE_TASK_TITLE = 'Review overdue deployment'
const PHONE_PAGE_WIDTH = 390
/** The page's box against the screen, plus the font steps of its text fields. */
interface ViewFacts {
width: number
documentWidth: number
smallestField: number
name: string
search: string
instruction: string
}
/** Open the Automation tasks page and return its panel. */
async function openTaskManager(page: Page, url: string): Promise<Locator> {
await page.goto(url, { waitUntil: 'load' })
await page.getByRole('button', { name: '自动化任务', exact: true }).click()
const panel = page.getByTestId('task-manager-page')
await panel.getByRole('heading', { name: '自动化任务', exact: true }).waitFor({ timeout: 30_000 })
await page.evaluate(() => document.fonts.ready)
return panel
}
/** Read the facts above from the open view. */
async function viewFacts(page: Page): Promise<ViewFacts> {
return page.evaluate(() => {
const panel = document.querySelector('[data-testid="task-manager-page"]')
const fields = Array.from(panel?.querySelectorAll('input, textarea, select') ?? [])
const steps = fields.map(element => Number.parseFloat(getComputedStyle(element).fontSize))
const step = (selector: string): string => {
const element = document.querySelector(selector)
return element === null ? 'absent' : getComputedStyle(element).fontSize
}
return {
width: window.innerWidth,
documentWidth: document.documentElement.scrollWidth,
smallestField: steps.length === 0 ? -1 : Math.min(...steps),
name: step('[class*="_editName"]'),
search: step('[class*="_searchField"] input'),
instruction: step('[class*="_instruction"]'),
}
})
}
it('keeps the automation page on screen with its text fields at the touch step', async () => {
const scaffold = await launchWebScaffold()
const url = scaffold.authenticatedUrl
try {
await scaffold.ctx.schedule.create(PHONE_TASK_SESSION, {
prompt: 'Check the deployment log', title: PHONE_TASK_TITLE, at: '2099-08-25T12:08:00.000Z',
})
const browser: Browser = await chromium.launch()
try {
// A coarse pointer is the touch device, so the phone context takes the
// 16px step.
const phone = await browser.newContext({
viewport: { width: PHONE_PAGE_WIDTH, height: 844 }, deviceScaleFactor: 3, isMobile: true, hasTouch: true, locale: 'zh-CN',
})
const phonePage = await phone.newPage()
const phoneConsole = watchConsole(phonePage)
const phonePanel = await openTaskManager(phonePage, url)
await expect.poll(() => viewFacts(phonePage)).toEqual({
width: PHONE_PAGE_WIDTH,
documentWidth: PHONE_PAGE_WIDTH,
smallestField: 16,
name: 'absent',
search: '16px',
instruction: 'absent',
})
// The detail keeps the floor too, and the name control keeps the 20px step
// the floor must not flatten.
await phonePanel.getByRole('button', { name: PHONE_TASK_TITLE, exact: true }).click()
await phonePanel.locator('[class*="_detailScroll"]').waitFor({ timeout: 10_000 })
await expect.poll(() => viewFacts(phonePage)).toEqual({
width: PHONE_PAGE_WIDTH,
documentWidth: PHONE_PAGE_WIDTH,
smallestField: 16,
name: '20px',
search: '16px',
instruction: '16px',
})
await phone.close()
// A fine pointer keeps the compact steps: the floor is pointer-scoped.
const desktop = await browser.newContext({ viewport: { width: 1024, height: 800 }, locale: 'zh-CN' })
const desktopPage = await desktop.newPage()
const desktopConsole = watchConsole(desktopPage)
const desktopPanel = await openTaskManager(desktopPage, url)
await expect.poll(() => viewFacts(desktopPage)).toEqual({
width: 1024,
documentWidth: 1024,
smallestField: 14,
name: 'absent',
search: '14px',
instruction: 'absent',
})
await desktopPanel.getByRole('button', { name: PHONE_TASK_TITLE, exact: true }).click()
await desktopPanel.locator('[class*="_detailScroll"]').waitFor({ timeout: 10_000 })
await expect.poll(() => viewFacts(desktopPage)).toEqual({
width: 1024,
documentWidth: 1024,
smallestField: 14,
name: '20px',
search: '14px',
instruction: '14px',
})
await desktop.close()
expect(phoneConsole.pageErrors).toEqual([])
expect(phoneConsole.warnings).toEqual([])
expect(desktopConsole.pageErrors).toEqual([])
expect(desktopConsole.warnings).toEqual([])
} finally {
await browser.close()
}
} finally {
await scaffold.close()
}
}, 180_000)
+7 -7
View File
@@ -46,12 +46,12 @@ it.skipIf(webSnapshotMode() === 'record')('inspects a replayed Session log and C
await page.locator('[data-sidebar-right-guide-entry="session-inspector-log"]').click()
const mode = page.getByRole('combobox', { name: 'Presentation', exact: true })
expect(await mode.inputValue()).toBe('session-log')
const raw = page.getByRole('table', { name: 'Raw Log', exact: true })
const raw = page.getByRole('table', { name: 'Raw log', exact: true })
await raw.getByRole('button', { name: 'Filter types', exact: true }).click()
await page.getByRole('combobox', { name: 'Type keyword', exact: true }).fill('user/message')
await page.getByRole('option', { name: 'user/message', exact: true }).click()
await raw.getByRole('button', { name: 'user/message', exact: true }).first().click()
const panel = page.locator('section[aria-label="Raw Log"]')
const panel = page.locator('section[aria-label="Raw log"]')
const details = panel.getByRole('separator').locator('..').locator('ul').first()
expect(await details.innerText()).toContain(prompts[0])
expect(await details.locator('details:not([open])').count()).toBe(0)
@@ -64,14 +64,14 @@ it.skipIf(webSnapshotMode() === 'record')('inspects a replayed Session log and C
const replacements = [...new Set(times)].filter(value => /^\d{2}:\d{2}:\d{2}\.\d{3}$/.test(value))
.map(value => [value, '{{time}}'] as const)
await compareOrRefreshGolden(join(directory, 'log.expected.md'),
await captureStableAria(page, 'section[aria-label="Raw Log"] table', scaffold.workspaceCwd, { replacements }), webSnapshotMode())
await captureStableAria(page, 'section[aria-label="Raw log"] table', scaffold.workspaceCwd, { replacements }), webSnapshotMode())
await mode.selectOption('chat-node')
const groups = page.getByRole('table', { name: 'Chat Group', exact: true })
const groups = page.getByRole('table', { name: 'Chat group', exact: true })
expect(await groups.getByRole('columnheader', { name: 'Time (UTC)' }).count()).toBe(0)
await groups.getByRole('button', { name: 'assistant-step', exact: true }).first().click()
await page.getByRole('navigation', { name: 'Raw data navigation', exact: true }).waitFor()
const groupPanel = page.locator('section[aria-label="Chat Group"]')
await groupPanel.locator('summary').filter({ hasText: 'Node Data' }).click()
const groupPanel = page.locator('section[aria-label="Chat group"]')
await groupPanel.locator('summary').filter({ hasText: 'Node data' }).click()
const blocks = groupPanel.getByText('blocks:', { exact: true }).locator('..')
await blocks.click()
await blocks.locator('..').getByText('Other properties:', { exact: true }).waitFor()
@@ -104,7 +104,7 @@ it.skipIf(webSnapshotMode() === 'record')('highlights nearby Chat content when t
await page.getByText('DONE', { exact: true }).waitFor()
await page.getByRole('button', { name: 'Open right sidebar', exact: true }).click()
await page.locator('[data-sidebar-right-guide-entry="session-inspector-log"]').click()
const table = page.getByRole('table', { name: 'Raw Log', exact: true })
const table = page.getByRole('table', { name: 'Raw log', exact: true })
await table.getByRole('button', { name: 'Filter types', exact: true }).click()
await page.getByRole('combobox', { name: 'Type keyword', exact: true }).fill('tool/call')
await page.getByRole('button', { name: 'Apply filter', exact: true }).click()
+105 -2
View File
@@ -84,6 +84,17 @@ describe('web e2e: settings modal and General preferences', () => {
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
const panelBox = await dialog.boundingBox()
const optionsBox = await dialog.locator('[class*="_options"]').boundingBox()
const firstSectionBox = await dialog.getByRole('navigation').getByRole('button').first().boundingBox()
expect(optionsBox!.y - panelBox!.y).toBe(54)
expect(firstSectionBox!.y - panelBox!.y).toBe(64)
const lastSection = dialog.getByRole('navigation').getByRole('button').last()
await lastSection.focus()
await page.keyboard.press('Tab')
expect(await dialog.getByRole('button', { name: '打开配置文件' }).evaluate(el => el === document.activeElement)).toBe(true)
await page.keyboard.press('Tab')
expect(await dialog.getByRole('button', { name: '关闭', exact: true }).evaluate(el => el === document.activeElement)).toBe(true)
// General is active by default; Permission, Language and Appearance are functional.
expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBe('true')
await dialog.getByRole('button', { name: '工作区内修改' }).waitFor({ timeout: 10_000 })
@@ -210,7 +221,7 @@ describe('web e2e: settings modal and General preferences', () => {
await page.keyboard.press('Escape')
await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
// Close path 2: the header close button (focus lands there on open).
// Close path 2: the header close button.
await openSettings(page, 'zh')
await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '关闭' }).click()
await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
@@ -227,7 +238,7 @@ describe('web e2e: settings modal and General preferences', () => {
const dialog = overflowPage.getByRole('dialog', { name: '设置', exact: true })
const navigation = dialog.getByRole('navigation')
const list = navigation.locator('[class*="_navList"]')
const title = navigation.getByText('设置', { exact: true })
const title = dialog.getByText('设置', { exact: true })
const options = dialog.locator('[class*="_options"]')
const last = navigation.getByRole('button').last()
await last.waitFor()
@@ -261,6 +272,98 @@ describe('web e2e: settings modal and General preferences', () => {
expect(console.warnings).toEqual([])
})
it('fills a phone viewport and turns the rail into the horizontal section strip', async () => {
const phone = await browser.newPage({ viewport: { width: 390, height: 844 }, locale: ZH_BROWSER_LOCALE })
onTestFinished(() => phone.close())
onTestFailed(() => saveSettingsFailureShot(phone, 'web-e2e-settings-phone'))
const console = watchConsole(phone)
await phone.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await openSettings(phone, 'zh')
const dialog = phone.getByRole('dialog', { name: '设置', exact: true })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: '通用设置', exact: true }).waitFor({ timeout: 10_000 })
// The panel is the viewport: no centered card, no side margins, no rail.
await expect.poll(() => dialog.boundingBox()).toEqual({ x: 0, y: 0, width: 390, height: 844 })
// The rail reads left to right on one line instead of stacking.
const navigation = dialog.getByRole('navigation')
const list = navigation.locator('[class*="_navList"]')
const cells = navigation.getByRole('button')
const first = await cells.first().boundingBox()
const second = await cells.nth(1).boundingBox()
expect(first).not.toBeNull()
expect(second).not.toBeNull()
expect(second!.x).toBeGreaterThanOrEqual(first!.x + first!.width - 1)
expect(Math.abs(second!.y - first!.y)).toBeLessThan(1)
expect(await list.evaluate(el => getComputedStyle(el).overflowX)).toBe('auto')
// The shipped sections overflow the strip, which scrolls sideways; the
// section column starts below it and keeps the panel's width.
const strip = await list.boundingBox()
const options = dialog.locator('[class*="_options"]')
const optionsBox = await options.boundingBox()
expect(strip).not.toBeNull()
expect(optionsBox).not.toBeNull()
expect(await list.evaluate(el => el.scrollWidth)).toBeGreaterThan(await list.evaluate(el => el.clientWidth))
expect(optionsBox!.y).toBeGreaterThanOrEqual(strip!.y + strip!.height - 1)
expect(Math.round(optionsBox!.width)).toBe(390)
expect(await phone.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390)
// Tapping a cell swaps the page, and a General row keeps its label and
// control on one line rather than stacking.
await cells.nth(1).click()
await expect.poll(() => cells.nth(1).getAttribute('aria-current')).toBe('true')
await expect.poll(() => dialog.getByText('权限', { exact: true }).count()).toBe(0)
await cells.first().click()
await expect.poll(() => dialog.getByText('权限', { exact: true }).count(), { timeout: 5_000 }).toBe(1)
const permissionRow = dialog.getByText('权限', { exact: true }).locator('..')
const permissionBox = await permissionRow.boundingBox()
const controlBox = await dialog.getByRole('button', { name: '工作区内修改' }).boundingBox()
expect(controlBox).not.toBeNull()
expect(permissionBox).not.toBeNull()
expect(controlBox!.y).toBeLessThan(permissionBox!.y + permissionBox!.height)
// The Appearance cubes share one row instead of wrapping to two.
const cubes = dialog.getByRole('button', { name: /浅色|深色|跟随系统/ })
const cubeBoxes = await cubes.evaluateAll(nodes => nodes.map(node => node.getBoundingClientRect().top))
expect(new Set(cubeBoxes.map(top => Math.round(top))).size).toBe(1)
await cells.first().focus()
for (let index = 1; index < await cells.count(); index += 1) {
await phone.keyboard.press('Tab')
}
const last = cells.last()
expect(await last.evaluate(el => el === document.activeElement)).toBe(true)
const lastBox = await last.boundingBox()
// Scroll offsets can round while glyph widths remain fractional.
expect(Math.round(lastBox!.x)).toBeGreaterThanOrEqual(0)
expect(Math.round(lastBox!.x + lastBox!.width)).toBeLessThanOrEqual(390)
await last.click()
await expect.poll(() => last.getAttribute('aria-current')).toBe('true')
await phone.keyboard.press('Escape')
await dialog.waitFor({ state: 'hidden' })
await openSettings(phone, 'zh')
const active = dialog.locator('[aria-current="true"]')
expect(await active.textContent()).toBe('通用设置')
expect(await active.evaluate(el => el === document.activeElement)).toBe(true)
const close = dialog.getByRole('button', { name: '关闭', exact: true })
const closeBox = await close.boundingBox()
expect(closeBox!.width).toBe(28)
expect(closeBox!.x).toBeGreaterThanOrEqual(0)
expect(closeBox!.y).toBeGreaterThanOrEqual(0)
expect(closeBox!.x + closeBox!.width).toBeLessThanOrEqual(390)
expect(closeBox!.y + closeBox!.height).toBeLessThanOrEqual(844)
await close.click()
await dialog.waitFor({ state: 'hidden' })
const trigger = phone.getByRole('button', { name: '账号菜单', exact: true })
await expect.poll(() => trigger.evaluate(el => el === document.activeElement)).toBe(true)
expect(console.pageErrors).toEqual([])
expect(console.warnings).toEqual([])
})
it('stores Permission as the default for future sessions without changing an existing session', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-permission'))
const createSession = async (id: string) => {
+5 -5
View File
@@ -42,7 +42,7 @@ it('toggles bottom DevTools from the page and iframe without a selected Session'
const close = panel.getByRole('button', { name: 'Collapse', exact: true })
expect(await close.textContent()).toBe('')
expect(await close.locator('svg').count()).toBe(1)
const divider = panel.getByRole('separator', { name: 'Resize NodeJS Inspector panel' })
const divider = panel.getByRole('separator', { name: 'Resize NodeJS inspector panel' })
const drag = async (deltaY: number): Promise<void> => {
const box = (await divider.boundingBox())!
const x = box.x + box.width / 2
@@ -93,10 +93,10 @@ it.each([
openSidebar: '打开右侧边栏', sidebarDescription: '在侧边栏分析当前会话原始日志和聊天分组数据',
},
{
locale: 'en-US', file: 'en', plugins: 'Plugins', open: 'View Developer Tools', bundle: 'Developer Tools',
locale: 'en-US', file: 'en', plugins: 'Plugins', open: 'View Developer tools', bundle: 'Developer tools',
description: 'Inspect and debug raw session data, grouped chat messages, and the NodeJS backend.',
inspector: 'NodeJS Inspector', inspectorDescription: 'Press Ctrl/Cmd+Shift+. to open Chrome DevTools in the bottom panel for development and debugging.',
session: 'Session Log', sessionDescription: "Enable analysis of the current session's raw data and grouped chat messages in the session sidebar.",
inspector: 'NodeJS inspector', inspectorDescription: 'Press Ctrl/Cmd+Shift+. to open Chrome DevTools in the bottom panel for development and debugging.',
session: 'Session log', sessionDescription: "Enable analysis of the current session's raw data and grouped chat messages in the session sidebar.",
openSidebar: 'Open right sidebar', sidebarDescription: 'Analyze raw logs and grouped chat messages for the current session in the sidebar.',
},
])('shows the Inspector bundle and component copy in $locale', async (copy) => {
@@ -150,7 +150,7 @@ it.each([
` description: ${await sessionGuide.getByText(copy.sidebarDescription, { exact: true }).innerText()}`,
`- NodeJS bottom: ${await bottomTitle.innerText()}`)
await sessionGuide.click()
await page.getByRole('table', { name: copy.locale === 'zh-CN' ? '原始数据' : 'Raw Log', exact: true }).waitFor()
await page.getByRole('table', { name: copy.locale === 'zh-CN' ? '原始数据' : 'Raw log', exact: true }).waitFor()
await compareOrRefreshGolden(fileURLToPath(new URL(`./expected/shared-inspector/metadata-${copy.file}.expected.md`, import.meta.url)),
snapshot.join('\n'), webSnapshotMode())
})
@@ -1,4 +1,4 @@
- dialog "Agent Team":
- dialog "Agent team":
- heading "Members" [level=3]
- button "lead Current chat Inactive" [disabled]
- heading "Shared tasks 2" [level=3]
@@ -1,4 +1,4 @@
- dialog "Agent Team":
- dialog "Agent team":
- heading "Members" [level=3]
- button "lead Current chat Inactive" [disabled]
- heading "Shared tasks 1" [level=3]
+4 -4
View File
@@ -70,9 +70,9 @@ it('persists the Git Worktrees switch and removes its capability while retaining
}
let panel = await openPlugins()
let card = panel.locator(`[data-plugin-group="more"] [data-plugin-package="${BUNDLE}"]`)
await card.getByRole('button', { name: 'View Git Worktrees', exact: true }).waitFor()
await card.getByRole('button', { name: 'View Git worktrees', exact: true }).waitFor()
expect(await card.getByText('Experimental', { exact: true }).count()).toBe(1)
let toggle = card.getByRole('switch', { name: 'Enable Git Worktrees', exact: true })
let toggle = card.getByRole('switch', { name: 'Enable Git worktrees', exact: true })
expect(await toggle.getAttribute('aria-checked')).toBe('false')
expect(await selectedBundles()).not.toContain(BUNDLE)
expect(rows()).toEqual([])
@@ -92,7 +92,7 @@ it('persists the Git Worktrees switch and removes its capability while retaining
await page.reload({ waitUntil: 'load' })
panel = await openPlugins()
card = panel.locator(`[data-plugin-group="more"] [data-plugin-package="${BUNDLE}"]`)
toggle = card.getByRole('switch', { name: 'Enable Git Worktrees', exact: true })
toggle = card.getByRole('switch', { name: 'Enable Git worktrees', exact: true })
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true')
expect(tools()).toContain('create_worktree')
await toggle.click()
@@ -108,7 +108,7 @@ it('persists the Git Worktrees switch and removes its capability while retaining
await page.reload({ waitUntil: 'load' })
panel = await openPlugins()
await expect.poll(() => panel.getByRole('switch', { name: 'Enable Git Worktrees', exact: true })
await expect.poll(() => panel.getByRole('switch', { name: 'Enable Git worktrees', exact: true })
.getAttribute('aria-checked')).toBe('false')
expect(tools()).not.toContain('create_worktree')
expect(await selectedBundles()).not.toContain(BUNDLE)
+2
View File
@@ -23,6 +23,7 @@
// this client-registered project must not also hold them (one program
// cannot see both sides of the cordis Context merges).
"exclude": [
"tests/rich-text-quotes.e2e.ts",
"tests/hover-input.e2e.ts",
"tests/collapse-timing.e2e.ts",
"tests/notice-wake-tail.e2e.ts",
@@ -89,6 +90,7 @@
"tests/cua-optin.e2e.ts",
"tests/cua-permissions.e2e.ts",
"tests/schedule-header.e2e.ts",
"tests/schedule-phone.e2e.ts",
"tests/worktree-opt-in.e2e.ts",
"tests/session-inspector.e2e.ts",
"tests/shared-inspector.e2e.ts",
@@ -30,7 +30,6 @@ import * as SessionTurnOutlinePlugin from '@deepseek-ai/dsh-session-turn-outline
import TokenMeter from '@deepseek-ai/dsh-token-meter'
// These Host-only adapters have no public Node export and are compiled into the benchmark worker.
import { SessionHistoryController } from '../../packages/api/session-controller/src/history.ts'
import { modelSelectionProjection } from '../../packages/api/session-controller/src/model-selection-projection.ts'
import { assertBuiltBenchmarkRuntime } from '../support/built-worker.ts'
import { SYNTHETIC_SESSION_ID } from './session-open.constants.ts'
@@ -131,7 +130,6 @@ function memoryDelta(
async function installProjectionSet(ctx: Context): Promise<void> {
// Mirrors projection owners mounted by the base and web-app bundles without timing profile boot.
await ctx.sessionProjections.register(agentPresetProjectionDefinition)
await ctx.sessionProjections.register(modelSelectionProjection)
await ctx.plugin(SessionTitleService, {
fallbackMaxWords: 5,
fallbackMaxBytes: 40,
+4 -4
View File
@@ -24,11 +24,11 @@
en: e89802f066e4955d
zh: ea2fb4bef94e086d
/deepseek-harness-architecture/turn-flow:
en: 7a2a9d68e4fa1fe4
zh: 67f40bac3d1959cc
en: d2e54d2e197cee82
zh: e085148bb4ae8edd
/deepseek-harness-architecture/session-log:
en: f1729d805c20df36
zh: 6b71b6039663e2e7
en: 8a7f19b95d426c99
zh: d5bb0538404e20ea
/deepseek-harness-architecture/capability-seams:
en: a5bc20c3f2a1b370
zh: 91773a281410b2f4
+3 -3
View File
@@ -83,7 +83,7 @@ A **step** is one model request plus the tools it calls. A **turn** is zero or m
```text
turn/start
claim next-step input plus one queued message
assemble prompt sections + tool schemas
snapshot Session model selection; assemble prompt sections + tool schemas
-> agent/pre-step; project runtime context at fallback
reject | enter(messages, startsRequestSeries?)
reject, or a first enter rewritten empty -> close the turn with no step
@@ -109,7 +109,7 @@ Factory setup follows SessionStore entry and `session/created`, preceding `agent
One inbox feeds the driver; injected context waits for a waking message. AgentLoop’s durable `inbox` projection exposes pending input without live Agents.
`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes append non-empty updates after cached history, including supported tool updates; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)).
The loop snapshots Session model selection before assembly, sharing it through `AssembleContext.modelSelection` and `agent/pre-step.modelSelection`; retries retain that selection, and omitted reasoning effort uses adapter defaults. `agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details.
The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes.
@@ -125,7 +125,7 @@ Session consumers know only the current logical format. Header-only `stat` and `
**Model-visible means logged.** Every model request must be reconstructable from the log. New model-visible inputs require session events. Plugins changing existing message content register [pure message projections](subsystems/session.md#plugin-owned-message-projections); detached readers supply the same definitions explicitly. Tool changes are capability-independent; [Session tool history](../packages/core/session/README.md) supplies provider declarations.
**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, and carriers batch cropped client views with `snapshot()`. Domain services await registration during activation, retain private handles, and expose business methods for other plugins. Releasing registration removes global participation; retained handles continue folding Session events. `AgentRegistry` owns turn and step state and exposes `turnBoundaryOf(session)` ([registration lifecycle](../packages/session/session-projection/README.md)).
**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, and carriers batch cropped client views with `snapshot()`. Domain services await registration during activation, retain private handles, and expose business methods for other plugins. Releasing registration removes global participation; retained handles continue folding Session events. `AgentRegistry` owns turn and step state and exposes `turnBoundaryOf(session)` ([registration lifecycle](../packages/session/session-projection/README.md)). `AgentLoop` owns `modelSelection` and constructs each Agent’s `model` instance alongside its inbox.
[Working directories](subsystems/working-directory.md) supply user context and execution paths without changing original project identity, sandbox write roots, or existing process directories.
+3 -3
View File
@@ -87,7 +87,7 @@ AgentLoop 在启动已排队工作前等待串行 `agent/created`,初始化失
```text
turn/start
claim next-step input plus one queued message
assemble prompt sections + tool schemas
snapshot Session model selection; assemble prompt sections + tool schemas
-> agent/pre-step; project runtime context at fallback
reject | enter(messages, startsRequestSeries?)
reject, or a first enter rewritten empty -> close the turn with no step
@@ -113,7 +113,7 @@ turn/end
输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。
`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新,包括同时发生的受支持工具更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。
循环在组装前快照 Session 模型选择,并通过 `AssembleContext.modelSelection` 与 `agent/pre-step.modelSelection` 传递该选择;重试保留该选择,省略推理强度时使用适配器默认值。`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。
循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。
@@ -129,7 +129,7 @@ Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list`
**模型可见即已记录。** 每个模型请求都必须能从日志重建。新增模型可见输入需要会话事件。修改现有消息内容的插件注册[纯消息投影](subsystems/session.zh.md#plugin-owned-message-projections),独立读取器显式传入相同的处理器。 工具变更不依赖能力;[Session 工具历史](../packages/core/session/README.zh.md)提供提供方声明。
**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。领域服务在激活时等待注册完成,在内部保留句柄,并向其他插件提供业务方法。释放注册会移除全局参与关系;已有句柄仍可继续折叠 Session 事件。`AgentRegistry` 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`([注册生命周期](../packages/session/session-projection/README.zh.md))。
**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。领域服务在激活时等待注册完成,在内部保留句柄,并向其他插件提供业务方法。释放注册会移除全局参与关系;已有句柄仍可继续折叠 Session 事件。`AgentRegistry` 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`([注册生命周期](../packages/session/session-projection/README.zh.md))。 `AgentLoop` 持有 `modelSelection`,并与 inbox 一起构建每个 Agent 的 `model` 实例。
[工作目录](subsystems/working-directory.zh.md) 提供用户上下文和执行路径,不改变原始项目标识、沙箱写入根目录或已有进程目录。
+5 -4
View File
@@ -16,7 +16,7 @@ Each package entry labels its data with three identifiers: `inject` lists the se
- `inject`: `agents` · `llm` · `sessionPersistence` · `sessions`
- `refs`: `Stream` (`@agentclientprotocol/sdk`)
- `source`: [`packages/acp/acp/src/index.ts:75`](../packages/acp/acp/src/index.ts)
- `source`: [`packages/acp/acp/src/index.ts:74`](../packages/acp/acp/src/index.ts)
```ts config-catalog
/** Plugin config: the provider/model selection used for each ACP-created agent. */
@@ -96,7 +96,7 @@ export interface Config {
- `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections`
- `refs`: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md) · `Volatile` (`@deepseek-ai/cosmokit`)
- `source`: [`packages/core/agent-loop/src/index.ts:248`](../packages/core/agent-loop/src/index.ts)
- `source`: [`packages/core/agent-loop/src/index.ts:251`](../packages/core/agent-loop/src/index.ts)
```ts config-catalog
/** Agent-loop plugin configuration. */
@@ -106,7 +106,7 @@ export interface Config {
* omission defaults to {@link DEFAULT_MAX_PARALLEL_TOOL_CALLS}.
*/
maxParallelToolCalls: Volatile<number>
/** Agents created or resumed at plugin startup. */
/** Agents created or resumed at plugin startup; a resumed agent records its configured route as the Session's model selection. */
agents: (AgentOptions & {
/** Stable config label used in logs and as the fresh combined-id prefix. */
id: string
@@ -2604,7 +2604,7 @@ export interface Config {
maxOutputBytes?: number
/** V8 old-generation heap limit in MiB; native allocations are excluded. */
maxOldGenerationSizeMb?: number
/** Maximum control frame, outstanding argument and queued control-output bytes. */
/** Maximum control message, outstanding binding-argument and queued control-output bytes. */
maxMessageBytes?: number
/** Maximum simultaneous host binding calls accepted from a program. */
maxPendingCalls?: number
@@ -4785,6 +4785,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| `@deepseek-ai/dsh-llm` | — | [`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts) |
| `@deepseek-ai/dsh-lsp` | — | [`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts) |
| `@deepseek-ai/dsh-mcp-resources` | `tools` | [`packages/mcp/mcp-resources/src/index.ts`](../packages/mcp/mcp-resources/src/index.ts) |
| `@deepseek-ai/dsh-message-quotes` | — | [`packages/session/message-quotes/src/index.ts`](../packages/session/message-quotes/src/index.ts) |
| `@deepseek-ai/dsh-otel` | — | [`packages/telemetry/otel/src/index.ts`](../packages/telemetry/otel/src/index.ts) |
| `@deepseek-ai/dsh-sandbox-ssh` | `ssh` | [`packages/ssh/sandbox-ssh/src/index.ts`](../packages/ssh/sandbox-ssh/src/index.ts) |
| `@deepseek-ai/dsh-session` | — | [`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts) |
+5 -4
View File
@@ -18,7 +18,7 @@
- `inject`: `agents` · `llm` · `sessionPersistence` · `sessions`
- `refs`: `Stream` (`@agentclientprotocol/sdk`)
- `source`: [`packages/acp/acp/src/index.ts:75`](../packages/acp/acp/src/index.ts)
- `source`: [`packages/acp/acp/src/index.ts:74`](../packages/acp/acp/src/index.ts)
```ts config-catalog
/** Plugin config: the provider/model selection used for each ACP-created agent. */
@@ -98,7 +98,7 @@ export interface Config {
- `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections`
- `refs`: [`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md) · `Volatile` (`@deepseek-ai/cosmokit`)
- `source`: [`packages/core/agent-loop/src/index.ts:248`](../packages/core/agent-loop/src/index.ts)
- `source`: [`packages/core/agent-loop/src/index.ts:251`](../packages/core/agent-loop/src/index.ts)
```ts config-catalog
/** Agent-loop plugin configuration. */
@@ -108,7 +108,7 @@ export interface Config {
* omission defaults to {@link DEFAULT_MAX_PARALLEL_TOOL_CALLS}.
*/
maxParallelToolCalls: Volatile<number>
/** Agents created or resumed at plugin startup. */
/** Agents created or resumed at plugin startup; a resumed agent records its configured route as the Session's model selection. */
agents: (AgentOptions & {
/** Stable config label used in logs and as the fresh combined-id prefix. */
id: string
@@ -2606,7 +2606,7 @@ export interface Config {
maxOutputBytes?: number
/** V8 old-generation heap limit in MiB; native allocations are excluded. */
maxOldGenerationSizeMb?: number
/** Maximum control frame, outstanding argument and queued control-output bytes. */
/** Maximum control message, outstanding binding-argument and queued control-output bytes. */
maxMessageBytes?: number
/** Maximum simultaneous host binding calls accepted from a program. */
maxPendingCalls?: number
@@ -4787,6 +4787,7 @@ export interface Config {
| `@deepseek-ai/dsh-llm` | — | [`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts) |
| `@deepseek-ai/dsh-lsp` | — | [`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts) |
| `@deepseek-ai/dsh-mcp-resources` | `tools` | [`packages/mcp/mcp-resources/src/index.ts`](../packages/mcp/mcp-resources/src/index.ts) |
| `@deepseek-ai/dsh-message-quotes` | — | [`packages/session/message-quotes/src/index.ts`](../packages/session/message-quotes/src/index.ts) |
| `@deepseek-ai/dsh-otel` | — | [`packages/telemetry/otel/src/index.ts`](../packages/telemetry/otel/src/index.ts) |
| `@deepseek-ai/dsh-sandbox-ssh` | `ssh` | [`packages/ssh/sandbox-ssh/src/index.ts`](../packages/ssh/sandbox-ssh/src/index.ts) |
| `@deepseek-ai/dsh-session` | — | [`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts) |
+2 -2
View File
@@ -21,8 +21,8 @@
en: 0894bd9f35430753
zh: 2d28090e71134edd
/cookbook-adding-a-workspace-package/5-add-optional-plugin-display-metadata:
en: ed501a72ab8ba6e7
zh: 8ad775b735887a10
en: cb799f5d561dd917
zh: 7739af36ef8ffb3f
/cookbook-adding-a-workspace-package/6-verify:
en: 80c7fb90b1175aae
zh: 2ba37962dbc57299
+1 -1
View File
@@ -136,7 +136,7 @@ Merge these entries into `package.json`, retaining existing runtime exports and
}
```
Keep language files together in `locale/`, with `en.json` as the discovery entry. Fields are optional; present values must be non-empty strings. Missing files or fields permit fallback, while malformed JSON or invalid fields produce a per-plugin diagnostic.
Keep language files together in `locale/`, with `en.json` as the discovery entry. Fields are optional; present values must be non-empty strings. English `meta.title` and `meta.description` use sentence case: capitalize only the first word, proper nouns, and product names, as in `Git worktrees` ([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location)). Missing files or fields permit fallback, while malformed JSON or invalid fields produce a per-plugin diagnostic.
Fields fall back independently before view-specific name formatting, using the existing locale language chain first:
+1 -1
View File
@@ -136,7 +136,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex
}
```
语言文件统一放在 `locale/`,并保留 `en.json` 作为发现入口。字段可选,填写时必须是非空字符串。文件或字段缺失时允许回退,JSON 损坏或字段无效时显示单插件诊断。
语言文件统一放在 `locale/`,并保留 `en.json` 作为发现入口。字段可选,填写时必须是非空字符串。英文 `meta.title` 和 `meta.description` 使用 sentence case:只大写首个单词、专有名词和产品名,例如 `Git worktrees`([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location))。文件或字段缺失时允许回退,JSON 损坏或字段无效时显示单插件诊断。
各字段先独立回退,再按页面规则格式化技术名称;回退优先经过现有 locale 语言链:
+2 -2
View File
@@ -21,5 +21,5 @@
en: f0a28d07c0bb4d1b
zh: 840962f512cb4031
/cookbook-extension-plugin-shapes/the-feature-mechanism-map:
en: fa917d46c071fad2
zh: 4062805237e2c847
en: af09d0fbb5f39df9
zh: 189166baa84868bc
+1 -1
View File
@@ -103,7 +103,7 @@ Every product feature maps to a listener on a documented extension point — the
| Product feature | Plugin mechanism |
|---|---|
| Hook system (user + project level) | listeners on `agent/created`, `agent/pre-step`, `agent/request`, `tools/pre-execute`, `tools/post-execute`, and `agent/turn-stopping`; the waterfalls return typed decisions, while `agent/turn-stopping` may steer another step; the `dsh-hooks-claude-code` / `dsh-hooks-codex` bridges map hook config files onto these extension points |
| Hook system (user + project level) | listeners on `agent/created`, `agent/pre-step`, `tools/pre-execute`, `tools/post-execute`, and `agent/turn-stopping`; the waterfalls return typed decisions, while `agent/turn-stopping` may steer another step; the `dsh-hooks-claude-code` / `dsh-hooks-codex` bridges map hook config files onto these extension points |
| `/goal` | `ctx.goals` owns durable state, `dsh-goal-round-driver` schedules same-session rounds through the public `Agent`, and separate command/tool producers expose human/model control |
| `/loop` | on the `turn/end` session event, `followup()` the next iteration; or force-continue |
| Dynamic workflow | `ctx.workflowEngine` + the PTC workflow engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and the structured-output execution's monotonic `concludeTurn()` marker |
+1 -1
View File
@@ -107,7 +107,7 @@ export function apply(ctx: Context) {
| 产品功能 | 插件机制 |
|---|---|
| 钩子系统(用户级 + 项目级) | `agent/created`、`agent/pre-step`、`agent/request`、`tools/pre-execute`、`tools/post-execute` 和 `agent/turn-stopping` 上的监听器;waterfall 返回类型化决策,`agent/turn-stopping` 则可通过 steering(中途引导)触发下一步;`dsh-hooks-claude-code` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些扩展点上 |
| 钩子系统(用户级 + 项目级) | `agent/created`、`agent/pre-step`、`tools/pre-execute`、`tools/post-execute` 和 `agent/turn-stopping` 上的监听器;waterfall 返回类型化决策,`agent/turn-stopping` 则可通过 steering(中途引导)触发下一步;`dsh-hooks-claude-code` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些扩展点上 |
| `/goal` | `ctx.goals` 管理持久状态,`dsh-goal-round-driver` 通过公共 `Agent` 调度同会话 Round,独立的命令/工具生产方分别提供人类/模型控制 |
| `/loop` | 在 `turn/end` 会话事件上 `followup()` 下一次迭代;或强制继续 |
| 动态工作流 | `ctx.workflowEngine` + PTC 工作流引擎 + `workflow` 工具;结构化的进程内子任务通过作用域化的提示词/工具注册、单调工具守卫、最终 `tools/result` 提交(包括外层 `run_code`)和结构化输出执行的单调 `concludeTurn()` 标记来强制输出 |
+2 -2
View File
@@ -12,5 +12,5 @@
en: 24799f579b04b2d4
zh: 97d4d82dc3a02c75
/4-events/waterfall-transform-or-short-circuit:
en: 8309aea415b395c3
zh: 480ee4a163543dd0
en: a0027cc61e674138
zh: c96dd1e7d559b748
+1 -1
View File
@@ -137,7 +137,7 @@ Walk through the second line: listener 1 runs first, calls `next()`, which invok
The discipline that follows: **a waterfall listener that only observes or annotates must call `next()`**; returning without it is a deliberate short-circuit. Forgetting `next()` in a logging listener silently swallows the default behavior for everyone downstream. It is a standing rule of this repository ([waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)).
The harness uses waterfalls for decisions that cooperating plugins may wrap or answer: [`agent/request`](../subsystems/core.md#agentrequest--waterfall) lets a plugin replace the model-call config, and [`approval/request`](../subsystems/approval.md#approvalrequest--waterfall) lets a policy answer instead of the user.
The harness uses waterfalls for decisions that cooperating plugins may wrap or answer: [`agent/pre-step`](../subsystems/core.md#agentpre-step--waterfall) lets a plugin reject a proposed step or replace its input, and [`approval/request`](../subsystems/approval.md#approvalrequest--waterfall) lets a policy answer instead of the user.
Next: [Configuration](05-config.md) — plugin options from `cordis.yml`.
+1 -1
View File
@@ -137,7 +137,7 @@ HELLO
由此得到一项纪律:**只负责观察或标注的 waterfall 监听器必须调用 `next()`**;不调用就直接返回代表有意短路。如果日志监听器忘记调用 `next()`,会悄无声息地吞掉所有下游的默认行为。这是本仓库的常设规则([waterfall 语义](../cordis-primer.zh.md#cordis-waterfall-semantics))。
harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`agent/request`](../subsystems/core.zh.md#agentrequest--waterfall) 允许插件替换模型调用配置,[`approval/request`](../subsystems/approval.zh.md#approvalrequest--waterfall) 允许策略代替用户作答。
harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`agent/pre-step`](../subsystems/core.zh.md#agentpre-step--waterfall) 允许插件拒绝拟议步骤或替换其输入,[`approval/request`](../subsystems/approval.zh.md#approvalrequest--waterfall) 允许策略代替用户作答。
下一章:[配置](05-config.zh.md):来自 `cordis.yml` 的插件选项。
+19 -19
View File
@@ -8,25 +8,24 @@ This matrix shows which packages dispatch each harness-owned event and which pac
<!-- BEGIN GENERATED event-producer-consumer:events -->
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:194`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:197`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`compaction-basic`](../packages/compaction/compaction-basic), [`webhook`](../packages/experimental/webhook) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:626`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:606`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:633`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:612`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:619`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:599`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:579`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:606`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:585`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:592`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:54`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - |
@@ -74,7 +73,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`working-directory`](../packages/session/working-directory) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
@@ -99,6 +98,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
<!-- BEGIN GENERATED event-producer-consumer:undeclared -->
| Event string | Dispatchers | Listeners |
| --- | --- | --- |
| `agent/request` | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic) |
| `internal/config` | [`config-editor`](../packages/boot/config-editor) (`waterfall`) | [`llm-pi-ai`](../packages/llm/llm-pi-ai) |
| `internal/dispatch` | - | [`terminal-bash`](../packages/terminal/terminal-bash) |
| `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` |
+19 -19
View File
@@ -10,25 +10,24 @@
<!-- BEGIN GENERATED event-producer-consumer:events -->
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:194`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:197`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`compaction-basic`](../packages/compaction/compaction-basic), [`webhook`](../packages/experimental/webhook) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:626`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:606`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:633`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:612`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:619`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:599`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:579`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:606`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:585`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:592`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:54`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - |
@@ -76,7 +75,7 @@
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`working-directory`](../packages/session/working-directory) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
@@ -101,6 +100,7 @@
<!-- BEGIN GENERATED event-producer-consumer:undeclared -->
| Event string | Dispatchers | Listeners |
| --- | --- | --- |
| `agent/request` | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic) |
| `internal/config` | [`config-editor`](../packages/boot/config-editor) (`waterfall`) | [`llm-pi-ai`](../packages/llm/llm-pi-ai) |
| `internal/dispatch` | - | [`terminal-bash`](../packages/terminal/terminal-bash) |
| `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` |
+2
View File
@@ -374,6 +374,7 @@ flowchart TD
pkg_sdk_protocol["sdk-protocol"]
end
subgraph group_session["packages/session"]
pkg_message_quotes["message-quotes"]
pkg_session_checkpoint_policy["session-checkpoint-policy"]
pkg_session_format["session-format"]
pkg_session_format_catalog["session-format-catalog"]
@@ -1564,6 +1565,7 @@ flowchart TD
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | — |
| [`host-open-in-app`](../packages/host/open-in-app) | `host` | — |
| [`host-webserver`](../packages/host/webserver) | `host` | — |
| [`message-quotes`](../packages/session/message-quotes) | `session` | — |
| [`session-format`](../packages/session/session-format) | `session` | — |
| [`session-format-v0-to-v1`](../packages/session/session-format-v0-to-v1) | `session` | — |
| [`session-format-v1-to-v2`](../packages/session/session-format-v1-to-v2) | `session` | — |
+2
View File
@@ -376,6 +376,7 @@ flowchart TD
pkg_sdk_protocol["sdk-protocol"]
end
subgraph group_session["packages/session"]
pkg_message_quotes["message-quotes"]
pkg_session_checkpoint_policy["session-checkpoint-policy"]
pkg_session_format["session-format"]
pkg_session_format_catalog["session-format-catalog"]
@@ -1566,6 +1567,7 @@ flowchart TD
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | — |
| [`host-open-in-app`](../packages/host/open-in-app) | `host` | — |
| [`host-webserver`](../packages/host/webserver) | `host` | — |
| [`message-quotes`](../packages/session/message-quotes) | `session` | — |
| [`session-format`](../packages/session/session-format) | `session` | — |
| [`session-format-v0-to-v1`](../packages/session/session-format-v0-to-v1) | `session` | — |
| [`session-format-v1-to-v2`](../packages/session/session-format-v1-to-v2) | `session` | — |
+16 -16
View File
@@ -21,8 +21,8 @@
en: 9188862058c6986b
zh: 9188862058c6986b
/session-persistence-event-catalog/events/agent/agent-inbox-spliced-log-only:
en: 8309b4babad90744
zh: b344ac640d79947e
en: 481727894895b43b
zh: 098bc9788bf88968
/session-persistence-event-catalog/events/agent-preset:
en: 49891ca9be5ac123
zh: 49891ca9be5ac123
@@ -141,8 +141,8 @@
en: 9edf4ef31e60e880
zh: 9edf4ef31e60e880
/session-persistence-event-catalog/events/model/model-selection-log-only:
en: 06589ec99fb2bb40
zh: 3b78f0418d44609b
en: c2a60ec7d297ed79
zh: a2f6b2d3c733ce96
/session-persistence-event-catalog/events/permission:
en: 19917bc5438e60af
zh: 19917bc5438e60af
@@ -1074,8 +1074,8 @@
en: a3087ab7bebe9371
zh: abf6afc2f9f5b692
/session-persistence-event-catalog/resolved-persistence-types/inboxtarget:
en: 938113c1b01efad1
zh: d6829dbe979e6819
en: b3b7b8a3414bcb6b
zh: 06be5b8e56bacedf
/session-persistence-event-catalog/resolved-persistence-types/jobregistereddata:
en: f604eebb973fe2cf
zh: 0c0aba1e17ecf66e
@@ -1143,11 +1143,11 @@
en: 8bd12543fcdcda5f
zh: d4c6b17083948856
/session-persistence-event-catalog/resolved-persistence-types/modelmessagesource:
en: 3b677f021d7dd4d3
zh: 2a875742b22879a2
en: 5f9154ca34235ab9
zh: a833105910ebfa82
/session-persistence-event-catalog/resolved-persistence-types/modelselection:
en: bf5990688bae50ac
zh: 9212bca7aa7d31e0
en: 6e301c0ea35d9e0e
zh: 6fbf1d318db9a4e0
/session-persistence-event-catalog/resolved-persistence-types/oneshotscheduledispatchchange:
en: a3300babe57b6b67
zh: 4aaf06a149b399c3
@@ -1347,8 +1347,8 @@
en: 2786fef560851bb4
zh: 5d787475356b2606
/session-persistence-event-catalog/resolved-persistence-types/string:
en: 41c03b03d1bd5a44
zh: 1b0e430af6c140b3
en: 967168de7dce17f6
zh: c43778dcbfea301e
/session-persistence-event-catalog/resolved-persistence-types/string~2:
en: 0089620bf4638263
zh: 1922bef1713094bb
@@ -1524,8 +1524,8 @@
en: 8a9199b1784c70f5
zh: 76c98bc74686fda0
/session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start:
en: 452ff7ab39ffb3a9
zh: 33af56489f0a45bc
en: 346d884a7a74e48d
zh: 646ac1021f9ca920
/session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream:
en: ec10cd8e9c202a81
zh: e08f859bcb13f0d4
@@ -1833,8 +1833,8 @@
en: 809a23c25ded8497
zh: e9cd23e281f8d31f
/session-persistence-event-catalog/resolved-persistence-types/kind-user:
en: 53b7abac679ee252
zh: 63eb6a015986ef6d
en: 43f0c6042d410afd
zh: a0b68de4e5d220bd
/session-persistence-event-catalog/resolved-persistence-types/kind-user-approval:
en: a52d2dfbaf844127
zh: 4c5306ca40f20873
+10 -10
View File
@@ -185,7 +185,7 @@ Sources: [`packages/core/session/src/types.ts:432`](../packages/core/session/src
}
```
Source: [`packages/core/agent/src/types.ts:84`](../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts)
### `agent-preset/*`
@@ -694,13 +694,13 @@ Source: [`packages/llm/llm-retry/src/types.ts:11`](../packages/llm/llm-retry/src
```ts persistence-catalog
/**
* Complete validated model selection requested for subsequent prompt
* assembly. Log-only: it never enters derived model history.
* Complete model selection that applies from the next model step until a
* later selection. Log-only: it never enters derived model history.
*/
'model/selection': ModelSelection
```
Source: [`packages/api/session-controller/src/types.ts:37`](../packages/api/session-controller/src/types.ts)
Source: [`packages/core/agent/src/types.ts:121`](../packages/core/agent/src/types.ts)
### `permission/*`
@@ -3788,7 +3788,7 @@ Array of [`ImageOffloadTarget`](#persistence-type-sha256-1cde1bedde79c5e8a56aee9
SHA-256: `96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c`
Sources: [`packages/core/agent/src/types.ts:39`](../packages/core/agent/src/types.ts)
Sources: [`packages/core/agent/src/types.ts:40`](../packages/core/agent/src/types.ts)
One of:
@@ -4331,13 +4331,13 @@ Sources: [`packages/llm/llm/src/message.ts:24`](../packages/llm/llm/src/message.
<a id="persistence-type-modelselection"></a>
<a id="persistence-type-packagesapisession-controllersrctypestsmodelselection"></a>
<a id="persistence-type-packagescoreagentsrctypestsmodelselection"></a>
### `ModelSelection`
SHA-256: `f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b`
Sources: [`packages/api/session-controller/src/types.ts:98`](../packages/api/session-controller/src/types.ts)
Sources: [`packages/core/agent/src/types.ts:79`](../packages/core/agent/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -5583,7 +5583,7 @@ Array of `number`.
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
Sources: [`packages/api/session-controller/src/types.ts:406`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
Sources: [`packages/api/session-controller/src/types.ts:379`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
`string`
@@ -6377,7 +6377,7 @@ Sources: [`packages/core/session/src/types.ts:428`](../packages/core/session/src
SHA-256: `fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42`
Sources: [`packages/core/agent/src/types.ts:84`](../packages/core/agent/src/types.ts)
Sources: [`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -7554,7 +7554,7 @@ Sources: [`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/inde
SHA-256: `6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def`
Sources: [`packages/api/session-controller/src/types.ts:415`](../packages/api/session-controller/src/types.ts)
Sources: [`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts)
| Property | Presence | Type |
|---|---|---|
+10 -10
View File
@@ -187,7 +187,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}
```
来源:[`packages/core/agent/src/types.ts:84`](../packages/core/agent/src/types.ts)
来源:[`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts)
### `agent-preset/*`
@@ -696,13 +696,13 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
```ts persistence-catalog
/**
* Complete validated model selection requested for subsequent prompt
* assembly. Log-only: it never enters derived model history.
* Complete model selection that applies from the next model step until a
* later selection. Log-only: it never enters derived model history.
*/
'model/selection': ModelSelection
```
来源:[`packages/api/session-controller/src/types.ts:37`](../packages/api/session-controller/src/types.ts)
来源:[`packages/core/agent/src/types.ts:121`](../packages/core/agent/src/types.ts)
### `permission/*`
@@ -3790,7 +3790,7 @@ SHA-256: `9e2c06d8653585b33d72d5c1a80da79c8a24a88c284409bca15cc0f0ead5ebfb`
SHA-256: `96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c`
来源:[`packages/core/agent/src/types.ts:39`](../packages/core/agent/src/types.ts)
来源:[`packages/core/agent/src/types.ts:40`](../packages/core/agent/src/types.ts)
以下类型之一:
@@ -4333,13 +4333,13 @@ SHA-256: `719c51f67074f97034a6c94297fe1f4b5d3c8d392c57586dab732aa5398c7a91`
<a id="persistence-type-modelselection"></a>
<a id="persistence-type-packagesapisession-controllersrctypestsmodelselection"></a>
<a id="persistence-type-packagescoreagentsrctypestsmodelselection"></a>
### `ModelSelection`
SHA-256: `f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b`
来源:[`packages/api/session-controller/src/types.ts:98`](../packages/api/session-controller/src/types.ts)
来源:[`packages/core/agent/src/types.ts:79`](../packages/core/agent/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -5585,7 +5585,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78`
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
来源:[`packages/api/session-controller/src/types.ts:406`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
来源:[`packages/api/session-controller/src/types.ts:379`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
`string`
@@ -6379,7 +6379,7 @@ SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3`
SHA-256: `fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42`
来源:[`packages/core/agent/src/types.ts:84`](../packages/core/agent/src/types.ts)
来源:[`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -7556,7 +7556,7 @@ SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
SHA-256: `6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def`
来源:[`packages/api/session-controller/src/types.ts:415`](../packages/api/session-controller/src/types.ts)
来源:[`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
+5 -5
View File
@@ -253,7 +253,7 @@
{"digest":"4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731","names":["packages/llm/llm/src/message.ts#ToolResultMessage"],"sources":["packages/llm/llm/src/message.ts:173"]},
{"digest":"4b7ddaf3710b88ff9b8df66304f5f97bf03688fd19730eedc2e9535203c820ea","names":[],"sources":[]},
{"digest":"4bb451330ff699f36b45539ceb0eb2606ead72084e2c5bb637c36cc80e2252db","names":[],"sources":["packages/core/session/src/types.ts:194"]},
{"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/agent-team/src/types.ts#TeamId","packages/experimental/agent-team/src/types.ts#TeamMessageId","packages/experimental/agent-team/src/types.ts#TeamTaskId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:406","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/agent-team/src/types.ts:21","packages/experimental/agent-team/src/types.ts:33","packages/experimental/agent-team/src/types.ts:9","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/workflow/workflow/src/types.ts:13"]},
{"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/agent-team/src/types.ts#TeamId","packages/experimental/agent-team/src/types.ts#TeamMessageId","packages/experimental/agent-team/src/types.ts#TeamTaskId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:379","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/agent-team/src/types.ts:21","packages/experimental/agent-team/src/types.ts:33","packages/experimental/agent-team/src/types.ts:9","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/workflow/workflow/src/types.ts:13"]},
{"digest":"4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159","names":[],"sources":[]},
{"digest":"4e44c9118152f6829634bfd508e47669ab0497caffa380c94b4be91f9390d35d","names":[],"sources":[]},
{"digest":"4e887768586528565381dadbddee6cef555874148089f4579e0b4b1ad096fc9c","names":["packages/llm/llm/src/types.ts#TextBlock"],"sources":["packages/llm/llm/src/types.ts:62","packages/web/web-search-deepseek/src/provider.ts:66"]},
@@ -335,7 +335,7 @@
{"digest":"6ccb6bc423fb3952458abf9c1b1c3284208b7e660344b612cc55da773d61afe4","names":[],"sources":[]},
{"digest":"6cda0d6d3b4336d83998fe580628426df81c2afbab5d768b8813b56d569120e8","names":[],"sources":[]},
{"digest":"6cf082d0fb6034dea61636787ff0ddf43b6e59aad71e644435a479bc9b83ad65","names":[],"sources":[]},
{"digest":"6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def","names":[],"sources":["packages/api/session-controller/src/types.ts:415"]},
{"digest":"6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def","names":[],"sources":["packages/api/session-controller/src/types.ts:388"]},
{"digest":"6e52dd55a1d48954ed5464ec46fe34d9eb65f709baabc9561e7a279910b0c406","names":[],"sources":["packages/preset/agent-preset-registry/src/session.ts:31"]},
{"digest":"6ea608bdc8a7a63d3d23b84c49721325094db88d136482e43a820945295eee3e","names":[],"sources":[]},
{"digest":"6ebcc2dbec9f2dabca9f668ae6d39e877899b180740570f453409db339221df3","names":[],"sources":[]},
@@ -430,7 +430,7 @@
{"digest":"95c0e88fcf0bafb2087f912ad9e2369de6ae6530cc0c4c2772c96205b57f7f60","names":[],"sources":[]},
{"digest":"9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114","names":[],"sources":[]},
{"digest":"96bfb916a727cd4445a594df264c523fc039749a31bfed924b8cc7bb308a5bb0","names":[],"sources":[]},
{"digest":"96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c","names":["packages/core/agent/src/types.ts#InboxTarget"],"sources":["packages/core/agent/src/types.ts:39"]},
{"digest":"96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c","names":["packages/core/agent/src/types.ts#InboxTarget"],"sources":["packages/core/agent/src/types.ts:40"]},
{"digest":"97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c","names":[],"sources":[]},
{"digest":"9890113e97dd3541787bfe6704f2b223d3a890b5325ca0acae2b0398909b413e","names":[],"sources":["packages/sandbox/sandbox-policy/src/session-mode.ts:33"]},
{"digest":"99135d8a3937977955eaac5f8f04f930dcd1562f8450a54a51cde9b921dd59a6","names":[],"sources":[]},
@@ -640,7 +640,7 @@
{"digest":"ef226a90ddac6cd8e631a3a337a3a3e27f9494be92befb7c718b404681d75e21","names":[],"sources":[]},
{"digest":"ef4f00923b9edbae15c7ef05320280bf61147ca17146cf59690359bfd5e5da45","names":[],"sources":[]},
{"digest":"f05fb85e715af4ab4de0b6ba111166dd0435bd8bb74a0fa0f32b9d174113fc7c","names":[],"sources":[]},
{"digest":"f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b","names":["packages/api/session-controller/src/types.ts#ModelSelection"],"sources":["packages/api/session-controller/src/types.ts:98"]},
{"digest":"f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b","names":["packages/core/agent/src/types.ts#ModelSelection"],"sources":["packages/core/agent/src/types.ts:79"]},
{"digest":"f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402","names":["packages/core/session/src/types.ts#TurnEndReason"],"sources":["packages/core/session/src/types.ts:233"]},
{"digest":"f1996af2602e6f62b5ab179ca5c4af00d8753938b74cc6ef102de09e99e74a7e","names":[],"sources":[]},
{"digest":"f19bee84bf3d20904553cdcd57465232bc1c2a194f54c4d195292ba0487223a6","names":["packages/attachment/attachment/src/types.ts#FileAttachmentRef"],"sources":["packages/attachment/attachment/src/types.ts:39"]},
@@ -666,7 +666,7 @@
{"digest":"f90eb4ab2d3897bc20c521a038e3119d581f791ec974b1a0004f63a8ab9e232d","names":[],"sources":["packages/core/session/src/types.ts:289","packages/deliverables/workspace-changes/src/types.ts:106"]},
{"digest":"f97f47b0eb87905ed77cc27c5e4ed75ef805cb372c98905253709de1aab9b63c","names":[],"sources":[]},
{"digest":"f9cfc89e8fe085b4fcc9cd32b74b6f103608c54d7d8cd97b47c01de34f252ba3","names":[],"sources":[]},
{"digest":"fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42","names":[],"sources":["packages/core/agent/src/types.ts:84"]},
{"digest":"fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42","names":[],"sources":["packages/core/agent/src/types.ts:110"]},
{"digest":"fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be","names":["packages/llm/llm/src/message.ts#AssistantMessage"],"sources":["packages/llm/llm/src/message.ts:167"]},
{"digest":"fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd","names":["packages/sandbox/sandbox/src/index.ts#SandboxMode"],"sources":["packages/sandbox/sandbox/src/index.ts:30"]},
{"digest":"fab35c4520883c0d69e42d079c9f543fc5ac7a2d15b537b5572fcd99059c7018","names":[],"sources":[]},
+2 -2
View File
@@ -12,8 +12,8 @@
en: e516e14228c6bd35
zh: dba25d7da1d5a44e
/core/the-agent-handle:
en: c21787347d8e2b14
zh: 25bc9cb60ee1520a
en: 6ee15fec81bc0686
zh: b6f3fcd48823ea75
/core/initiating-agent:
en: 6c8c47ac78de2dde
zh: c9d527a25a85d1d9
+74 -35
View File
@@ -61,10 +61,12 @@ Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types
interface Agent {
/** Session-backed Agent identity. */
readonly id: SessionId
/** The provider route and model this agent's requests use. */
/** Route fallback and output controls; live accessors apply until the Session records a selection or request. */
readonly options: AgentOptions
/** The live session this agent drives; its log is the durable source of truth. */
readonly session: Session
/** Agent-owned access to durable model selection for future steps. */
readonly model: Model
/** Agent-owned access to durable pending work. */
readonly inbox: Inbox
/** The current lifecycle state, mirrored on every `agent/status` transition. */
@@ -193,20 +195,86 @@ type AssistantStreamFrame =
`running` describes the driver-wide drain interval and may span consecutive queued turns; it does not prove a turn is still open. Disposal removes the agent from the registry and emits `agent/disposed`; it is not a terminal status value. `followup()` returns no handle: its `MessageId` identifies durable inbox insertion, claim, and discard facts, not a later assistant output or turn ending. `whenIdle()` observes the whole agent, so callers may call a receipt-to-idle interval a run only when they explicitly own that interval ([decision](../../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md)).
```ts type-equiv
/** Merge-extensible agent creation options. Persona belongs to system-prompt sections. */
/**
* Merge-extensible agent options. Route fields may be live accessors until the
* Session records a selection or request; step preparation captures their value.
* Persona belongs to system-prompt sections.
*/
interface AgentOptions {
/** Provider route (must have a registered adapter at call time). */
provider?: string
/** Model id interpreted by the selected provider adapter. */
model?: string
/** Adapter-owned reasoning effort for the selected provider/model route. */
reasoningEffort?: ReasoningEffortId
reasoningEffort?: ReasoningEffortId | undefined
/** Maximum output tokens for each conversation-model request. */
maxTokens?: number
}
```
Dispatch requires `provider` and `model` after `agent/request`. An explicit `reasoningEffort` seeds the first request on that route; exact-model resolution validates it, while omission allows the adapter default to materialize. When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. An agent-scoped `deployment:persona-prefix` prompt section may shadow the global default persona.
Step preparation requires `provider` and `model` before prompt assembly. The deprecated `agent/request` hook may change request controls but must preserve that route. An explicit `reasoningEffort` seeds the first request on that route; exact-model resolution validates it, while omission allows the adapter default to materialize. When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. An agent-scoped `deployment:persona-prefix` prompt section may shadow the global default persona.
`AgentLoop` registers `modelSelection` on `ctx.sessionProjections` and exposes selection operations through each `agent.model` instance. `model/selection` records a complete choice without entering model history. A matching request header clears `pending`; the latest request remains `lastUsed`. Adapter-defaulted effort is omitted so later requests resolve the default again. `next` is null only before either a choice or a request is recorded; in that case `AgentOptions` supplies the route. The [Agent model API](../../packages/core/agent/README.md) describes selection precedence and resume policy.
```ts type-equiv
/** Agent-owned access to the model choice for future steps. */
interface Model {
/**
* Resolve the choice for the next step to begin preparation. Recorded choices
* take precedence over the last request and then the Agent's live options.
* This read neither starts a step nor changes an already-prepared step.
* @returns a detached choice, or undefined when no source names both provider and model.
*/
current(): Readonly<ModelSelection> | undefined
/**
* Replace the next step's complete selection. Omitted effort follows adapter
* defaults without inheriting a previous effort or resolving defaults here.
* An equal recorded choice is a no-op; an options-only fallback is
* always recorded. Callers own model availability and capability validation.
* The append uses the Session's existing persistence and failure guarantees.
* @param selection - complete provider, model, and optional explicit effort.
*/
select(selection: Readonly<ModelSelection>): void
}
```
```ts type-equiv
/** Complete provider, model, and optional reasoning effort chosen for one Session's model steps. */
interface ModelSelection {
/** Registered provider route. */
provider: string
/** Provider-owned model id. */
model: string
/** Adapter-owned reasoning effort, or provider/default behavior when absent. */
reasoningEffort?: ReasoningEffortId
}
```
```ts type-equiv
/**
* Durable model-selection state for one Session. An effort the adapter
* defaulted on a recorded request is omitted, so later requests resolve the
* default again instead of pinning it as a choice.
*/
interface ModelSelectionProjectionState {
/** Selection used by the latest recorded model request. */
readonly lastUsed: ModelSelection | null
/** Later recorded selection not yet used by a matching model request. */
readonly pending: ModelSelection | null
}
```
```ts type-equiv
/** Client view of the durable model-selection fold. */
interface ModelSelectionProjection {
/** Selection used by the latest recorded model request. */
readonly lastUsed: ModelSelection | null
/** Selection the next model step uses; null while the Agent's initial options supply the route. */
readonly next: ModelSelection | null
}
```
The inbox is the delivery vocabulary — two ordered pending-message lists the agent owns as a durable projection:
@@ -976,46 +1044,17 @@ Reject a proposed step or replace the messages that enter it. Calling `next()` p
* @param payload.turn - the turn that will own the step.
* @param payload.step - the step proposed by the loop.
* @param payload.signal - the current turn's cancellation signal.
* @param payload.modelSelection - captured step selection, retained across retries; omitted effort uses adapter defaults.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/pre-step'(this: Scoped<Agent>, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
'agent/pre-step'(this: Scoped<Agent>, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; modelSelection: Readonly<ModelSelection> }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
```
Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
<a id="agentrequest--waterfall"></a>
#### `agent/request` — waterfall
Replace the frozen call configuration. `await next()` yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. On step admission, this runs after assembly and `step/start`, before the system prompt and accepted user batch are committed. Cancellation here or during subsequent `prepareCall()` resolution commits neither. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages.
```ts cordis-catalog
/**
* Replace the frozen call configuration. `await next()` yields the config
* the machine would use (agent options on the first request, the logged
* header afterwards); return a replacement to switch. On step admission,
* this runs after assembly and `step/start`, before the system prompt and
* accepted user batch are committed. Cancellation here or during subsequent
* `prepareCall()` resolution commits neither. The prepared call capability
* governs prompt admission. Model-visible content must use logged channels;
* this waterfall cannot mutate messages.
* @param payload.agent - the agent making the model call.
* @param payload.turn - the open turn number.
* @param payload.step - the step whose request this is.
* @param payload.signal - the current turn's explicit abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/request'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
```
Types: [LlmCallConfig](llm-streaming.md) · [Scoped](scope.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
<a id="agentrequest-error--waterfall"></a>
#### `agent/request-error` — waterfall
+74 -35
View File
@@ -65,10 +65,12 @@ interface AgentHandle {
interface Agent {
/** Session-backed Agent identity. */
readonly id: SessionId
/** The provider route and model this agent's requests use. */
/** Route fallback and output controls; live accessors apply until the Session records a selection or request. */
readonly options: AgentOptions
/** The live session this agent drives; its log is the durable source of truth. */
readonly session: Session
/** Agent-owned access to durable model selection for future steps. */
readonly model: Model
/** Agent-owned access to durable pending work. */
readonly inbox: Inbox
/** The current lifecycle state, mirrored on every `agent/status` transition. */
@@ -197,20 +199,86 @@ type AssistantStreamFrame =
`running` 描述整个驱动器的排空区间,可能跨越连续的排队轮次;它不能证明某个轮次仍然打开。dispose 会把 agent 从注册表移除并发出 `agent/disposed`;它不是一个终态 status 值。`followup()` 不返回句柄:其 `MessageId` 标识的是持久的 inbox 插入、认领与丢弃事实,而非之后的助手输出或轮次结束。`whenIdle()` 观察的是整个 agent,因此只有当调用方明确拥有从回执到空闲的这段区间时,才能把它称为一次 run([决策](../../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.zh.md))。
```ts type-equiv
/** Merge-extensible agent creation options. Persona belongs to system-prompt sections. */
/**
* Merge-extensible agent options. Route fields may be live accessors until the
* Session records a selection or request; step preparation captures their value.
* Persona belongs to system-prompt sections.
*/
interface AgentOptions {
/** Provider route (must have a registered adapter at call time). */
provider?: string
/** Model id interpreted by the selected provider adapter. */
model?: string
/** Adapter-owned reasoning effort for the selected provider/model route. */
reasoningEffort?: ReasoningEffortId
reasoningEffort?: ReasoningEffortId | undefined
/** Maximum output tokens for each conversation-model request. */
maxTokens?: number
}
```
在 `agent/request` 之后,分发要求 `provider` 与 `model` 都存在。显式 `reasoningEffort` 会为该路由的首次请求提供初始值;确切模型解析会校验该值,省略时则允许填入适配器默认值。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。agent 作用域的 `deployment:persona-prefix` 提示词段落可以遮蔽全局默认 persona。
步骤准备在提示词组装前要求 `provider` 与 `model` 都存在。已弃用的 `agent/request` hook 可以更改请求控制参数,但必须保留该路由。显式 `reasoningEffort` 会为该路由的首次请求提供初始值;确切模型解析会校验该值,省略时则允许填入适配器默认值。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。agent 作用域的 `deployment:persona-prefix` 提示词段落可以遮蔽全局默认 persona。
`AgentLoop` 在 `ctx.sessionProjections` 上注册 `modelSelection`,并通过每个 `agent.model` 实例提供选择操作。`model/selection` 记录完整选择,但不进入模型历史。匹配的请求头会清除 `pending`,最近请求仍保留为 `lastUsed`。适配器默认的推理强度会被省略,使后续请求重新解析默认值。只有尚未记录选择或请求时,`next` 才为 null,此时由 `AgentOptions` 提供路由。[Agent 模型 API](../../packages/core/agent/README.zh.md)说明选择优先级与恢复策略。
```ts type-equiv
/** Agent-owned access to the model choice for future steps. */
interface Model {
/**
* Resolve the choice for the next step to begin preparation. Recorded choices
* take precedence over the last request and then the Agent's live options.
* This read neither starts a step nor changes an already-prepared step.
* @returns a detached choice, or undefined when no source names both provider and model.
*/
current(): Readonly<ModelSelection> | undefined
/**
* Replace the next step's complete selection. Omitted effort follows adapter
* defaults without inheriting a previous effort or resolving defaults here.
* An equal recorded choice is a no-op; an options-only fallback is
* always recorded. Callers own model availability and capability validation.
* The append uses the Session's existing persistence and failure guarantees.
* @param selection - complete provider, model, and optional explicit effort.
*/
select(selection: Readonly<ModelSelection>): void
}
```
```ts type-equiv
/** Complete provider, model, and optional reasoning effort chosen for one Session's model steps. */
interface ModelSelection {
/** Registered provider route. */
provider: string
/** Provider-owned model id. */
model: string
/** Adapter-owned reasoning effort, or provider/default behavior when absent. */
reasoningEffort?: ReasoningEffortId
}
```
```ts type-equiv
/**
* Durable model-selection state for one Session. An effort the adapter
* defaulted on a recorded request is omitted, so later requests resolve the
* default again instead of pinning it as a choice.
*/
interface ModelSelectionProjectionState {
/** Selection used by the latest recorded model request. */
readonly lastUsed: ModelSelection | null
/** Later recorded selection not yet used by a matching model request. */
readonly pending: ModelSelection | null
}
```
```ts type-equiv
/** Client view of the durable model-selection fold. */
interface ModelSelectionProjection {
/** Selection used by the latest recorded model request. */
readonly lastUsed: ModelSelection | null
/** Selection the next model step uses; null while the Agent's initial options supply the route. */
readonly next: ModelSelection | null
}
```
inbox 即投递词汇——agent 以持久投影形式拥有的两条有序待处理消息列表:
@@ -988,46 +1056,17 @@ Reject a proposed step or replace the messages that enter it. Calling `next()` p
* @param payload.turn - the turn that will own the step.
* @param payload.step - the step proposed by the loop.
* @param payload.signal - the current turn's cancellation signal.
* @param payload.modelSelection - captured step selection, retained across retries; omitted effort uses adapter defaults.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/pre-step'(this: Scoped<Agent>, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
'agent/pre-step'(this: Scoped<Agent>, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; modelSelection: Readonly<ModelSelection> }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
```
Types: [Scoped](scope.zh.md) · [UserMessage](session.zh.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
<a id="agentrequest--waterfall"></a>
#### `agent/request` — waterfall
Replace the frozen call configuration. `await next()` yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. On step admission, this runs after assembly and `step/start`, before the system prompt and accepted user batch are committed. Cancellation here or during subsequent `prepareCall()` resolution commits neither. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages.
```ts cordis-catalog
/**
* Replace the frozen call configuration. `await next()` yields the config
* the machine would use (agent options on the first request, the logged
* header afterwards); return a replacement to switch. On step admission,
* this runs after assembly and `step/start`, before the system prompt and
* accepted user batch are committed. Cancellation here or during subsequent
* `prepareCall()` resolution commits neither. The prepared call capability
* governs prompt admission. Model-visible content must use logged channels;
* this waterfall cannot mutate messages.
* @param payload.agent - the agent making the model call.
* @param payload.turn - the open turn number.
* @param payload.step - the step whose request this is.
* @param payload.signal - the current turn's explicit abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/request'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
```
Types: [LlmCallConfig](llm-streaming.zh.md) · [Scoped](scope.zh.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
<a id="agentrequest-error--waterfall"></a>
#### `agent/request-error` — waterfall
+2 -2
View File
@@ -39,8 +39,8 @@
en: 2b0831aaae8e1cdf
zh: f669d93d1b9d6025
/llm-streaming/the-model-request/the-request-envelope-llmcallconfig-and-the-logged-header:
en: f2e6932e211f179b
zh: 75112586a6f3f4aa
en: b0eb4620c6953646
zh: fabf51c7245a4855
/llm-streaming/official-deepseek-request-extensions:
en: 42bc50394b070e96
zh: 007caa743ec3c790
+1 -1
View File
@@ -760,7 +760,7 @@ interface LlmDiscoveredModel {
The loop builds each request from logged state. `EpochHeader` records call config, marks the fields supplied by adapter defaults, and records the authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. The rendered prompt is derived history — the `system/message` at surface node 0, plus any later system node an `in-history` route appended — so the header and the derived history together make the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus the fields supplied by adapter defaults under the turn signal. On step admission, this waterfall and preparation run after assembly and `step/start` but before the system prompt and accepted user batch are committed; cancellation during either commits neither. The prepared capability governs prompt reconciliation, and the call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
The deprecated `agent/request` hook remains available to existing listeners. New code selects models and reasoning effort through [Session model selection](../../packages/core/agent/README.md#use-this-package). The hook receives a frozen call-config seed based on the step's captured model selection and may replace controls such as sampling. A replacement must preserve provider, model, and the optional explicit reasoning effort; record a new Session selection before the next step to change them. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus the fields supplied by adapter defaults under the turn signal. On step admission, this waterfall and preparation run after assembly and `step/start` but before the system prompt and accepted user batch are committed; cancellation during either commits neither. The prepared capability governs prompt reconciliation, and the call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
On the wire, a loop-built request is the derived history alone: the rendered prompt travels as the leading `system`-role message (surface node 0, a `system/message` event) and, when the prepared call declares `systemPromptUpdate: 'in-history'`, a non-empty changed prompt may follow the cached history as a later `system`-role message that the model reads as the effective prompt; the request's `system` field is unset — `GenerateOptions.system` serves direct one-shot callers such as title providers. An empty rendering leaves no system messages in derived history, even when earlier requests retained several prompt versions. The logged request ends with the newest `user/message` on a turn's first step and the previous step's tool results on later steps.
+1 -1
View File
@@ -766,7 +766,7 @@ interface LlmDiscoveredModel {
循环从已记录状态构建每个请求。`EpochHeader` 记录调用配置,标记由适配器默认值提供的字段,并通过完整的 `request/header` 快照记录权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。渲染后的提示词是派生历史——surface 第 0 号节点上的 `system/message`,加上 `in-history` 路由追加的任何后续系统节点——因此请求头与派生历史共同使请求可由会话日志重建。见 [session.md](session.zh.md#the-request-header-event-requestheader) 与[可重建性 Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md)。
`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall(瀑布式事件)开始前,循环会移除标记为适配器默认值的值,使确切模型准备过程填入所选路由的当前值;未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID(不自动调整),并记录生效配置以及由适配器默认值提供的字段。步骤准入时,该 waterfall 与准备过程在组装和 `step/start` 之后、系统提示词与已接纳用户批次提交之前运行;在任一阶段取消都不会提交这两者。已准备调用的能力决定提示词协调,调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
已弃用的 `agent/request` hook 仍可供现有监听器使用。新代码通过 [Session 模型选择](../../packages/core/agent/README.zh.md#use-this-package) 设置模型与推理强度。该 hook 收到基于步骤已捕获模型选择的冻结调用配置初值,可以替换采样等控制参数。替换值必须保留提供方、模型及可选的显式推理强度;要改变这些选择,需在下一个步骤之前记录新的 Session 选择。waterfall(瀑布式事件)开始前,循环会移除标记为适配器默认值的值,使确切模型准备过程填入所选路由的当前值;未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID(不自动调整),并记录生效配置以及由适配器默认值提供的字段。步骤准入时,该 waterfall 与准备过程在组装和 `step/start` 之后、系统提示词与已接纳用户批次提交之前运行;在任一阶段取消都不会提交这两者。已准备调用的能力决定提示词协调,调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
在协议中,循环构建的请求只有派生历史:渲染后的提示词作为开头的 `system` 角色消息(surface 第 0 号节点,即一个 `system/message` 事件)传输,并且当已准备调用声明 `systemPromptUpdate: 'in-history'` 时,变化后的非空提示词可以作为后续的 `system` 角色消息跟在已缓存历史之后,由模型读作有效提示词;请求的 `system` 字段不设置——`GenerateOptions.system` 服务于标题提供方等直接单次调用方。空渲染文本使派生历史不包含任何系统消息,即使先前请求保留了多个提示词版本。已记录的请求会以最新的 `user/message`(轮次首步)或上一步的工具结果(后续步骤)结尾。
+4 -3
View File
@@ -205,20 +205,21 @@ async listCandidates( agent: Agent, query: string = '', limit: number = this.con
/**
* Snapshot all references for one accepted direct message and return one aggregated durable context.
* Automatic budgets use the last assembled route, or agent options before any assembly.
* Automatic budgets use the context capacity of `route`.
* Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation.
* Truncated previews include omission facts and a full-snapshot spill locator, or an explicit unavailable notice.
* Cancellation prevents context publication, including when storage completes after cancellation.
* @param agent - target agent; references to it are rejected.
* @param content - already host-normalized readable message content.
* @param references - structured source sessions in mention order.
* @param route - provider and model whose context capacity sizes the previews.
* @param signal - optional cancellation boundary for the active turn.
* @returns detached content and optional referenced-session context.
*/
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], route: Readonly<Pick<LlmCallConfig, 'provider' | 'model'>>, signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
```
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md)
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md) · [LlmCallConfig](llm-streaming.md)
Source: [`packages/context/session-reference/src/index.ts`](../../packages/context/session-reference/src/index.ts)
<!-- END GENERATED cordis-surface -->
+4 -3
View File
@@ -205,20 +205,21 @@ async listCandidates( agent: Agent, query: string = '', limit: number = this.con
/**
* Snapshot all references for one accepted direct message and return one aggregated durable context.
* Automatic budgets use the last assembled route, or agent options before any assembly.
* Automatic budgets use the context capacity of `route`.
* Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation.
* Truncated previews include omission facts and a full-snapshot spill locator, or an explicit unavailable notice.
* Cancellation prevents context publication, including when storage completes after cancellation.
* @param agent - target agent; references to it are rejected.
* @param content - already host-normalized readable message content.
* @param references - structured source sessions in mention order.
* @param route - provider and model whose context capacity sizes the previews.
* @param signal - optional cancellation boundary for the active turn.
* @returns detached content and optional referenced-session context.
*/
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], route: Readonly<Pick<LlmCallConfig, 'provider' | 'model'>>, signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
```
Types: [Agent](core.zh.md) · [ContentBlock](llm-streaming.zh.md)
Types: [Agent](core.zh.md) · [ContentBlock](llm-streaming.zh.md) · [LlmCallConfig](llm-streaming.zh.md)
Source: [`packages/context/session-reference/src/index.ts`](../../packages/context/session-reference/src/index.ts)
<!-- END GENERATED cordis-surface -->
+1
View File
@@ -137,6 +137,7 @@ Factory: conversation.content [ConversationContent]
├─ conversation.session [ConversationSession / DefaultConversationViews]
│ └─ conversation.view
│ ├─ id=chat [ChatView]
│ │ ├─ conversation.chat.text-interaction [list]
│ │ └─ conversation.chat.flow [ChatFlow]
│ │ └─ conversation.chat.node [ChatNodeSeat dispatch]
│ │ ├─ key=user,steering [UserMessageNodeView]
+1
View File
@@ -139,6 +139,7 @@ Factory: conversation.content [ConversationContent]
├─ conversation.session [ConversationSession / DefaultConversationViews]
│ └─ conversation.view
│ ├─ id=chat [ChatView]
│ │ ├─ conversation.chat.text-interaction [list]
│ │ └─ conversation.chat.flow [ChatFlow]
│ │ └─ conversation.chat.node [ChatNodeSeat dispatch]
│ │ ├─ key=user,steering [UserMessageNodeView]
+2 -2
View File
@@ -6,8 +6,8 @@
en: 7ad02b2146520e7b
zh: 93cb93c7aa12ceb4
/system-prompt-assembly/assembly-context:
en: 371539c21c1ffc45
zh: ea429ea5a0c19215
en: a5d904293afc7fcb
zh: 555883610ac09646
/system-prompt-assembly/tool-provider-result:
en: 89cb015bf2e3e7a5
zh: b4340e853b34c777
+1 -1
View File
@@ -8,7 +8,7 @@ Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-
## Assembly context
`AssembleContext` identifies the scope layer one assembly resolves and may carry the explicit control signal for that request. It is merge-extensible: `dsh-agent` adds the optional live `agent` field, and `assembleContextFor(agent, signal)` sets the explicit fields together. A bare assembly has neither scope nor signal.
`AssembleContext` identifies the scope layer one assembly resolves and may carry the explicit control signal for that request. It is merge-extensible: `dsh-agent` adds optional `agent` and `modelSelection` fields. `assembleContextFor(agent, signal)` sets the agent, scope, and signal; the loop supplies the model selection captured for the step and retained across retries, with omitted reasoning effort using adapter defaults. A bare assembly has neither scope nor signal.
```ts type-equiv
/** Merge-extensible context for one prompt assembly. */
+1 -1
View File
@@ -8,7 +8,7 @@
## 组装上下文
`AssembleContext` 标识一次组装所解析的作用域层,并可携带该请求的显式控制信号。它可合并扩展:`dsh-agent` 添加可选字段 `agent`,用于携带当前的 agent(智能体)实例;`assembleContextFor(agent, signal)` 则一起设置这些显式字段。裸组装既没有作用域,也没有信号。
`AssembleContext` 标识一次组装所解析的作用域层,并可携带该请求的显式控制信号。它可合并扩展:`dsh-agent` 添加可选字段 `agent` 和 `modelSelection`。`assembleContextFor(agent, signal)` 设置 agent(智能体)、作用域和信号;循环提供为该步骤捕获并在重试期间保留的模型选择,省略推理强度时使用适配器默认值。裸组装既没有作用域,也没有信号。
```ts type-equiv
/** Merge-extensible context for one prompt assembly. */
@@ -0,0 +1,16 @@
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.4/session-model-selection/guide.md
/:
en: 3890f393c1af5500
zh: 64fce473a9f35fae
/session-model-selection-applies-at-the-next-step:
en: ac99540fa45af789
zh: 68b33ea8d0063615
/session-model-selection-applies-at-the-next-step/change:
en: 5ab837106ca3bd0c
zh: 5a6d2e65f3cdb191
/session-model-selection-applies-at-the-next-step/migration:
en: bf4f6d902cfe5a78
zh: f19cb24e45d50dc4
@@ -0,0 +1,28 @@
---
kind: upgrade-guide
description: "Session model choices are fixed at step preparation, and projection types move to their owning package."
---
# Session model selection applies at the next step
English | [中文](guide.zh.md)
## Change
ACP model and reasoning changes take effect at the next model step, including a tool continuation in the current turn. Previously, ACP pinned the selection for the complete prompt turn. A prepared step and its retries retain their captured selection. Image admission checks the route selected when admission begins; a later option change can select a different route before the step starts.
`AgentLoop` owns the durable `modelSelection` projection and constructs an `AgentModel` instance on each Agent’s `model` property. The API Session controller no longer registers it. Programmatic resume restores recorded choices, including unused selections, before falling back to `AgentOptions`. Configured Agent-loop resumes, headless adoption, and SDK resume record their deployment route; an omitted effort preserves saved explicit effort only for the same route.
Plugins must select provider, model, and optional explicit reasoning effort before prompt assembly. `agent/request` is deprecated. Existing listeners still run on each main-loop request attempt and can adjust other controls, but cannot change provider or model, or add, change, or clear reasoning effort. Absent effort still resolves through adapter defaults on each attempt. Auxiliary calls such as compaction do not dispatch this event. `installModelSelection` now installs only the switch-notice listener; its selection argument and `ModelSelectionRef` are removed.
The client projection and Host state types move to `dsh-agent/types`; the API package no longer exports them.
## Migration
1. In custom Cordis compositions, mount `dsh-session-projection` with `dsh-agent-loop`. The loop constructs `agent.model`; consumers need no separate model-selection service.
2. Replace mutable references and selection hooks with `agent.model.select(selection)` after validation. Read with `current()`. Keep `installModelSelection(agentCtx)` in Agent setup for switch notices and remove its old selection argument. Do not add `agent/request` listeners.
3. Set the output cap through `AgentOptions.maxTokens`. Other per-request control overrides have no replacement API; retain an existing listener where that behavior is still required.
4. To override a saved route during programmatic resume, merge the configured selection with the saved explicit effort only when the route is unchanged and the configuration omits effort, then call `agent.model.select(selection)` in the Agent setup callback. Supplying `agentOptions` alone provides only a fallback for a Session without a selection or request.
5. Pass the captured `Readonly<ModelSelection>` in `agent/pre-step.modelSelection`, including explicit effort when selected; `AssembleContext.modelSelection` carries the same selection. Direct `SessionReferenceResolver.prepare()` callers pass the route as the fourth argument, before the optional signal.
6. ACP controllers that require one model throughout a turn must wait for the active prompt to finish before changing its options. Verify that a mid-turn change is recorded as `model/selection`, the next step uses it, and resume retains an unused choice.
7. Import `ModelSelectionProjection` from `@deepseek-ai/dsh-agent/types`, replacing API package aliases. `ModelSelection` remains in `dsh-agent/types`. Import `ModelSelectionProjectionState` from `@deepseek-ai/dsh-agent` only when implementing a driver; ordinary reads use `agent.model.current()`. Effort values use `ReasoningEffortId` from `dsh-llm/brand`.
@@ -0,0 +1,28 @@
---
kind: upgrade-guide
description: "Session 模型选择在步骤准备时固定,投影类型移至其所属包。"
---
# Session 模型选择从下一个步骤开始生效
[English](guide.md) | 中文
## 变更
ACP(Agent Client Protocol)的模型与推理强度变更从下一个模型步骤开始生效,包括当前轮次的工具后续步骤。此前,ACP 会在整个提示词轮次中固定选择。已准备的步骤及其重试保留捕获的选择。图片准入检查准入开始时选择的路由;后续选项变更可能在步骤开始前选中另一条路由。
`AgentLoop` 拥有持久 `modelSelection` 投影,并为每个 Agent 的 `model` 属性构建一个 `AgentModel` 实例。API Session 控制器不再注册该投影。程序化恢复优先还原记录中的选择,包括尚未使用的选择,然后才回退到 `AgentOptions`。配置式 Agent-loop 恢复、headless 接管与 SDK 恢复会记录各自的部署路由;省略推理强度时,仅在路由相同的情况下保留已保存的显式强度。
插件必须在提示词组装前选定提供方、模型及可选的显式推理强度。`agent/request` 已弃用。现有监听器仍在主循环的每次请求尝试中运行,可以调整其他控制参数,但不能改变提供方或模型,也不能添加、修改或清除推理强度。未指定的强度仍在每次尝试时通过适配器默认值解析。压缩等辅助调用不会分发该事件。`installModelSelection` 现在只安装切换提示监听器;其选择参数与 `ModelSelectionRef` 已移除。
客户端投影与 Host 状态类型移至 `dsh-agent/types`;API 包不再导出这些类型。
## 迁移
1. 在自定义 Cordis 组合中将 `dsh-session-projection` 与 `dsh-agent-loop` 一起挂载。循环构建 `agent.model`;消费方不需要独立的模型选择服务。
2. 将可变引用与选择 hook 替换为校验后的 `agent.model.select(selection)`,使用 `current()` 读取选择。在 Agent setup 中保留 `installModelSelection(agentCtx)` 以提供切换提示,并移除原来的选择参数。不要添加 `agent/request` 监听器。
3. 通过 `AgentOptions.maxTokens` 设置输出上限。其他逐请求控制参数覆盖没有替代 API;仍需要该行为时,保留现有监听器。
4. 要在程序化恢复时覆盖已保存路由,先形成配置选择,仅在路由不变且配置省略强度时保留已保存的显式强度,再在 Agent setup 回调中调用 `agent.model.select(selection)`。仅提供 `agentOptions` 时,它只为没有选择或请求记录的 Session 提供回退值。
5. 通过 `agent/pre-step.modelSelection` 传入捕获的 `Readonly<ModelSelection>`,包含已选定的显式强度;`AssembleContext.modelSelection` 携带同一选择。直接调用 `SessionReferenceResolver.prepare()` 时,将路由作为第四个参数,放在可选信号之前。
6. 要求整轮使用同一个模型的 ACP 控制器,必须等待活跃提示词完成后再更改选项。验证轮次中的变更被记录为 `model/selection`、下一个步骤使用该选择,且恢复保留尚未使用的选择。
7. 从 `@deepseek-ai/dsh-agent/types` 导入 `ModelSelectionProjection`,替换 API 包别名。`ModelSelection` 仍位于 `dsh-agent/types`。仅在实现驱动器时从 `@deepseek-ai/dsh-agent` 导入 `ModelSelectionProjectionState`;普通读取使用 `agent.model.current()`。强度值使用 `dsh-llm/brand` 中的 `ReasoningEffortId`。
@@ -0,0 +1,16 @@
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/cua-relative-coordinates/guide.md
/:
en: ffbf3ccb8ac486ea
zh: 1712a76e8e3a664a
/cua-relative-coordinates-use-0-999:
en: 1952d155be13e0d1
zh: 01b2f9f27499f033
/cua-relative-coordinates-use-0-999/change:
en: 311311c2056c8e93
zh: 63f4dc87c9d22f99
/cua-relative-coordinates-use-0-999/migration:
en: de5b600f1d9ae83c
zh: e2de8ebd5c7ca2bb
@@ -0,0 +1,17 @@
---
kind: upgrade-guide
description: "CUA relative coordinates accept integers from 0 to 999 and reject the former endpoint 1000."
---
# CUA relative coordinates use 0–999
English | [中文](guide.zh.md)
## Change
The `cua` tool and application/window object methods now accept integers from 0 to 999 when `relative: true`. The previous range was 0 to 1000. Coordinates still use thousandths: divide by 1000, scale to window dimensions, and clamp inside the window. Values from 0 to 999 keep their existing positions. This affects `click`, `hover`, `scroll`, and both `drag` endpoints. Absolute screenshot pixels and AX element indices are unchanged.
## Migration
1. Update scripts and prompts that generate relative coordinates. Keep existing values from 0 to 999 unchanged. Replace the old endpoint 1000 with 999, for example with `Math.min(oldValue, 999)` for valid old coordinates. On larger windows, 999 can land farther inside than the former endpoint 1000.
2. Read fresh window state, then invoke the affected action with integer coordinates in 0–999. Verify the result with a fresh observation. Relative values of 1000, negative values, and fractions are rejected before native input.
@@ -0,0 +1,17 @@
---
kind: upgrade-guide
description: "CUA 相对坐标接受 0~999 的整数,并拒绝原端点 1000。"
---
# CUA 相对坐标使用 0~999
[English](guide.md) | 中文
## 变更
`cua` 工具和应用或窗口对象方法在 `relative: true` 时接受 0~999 的整数,原范围为 0~1000。坐标仍使用千分比:除以 1000 后按窗口尺寸缩放,并限制在窗口内。0~999 的坐标保持原有落点。这影响 `click`、`hover`、`scroll` 和 `drag` 的起终点。绝对截图像素和 AX 元素编号保持原有行为。
## 迁移
1. 更新生成相对坐标的脚本和提示。0~999 的现有数值保持不变。将旧端点 1000 改为 999,例如对原本合法的旧坐标使用 `Math.min(oldValue, 999)`。在较大的窗口中,999 可能比原端点 1000 更靠内。
2. 读取新的窗口状态后,使用 0~999 的整数坐标调用相应动作,再读取新状态验证结果。相对坐标为 1000、负数或小数时,会在原生输入前被拒绝。
+2 -2
View File
@@ -33,8 +33,8 @@
en: 18070de55501a230
zh: 0a34d22dc2662599
/event-system/cordis-events-and-session-records:
en: 93102b27250eea35
zh: a97a0197f785a4aa
en: 866ef8c0f99b2e8a
zh: 14c9b69b27ffd850
/event-system/event-listeners-are-effects:
en: b7cc96734661caef
zh: 442ec0da611cf644
+1 -1
View File
@@ -101,7 +101,7 @@ declare module '@deepseek-ai/cordis' {
## Cordis events and session records
Harness Cordis events use `namespace/action` names, including `agent/pre-step`, `agent/request`, `agent/request-error`, `tools/result`, and `session/event`. The generated `cordis-surface` regions on the [subsystem pages](../../../subsystems/core.md) record complete signatures and modes.
Harness Cordis events use `namespace/action` names, including `agent/pre-step`, `agent/request-error`, `tools/result`, and `session/event`. The generated `cordis-surface` regions on the [subsystem pages](../../../subsystems/core.md) record complete signatures and modes.
`turn/*`, `step/*`, `tool/call`, `tool/result`, and `compaction/*` are durable session-event types, not same-named Cordis events. To observe them, listen to `session/event` and inspect `event.type`.
+1 -1
View File
@@ -101,7 +101,7 @@ declare module '@deepseek-ai/cordis' {
## Cordis 事件与会话记录
Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-step`、`agent/request`、`agent/request-error`、`tools/result` 和 `session/event`。完整签名与触发模式见[子系统页面](../../../subsystems/core.zh.md)上生成的 `cordis-surface` 区块。
Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-step`、`agent/request-error`、`tools/result` 和 `session/event`。完整签名与触发模式见[子系统页面](../../../subsystems/core.zh.md)上生成的 `cordis-surface` 区块。
`turn/*`、`step/*`、`tool/call`、`tool/result` 和 `compaction/*` 是持久化的会话事件类型,不是同名 Cordis 事件。需要观察它们时,监听 `session/event` 并检查 `event.type`。
+2 -2
View File
@@ -27,8 +27,8 @@
en: 4ed8e37834267edb
zh: a32d5dedb4e8e919
/deepseek-ai-dsh-acp/use-this-package/protocol-contract:
en: 29099e63f9a173c6
zh: 80d98e87f442d335
en: 8f358b006f93f6ab
zh: a02dc9640cac4edd
/deepseek-ai-dsh-acp/understand-the-implementation:
en: ea8afe89dbdd1934
zh: 6567979f92f34afd
Loaded 100 of 510 files, more files were not shown because too many files have changed in this diff. Show more