refactor(llm): separate durable producer sources from request inputs

Give durable user context its producer-owned source kind instead of a generic plugin wrapper. The V3-to-V4 conversion preserves the frozen rename and collision rules, all own JSON metadata, event coordinates, and historical request-only kinds. Native catalog and JSONL admission retain source validation so unknown attribution can survive without its producer while malformed owned sources are refused.

Use an explicit user-only RequestUserInput for auxiliary LLM prompts that do not own durable message identity. Auto Review and compaction remove their request-only source registrations while retaining prompt content and freezing; provider serializers and projections keep the same wire data, assistant replay isolation, image ordering, and tool correlation. Session writes, Agent delivery, and recorded title requests continue to require identified durable messages.

Bind the saved compatibility policy only to user sources and qualify tmux location attribution without weakening fixed system, model, or tool semantics. Rebuild the current inventory and unpublished V4 acknowledgement on the actual tool-role parent, preserving its rationale and all accepted historical records. The catalog format version records tooling metadata; Session remains V4. Developer-role policy and activation belong to their own feature.

The latest-master Messages adapter, serialization, request-input typing, EpochHeader reservation, and attribution checks pass 103 tests across five files. The retained V3 Messages input-history snapshot passes through the source profile; Host/Client typechecks and all 41 documentation gates pass. The generated comparison records four changed roots and 447 unchanged type fingerprints. Latest-master ARIA changes are retained, with producer source labels applied independently; new Messages fixtures retain their producer names as declared source kinds and use the fixed system-message helper; recorded Session generations are unchanged by this regrouping.

Co-authored-by: Tianyi Cui <53024+tianyicui@users.noreply.github.com>
This commit is contained in:
_KermanandTianyi Cui authored and Tianyi Cui committed 2026-09-18 19:07:12 +08:00
1 parent 29f7e7bdf5
commit fb79a944f5
298 files changed
+48917 -17043

No files matched your search

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-09-producer-owned-message-sources.md
2026-09-09-producer-owned-message-sources.md: 9bc3e57ff2a2fa251bc72a72f9ea9e39060a0912
2026-09-09-producer-owned-message-sources.zh.md: f587aee39828535f284c9698c8fc5d2948f28ddc
@@ -0,0 +1,33 @@
# Agent Note: Producer-owned message sources at the V4 edge
Status: implemented
English | [中文](2026-09-09-producer-owned-message-sources.zh.md)
## Problem
Released V3 writers stamped every plugin-produced message with the shared wrapper `{ kind: 'plugin', plugin: '<producer>' }` plus the producer's context-form fields, so consumers had to treat one synthetic kind specially instead of switching on the producer identity. Once tool results became first-class tool-role messages and the current generation moved to V4, the format layer also had to keep reading released V3 files whose rows still carry the wrapper.
## Decision
Message sources are producer-owned: the producer identity is the `kind` itself, declared per plugin through `MessageSourceMap` augmentation, and the released `plugin` field no longer exists in current rows. The V3-to-V4 migration rewrites released plugin attribution onto producer kinds with a frozen rename table in `packages/session/session-format-v3-to-v4/src/sources.ts`: `@deepseek-ai/dsh-system-prompt` becomes `system-prompt` for system-role messages and `runtime-context` otherwise; `compact` becomes `compact-checkpoint`; `tools-code-mode` and `tools-ptc` become `code-mode`; `dsh-compaction-basic` becomes `compact-basic`; any other plugin string keeps its own name as the kind. The wrapper drops without losing identity, context-form fields survive, and tool results lift from released user-role wrapper rows into first-class tool-role messages in the same migration. Rename lookup uses only own keys, and reconstruction preserves every own JSON metadata property, including `__proto__`; legal external producer names cannot resolve to JavaScript prototype members.
Native V4 source admission rejects the retired `kind: 'plugin'` wrapper in every declared durable message slot, including inbox and title-request messages. This refusal applies before recoverable scanning can discard a suffix. Complete source-slot validation runs in the shared known-event pass used by both the format catalog and native JSONL scanner, before either exposes a restored artifact or handle. Unknown nonempty attribution kinds and their extra JSON fields survive without their producer installed. Native compaction and title relationship checks use the producer kinds directly; the [mandatory validation decision](2026-09-17-native-v4-read-validation.md) owns common lifecycle rules.
The source rewrite is owned by the V3-to-V4 migration stage and runs once while historical events become V4 events. The current V4 codec retains only the released physical framing and native V4 admission; it never sends a V4 row through the released V3 validator and has no source-conversion view in its encode, decode, restore, or row-admission paths.
Recorded current V4 fixtures and writer, notification, and stream-json sidecars are producer-owned; released V2/V3 fixtures and sidecars keep plugin attribution and convert at read time through the migration. The [released session-format migrations](2026-08-31-released-session-format-migrations.md) note owns the versioning rules this edge follows.
The [source-attribution policy](2026-09-17-persistence-attribution-policy.md) distinguishes durable source declarations from request-only user inputs and records the narrowly scoped compatibility promise for new attribution kinds. Frozen migration rules retain historical producer kinds even when no active producer declares them.
## Alternatives considered
**Keep the released plugin wrapper as the current shape.** Consumers would keep switching on one synthetic kind and the producer identity stays an untyped string; the current semantic validators would keep a second, unreleased source vocabulary.
**Rewrite released rows only at scan time instead of migrating them.** Scan admission is deliberately shape-only and refuses, never repairs; a scan-time rewrite would blur released generations with current rows and leave the current artifact with version-inappropriate source identities.
**Reject plugin-kind rows as ordinary corruption in the scanner.** That would let a stale released row silently truncate a current Session instead of surfacing a refusal, and would misclassify rows that full decoding still needs to diagnose.
## Consequences
The recorded corpus for this migration applies the frozen rename table, while current-generation validation consumes producer-owned kinds directly. Web context labels render the producer kind (`runtime-context`) instead of the released plugin string. The shared rewriter reports malformed released sources as `SessionFormatError`, so a malformed historical source is a migration refusal rather than a silent payload loss.
@@ -0,0 +1,33 @@
# Agent Note: V4 边缘上的生产者归属消息源
Status: implemented
[English](2026-09-09-producer-owned-message-sources.md) | 中文
## Problem
已发布的 V3 写入方把每个插件产出的消息都盖上共享包装`{ kind: 'plugin', plugin: '<producer>' }` 外加生产者的上下文形态字段,消费方因此必须特殊对待一个合成 kind,而不是直接按生产者身份切换。工具结果成为一等 tool-role 消息、当前世代进入 V4 之后,格式层还必须继续读取仍带包装的已发布 V3 文件。
## Decision
消息源是生产者归属的:生产者身份就是 `kind` 本身,由各插件通过 `MessageSourceMap` 声明合并,当前行中不再存在已发布的 `plugin` 字段。V3→V4 迁移用 `packages/session/session-format-v3-to-v4/src/sources.ts`中的冻结重命名表把已发布 plugin 归属改写到生产者 kind:`@deepseek-ai/dsh-system-prompt` 在 system-role 消息上变成 `system-prompt`,否则变成 `runtime-context`;`compact` 变成 `compact-checkpoint`;`tools-code-mode` 与 `tools-ptc` 变成 `code-mode`;`dsh-compaction-basic` 变成 `compact-basic`;其它 plugin 字符串保留自身名字作为 kind。包装丢弃时不丢失身份,上下文形态字段保留,工具结果在同一迁移中从已发布的 user-role 包装行提升为一等 tool-role 消息。重命名查找只读取自有键,重建时保留所有自有 JSON 元数据属性,包括 `__proto__`;合法的外部生产者名字不会被解析为 JavaScript 原型成员。
原生 V4 消息源准入在每个声明的持久化消息槽位拒绝退役的 `kind: 'plugin'` 包装,包括 inbox 和标题请求消息。可恢复扫描丢弃后缀之前就会执行此拒绝。完整消息源槽位校验在格式目录和原生 JSONL 扫描器共用的已知事件校验中执行,先于两者公开恢复产物或句柄。即使生产者未安装,未知的非空归属 kind 及额外 JSON 字段仍原样保留。原生压缩和标题关系校验直接使用生产者 kind;[强制校验决策](2026-09-17-native-v4-read-validation.zh.md)负责通用生命周期规则。
来源重写由 V3 到 V4 迁移 Stage 负责,在历史事件转换为 V4 事件时只执行一次。当前 V4 编解码器只保留已发布的物理分帧和原生 V4 准入;它不会让 V4 行进入已发布 V3 校验器,编码、解码、恢复和行准入路径也没有来源转换视图。
录制的当前 V4 fixtures 与 writer、notification、stream-json 侧车是生产者归属的;已发布的 V2/V3fixtures 与侧车保留 plugin 归属,读取时经迁移转换。[released session-format migrations](2026-08-31-released-session-format-migrations.zh.md)笔记拥有本边所遵循的版本规则。
[消息源归属策略](2026-09-17-persistence-attribution-policy.zh.md)区分持久化 source 声明与仅用于请求的 user 输入,并记录新增归属 kind 的受限兼容性承诺。即使没有活动生产者声明,冻结迁移规则仍保留历史生产者 kind。
## Alternatives considered
**保留 released plugin 包装作为当前形态。** 消费方将继续特殊对待一个合成 kind,生产者身份仍是无类型字符串;当前语义校验器也会保留第二套非发布的消息源词汇。
**只在扫描时重写 released 行而不是迁移它们。** 扫描准入刻意只查形态并拒绝、从不修复;扫描时重写会模糊 released 世代与当前行的界限,使当前产物保留与版本不符的来源身份。
**在 scanner 中把 plugin-kind 行当作普通损坏拒绝。** 这会让过期的 released 行静默截断当前 Session,而不是给出拒绝;也会把完整解码仍需要诊断的行错误分类。
## Consequences
本迁移的记录语料库应用冻结重命名表,而当前代校验器直接消费生产者自有 kind。Web 上下文标签渲染生产者 kind(`runtime-context`)而不是 released plugin 字符串。共享重写器将畸形历史来源报告为 `SessionFormatError`,因此迁移会拒绝它,而不会静默丢失载荷。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-persistence-attribution-policy.md
2026-09-17-persistence-attribution-policy.md: fa9741ba9343d77c52c38e181a3d2b2f445293e9
2026-09-17-persistence-attribution-policy.zh.md: 0c32adf45938889b5325d0db904be527a969fae0
@@ -0,0 +1,35 @@
# Agent Note: Recorded user-source attribution compatibility
Status: implemented
English | [中文](2026-09-17-persistence-attribution-policy.zh.md)
## Problem
A producer-owned message source appears transitively in several persisted event types. A new attribution kind changes their union fingerprints even when existing readers preserve its fields without the producer. Request-only prompts need no durable identity, but constructing them as Session messages adds unnecessary source alternatives to that union.
## Decision
The core-owned user-message source property binds the [recorded compatibility policy](../process/2026-09-17-persistence-schema-review.md). A producer separately qualifies its literal wire kind as attribution-only. Both saved schemas must carry compatible policy state before a qualified addition receives a same-version classification. Existing alternatives remain structurally compared; missing policy, unqualified additions, removals, and changes to existing semantic groups retain strict classification. Fixed system, model, and tool source fields do not participate.
The tmux-context producer qualifies its location attribution. Its own projection uses the kind to avoid repeated injection, while readers without that producer retain the recorded content and every source JSON property. The qualification promises reader independence; it does not forbid producer-local duplicate suppression.
`RequestMessage` accepts a durable `Message` or a user-only `RequestUserInput` with content and no `id` or `source`. The LLM service and provider serializers accept both; Session writes, Agent delivery, and persisted title-request inputs require durable messages. Auto Review's outer prompt and the compaction summarizer's final instruction use request-only inputs and retain their content and freezing. The caller receives no implicit durable conversion. Assistant replay metadata and tool-result correlation retain their existing requirements.
The unreleased V4 acknowledgement records the user-source policy and removes the two request-only source registrations. Its original predecessors and header transition remain intact. This does not grant a general exception for removing accepted variants: the [producer-source migration](2026-09-09-producer-owned-message-sources.md) preserves its frozen rename and collision rules, including historical request-only kinds, independently of active source declarations. Catalog generation and comparison remain owned by the [persistence reference](../../../../docs/persistence-changes/README.md#compatibility-rules).
## Alternatives considered
**Treat every source addition as breaking.** This would require a Session-format increment for attribution that readers already retain safely, without distinguishing it from semantic changes.
**Relax every discriminated union or infer compatibility from current names.** A generic exception would admit semantic variants. Current declarations cannot establish what an older snapshot promised, and structural equality does not establish ownership of a source property.
**Use durable messages for every request.** Detached prompts would retain manufactured identities and unnecessary source declarations. The explicit user-only alternative permits their removal while typed Session interfaces retain durable identity requirements.
**Narrow historical context forms to current writer examples.** Writer examples do not establish every value admitted by released readers. Those read-facing variants remain represented.
## Consequences
A qualified addition still changes affected event fingerprints and requires an acknowledgement. Reviewers must verify the producer's attribution promise; type analysis cannot establish replay semantics or detect behavior hidden in opaque values.
Codec-to-Session tests retain unknown user attribution and extra JSON fields without the producer. Provider equality tests compare request-only and durable inputs, while type assertions reject request-only inputs at durable writes. Recorded Session generations and the V3-to-V4 conversion remain independent of catalog policy and rendering.
@@ -0,0 +1,35 @@
# Agent Note:已记录的 user-source 归属兼容性
Status: implemented
[English](2026-09-17-persistence-attribution-policy.md) | 中文
## 问题
生产者拥有的消息 source 会传递性地出现在多个持久化事件类型中。即使已有读取方无需该生产者也能保留字段,新增归属 kind 仍会改变这些联合类型的指纹。仅供请求使用的提示不需要持久身份,但将其构造成 Session 消息会为联合类型增加不必要的 source 分支。
## 决策
核心拥有的 user-message source 属性绑定[已记录的兼容性策略](../process/2026-09-17-persistence-schema-review.zh.md)。生产者另行将其 wire 字面量 kind 声明为仅用于归属。双方保存的 schema 必须具有兼容的策略状态,符合条件的添加才能获得同版本分类。已有分支继续接受结构比较;缺失策略、未标记的添加、删除以及已有语义分组的变更继续采用严格分类。固定的 system、model 和 tool source 字段不参与。
tmux-context 生产者将位置归属标记为符合条件。其自身投影使用 kind 避免重复注入,而未安装该生产者的读取方保留已记录内容及 source 的每个 JSON 属性。该标记承诺读取方不依赖生产者,并不禁止生产者内部的去重。
`RequestMessage` 接受持久化 `Message`,或仅含 content、没有 `id` 和 `source` 的 user 专用 `RequestUserInput`。LLM 服务和 provider 序列化器接受两者;Session 写入、Agent 交付及持久化标题请求输入要求持久化消息。Auto Review 的外层提示和压缩摘要器的最终指令使用仅供请求使用的输入,并保留内容与冻结行为。调用方不会获得隐式持久化转换。Assistant 回放元数据和工具结果关联保持原有要求。
未发布的 V4 确认记录保存 user-source 策略,并移除两个仅供请求使用的 source 注册。原有前驱和 header 迁移保持不变。这不构成删除已接受分支的通用例外:[producer-source 迁移](2026-09-09-producer-owned-message-sources.zh.md)独立于活动 source 声明保留冻结的重命名和冲突规则,包括历史的仅供请求使用的 kind。目录生成和比较仍由[持久化参考](../../../../docs/persistence-changes/README.zh.md#compatibility-rules)负责。
## 考虑过的替代方案
**将所有 source 添加视为破坏性变更。** 这会要求为读取方已能安全保留的归属信息提升 Session 格式版本,无法区分它与语义变更。
**放宽所有可辨识联合或从当前名称推断兼容性。** 通用例外会接纳语义分支。当前声明不能确定旧快照作出的承诺,结构相等也不能确定 source 属性的归属。
**为每个请求使用持久化消息。** 独立提示会保留人为制造的身份和不必要的 source 声明。显式的 user 专用分支允许移除它们,同时让带类型的 Session 接口继续要求持久身份。
**将历史 context 形式收窄到当前写入方示例。** 写入方示例不能确定已发布读取方接纳的全部值。这些面向读取的分支继续保留。
## 影响
符合条件的添加仍会改变相关事件指纹,并要求确认记录。Review 者必须验证生产者的归属承诺;类型分析不能确定回放语义,也不能发现不透明值中隐藏的行为。
Codec 到 Session 的测试在未安装生产者时保留未知 user 归属和额外 JSON 字段。Provider 等价测试比较仅供请求使用的输入与持久化输入,类型断言则拒绝在持久化写入处使用仅供请求使用的输入。已记录的 Session 世代及 V3→V4 转换不依赖目录策略和渲染。
@@ -43,7 +43,7 @@ class ControlSurfaceAdapter extends LlmAdapter {
}
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const lastUserIndex = options.messages.findLastIndex(message => message.source.kind === 'user')
const lastUserIndex = options.messages.findLastIndex(message => message.role === 'user' && message.source?.kind === 'user')
const current = options.messages.slice(lastUserIndex)
const userText = current.flatMap(message => message.content)
.flatMap(block => block.type === 'text' ? [block.text] : [])
@@ -150,7 +150,7 @@ describe('same-session goal snapshot through the ACP automation driver', () => {
// The wrap-up instruction is one plugin-sourced context injected after the
// terminal tool result, and the model still answers inside the same turn.
const wrapups = events.filter(event => event.type === 'user/message'
&& event.data.source.kind === 'plugin' && event.data.source.plugin === 'tool-goal')
&& event.data.source.kind === 'tool-goal')
expect(wrapups).toHaveLength(1)
const wrapupText = wrapups.map(event => event.type === 'user/message' ? event.data.content : [])[0]
expect(JSON.stringify(wrapupText)).toContain('<goal_complete>')
@@ -2,13 +2,13 @@
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":4,"time":0,"data":{"turn":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":5,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Probe strict-schema fillers against miss","messageSeqs":[8],"source":{"kind":"fallback"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it.\"}]"}],"source":{"kind":"plugin","plugin":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_goal_probe","name":"update_goal","arguments":"{\"goal_id\":\"missing-goal\",\"revision\":1,\"action\":\"pause\",\"objective\":\"\",\"max_goal_rounds\":0,\"blocked_reason\":\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":15,"outputTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_goal_probe","name":"update_goal","args":["{\"goal_id\":\"missing-goal\",\"revision\":1,\"action\":\"pause\",\"objective\":\"\",\"max_goal_rounds\":0,\"blocked_reason\":\"\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_goal_probe","name":"update_goal","arguments":"{\"goal_id\":\"missing-goal\",\"revision\":1,\"action\":\"pause\",\"objective\":\"\",\"max_goal_rounds\":0,\"blocked_reason\":\"\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":15,"outputTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"call_goal_probe","name":"update_goal","arguments":"{\"goal_id\":\"missing-goal\",\"revision\":1,\"action\":\"pause\",\"objective\":\"\",\"max_goal_rounds\":0,\"blocked_reason\":\"\"}"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":16,"time":0,"data":{"title":"Goal tool snapshot","messageSeqs":[8],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}}
@@ -2,13 +2,13 @@
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":4,"time":0,"data":{"turn":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":5,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"plugin","plugin":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/attempt","seq":14,"time":0,"data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-deepseek: the API key resolved from DEEPSEEK_API_KEY contains characters no HTTP header can carry; set DEEPSEEK_API_KEY to the raw key alone (the web Models page writes it)","code":"INVALID_CREDENTIAL"}}}}]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":15,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":16,"time":0,"data":{"turn":1,"reason":{"kind":"error","error":{"message":"llm-deepseek: the API key resolved from DEEPSEEK_API_KEY contains characters no HTTP header can carry; set DEEPSEEK_API_KEY to the raw key alone (the web Models page writes it)","code":"INVALID_CREDENTIAL"}}}}}
@@ -2,13 +2,13 @@
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":4,"time":0,"data":{"turn":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":5,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"plugin","plugin":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/attempt","seq":14,"time":0,"data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-deepseek: no API key for provider route \"deepseek-official\"; store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it), or export DEEPSEEK_API_KEY in the launching environment","code":"MISSING_CREDENTIAL"}}}}]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":15,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":16,"time":0,"data":{"turn":1,"reason":{"kind":"error","error":{"message":"llm-deepseek: no API key for provider route \"deepseek-official\"; store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it), or export DEEPSEEK_API_KEY in the launching environment","code":"MISSING_CREDENTIAL"}}}}}
@@ -2,13 +2,13 @@
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":4,"time":0,"data":{"turn":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":5,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"retry the transient provider failure"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"retry the transient provider failure","messageSeqs":[8],"source":{"kind":"fallback"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"retry the transient provider failure\"}]"}],"source":{"kind":"plugin","plugin":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"retry the transient provider failure\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/attempt","seq":14,"time":0,"data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"snapshot transient failure","code":"RATE_LIMIT","status":429}}}}]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"llm/retry","seq":15,"time":0,"data":{"retryId":"{{sessionId}}","turn":1,"step":1,"provider":"deepseek-official","mode":"normal","policyKey":"[\"normal\",1,[\"RATE_LIMIT\"],1,1,0]","retry":1,"maxRetries":1,"delayMs":1,"failure":{"message":"snapshot transient failure","code":"RATE_LIMIT","status":429}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"llm/retry-started","seq":16,"time":0,"data":{"retryId":"{{sessionId}}","turn":1,"step":1,"retry":1}}}
@@ -2,13 +2,13 @@
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":4,"time":0,"data":{"turn":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":5,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, job_output, or job_list."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Start one continuable background subagen","messageSeqs":[8],"source":{"kind":"fallback"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, job_output, or job_list.\"}]"}],"source":{"kind":"plugin","plugin":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, job_output, or job_list.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"start-child","name":"subagent","args":["{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":16,"time":0,"data":{"title":"Subagent settlement","messageSeqs":[8],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}}
@@ -52,7 +52,7 @@ async function seedInterruptedSession(root: string, cwd: string): Promise<string
{ type: 'step/start', seq: SessionSeq(1), time: 11, data: { turn: 1, step: 1 } },
{
type: 'system/message', seq: SessionSeq(2), time: 12,
data: { turn: 1, step: 1, message: createMessage({ role: 'system', content: [], source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' } }) },
data: { turn: 1, step: 1, message: createMessage({ role: 'system', content: [], source: { kind: 'system-prompt' } }) },
surfaceOp: 'append',
},
{ type: 'user/message', seq: SessionSeq(3), time: 13, data: createUserMessage({
@@ -60,7 +60,7 @@ async function seedCataloguedDescriptorlessChild(root: string, cwd: string): Pro
{ type: 'step/start', seq: SessionSeq(1), time: 11, data: { turn: 1, step: 1 } },
{
type: 'system/message', seq: SessionSeq(2), time: 12,
data: { turn: 1, step: 1, message: createMessage({ role: 'system', content: [], source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' } }) },
data: { turn: 1, step: 1, message: createMessage({ role: 'system', content: [], source: { kind: 'system-prompt' } }) },
surfaceOp: 'append',
},
{ type: 'user/message', seq: SessionSeq(3), time: 13, data: createUserMessage({ content: [{ type: 'text', text: 'Start a background job.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
@@ -21,7 +21,8 @@ import { describe, expect, it } from 'vitest'
const fixtureDir = fileURLToPath(new URL('./expected/subagent-inheritance', import.meta.url))
const replayOverride = join(fixtureDir, 'replay.override.json')
// Native replay preserves the V0 script's model outputs without migrating its pre-step user event.
// The released V3 child fixture replays through the V3-to-V4 migration, so its
// tool results lift and its plugin sources move onto producer kinds.
const childReplay = join(fixtureDir, 'child.replay.v3.jsonl')
const parentExpected = join(fixtureDir, 'parent.expected.jsonl')
const childExpected = join(fixtureDir, 'child.expected.jsonl')
@@ -59,7 +60,7 @@ async function seedReadOnlyParent(root: string, cwd: string): Promise<void> {
{ type: 'step/start', seq: SessionSeq(1), time: 11, data: { turn: 1, step: 1 } },
{
type: 'system/message', seq: SessionSeq(2), time: 12,
data: { turn: 1, step: 1, message: createMessage({ role: 'system', content: [], source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' } }) },
data: { turn: 1, step: 1, message: createMessage({ role: 'system', content: [], source: { kind: 'system-prompt' } }) },
surfaceOp: 'append',
},
{ type: 'user/message', seq: SessionSeq(3), time: 13, data: createUserMessage({ content: [{ type: 'text', text: 'Tighten this session to read-only.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
@@ -140,11 +141,10 @@ describe('parent-only override inheritance snapshot', () => {
const runtimeContexts = (content: string): string[] => content.trimEnd().split('\n').flatMap((line) => {
const record = JSON.parse(line) as {
type?: string
data?: { source?: { kind?: string; plugin?: string }; content?: Array<{ type?: string; text?: unknown }> }
data?: { source?: { kind?: string }; content?: Array<{ type?: string; text?: unknown }> }
}
if (record.type !== 'user/message'
|| record.data?.source?.kind !== 'plugin'
|| record.data.source.plugin !== '@deepseek-ai/dsh-system-prompt') return []
|| record.data?.source?.kind !== 'runtime-context') return []
return record.data.content?.flatMap(block => block.type === 'text' && typeof block.text === 'string' ? [block.text] : []) ?? []
})
const policyContexts = [...runtimeContexts(parent), ...runtimeContexts(child)]
@@ -90,7 +90,7 @@ async function seedVisibleBaseline(
{ type: 'step/start', seq: SessionSeq(1), time: 11, data: { turn: 1, step: 1 } },
{
type: 'system/message', seq: SessionSeq(2), time: 12,
data: { turn: 1, step: 1, message: createMessage({ role: 'system', content: [], source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' } }) },
data: { turn: 1, step: 1, message: createMessage({ role: 'system', content: [], source: { kind: 'system-prompt' } }) },
surfaceOp: 'append',
},
{
+1 -1
View File
@@ -101,7 +101,7 @@ function seedLog(): string {
at(1, { type: 'step/start', data: { turn: 1, step: 1 } }),
at(2, {
type: 'system/message',
data: { turn: 1, step: 1, message: createSystemMessage('', '@deepseek-ai/dsh-system-prompt') },
data: { turn: 1, step: 1, message: createSystemMessage('') },
surfaceOp: 'append',
}),
at(3, {
+1 -4
View File
@@ -69,10 +69,7 @@ function appendSystemPrompt(session: Session, turn: number, step: number): void
session.append('system/message', {
turn,
step,
message: createSystemMessage(
'Synthetic chat-scroll system prompt.',
'@deepseek-ai/dsh-system-prompt',
),
message: createSystemMessage('Synthetic chat-scroll system prompt.'),
}, { surfaceOp: 'append' })
}
+1 -4
View File
@@ -201,10 +201,7 @@ function appendSystemPrompt(session: Session, turn: number, step: number): void
session.append('system/message', {
turn,
step,
message: createSystemMessage(
'Synthetic performance system prompt.',
'@deepseek-ai/dsh-system-prompt',
),
message: createSystemMessage('Synthetic performance system prompt.'),
}, { surfaceOp: 'append' })
}
@@ -14,7 +14,7 @@
- text: "Use only Cordis tools. First call cordis_inspect_self with no arguments. Then call cordis_define with plugin kind \"new\", idPrefix \"snap\", name \"snapshot noop\", purpose \"does nothing, for the snapshot\", code.host exactly \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\" and code.client exactly \"return { inject: [\\\"slots\\\"], apply(ctx) { ctx.slots.register({ name: \\\"shell.overlay\\\", id: \\\"snapshot-probe\\\" }, () => React.createElement(\\\"div\\\", { \\\"data-snapshot-probe\\\": \\\"loaded\\\" })) } }\". Read its returned pluginId and packageId, then call cordis_run with those exact IDs and mode \"run\". After the run request returns, reply exactly CORDIS_UI_READY and stop. 9/1 {{clock}}"
- button "Copy"
- button "3 tool calls" [expanded]
- button "Context injection @deepseek-ai/dsh-system-prompt"
- button "Context injection runtime-context"
- button "Think I will inspect the current Session's dynamic Cordis Plugins before defining the snapshot Package."
- button "Inspect plugins 0 dynamic plugins"
- button "Think No dynamic Plugins are present, so I will define the requested Host and Client Package."
@@ -18,7 +18,7 @@
- button "System prompt"
- button "Thought for a while" [expanded]
- button "Context injection webhook github webhook handled by review-pr-when-ready"
- button "Context injection @deepseek-ai/dsh-system-prompt"
- button "Context injection runtime-context"
- paragraph: "Review complete: no actionable findings."
- button "Copy"
- button "Good response"
@@ -13,7 +13,7 @@
- text: and confirm the fixture wiring {{clock}}
- button "Copy"
- button "Thought for a while" [expanded]
- button "Context injection @deepseek-ai/dsh-system-prompt"
- button "Context injection runtime-context"
- button "Context injection user-invoke-demo"
- paragraph: USER_INVOKE_REPLY acknowledged; following the injected skill.
- button "Copy"
@@ -10,7 +10,7 @@
- button "System prompt"
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
- button "Copy"
- button "Context injection @deepseek-ai/dsh-system-prompt"
- button "Context injection runtime-context"
- button "Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that."
- status: Deep diving...
- text: "Interjection: include the word BANANA in your final reply."
@@ -11,7 +11,7 @@
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
- button "Copy"
- button "1 tool call" [expanded]
- button "Context injection @deepseek-ai/dsh-system-prompt"
- button "Context injection runtime-context"
- button "Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that."
- button "Ask question 1/1 answered"
- text: "Interjection: include the word BANANA in your final reply. {{clock}}"
+1 -1
View File
@@ -179,7 +179,7 @@ describe.skipIf(MODE === 'record')('web e2e: GitHub ready-for-review', () => {
expect(agent).toBeDefined()
const workspace = await scaffold.ctx.workspaceRegistry.resolveByPath(scaffold.workspaceCwd)
expect(workspace?.sessionIds).toContain(agent?.id)
const webhookMessage = adapter.requests[0]?.messages.find(message => message.source.kind === 'webhook')
const webhookMessage = adapter.requests[0]?.messages.find(message => message.role === 'user' && message.source?.kind === 'webhook')
expect(webhookMessage?.content).toHaveLength(1)
const [content] = webhookMessage?.content ?? []
expect(content?.type).toBe('text')
+1 -2
View File
@@ -82,8 +82,7 @@ describe('minimal agent preset', () => {
const systemPrompt = systemPromptText(agentHandle.agent.session)
if (systemPrompt === undefined) throw new Error('the minimal agent issued no system prompt')
expect(agentHandle.agent.session.snapshotEvents().some(event => event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === '@deepseek-ai/dsh-system-prompt')).toBe(false)
&& event.data.source.kind === 'runtime-context')).toBe(false)
expect(scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'fs')).toBeUndefined()
expect(scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'compaction')).toBeUndefined()
@@ -43,8 +43,7 @@ function systemPrompts(events: readonly SessionEvent[]): string[] {
function runtimeContexts(events: readonly SessionEvent[]): string[] {
return events.flatMap((event) => {
if (event.type !== 'user/message'
|| event.data.source.kind !== 'plugin'
|| event.data.source.plugin !== '@deepseek-ai/dsh-system-prompt') return []
|| event.data.source.kind !== 'runtime-context') return []
return event.data.content.flatMap(block => block.type === 'text' ? [block.text] : [])
})
}
+10 -5
View File
@@ -9,7 +9,7 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
import { composeEntries, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import { ToolCallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import type { ContextFormed, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import {
ScheduleId,
@@ -35,6 +35,12 @@ import {
saveFailureShot,
} from './support.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'schedule-web-e2e': { kind: 'schedule-web-e2e' } & ContextFormed
}
}
const MODE = webSnapshotMode()
const OVERLAY = fileURLToPath(new URL('../../cli/config/examples/schedule/cordis.yml', import.meta.url))
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/schedule-after', import.meta.url))
@@ -190,7 +196,7 @@ function requestText(options: GenerateOptions): string {
/** Require one assembled request to preserve the reminder-content trust boundary. */
function expectReminderFraming(options: GenerateOptions): void {
const reminder = options.messages.find(message => (
message.source.kind === 'plugin' && message.source.plugin === 'schedule'
message.role === 'user' && message.source?.kind === 'schedule'
))
expect(reminder?.role).toBe('user')
const text = reminder?.content.find(block => block.type === 'text')?.text
@@ -388,7 +394,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
})
atHandle.agent.followup(createUserMessage({
content: [{ type: 'text', text: 'Prepare the reminder test session.' }],
source: { kind: 'plugin', plugin: 'schedule-web-e2e' },
source: { kind: 'schedule-web-e2e' },
}))
await atHandle.agent.whenIdle()
expect(atAdapter.requests).toHaveLength(1)
@@ -472,8 +478,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
const batch = everyHandle.agent.session.snapshotEvents().find(event => (
event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === 'schedule'
&& event.data.source.kind === 'schedule'
&& event.data.content.some(block => block.type === 'text'
&& block.text.startsWith('[SCHEDULE REMINDER BATCH]'))
))
+14 -5
View File
@@ -17,7 +17,7 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { createLaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, ContextFormed, Message, MessageSource } from '@deepseek-ai/dsh-llm'
import { deriveEventMessage, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { TokenMeter } from '@deepseek-ai/dsh-token-meter'
@@ -31,6 +31,15 @@ import {
} from './scaffold.ts'
import { expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'fixture': { kind: 'fixture' } & ContextFormed
}
}
type CheckpointSource = Extract<MessageSource, { readonly kind: 'compact-checkpoint' }>
type CheckpointCommandId = NonNullable<CheckpointSource['sourceCommandId']>
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/seeded-history', import.meta.url))
const SEED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/session.v3.jsonl', import.meta.url))
const UI_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/ui.expected.md', import.meta.url))
@@ -105,8 +114,8 @@ function withCompaction(raw: string, meter: TokenMeter): string {
events.push({ ...event, seq: taken, time: time++ })
return taken
}
const commandId = 'cmd-seeded-manual-compact'
const compactionId = 'compact-seeded-manual-compact'
const commandId = 'cmd-seeded-manual-compact' as CheckpointCommandId
const compactionId = 'compact-seeded-manual-compact' as CheckpointSource['compactionId']
at({
type: 'command/run',
data: { commandId, name: 'compact', args: '', source: { kind: 'user' } },
@@ -173,7 +182,7 @@ function withCompaction(raw: string, meter: TokenMeter): string {
text: '<context_checkpoint>Model-only compact checkpoint.</context_checkpoint>',
}],
source: {
kind: 'plugin', plugin: 'compact', compactionId, sourceCommandId: commandId,
kind: 'compact-checkpoint', compactionId, sourceCommandId: commandId,
},
}),
surfaceOp: { op: 'replace', startSeq: first, endSeq: last },
@@ -781,7 +790,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
if (agent === undefined) throw new Error('seeded session did not attach an agent')
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'Short injected context.' }],
source: { kind: 'plugin', plugin: 'fixture' },
source: { kind: 'fixture' },
}), { surfaceOp: 'append' })
const disclosure = page.getByRole('button', { name: 'Context injection fixture', exact: true })
+14 -4
View File
@@ -45,6 +45,18 @@ const AUTO_PARENT_ONE_SHOT = 'AUTO_PARENT_ONE_SHOT'
const AUTO_PARENT_CONTINUABLE = 'AUTO_PARENT_CONTINUABLE'
const AUTO_PARENT_ADJUST = 'AUTO_PARENT_ADJUST'
/** Identify the one-shot Auto Review request and check its request-only outer input. */
function isAutoReviewRequest(options: GenerateOptions): boolean {
if (options.system?.startsWith('REVIEW_POLICY\n') !== true) return false
expect(options.messages).toHaveLength(1)
const message = options.messages[0]
expect(message).toMatchObject({ role: 'user', content: [{ type: 'text' }] })
expect(message?.content).toHaveLength(1)
expect(message).not.toHaveProperty('id')
expect(message).not.toHaveProperty('source')
return true
}
type RpcResult<T> = { ok: true; value: T } | { ok: false; error: { code: string; message: string } }
/** POST one generated Remote unary through the authenticated Web carrier. */
@@ -94,8 +106,7 @@ class ShippedAutoAdapter extends LlmAdapter {
override async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
this.requests.push(options)
const source = options.messages[0]?.source
if (source?.kind === 'plugin' && source.plugin === 'dsh-experimental-auto-review') {
if (isAutoReviewRequest(options)) {
yield* textChunks(JSON.stringify({
risk: 'medium', decision: 'deny', reason: AUTO_RAW_REASON,
}))
@@ -214,8 +225,7 @@ class ShippedChildAutoAdapter extends LlmAdapter {
override async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
this.requests.push(options)
const source = options.messages[0]?.source
const response = source?.kind === 'plugin' && source.plugin === 'dsh-experimental-auto-review'
const response = isAutoReviewRequest(options)
? this.reviewResponse(options)
: this.mainResponse(options)
yield* response
@@ -10,7 +10,7 @@
- button "System prompt"
- text: Stream one TypeScript fence for the highlighting snapshot. {{clock}}
- button "Copy"
- button "Context injection @deepseek-ai/dsh-system-prompt"
- button "Context injection runtime-context"
- text: ts
- button "Copy"
- code: "const first: number = 1 const second = \"two\" let tail"
+1 -1
View File
@@ -50,7 +50,7 @@ function buildSeed(turns: number): string {
if (turn === 1) {
at({
type: 'system/message',
data: { turn, step: 1, message: createSystemMessage('', '@deepseek-ai/dsh-system-prompt') },
data: { turn, step: 1, message: createSystemMessage('') },
surfaceOp: 'append',
})
}
@@ -23,7 +23,7 @@ for (const [name, generate] of [
session.append('turn/start', { turn })
session.append('step/start', { turn, step: 1 })
const replacement = session.append('system/message', {
turn, step: 1, message: createSystemMessage('Next synthetic prompt', '@deepseek-ai/dsh-system-prompt'),
turn, step: 1, message: createSystemMessage('Next synthetic prompt'),
}, { surfaceOp: { op: 'replace', startSeq: head, endSeq: head }, sourceEventSeqs: [head] })
const restored = Session.create(SessionId(header.id), parseSessionLog([
JSON.stringify(header), ...session.snapshotEvents().map(event => JSON.stringify(event)),
+1 -1
View File
@@ -69,7 +69,7 @@ export function syntheticHistory(turns: number): SessionEvent[] {
session.append('turn/start', { turn })
session.append('step/start', { turn, step: 1 })
if (turn === 1) session.append('system/message', {
turn, step: 1, message: { id: MessageId('system-head'), role: 'system', content: [], source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' } },
turn, step: 1, message: { id: MessageId('system-head'), role: 'system', content: [], source: { kind: 'system-prompt' } },
}, { surfaceOp: 'append' })
session.append('user/message', {
id: MessageId('prompt-' + String(turn)), role: 'user',
@@ -28,7 +28,7 @@ export function syntheticHistory(): string {
session.append('turn/start', { turn })
session.append('step/start', { turn, step: 1 })
if (turn === 1) session.append('system/message', {
turn, step: 1, message: createSystemMessage('', '@deepseek-ai/dsh-system-prompt'),
turn, step: 1, message: createSystemMessage(''),
}, { surfaceOp: 'append' })
const user = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'Review synthetic change ' + String(turn) + ': 检查增量渲染。 '.repeat(30) }],
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: 3d968a3667ca483251b5beabf1f4843518d42093
config-catalog.zh.md: cc8a30329457181508b867663cedbf2ccef17cb8
config-catalog.md: d617ed628bf1ca0235bc68660418c860ff952dc8
config-catalog.zh.md: 067d5a23b699087ddaeedc96db674f807f509fbe
+11 -11
View File
@@ -111,7 +111,7 @@ export interface Config {
Depends on: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md)
Source: [`packages/core/agent-loop/src/index.ts:318`](../packages/core/agent-loop/src/index.ts)
Source: [`packages/core/agent-loop/src/index.ts:319`](../packages/core/agent-loop/src/index.ts)
<a id="deepseek-aidsh-agent-presets"></a>
@@ -549,7 +549,7 @@ export interface Config {
}
```
Source: [`packages/extensions/cordis-host-runner/src/index.ts:88`](../packages/extensions/cordis-host-runner/src/index.ts)
Source: [`packages/extensions/cordis-host-runner/src/index.ts:94`](../packages/extensions/cordis-host-runner/src/index.ts)
<a id="deepseek-aidsh-credentials-local"></a>
@@ -996,7 +996,7 @@ export interface Config {
}
```
Source: [`packages/hooks/hooks-claude-code/src/index.ts:44`](../packages/hooks/hooks-claude-code/src/index.ts)
Source: [`packages/hooks/hooks-claude-code/src/index.ts:51`](../packages/hooks/hooks-claude-code/src/index.ts)
<a id="deepseek-aidsh-hooks-codex"></a>
@@ -1023,7 +1023,7 @@ export interface Config {
}
```
Source: [`packages/hooks/hooks-codex/src/index.ts:43`](../packages/hooks/hooks-codex/src/index.ts)
Source: [`packages/hooks/hooks-codex/src/index.ts:50`](../packages/hooks/hooks-codex/src/index.ts)
<a id="deepseek-aidsh-host-directory-picker-browse"></a>
@@ -1874,7 +1874,7 @@ export interface PlanModeConfig {
}
```
Source: [`packages/plan/plan-mode/src/index.ts:64`](../packages/plan/plan-mode/src/index.ts)
Source: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts)
<a id="deepseek-aidsh-plugin-manager"></a>
@@ -2037,7 +2037,7 @@ export interface Config {
}
```
Source: [`packages/guard/repeat-tool-reminder/src/index.ts:28`](../packages/guard/repeat-tool-reminder/src/index.ts)
Source: [`packages/guard/repeat-tool-reminder/src/index.ts:35`](../packages/guard/repeat-tool-reminder/src/index.ts)
<a id="deepseek-aidsh-sandbox-local"></a>
@@ -2981,7 +2981,7 @@ export interface Config {
}
```
Source: [`packages/context/time-context/src/index.ts:49`](../packages/context/time-context/src/index.ts)
Source: [`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts)
<a id="deepseek-aidsh-tmux-context"></a>
@@ -2997,7 +2997,7 @@ export interface Config {
}
```
Source: [`packages/context/tmux-context/src/index.ts:36`](../packages/context/tmux-context/src/index.ts)
Source: [`packages/context/tmux-context/src/index.ts:47`](../packages/context/tmux-context/src/index.ts)
<a id="deepseek-aidsh-token-meter"></a>
@@ -3121,7 +3121,7 @@ export interface Config {
}
```
Source: [`packages/goal/tool-goal/src/index.ts:25`](../packages/goal/tool-goal/src/index.ts)
Source: [`packages/goal/tool-goal/src/index.ts:32`](../packages/goal/tool-goal/src/index.ts)
<a id="deepseek-aidsh-tool-jobs"></a>
@@ -3155,7 +3155,7 @@ export interface Config {
export type CompletionDelivery = 'quiet' | 'wakeup'
```
Source: [`packages/jobs/tool-jobs/src/index.ts:31`](../packages/jobs/tool-jobs/src/index.ts)
Source: [`packages/jobs/tool-jobs/src/index.ts:37`](../packages/jobs/tool-jobs/src/index.ts)
<a id="deepseek-aidsh-tool-lsp"></a>
@@ -3543,7 +3543,7 @@ export interface Config {
export type ApprovalPolicy = 'ask' | 'never'
```
Source: [`packages/interaction/user-approval/src/index.ts:128`](../packages/interaction/user-approval/src/index.ts)
Source: [`packages/interaction/user-approval/src/index.ts:135`](../packages/interaction/user-approval/src/index.ts)
<a id="deepseek-aidsh-web"></a>
+11 -11
View File
@@ -113,7 +113,7 @@ export interface Config {
Depends on: [`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md)
来源:[`packages/core/agent-loop/src/index.ts:318`](../packages/core/agent-loop/src/index.ts)
来源:[`packages/core/agent-loop/src/index.ts:319`](../packages/core/agent-loop/src/index.ts)
<a id="deepseek-aidsh-agent-presets"></a>
@@ -551,7 +551,7 @@ export interface Config {
}
```
来源:[`packages/extensions/cordis-host-runner/src/index.ts:88`](../packages/extensions/cordis-host-runner/src/index.ts)
来源:[`packages/extensions/cordis-host-runner/src/index.ts:94`](../packages/extensions/cordis-host-runner/src/index.ts)
<a id="deepseek-aidsh-credentials-local"></a>
@@ -998,7 +998,7 @@ export interface Config {
}
```
来源:[`packages/hooks/hooks-claude-code/src/index.ts:44`](../packages/hooks/hooks-claude-code/src/index.ts)
来源:[`packages/hooks/hooks-claude-code/src/index.ts:51`](../packages/hooks/hooks-claude-code/src/index.ts)
<a id="deepseek-aidsh-hooks-codex"></a>
@@ -1025,7 +1025,7 @@ export interface Config {
}
```
来源:[`packages/hooks/hooks-codex/src/index.ts:43`](../packages/hooks/hooks-codex/src/index.ts)
来源:[`packages/hooks/hooks-codex/src/index.ts:50`](../packages/hooks/hooks-codex/src/index.ts)
<a id="deepseek-aidsh-host-directory-picker-browse"></a>
@@ -1876,7 +1876,7 @@ export interface PlanModeConfig {
}
```
来源:[`packages/plan/plan-mode/src/index.ts:64`](../packages/plan/plan-mode/src/index.ts)
来源:[`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts)
<a id="deepseek-aidsh-plugin-manager"></a>
@@ -2039,7 +2039,7 @@ export interface Config {
}
```
来源:[`packages/guard/repeat-tool-reminder/src/index.ts:28`](../packages/guard/repeat-tool-reminder/src/index.ts)
来源:[`packages/guard/repeat-tool-reminder/src/index.ts:35`](../packages/guard/repeat-tool-reminder/src/index.ts)
<a id="deepseek-aidsh-sandbox-local"></a>
@@ -2983,7 +2983,7 @@ export interface Config {
}
```
来源:[`packages/context/time-context/src/index.ts:49`](../packages/context/time-context/src/index.ts)
来源:[`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts)
<a id="deepseek-aidsh-tmux-context"></a>
@@ -2999,7 +2999,7 @@ export interface Config {
}
```
来源:[`packages/context/tmux-context/src/index.ts:36`](../packages/context/tmux-context/src/index.ts)
来源:[`packages/context/tmux-context/src/index.ts:47`](../packages/context/tmux-context/src/index.ts)
<a id="deepseek-aidsh-token-meter"></a>
@@ -3123,7 +3123,7 @@ export interface Config {
}
```
来源:[`packages/goal/tool-goal/src/index.ts:25`](../packages/goal/tool-goal/src/index.ts)
来源:[`packages/goal/tool-goal/src/index.ts:32`](../packages/goal/tool-goal/src/index.ts)
<a id="deepseek-aidsh-tool-jobs"></a>
@@ -3157,7 +3157,7 @@ export interface Config {
export type CompletionDelivery = 'quiet' | 'wakeup'
```
来源:[`packages/jobs/tool-jobs/src/index.ts:31`](../packages/jobs/tool-jobs/src/index.ts)
来源:[`packages/jobs/tool-jobs/src/index.ts:37`](../packages/jobs/tool-jobs/src/index.ts)
<a id="deepseek-aidsh-tool-lsp"></a>
@@ -3545,7 +3545,7 @@ export interface Config {
export type ApprovalPolicy = 'ask' | 'never'
```
来源:[`packages/interaction/user-approval/src/index.ts:128`](../packages/interaction/user-approval/src/index.ts)
来源:[`packages/interaction/user-approval/src/index.ts:135`](../packages/interaction/user-approval/src/index.ts)
<a id="deepseek-aidsh-web"></a>
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
event-producer-consumer.md: 4addf64cc2d695ce28033a6051b63c09b5a6b0f1
event-producer-consumer.zh.md: 9b7873a030db8f09c0acfa05c77b08f297650ba3
event-producer-consumer.md: de9a96089397614e32487c29f34302d77f1f07a9
event-producer-consumer.zh.md: bf668ddc86a610f05cf9f2c9a04b892a37dbcd4e
+3 -3
View File
@@ -7,7 +7,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:246`](../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:247`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:82`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `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-presets`](../packages/preset/agent-presets), `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
@@ -29,7 +29,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
| `compaction/summary-error` | `waterfall` | [`packages/compaction/compaction/src/index.ts:99`](../packages/compaction/compaction/src/index.ts) | [`compaction-basic`](../packages/compaction/compaction-basic) (`waterfall`) | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) |
| `compaction/summary-error` | `waterfall` | [`packages/compaction/compaction/src/index.ts:106`](../packages/compaction/compaction/src/index.ts) | [`compaction-basic`](../packages/compaction/compaction-basic) (`waterfall`) | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) |
| `connection/request` | `waterfall` | [`packages/client/connection/src/index.ts:62`](../packages/client/connection/src/index.ts) | `connection` (`waterfall`) | - |
| `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:380`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:386`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
@@ -49,7 +49,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:30`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:72`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:73`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:44`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:189`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:195`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+3 -3
View File
@@ -9,7 +9,7 @@
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:246`](../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:247`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:82`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `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-presets`](../packages/preset/agent-presets), `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
@@ -31,7 +31,7 @@
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
| `compaction/summary-error` | `waterfall` | [`packages/compaction/compaction/src/index.ts:99`](../packages/compaction/compaction/src/index.ts) | [`compaction-basic`](../packages/compaction/compaction-basic) (`waterfall`) | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) |
| `compaction/summary-error` | `waterfall` | [`packages/compaction/compaction/src/index.ts:106`](../packages/compaction/compaction/src/index.ts) | [`compaction-basic`](../packages/compaction/compaction-basic) (`waterfall`) | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) |
| `connection/request` | `waterfall` | [`packages/client/connection/src/index.ts:62`](../packages/client/connection/src/index.ts) | `connection` (`waterfall`) | - |
| `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:380`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:386`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
@@ -51,7 +51,7 @@
| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:30`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:72`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:73`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:44`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:189`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:195`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
persistence-catalog.md: 06f53ee170ae77985f1f6647a115bae217d60180
persistence-catalog.zh.md: 4c5b640b694a91ac690110a1ec54b71624349322
persistence-catalog.md: 0d97fb37b921effbab4988770af6e93efc360be6
persistence-catalog.zh.md: ea5d1c8dd1691549159bcd24def1116719f0176c
+1315 -124
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-09-16-session-format-v4.md
2026-09-16-session-format-v4.md: 81327a4a76751680ae5c4bd18cff930650fbe1fc
2026-09-16-session-format-v4.zh.md: dbce32b89abfe233f3ddb0a71e3f9b7594523f84
2026-09-16-session-format-v4.md: 625e977196d0395c2a3d7c662831914f0c3e6f23
2026-09-16-session-format-v4.zh.md: bd11a72eba0ef1b7653d32ef6f987e500d0b2ba0
@@ -9,7 +9,7 @@ English | [中文](2026-09-16-session-format-v4.zh.md)
## Summary
Advances the declared SessionHeader.version from 3 to 4 for the V4 integration writer, records first-class tool-role results, and adds the forked variant to turn/end.reason.
Advances the declared SessionHeader.version from 3 to 4 for the V4 integration writer, records first-class tool-role results and producer-owned sources, and adds the forked variant to turn/end.reason.
## Table of Contents
@@ -32,7 +32,7 @@ changes:
decision: version-bump
- root: "event:agent/inbox/spliced"
previous: "2026-09-14-image-offload"
after: "cbe418d5a3727fbd3599f90101680dae254ac1d6163b1b8b2ca30f28afb5212a"
after: "ab1acc0685cc410d0d1e85ff0e0bd29f4b2b4352dd170db54988319d58d4362f"
decision: version-bump
- root: "event:assistant/attempt"
previous: "2026-09-14-image-offload"
@@ -52,11 +52,11 @@ changes:
decision: version-bump
- root: "event:session/title-llm-request"
previous: "2026-09-14-image-offload"
after: "2a47ef7025d7dcf7e9977aadd738e7408a210b101a7dbbd37f4d87f22166eff0"
after: "eb655047c0dd3f2f3aefd2c9c719df0d060dd88032ba7caf731ea39db5a6725f"
decision: version-bump
- root: "event:system/message"
previous: "2026-09-14-image-offload"
after: "1772581b17e1fab970fda49f04ffae2a181b7992ca4efa63bffe5f8b2bd62300"
after: "54a8682615a7ffc3e1e3c41ab003ae61082ae2792c57407f5bd8bb38fded3f0e"
decision: version-bump
- root: "event:team/message/queued"
previous: "2026-09-14-image-offload"
@@ -76,7 +76,7 @@ changes:
decision: version-bump
- root: "event:user/message"
previous: "2026-09-14-image-offload"
after: "5274b395e7bf6660d020fba7e29a1e20ef2e77097fd7597e9debb13e45c540d6"
after: "46c44941f6e93a1764c49f1487e64a57f73ee68a75a657e94c326a8de5d8ad95"
decision: version-bump
```
@@ -91,6 +91,10 @@ The V3-to-V4 migration lifts released user-role tool results into tool-role mess
The `request/header` schema also records the retired `system` key as forbidden. This declaration captures the existing native-reader refusal without changing stored data or prompt reconstruction; later permitting a value requires a version bump instead of being classified as an ordinary optional-field addition.
Producer-owned sources replace released plugin wrappers through the [V3-to-V4 migration](../../packages/session/session-format-v3-to-v4/README.md#v3-to-v4-specification). The frozen rename table and collision rules preserve source fields and event coordinates; unknown producer attribution retains every own JSON property. Native read and write opens validate source fields before exposing the Session. Existing V4 files do not rerun the incoming migration edge.
The core-owned user source property records the attribution-preservation policy; tmux-context qualifies its location attribution while retaining producer-local duplicate suppression. Auto Review and the compaction summarizer use request-only user inputs, removing their active source registrations without removing historical migration support. These inputs cannot be written as durable Session messages. Catalog formatVersion 2 stores the policy metadata; it does not change the Session version or rewrite frozen schema records. System, model, and tool sources retain their strict semantic rules.
<a id="verification"></a>
## Verification
@@ -98,6 +102,8 @@ The focused Session, agent-loop, Session Controller, V4, chat-view, and compacti
The generated request-header reservation regression and existing retired-syntax and Session surface tests pass 65 tests across three files. The generated field retains optional `never`; permitting an optional string produces a version-bump diagnostic while the native reader still refuses the retired key.
The producer-source and request-input checks pass 447 tests across 12 files, including provider equality, request-only type rejection, user-attribution retention, source migration, and native source admission. Comparing the generated inventories with the tool-role parent reports four changed roots and 447 unchanged type fingerprints.
<a id="dev-note"></a>
## Dev Note
File diff suppressed because it is too large. Load diff
@@ -9,7 +9,7 @@ kind: persistence-change
## 概述
将 V4 集成写入方声明的 SessionHeader.version 从 3 推进到 4,记录一等 tool 角色结果,并向 turn/end.reason 添加 forked 变体。
将 V4 集成写入方声明的 SessionHeader.version 从 3 推进到 4,记录一等 tool 角色结果与生产者拥有的 source,并向 turn/end.reason 添加 forked 变体。
## 目录
@@ -32,7 +32,7 @@ changes:
decision: version-bump
- root: "event:agent/inbox/spliced"
previous: "2026-09-14-image-offload"
after: "cbe418d5a3727fbd3599f90101680dae254ac1d6163b1b8b2ca30f28afb5212a"
after: "ab1acc0685cc410d0d1e85ff0e0bd29f4b2b4352dd170db54988319d58d4362f"
decision: version-bump
- root: "event:assistant/attempt"
previous: "2026-09-14-image-offload"
@@ -52,11 +52,11 @@ changes:
decision: version-bump
- root: "event:session/title-llm-request"
previous: "2026-09-14-image-offload"
after: "2a47ef7025d7dcf7e9977aadd738e7408a210b101a7dbbd37f4d87f22166eff0"
after: "eb655047c0dd3f2f3aefd2c9c719df0d060dd88032ba7caf731ea39db5a6725f"
decision: version-bump
- root: "event:system/message"
previous: "2026-09-14-image-offload"
after: "1772581b17e1fab970fda49f04ffae2a181b7992ca4efa63bffe5f8b2bd62300"
after: "54a8682615a7ffc3e1e3c41ab003ae61082ae2792c57407f5bd8bb38fded3f0e"
decision: version-bump
- root: "event:team/message/queued"
previous: "2026-09-14-image-offload"
@@ -76,7 +76,7 @@ changes:
decision: version-bump
- root: "event:user/message"
previous: "2026-09-14-image-offload"
after: "5274b395e7bf6660d020fba7e29a1e20ef2e77097fd7597e9debb13e45c540d6"
after: "46c44941f6e93a1764c49f1487e64a57f73ee68a75a657e94c326a8de5d8ad95"
decision: version-bump
```
@@ -91,6 +91,10 @@ V3-to-V4 迁移将已发布的 user 角色工具结果提升为 tool 角色消
`request/header` schema 还将退役的 `system` 键记录为禁止字段。这项声明记录已有的原生读取拒绝规则,不改变存储数据或提示词重建;以后允许该字段携带值时,必须提升格式版本,而不能归类为普通可选字段添加。
生产者拥有的 source 通过 [V3→V4 迁移](../../packages/session/session-format-v3-to-v4/README.zh.md#v3-to-v4-specification)替代已发布的 plugin wrapper。冻结的重命名表和冲突规则保留 source 字段与事件坐标;未知生产者归属保留每个自有 JSON 属性。原生读取和写入打开在公开 Session 之前校验 source 字段。已有 V4 文件不会重新运行迁入边。
核心拥有的 user source 属性记录归属保留策略;tmux-context 将位置归属标记为符合条件,同时保留生产者内部的去重。Auto Review 和压缩摘要器使用仅供请求使用的 user 输入,移除其活动 source 注册,同时保留历史迁移支持。这些输入不能写为持久化 Session 消息。目录 formatVersion 2 保存策略元数据,不改变 Session 版本,也不重写冻结的 schema 记录。System、model 和 tool source 保持严格的语义规则。
<a id="verification"></a>
## 验证
@@ -98,6 +102,8 @@ V3-to-V4 迁移将已发布的 user 角色工具结果提升为 tool 角色消
生成的请求头保留字段回归测试以及已有的退役语法和 Session surface 测试共通过三个文件中的 65 项测试。生成字段保留可选的 `never`;允许可选字符串会产生必须提升版本的诊断,而原生读取方仍拒绝该退役键。
Producer-source 与 request-input 检查共通过 12 个文件中的 447 项测试,覆盖 provider 等价性、仅供请求使用的输入的类型拒绝、user 归属保留、source 迁移及原生 source 准入。与 tool-role 父层的生成目录相比,有四个 root 发生变化,447 个类型指纹保持不变。
<a id="dev-note"></a>
## 开发备注
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-changes/historical-formats/README.md
README.md: 3eb9052160ef6ce71d0a4fb8c55d5430bf47600f
README.zh.md: fb96711d673eed7bd405d41a86b84c44fbfbc490
README.md: a4ae7ea4e92cb05b16a989de72605621307fa5bb
README.zh.md: e76ed913b79baa1d876e8dd33edeeb72223799b8
@@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant.
| 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 |
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 |
| 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 |
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 61 / 469 |
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 61 / 572 |
<!-- persistence-format-index:end -->
@@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式
| 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 |
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 |
| 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 |
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 61 / 469 |
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 61 / 572 |
<!-- persistence-format-index:end -->
+37139 -14119
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md
llm-streaming.md: a5a42ef50dccebbc40b0c35e0dd4e76f7a8f764b
llm-streaming.zh.md: 653a216de81fd2a11f45d8bbcb04231f7dd3f7fb
llm-streaming.md: 55976c9724413eea2230929747e27152011f2c1e
llm-streaming.zh.md: 43583597ff356d067869356b64d71d373f610682
+28 -8
View File
@@ -2,7 +2,7 @@
English | [中文](llm-streaming.zh.md)
The conversation and streaming types from [`packages/llm`](../../packages/llm/README.md): the `Message`/`ContentBlock` variants every request and durable history share, the fully assembled model request, the raw `StreamChunk` protocol, the adapter contract every adapter must implement, and the shared assembler. The [core packages](core.md) hold and log these values on every turn; this page declares them.
The conversation and streaming types from [`packages/llm`](../../packages/llm/README.md): durable `Message` values, request-only user inputs, shared `ContentBlock` variants, the fully assembled model request, the raw `StreamChunk` protocol, the adapter contract every adapter must implement, and the shared assembler. The [core packages](core.md) hold and log these values on every turn; this page declares them.
Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)
@@ -71,14 +71,17 @@ Where a message came from is itself a merge-extensible sum type:
```ts type-equiv
/**
* Where a message (or injected content) came from.
* Merge-extensible sum type — plugins add their own `kind`s.
* Where a message (or injected content) came from, in the harness's own
* vocabulary. Merge-extensible sum type — each producer declares its own
* `kind` in its own module; there is no shared catch-all `plugin` kind.
* Model and tool sources answer their role messages; user messages carry any
* producer's kind, and consumers fall through unknown kinds.
*/
interface MessageSourceMap {
user: { kind: 'user' }
plugin: { kind: 'plugin'; plugin: string } & ContextFormed
model: ModelMessageSource
tool: ToolMessageSource
'system-prompt': SystemPromptMessageSource
}
```
@@ -567,6 +570,23 @@ interface LlmResolvedModelInfo extends LlmModelInfo {
}
```
Auxiliary callers can provide user content without a durable identity or source. Existing `Message[]` histories remain valid request inputs. Session writes, Agent delivery, and recorded title requests continue to require durable messages.
```ts type-equiv
/** User input for one LLM request; it has no durable Session identity or source. */
interface RequestUserInput {
readonly role: 'user'
readonly content: UserMessage['content']
readonly id?: never
readonly source?: never
}
```
```ts type-equiv
/** A durable conversation message or a user input used only for one request. */
type RequestMessage = Message | RequestUserInput
```
```ts type-equiv
/** A single model request, fully assembled. */
interface GenerateOptions {
@@ -579,9 +599,9 @@ interface GenerateOptions {
* Ordered conversation messages, exactly as the provider sees them. A
* loop-built request passes the derived history (dsh-agent-loop), whose
* leading system-role message carries the system prompt; a hand-built
* one-shot passes any list.
* one-shot may include identity-free user inputs.
*/
messages: Message[]
messages: RequestMessage[]
/**
* System prompt text for one-shot callers; adapters map it to the provider's
* system slot ahead of `messages`. Loop-built requests leave it undefined.
@@ -1079,8 +1099,8 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
* process-local {@link markAgentLoopRequest} identity and arrives deep-frozen
* (mutation throws): its content is a pure function of the session log (the
* reconstructability Agent Note), so listeners read it, never rewrite it.
* Hand-built calls do not carry that marker; their messages already obey
* the immutable creation contract.
* Hand-built calls do not carry that marker; callers own their request
* inputs and must keep them unchanged until the stream settles.
* @mode waterfall
*/
'llm/stream'(this: LlmRuntime, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
+28 -8
View File
@@ -2,7 +2,7 @@
[English](llm-streaming.md) | 中文
[`packages/llm`](../../packages/llm/README.zh.md) 提供对话与流式输出类型:每个请求和持久历史共用的 `Message`/`ContentBlock` 变体、完整组装的模型请求、原始 `StreamChunk` 协议、每个适配器必须实现的适配器约定(adapter contract),以及共享的 assembler。[核心包](core.zh.md)在每个轮次持有并记录这些值;本页声明它们。
[`packages/llm`](../../packages/llm/README.zh.md) 提供对话与流式输出类型:持久 `Message` 值、仅供请求使用的 user 输入、共用的 `ContentBlock` 变体、完整组装的模型请求、原始 `StreamChunk` 协议、每个适配器必须实现的适配器约定(adapter contract),以及共享的 assembler。[核心包](core.zh.md)在每个轮次持有并记录这些值;本页声明它们。
源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)
@@ -71,14 +71,17 @@ type Message = MessageRoleMap[keyof MessageRoleMap]
```ts type-equiv
/**
* Where a message (or injected content) came from.
* Merge-extensible sum type — plugins add their own `kind`s.
* Where a message (or injected content) came from, in the harness's own
* vocabulary. Merge-extensible sum type — each producer declares its own
* `kind` in its own module; there is no shared catch-all `plugin` kind.
* Model and tool sources answer their role messages; user messages carry any
* producer's kind, and consumers fall through unknown kinds.
*/
interface MessageSourceMap {
user: { kind: 'user' }
plugin: { kind: 'plugin'; plugin: string } & ContextFormed
model: ModelMessageSource
tool: ToolMessageSource
'system-prompt': SystemPromptMessageSource
}
```
@@ -573,6 +576,23 @@ interface LlmResolvedModelInfo extends LlmModelInfo {
}
```
辅助调用方可以提供不含持久身份或来源的 user 内容。既有 `Message[]` 历史仍然是有效的请求输入。Session 写入、Agent 投递和已记录的标题请求仍然要求持久消息。
```ts type-equiv
/** User input for one LLM request; it has no durable Session identity or source. */
interface RequestUserInput {
readonly role: 'user'
readonly content: UserMessage['content']
readonly id?: never
readonly source?: never
}
```
```ts type-equiv
/** A durable conversation message or a user input used only for one request. */
type RequestMessage = Message | RequestUserInput
```
```ts type-equiv
/** A single model request, fully assembled. */
interface GenerateOptions {
@@ -585,9 +605,9 @@ interface GenerateOptions {
* Ordered conversation messages, exactly as the provider sees them. A
* loop-built request passes the derived history (dsh-agent-loop), whose
* leading system-role message carries the system prompt; a hand-built
* one-shot passes any list.
* one-shot may include identity-free user inputs.
*/
messages: Message[]
messages: RequestMessage[]
/**
* System prompt text for one-shot callers; adapters map it to the provider's
* system slot ahead of `messages`. Loop-built requests leave it undefined.
@@ -1085,8 +1105,8 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
* process-local {@link markAgentLoopRequest} identity and arrives deep-frozen
* (mutation throws): its content is a pure function of the session log (the
* reconstructability Agent Note), so listeners read it, never rewrite it.
* Hand-built calls do not carry that marker; their messages already obey
* the immutable creation contract.
* Hand-built calls do not carry that marker; callers own their request
* inputs and must keep them unchanged until the stream settles.
* @mode waterfall
*/
'llm/stream'(this: LlmRuntime, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
+9 -2
View File
@@ -1,10 +1,17 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { createUserMessage, ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { makeBridgeHarness, textResponse, type BridgeHarness } from './harness.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'test': { kind: 'test' } & ContextFormed
}
}
function toolCallResponse(): StreamChunk[] {
return [
{ type: 'block-start', index: 0, blockType: 'reasoning' },
@@ -98,7 +105,7 @@ describe('ACP automation output boundary', () => {
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(SessionId(sessionId))!
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'plugin', plugin: 'test' } }))
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'test' } }))
await agent.whenIdle()
await vi.waitFor(() => { expect(harness!.updates.at(-1)?.sessionUpdate).toBe('usage_update') })
@@ -133,7 +140,7 @@ describe('ACP automation output boundary', () => {
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(SessionId(sessionId))!
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'plugin', plugin: 'test' } }))
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'test' } }))
await agent.whenIdle()
await vi.waitFor(() => { expect(warn).toHaveBeenCalledWith(expect.stringContaining('output conversion failed')) })
expect(harness.updates).toEqual([])
+13 -6
View File
@@ -1,4 +1,5 @@
import { createUserMessage, type StreamChunk } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { SessionId } from '@deepseek-ai/dsh-session'
@@ -10,6 +11,12 @@ import {
type BridgeHarness,
} from './harness.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'test': { kind: 'test' } & ContextFormed
}
}
async function newSession(harness: BridgeHarness): Promise<string> {
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
return (await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })).sessionId
@@ -188,7 +195,7 @@ describe('ACP prompt lifecycle', () => {
harness.ctx.on('agent/inbox/inserted', ({ agent: subject, message }) => {
if (subject === agent && message.source.kind === 'user' && !injected) {
injected = true
agent.inject(createUserMessage({ content: [{ type: 'text', text: 'context' }], source: { kind: 'plugin', plugin: 'test' } }))
agent.inject(createUserMessage({ content: [{ type: 'text', text: 'context' }], source: { kind: 'test' } }))
}
})
@@ -208,7 +215,7 @@ describe('ACP prompt lifecycle', () => {
})
agent.followup(createUserMessage({
content: [{ type: 'text', text: 'autonomous work' }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
}))
await started
@@ -230,7 +237,7 @@ describe('ACP prompt lifecycle', () => {
kind: 'enter',
messages: [createUserMessage({
content: [{ type: 'text', text: 'rewritten prompt' }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
})],
}))
const sessionId = await newSession(harness)
@@ -332,7 +339,7 @@ describe('ACP prompt lifecycle', () => {
const agent = harness.ctx.agents.get(SessionId(sessionId))!
agent.followup(createUserMessage({
content: [{ type: 'text', text: 'unrelated work' }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
}))
await vi.waitFor(() => { expect(harness!.adapter.requests).toHaveLength(1) })
@@ -375,7 +382,7 @@ describe('ACP prompt lifecycle', () => {
agent.followup(createUserMessage({
content: [{ type: 'text', text: 'unrelated work' }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
}))
await agent.whenIdle()
releaseValidation.resolve(undefined)
@@ -465,7 +472,7 @@ describe('ACP prompt lifecycle', () => {
const agent = harness.ctx.agents.get(SessionId(sessionId))!
agent.followup(createUserMessage({
content: [{ type: 'text', text: 'autonomous work' }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
}))
await vi.waitFor(() => {
expect(agent.session.snapshotEvents().some(event => event.type === 'turn/start')).toBe(true)
@@ -5,6 +5,7 @@ import type { Agent, Inbox, ModelSelectionRef } from '@deepseek-ai/dsh-agent'
import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import { createAssistantMessage, createUserMessage, MessageId } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import SessionStore, {
SESSION_FORMAT_VERSION, Session, SessionId, SessionLogOffset, SessionSeq,
} from '@deepseek-ai/dsh-session'
@@ -17,6 +18,12 @@ import { SessionCommandController } from '../src/commands.ts'
import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit'
import { installSessionReadTestServices, testSessionPersistence } from './test-remote.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'test': { kind: 'test' } & ContextFormed
}
}
async function commandHarness(
childMode?: 'continuable' | 'seeded-continuable' | 'seed-only' | 'one-shot' | 'unknown' | 'corrupt',
): Promise<{
@@ -229,7 +236,7 @@ describe('Session queue commands', () => {
content: [{ type: 'text', text: 'queued' }], source: { kind: 'user' },
})
const context = createUserMessage({
content: [{ type: 'text', text: 'context' }], source: { kind: 'plugin', plugin: 'test' },
content: [{ type: 'text', text: 'context' }], source: { kind: 'test' },
})
inbox.append('next-turn', queued)
inbox.append('next-step', context)
@@ -1,6 +1,7 @@
import { Context } from '@deepseek-ai/cordis'
import type { Agent, Inbox, InboxState } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import { afterEach, describe, expect, it } from 'vitest'
import { SessionControlController } from '../src/control.ts'
@@ -10,6 +11,12 @@ import {
mountAgentLoopTestHarness,
} from '@deepseek-ai/dsh-agent-loop-testkit'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'fixture': { kind: 'fixture' } & ContextFormed
}
}
const ownedContexts = new Set<Context>()
afterEach(async () => {
await Promise.all([...ownedContexts].map(ctx => ctx.fiber.dispose()))
@@ -33,7 +40,7 @@ async function harness(): Promise<{
function message(text: string, source: 'user' | 'plugin' = 'user') {
return createUserMessage({
content: [{ type: 'text', text }],
source: source === 'user' ? { kind: 'user' } : { kind: 'plugin', plugin: 'fixture' },
source: source === 'user' ? { kind: 'user' } : { kind: 'fixture' },
})
}
@@ -12,6 +12,19 @@ import type {
SessionWireEvent,
} from '../src/types.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'compact-checkpoint': { kind: 'compact-checkpoint'; compactionId: string }
}
}
type CheckpointSource = { kind: 'compact-checkpoint'; compactionId: string }
/** Build a typed checkpoint source for the history event script. */
function checkpointSource(): CheckpointSource {
return { kind: 'compact-checkpoint', compactionId: 'event-script-compaction' }
}
/** One text content block (local helper). */
const text = (t: string): ContentBlock[] => [{ type: 'text', text: t }]
@@ -149,7 +162,7 @@ export const ev = {
sourceEventSeqs: [summarySeq, start, end],
data: createUserMessage({
content: text('<context_checkpoint>model only</context_checkpoint>'),
source: { kind: 'plugin', plugin: 'compact' },
source: checkpointSource(),
}),
}),
}
@@ -5,11 +5,18 @@ import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent, type AssistantStreamFrame } from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'
import { LlmAttemptId, ToolCallId, createMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { MessageSource } from '@deepseek-ai/dsh-llm'
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import { SessionHistoryController } from '@deepseek-ai/dsh-api-session-controller/src/history.ts'
import type { SessionFollowFrame, SessionPage, SessionWireEvent } from '@deepseek-ai/dsh-api-session-controller/types'
import { createSessionTestRemote, installSessionReadTestServices } from './test-remote.ts'
type CheckpointSource = Extract<MessageSource, { readonly kind: 'compact-checkpoint' }>
/** Build a typed checkpoint source for a journal fixture without owning compaction. */
function checkpointSource(compactionId: string): CheckpointSource {
return { kind: 'compact-checkpoint', compactionId: compactionId as CheckpointSource['compactionId'] }
}
/** Append a production-shaped human prompt to the session surface. */
function appendUserText(session: Session, text: string): SessionEvent {
return session.append('user/message', createUserMessage({
@@ -768,7 +775,7 @@ describe('Session history raw journal', () => {
})
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: '<context_checkpoint>summary</context_checkpoint>' }],
source: { kind: 'plugin', plugin: 'compact' },
source: checkpointSource('journal-compaction'),
}), {
surfaceOp: { op: 'replace', startSeq: shadowedStart, endSeq: shadowedEnd },
sourceEventSeqs: [...shadowed, summary.seq],
@@ -298,7 +298,7 @@ describe('Web session model selection', () => {
}))).selected).toEqual({ provider: 'text-only', model: 'plain' })
agent.session.append('user/message', {
id: 'summary', role: 'user', source: { kind: 'plugin', plugin: 'compact' },
id: 'summary', role: 'user', source: { kind: 'compact-checkpoint', compactionId: 'model-selection-compaction' },
content: [{ type: 'text', text: 'image summarized' }],
} as never, {
surfaceOp: { op: 'replace', startSeq: imageEvent.seq, endSeq: imageEvent.seq },
@@ -20,7 +20,7 @@ declare module '../contract/chat-nodes.ts' {
type CommandId = CommandNode['commandId']
const COMPACT_PLUGIN: CompactionCheckpointSource['plugin'] = 'compact'
const COMPACT_KIND: CompactionCheckpointSource['kind'] = 'compact-checkpoint'
interface CommandState {
readonly command: CommandNode
@@ -82,11 +82,10 @@ function compactSource(event: Parameters<ConversationNodeDefinition['match']>[0]
if (event.type !== 'user/message' || !isReplacementSurfaceEvent(event)) return undefined
const source = event.data.source as unknown as {
kind?: unknown
plugin?: unknown
compactionId?: unknown
sourceCommandId?: CommandId
}
if (source.kind !== 'plugin' || source.plugin !== COMPACT_PLUGIN || typeof source.compactionId !== 'string') return undefined
if (source.kind !== COMPACT_KIND || typeof source.compactionId !== 'string') return undefined
return {
compactionId: source.compactionId,
...source.sourceCommandId === undefined ? {} : { sourceCommandId: source.sourceCommandId },
@@ -66,8 +66,6 @@ export function contextProducer(source: unknown): ContextProducerView {
return { role: 'recall', label: joined(collect(record, 'references', 'label')) ?? kind }
case 'agent-instructions':
return { role: 'inject', label: joined(collect(record, 'changes', 'path')) ?? kind }
case 'plugin':
return { role: 'inject', label: readString(record, 'plugin') ?? kind }
case 'skill-invocation':
return { role: 'inject', label: readString(record, 'name') ?? kind }
default:
@@ -36,8 +36,8 @@ declare module '../contract/chat-nodes.ts' {
function isCompactionCheckpoint(event: Parameters<ConversationNodeDefinition['match']>[0]): boolean {
if (event.type !== 'user/message' || !isReplacementSurfaceEvent(event)) return false
const source = event.data.source
return source.kind === 'plugin' && source.plugin === 'compact'
const source = event.data.source as { kind?: unknown }
return source.kind === 'compact-checkpoint'
}
/** User, steering, and injected-context message classification Definition. */
@@ -331,7 +331,7 @@ describe('MessageItem arms', () => {
kind: 'context',
seq: 3,
content: [{ type: 'text', text: 'line one\n\nline two' }],
source: { kind: 'plugin', plugin: 'fixture', empty: {}, list: [] },
source: { kind: 'fixture', empty: {}, list: [] },
producer: { role: 'inject', label: 'fixture' },
form: null,
} as never}
@@ -351,7 +351,7 @@ describe('MessageItem arms', () => {
expect(ctxView.container.querySelector('[data-context-text]')?.textContent)
.toBe('line one\n\nline two')
const fields = [...ctxView.container.querySelectorAll('[data-context-fields] dt')].map(node => node.textContent)
expect(fields).toEqual(['plugin', 'empty', 'list'])
expect(fields).toEqual(['empty', 'list'])
fireEvent.keyDown(disclosure, { key: ' ' })
expect(disclosure.getAttribute('aria-expanded')).toBe('false')
@@ -650,7 +650,7 @@ describe('MessageItem arms', () => {
const view = render(
<MessageItem t={t} node={{
kind: 'context', seq: 3, content: [{ type: 'text', text: 'x' }],
source: { kind: 'plugin', plugin: 'later', form: 'a-later-form' },
source: { kind: 'later', form: 'a-later-form' },
producer: { role: 'inject', label: 'later' },
form: null,
} as never}
@@ -658,7 +658,7 @@ describe('MessageItem arms', () => {
)
fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*later$/ }))
const fields = [...view.container.querySelectorAll('[data-context-fields] dt')].map(node => node.textContent)
expect(fields).toEqual(['plugin', 'form'])
expect(fields).toEqual(['form'])
})
it('the snapshot form attributes each part to the subsystem that produced it', () => {
@@ -668,8 +668,7 @@ describe('MessageItem arms', () => {
seq: 3,
content: [{ type: 'text', text: 'Current runtime context.\n\nsandbox\n\nworkspace' }],
source: {
kind: 'plugin',
plugin: '@deepseek-ai/dsh-system-prompt',
kind: 'runtime-context',
form: 'snapshot',
sections: [{ name: 'sandbox:policy', text: 'workspace-write' }, { name: 'workspace', text: '/repo' }],
},
@@ -690,7 +689,7 @@ describe('MessageItem arms', () => {
kind: 'context',
seq: 3,
content: [{ type: 'text', text: 'background job bash-1 finished.' }],
source: { kind: 'plugin', plugin: 'tool-jobs', form: 'notice', summary: 'bash pnpm test [status: completed]' },
source: { kind: 'tool-jobs', form: 'notice', summary: 'bash pnpm test [status: completed]' },
producer: { role: 'inject', label: 'tool-jobs' },
form: 'notice',
} as never}
@@ -705,7 +704,7 @@ describe('MessageItem arms', () => {
const view = render(
<MessageItem t={t} node={{
kind: 'context', seq: 3, content: [{ type: 'text', text: 'notice prose' }],
source: { kind: 'plugin', plugin: 'tool-jobs', form: 'notice' },
source: { kind: 'tool-jobs', form: 'notice' },
producer: { role: 'inject', label: 'tool-jobs' },
form: 'notice',
} as never}
@@ -180,7 +180,7 @@ function systemMessage(text: string) {
id: `system-${text}`,
role: 'system',
content: text === '' ? [] : [{ type: 'text', text }],
source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' },
source: { kind: 'system-prompt' },
}
}
@@ -320,7 +320,7 @@ describe('built-in conversation node Definitions', () => {
...textMessage('context-1', 'workspace context'),
turn: 1,
step: 1,
source: { kind: 'plugin', plugin: 'context' },
source: { kind: 'context' },
}, { surfaceOp: 'append' }),
at(4, 'assistant/live-chunk', {
turn: 1, step: 1, chunk: { type: 'reasoning-delta', index: 0, text: 'thinking' },
@@ -453,7 +453,7 @@ describe('built-in conversation node Definitions', () => {
at(1, 'turn/start', { turn: 1 }),
at(2, 'user/message', {
...textMessage('context-1', 'runtime context'),
source: { kind: 'plugin', plugin: 'context' },
source: { kind: 'context' },
}, { surfaceOp: 'append' }),
at(3, 'user/message', textMessage('user-1', 'question'), { surfaceOp: 'append' }),
at(4, 'step/start', { turn: 1, step: 1 }),
@@ -619,7 +619,7 @@ describe('built-in conversation node Definitions', () => {
at(1, 'turn/start', { turn: 1 }),
at(2, 'user/message', {
...textMessage('context-1', 'runtime context'),
source: { kind: 'plugin', plugin: 'context' },
source: { kind: 'context' },
}, { surfaceOp: 'append' }),
at(3, 'step/start', { turn: 1, step: 1 }),
at(4, 'assistant/live-chunk', {
@@ -1609,7 +1609,7 @@ describe('built-in conversation node Definitions', () => {
at(4, 'user/message', textMessage('direct-user', 'prompt'), { surfaceOp: 'append' }),
at(5, 'user/message', {
...textMessage('runtime-context', 'runtime facts'),
source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt', form: 'snapshot' },
source: { kind: 'runtime-context', form: 'snapshot' },
}, { surfaceOp: 'append' }),
at(6, 'request/header', {
reason: 'initial',
@@ -1644,7 +1644,7 @@ describe('built-in conversation node Definitions', () => {
at(11, 'step/start', { turn: 1, step: 3 }),
at(12, 'user/message', {
turn: 1, step: 3, id: 'summary', role: 'user',
content: [{ type: 'text', text: 'summary' }], source: { kind: 'plugin', plugin: 'compaction' },
content: [{ type: 'text', text: 'summary' }], source: { kind: 'compact-checkpoint', compactionId: 'compaction-1' },
}, { surfaceOp: { op: 'replace', startSeq: 5, endSeq: 9 }, sourceEventSeqs: [5, 8, 9] }),
at(13, 'request/header', {
reason: 'series', header: { config: { provider: 'test', model: 'test' }, tools: [] },
@@ -1788,7 +1788,7 @@ describe('built-in conversation node Definitions', () => {
at(4, 'user/message', textMessage('direct-user', 'prompt'), { surfaceOp: 'append' }),
at(5, 'user/message', {
...textMessage('runtime-context', 'runtime facts'),
source: { kind: 'plugin', plugin: 'context' },
source: { kind: 'context' },
}, { surfaceOp: 'append' }),
at(6, 'request/header', {
reason: 'initial',
@@ -1907,7 +1907,7 @@ describe('built-in conversation node Definitions', () => {
}),
at(6, 'user/message', {
...textMessage('compacted', 'summary'),
source: { kind: 'plugin', plugin: 'compact' },
source: { kind: 'compact-checkpoint', compactionId: 'chat-compaction-1' },
}, { surfaceOp: { op: 'replace', startSeq: 4, endSeq: 4 } }),
at(7, 'request/header', {
reason: 'series',
@@ -2088,7 +2088,7 @@ describe('built-in conversation node Definitions', () => {
at(2, 'step/start', { turn: 1, step: 1 }),
at(3, 'user/message', {
...textMessage('replacement-user', 'model-only context'),
source: { kind: 'plugin', plugin: 'foreign' },
source: { kind: 'foreign' },
}, { surfaceOp: { op: 'replace', startSeq: 1, endSeq: 1 } }),
at(4, 'assistant/message', {
turn: 1,
@@ -2176,8 +2176,7 @@ describe('built-in conversation node Definitions', () => {
at(13, 'user/message', {
...textMessage('manual-checkpoint', 'checkpoint'),
source: {
kind: 'plugin',
plugin: 'compact',
kind: 'compact-checkpoint',
compactionId: 'manual-1',
sourceCommandId: 'command-1',
},
@@ -2201,7 +2200,7 @@ describe('built-in conversation node Definitions', () => {
}),
at(22, 'user/message', {
...textMessage('automatic-checkpoint', 'checkpoint'),
source: { kind: 'plugin', plugin: 'compact', compactionId: 'automatic-1' },
source: { kind: 'compact-checkpoint', compactionId: 'automatic-1' },
}, { surfaceOp: { op: 'replace', startSeq: 3, endSeq: 4 } }),
at(23, 'compaction/end', { compactionId: 'automatic-1', turn: null }),
])
@@ -2220,7 +2219,7 @@ describe('built-in conversation node Definitions', () => {
const value = assembler([
at(13, 'user/message', {
...textMessage('checkpoint', 'checkpoint'),
source: { kind: 'plugin', plugin: 'compact', compactionId: 'compact-1' },
source: { kind: 'compact-checkpoint', compactionId: 'compact-1' },
}, { surfaceOp: { op: 'replace', startSeq: 1, endSeq: 8 } }),
], true)
const before = node(snapshot(value), 'compaction')
@@ -2261,7 +2260,7 @@ describe('built-in conversation node Definitions', () => {
}),
at(11, 'user/message', {
...textMessage('checkpoint-windowed', 'checkpoint'),
source: { kind: 'plugin', plugin: 'compact', compactionId: 'compact-windowed' },
source: { kind: 'compact-checkpoint', compactionId: 'compact-windowed' },
}, { surfaceOp: { op: 'replace', startSeq: 1, endSeq: 3 } }),
], true)
@@ -2285,7 +2284,7 @@ describe('built-in conversation node Definitions', () => {
}),
at(22, 'user/message', {
...textMessage('legacy-checkpoint', 'checkpoint'),
source: { kind: 'plugin', plugin: 'compact' },
source: { kind: 'compact-checkpoint' },
}, { surfaceOp: { op: 'replace', startSeq: 1, endSeq: 3 } }),
at(23, 'compaction/end', { turn: null }),
], true)
@@ -2491,8 +2490,7 @@ describe('built-in conversation node Definitions', () => {
at(21, 'user/message', {
...textMessage('manual-checkpoint', 'checkpoint'),
source: {
kind: 'plugin',
plugin: 'compact',
kind: 'compact-checkpoint',
compactionId: 'manual-1',
sourceCommandId: 'command-1',
},
@@ -887,12 +887,6 @@ function messageSourceLabel(source: unknown, t: TrajectoryTranslate): string {
const properties = source as Record<string, unknown>
const kind = properties.kind
if (kind === 'user') return t('source.user')
if (kind === 'plugin') {
const plugin = properties.plugin
return typeof plugin === 'string' && plugin !== ''
? t('source.pluginNamed', { plugin })
: t('source.plugin')
}
if (kind === 'goal') {
const round = properties.round
return typeof round === 'number' && round > 0
@@ -21,7 +21,7 @@ function checkpointId(
readonly plugin?: unknown
readonly compactionId?: unknown
}
return source.kind === 'plugin' && source.plugin === 'compact'
return source.kind === 'compact-checkpoint'
&& typeof source.compactionId === 'string' && source.compactionId !== ''
? source.compactionId
: undefined
@@ -66,8 +66,6 @@ export function contextProducer(source: unknown): ContextProducerView {
return { role: 'recall', label: joined(collect(record, 'references', 'label')) ?? kind }
case 'agent-instructions':
return { role: 'inject', label: joined(collect(record, 'changes', 'path')) ?? kind }
case 'plugin':
return { role: 'inject', label: readString(record, 'plugin') ?? kind }
case 'skill-invocation':
return { role: 'inject', label: readString(record, 'name') ?? kind }
default:
@@ -167,7 +167,7 @@ function systemMessage(text: string) {
id: `system-${text}`,
role: 'system',
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' },
source: { kind: 'system-prompt' },
}
}
@@ -510,7 +510,7 @@ describe('Trajectory conversation Definitions', () => {
id: 'checkpoint',
role: 'user',
content: [{ type: 'text', text: 'summary checkpoint' }],
source: { kind: 'plugin', plugin: 'compact', compactionId: 'complete' },
source: { kind: 'compact-checkpoint', compactionId: 'complete' },
}),
at(4, 'compaction/end', { compactionId: 'complete', turn: null }),
at(5, 'compaction/start', { compactionId: 'orphan', turn: null }),
@@ -803,7 +803,7 @@ describe('Trajectory conversation Definitions', () => {
at(11, 'step/start', { turn: 1, step: 3 }),
at(12, 'user/message', {
turn: 1, step: 3, id: 'summary', role: 'user',
content: [{ type: 'text', text: 'summary' }], source: { kind: 'plugin', plugin: 'compaction' },
content: [{ type: 'text', text: 'summary' }], source: { kind: 'compact-checkpoint', compactionId: 'compaction-1' },
}, { surfaceOp: { op: 'replace', startSeq: 5, endSeq: 9 }, sourceEventSeqs: [5, 8, 9] }),
at(13, 'request/header', {
reason: 'series', header: { config: { provider: 'test', model: 'test' }, tools: [] },
@@ -856,7 +856,7 @@ describe('Trajectory conversation Definitions', () => {
at(6, 'system/message', { turn: 1, step: 1, message: systemMessage('B') }, { surfaceOp: 'append' }),
at(7, 'request/header', { reason: 'resume', header: { config: { provider: 'test', model: 'test' } } }),
at(8, 'user/message', {
...systemMessage('summary'), role: 'user', source: { kind: 'plugin', plugin: 'compaction' },
...systemMessage('summary'), role: 'user', source: { kind: 'compact-checkpoint', compactionId: 'compaction-1' },
}, { surfaceOp: { op: 'replace', startSeq: 2, endSeq: 6 } }),
at(9, 'assistant/message', { turn: 1, step: 1, message: assistantMessage('reply', 'reply') }),
])
@@ -15,7 +15,7 @@ describe('Trajectory event projection', () => {
.toEqual({ role: 'inject', label: 'AGENTS.md' })
expect(contextProducer({ kind: 'agent-instructions', changes: 'bad' }).label)
.toBe('agent-instructions')
expect(contextProducer({ kind: 'plugin', plugin: 'p' }).label).toBe('p')
expect(contextProducer({ kind: 'p' }).label).toBe('p')
expect(contextProducer({ kind: 'plugin', plugin: 1 }).label).toBe('plugin')
expect(contextProducer({ kind: 'skill-invocation', name: 's' }).label).toBe('s')
expect(contextProducer({ kind: 'future' }).label).toBe('future')
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/compaction/compaction-basic/README.md
README.md: 68d5a321e04a90bfb726392d1fb179612f6e0626
README.zh.md: f941cac74053a680cad859b68c7836e181065bc9
README.md: cb31438816088070fe996e7f3d38b8480404aa84
README.zh.md: a038fecc9880160a4fc23377cd5fec6e931a7b01
@@ -115,7 +115,7 @@ Pressure policy resolves capacity from the adapter that owns the durable route.
### Summarization mechanics
A direct `ctx.llm.stream()` call uses the configured provider/model pair and cap, falling back to the latest logged request target and then the `AgentOptions` pair, without running the loop-only `agent/request` extension point. The call replays the derived `system/message` at surface node 0 as the leading entry of `messages`, followed by the shadowed-region messages (including a shadowed in-history `system/message` in its surface position), and carries the header's tools verbatim — including image references, which the selected adapter must resolve or explicitly reject — and appends the compaction instruction as the final user message, so it reuses the provider's warm prefix cache instead of invalidating it. An empty-content system head contributes no message but remains outside the compacted range. The call sets `GenerateOptions.purpose` to `compaction`; only returned text enters the checkpoint, excluding reasoning and tool calls. Image output fails with `UNSUPPORTED_CONTENT` rather than disappearing. The replacement user message frames the summary with `<compacted-summary>` tags; the raw summary remains on the `compaction/summary` event.
A direct `ctx.llm.stream()` call uses the configured provider/model pair and cap, falling back to the latest logged request target and then the `AgentOptions` pair, without running the loop-only `agent/request` extension point. The call replays the derived `system/message` at surface node 0 as the leading entry of `messages`, followed by the shadowed-region messages (including a shadowed in-history `system/message` in its surface position), and carries the header's tools verbatim — including image references, which the selected adapter must resolve or explicitly reject — and appends the compaction instruction as the final user message, so it reuses the provider's warm prefix cache instead of invalidating it. An empty-content system head contributes no message but remains outside the compacted range. The final instruction is a frozen `RequestUserInput` without durable identity or source; the replayed history and persisted checkpoint remain durable messages. The call sets `GenerateOptions.purpose` to `compaction`; only returned text enters the checkpoint, excluding reasoning and tool calls. Image output fails with `UNSUPPORTED_CONTENT` rather than disappearing. The replacement user message frames the summary with `<compacted-summary>` tags; the raw summary remains on the `compaction/summary` event.
### The region transaction
@@ -115,7 +115,7 @@ kind: "package-reference"
### 摘要机制
直接 `ctx.llm.stream()` 调用使用已配置的提供方/模型对与上限,回退到最新已记录请求目标,然后再回退到 `AgentOptions` 对,而不运行仅用于 agent loop 的 `agent/request` 扩展点。该调用将 surface 节点 0 处派生的 `system/message` 作为 `messages` 的首项回放,后接已遮蔽区域消息(包括位于其 surface 位置的被遮蔽历史内 `system/message`),并逐字携带 header 的工具——包括所选适配器必须解析或明确拒绝的图片引用——并将压缩指令作为最后一条 user 消息追加,从而复用提供方的热前缀 cache,而非使它失效。空内容系统头节点不贡献消息,但仍处于压缩范围之外。调用将 `GenerateOptions.purpose` 设为 `compaction`;只有返回文本进入检查点,推理与工具调用都会被排除。图片输出会以 `UNSUPPORTED_CONTENT` 失败,而不是消失。替换 user 消息用 `<compacted-summary>` 标签框定摘要;原始摘要保留在 `compaction/summary` 事件上。
直接 `ctx.llm.stream()` 调用使用已配置的提供方/模型对与上限,回退到最新已记录请求目标,然后再回退到 `AgentOptions` 对,而不运行仅用于 agent loop 的 `agent/request` 扩展点。该调用将 surface 节点 0 处派生的 `system/message` 作为 `messages` 的首项回放,后接已遮蔽区域消息(包括位于其 surface 位置的被遮蔽历史内 `system/message`),并逐字携带 header 的工具——包括所选适配器必须解析或明确拒绝的图片引用——并将压缩指令作为最后一条 user 消息追加,从而复用提供方的热前缀 cache,而非使它失效。空内容系统头节点不贡献消息,但仍处于压缩范围之外。最终指令是冻结的 `RequestUserInput`,不含持久身份或来源;回放历史和持久化检查点仍然使用持久消息。调用将 `GenerateOptions.purpose` 设为 `compaction`;只有返回文本进入检查点,推理与工具调用都会被排除。图片输出会以 `UNSUPPORTED_CONTENT` 失败,而不是消失。替换 user 消息用 `<compacted-summary>` 标签框定摘要;原始摘要保留在 `compaction/summary` 事件上。
### 区域事务
@@ -23,7 +23,6 @@ import { SessionSeq, type Session, type SessionEvent } from '@deepseek-ai/dsh-se
import type { Agent } from '@deepseek-ai/dsh-agent'
import { frameSummary } from './summarizer.ts'
import type { SummarizationInput, SummaryResult } from './summarizer.ts'
interface RegionDependencies {
readonly meter: TokenMeter
summarize(input: SummarizationInput, agent: Agent, signal?: AbortSignal): Promise<SummaryResult>
@@ -5,9 +5,10 @@
*/
import type { Context } from '@deepseek-ai/cordis'
import { contentHasImage, createUserMessage, BlockAssembler, LlmError } from '@deepseek-ai/dsh-llm'
import { contentHasImage, BlockAssembler, LlmError } from '@deepseek-ai/dsh-llm'
import { deepFreeze } from '@deepseek-ai/dsh-util-values'
import type {
ContentBlock, FinishReason, GenerateOptions, Message, TokenUsage, ToolSchema,
ContentBlock, FinishReason, GenerateOptions, Message, RequestMessage, TokenUsage, ToolSchema,
} from '@deepseek-ai/dsh-llm'
import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -141,11 +142,11 @@ export async function summarizeWithLlm(
}
const assembler = new BlockAssembler()
const messages: Message[] = [
const messages: RequestMessage[] = [
...input.messages,
createUserMessage({
deepFreeze({
role: 'user',
content: [{ type: 'text', text: COMPACTION_INSTRUCTION }],
source: { kind: 'plugin', plugin: 'dsh-compaction-basic' },
}),
]
const options: GenerateOptions = {
@@ -14,6 +14,7 @@ import {
} from '@deepseek-ai/dsh-compaction-basic/src/config.ts'
import type { CompactionResult } from '@deepseek-ai/dsh-compaction'
import LlmRuntime, { createUserMessage, ToolCallId, CONTEXT_WINDOW_EXCEEDED_CODE, createSystemMessage, createToolResultMessage, LlmAdapter , createMessage } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import type {
ContentBlock,
GenerateOptions,
@@ -29,6 +30,12 @@ import TokenMeter from '@deepseek-ai/dsh-token-meter'
import { agentEvents, type Agent, type RequestErrorAction } from '@deepseek-ai/dsh-agent'
import ToolResultPruner from '@deepseek-ai/dsh-compaction-tool-result-pruner'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'test': { kind: 'test' } & ContextFormed
}
}
const SIGNAL = new AbortController().signal
const MODEL = 'test-model'
@@ -100,12 +107,10 @@ function summarizedText(input: SummarizationInput): string {
function promptInput(text: string): SummarizationInput {
return { messages: [createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
})] }
}
const SYSTEM_PROMPT_PLUGIN = '@deepseek-ai/dsh-system-prompt'
/**
* Closed two-message turns followed by one open turn for durable compaction events.
* A `system` text puts a `system/message` at surface node 0 before turn 1's user message.
@@ -118,7 +123,7 @@ function conversation(turns = 4, text = 'fixture '.repeat(40).trim(), system?: s
session.append('system/message', {
turn,
step: 1,
message: createSystemMessage(system, SYSTEM_PROMPT_PLUGIN),
message: createSystemMessage(system),
}, { surfaceOp: 'append' })
}
session.append('user/message', createUserMessage({
@@ -1132,7 +1137,7 @@ describe('compaction region transaction', () => {
compact.mutateDuringSummary = () => {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'concurrent surface mutation' }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
}), { surfaceOp: 'append' })
}
const nodes = session.surface.nodes
@@ -1272,7 +1277,12 @@ describe('default one-shot summarizer', () => {
expect(adapter.lastOptions).not.toHaveProperty('system')
expect(adapter.lastOptions?.tools).toEqual(tools)
expect(adapter.lastOptions?.messages.slice(0, -1)).toEqual(prefix)
expect(adapter.lastOptions?.messages.at(-1)).toMatchObject({ role: 'user' })
const instruction = adapter.lastOptions?.messages.at(-1)
expect(instruction).toMatchObject({ role: 'user' })
expect(instruction).not.toHaveProperty('id')
expect(instruction).not.toHaveProperty('source')
expect(Object.isFrozen(instruction)).toBe(true)
expect(Object.isFrozen(instruction?.content[0])).toBe(true)
expect(result.shadowedSeqs).toEqual(nodes.slice(start, start + 2))
if (system !== undefined) expect(session.surface.nodes[0]).toBe(nodes[0])
})
@@ -1343,9 +1353,9 @@ describe('default one-shot summarizer', () => {
},
},
],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
})
const system = createSystemMessage('REPLAYED SYSTEM', SYSTEM_PROMPT_PLUGIN)
const system = createSystemMessage('REPLAYED SYSTEM')
await compact.runSummarize({
tools,
messages: [system, prefix],
@@ -1383,10 +1393,10 @@ describe('default one-shot summarizer', () => {
ctx.llm.registerAdapter(['policy-summary'], policyAdapter)
const prefix: Message = createUserMessage({
content: [{ type: 'text', text: 'warm prefix' }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
})
const system = createSystemMessage('WARM SYSTEM', SYSTEM_PROMPT_PLUGIN)
const system = createSystemMessage('WARM SYSTEM')
const output = await compact.runSummarize({
messages: [system, prefix],
}, agent(conversation(1), 'fallback'))
@@ -2091,7 +2101,7 @@ describe('route-priced image pressure', () => {
}]
const framed = ctx.tokenMeter.estimateMessage(createUserMessage({
content: frameSummary(compact.summary),
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
}))
expect(framed).toBeGreaterThan(imageNode.heuristicTokens)
expect(framed).toBeLessThan(imageNode.tokens)
@@ -336,7 +336,7 @@ describe('token pressure after loop-admitted system prompts', () => {
agent.session.append('system/message', {
turn,
step,
message: createSystemMessage('retry guidance', '@deepseek-ai/dsh-system-prompt'),
message: createSystemMessage('retry guidance'),
}, { surfaceOp: { op: 'replace', startSeq: node, endSeq: node }, sourceEventSeqs: [node] })
return { kind: 'retry' }
})
@@ -21,12 +21,14 @@ import type {
ContentBlock,
LlmResolvedModelInfo,
Message,
RequestMessage,
StreamChunk,
TokenUsage,
} from '@deepseek-ai/dsh-llm'
import SessionStore, { buildForkSeed, Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type {
@@ -34,6 +36,14 @@ import type {
SummaryResult,
} from '@deepseek-ai/dsh-compaction-basic/src/summarizer.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'listener': { kind: 'listener' } & ContextFormed
'rival': { kind: 'rival' } & ContextFormed
'test': { kind: 'test' } & ContextFormed
}
}
const MODEL = 'mock'
const SIGNAL = new AbortController().signal
const PROMPT = 'older conversation history '.repeat(60)
@@ -69,7 +79,7 @@ class GatedCompactionEngine extends BasicCompactionEngine {
/** One text answer per request, with a context window large enough to avoid pressure. */
class TextAdapter extends LlmAdapter {
readonly requests: Message[][] = []
readonly requests: RequestMessage[][] = []
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
return Promise.resolve({
@@ -80,7 +90,7 @@ class TextAdapter extends LlmAdapter {
})
}
override async * stream(options: { messages: readonly Message[] }): AsyncIterable<StreamChunk> {
override async * stream(options: { messages: readonly RequestMessage[] }): AsyncIterable<StreamChunk> {
this.requests.push([...options.messages])
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'block-end', index: 0, block: { type: 'text', text: 'answer' } }
@@ -312,7 +322,7 @@ describe('compactNow through the real loop', () => {
compact.duringSummary = () => {
agent.inject(createUserMessage({
content: [{ type: 'text', text: 'INJECTED CONTEXT' }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
}))
}
@@ -321,7 +331,7 @@ describe('compactNow through the real loop', () => {
expect(result).not.toBeNull()
const start = agent.session.snapshotEvents().findLast(event => event.type === 'compaction/start')
const injected = agent.inbox.nextStep.find(message =>
message.source.kind === 'plugin' && message.source.plugin === 'test')
message.source.kind === 'test')
const end = agent.session.snapshotEvents().findLast(event => event.type === 'compaction/end')
expect(start).toBeDefined()
expect(injected).toBeDefined()
@@ -350,7 +360,7 @@ describe('compactNow through the real loop', () => {
attempts.push(event.type)
agent.inject(createUserMessage({
content: [{ type: 'text', text: `from ${event.type}` }],
source: { kind: 'plugin', plugin: 'listener' },
source: { kind: 'listener' },
}))
})
@@ -361,7 +371,7 @@ describe('compactNow through the real loop', () => {
expect(agent.session.deriveMessages()[0]?.role).toBe('system')
expect(derivedText(agent.session)[1]).toContain('checkpoint')
expect(agent.session.snapshotEvents().filter(event => event.type === 'user/message'
&& event.data.source.kind === 'plugin' && event.data.source.plugin === 'listener')).toHaveLength(0)
&& event.data.source.kind === 'listener')).toHaveLength(0)
const types = compactEvents(agent.session).map(event => event.type)
expect(types).toEqual(['compaction/start', 'compaction/summary', 'compaction/end'])
})
@@ -543,7 +553,7 @@ describe('compactNow transaction and failure classification', () => {
const [head] = session.surface.nodes
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'competing replacement' }],
source: { kind: 'plugin', plugin: 'rival' },
source: { kind: 'rival' },
}), {
surfaceOp: { op: 'replace', startSeq: head!, endSeq: head! },
sourceEventSeqs: [head!],
@@ -564,7 +574,7 @@ describe('compactNow transaction and failure classification', () => {
const middle = session.surface.nodes[1]
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'rewritten middle node' }],
source: { kind: 'plugin', plugin: 'rival' },
source: { kind: 'rival' },
}), {
surfaceOp: { op: 'replace', startSeq: middle!, endSeq: middle! },
sourceEventSeqs: [middle!],
@@ -595,7 +605,7 @@ describe('compactNow transaction and failure classification', () => {
queueMicrotask(() => {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'late competing replacement' }],
source: { kind: 'plugin', plugin: 'rival' },
source: { kind: 'rival' },
}), {
surfaceOp: { op: 'replace', startSeq: head, endSeq: head },
sourceEventSeqs: [head],
@@ -16,7 +16,7 @@ import type { MessageSource } from '@deepseek-ai/dsh-llm/message'
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
import type { CompactionId } from './brand.ts'
const COMPACT_CHECKPOINT_MARKER = Object.freeze({ kind: 'plugin', plugin: 'compact' } as const)
const COMPACT_CHECKPOINT_MARKER = Object.freeze({ kind: 'compact-checkpoint' } as const)
/** Message source carried by a concrete compaction checkpoint. */
export type CompactionCheckpointSource = typeof COMPACT_CHECKPOINT_MARKER & {
@@ -46,6 +46,6 @@ export function compactCheckpointSource(
* @param source - source restored from a surface user message.
* @returns whether the source carries the backend-independent checkpoint marker.
*/
export function isCompactCheckpointSource(source: MessageSource): boolean {
return source.kind === 'plugin' && source.plugin === COMPACT_CHECKPOINT_MARKER.plugin
export function isCompactCheckpointSource(source: MessageSource): source is CompactionCheckpointSource {
return source.kind === 'compact-checkpoint'
}
@@ -11,6 +11,7 @@ import { Context, Service } from '@deepseek-ai/cordis'
import type { Session, SessionSeq } from '@deepseek-ai/dsh-session'
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
import type { CompactionResult } from './types.ts'
import type { CompactionCheckpointSource } from './checkpoint.ts'
export type { CompactionResult } from './types.ts'
export { CompactionId } from './brand.ts'
@@ -21,6 +22,12 @@ export { toolPairingBalancedAfter, toolPairingBalancedBefore } from './tool-pair
export { compactCheckpointSource, isCompactCheckpointSource } from './checkpoint.ts'
export type { CompactionCheckpointSource } from './checkpoint.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'compact-checkpoint': CompactionCheckpointSource
}
}
/** Why automatic policy is asking a backend to consider compaction. */
export type CompactionTrigger = 'pressure' | 'context-overflow'
@@ -97,10 +97,9 @@ function validateSourceCommandId(
/** Validate one replacement checkpoint against its open compaction transaction. */
function validateCheckpoint(
trace: SessionTrace,
event: SessionEvent<'user/message'>,
source: CompactionCheckpointSource,
fail: InvariantFailure,
): void {
const source = event.data.source as typeof event.data.source & Partial<CompactionCheckpointSource>
validateId(source.compactionId, 'compaction checkpoint compactionId', fail)
if (source.sourceCommandId !== undefined) {
validateId(source.sourceCommandId, 'compaction checkpoint sourceCommandId', fail)
@@ -190,7 +189,7 @@ function validateCompactionEvent(
if (event.type === 'user/message'
&& isReplacementSurfaceEvent(event)
&& isCompactCheckpointSource(event.data.source)) {
validateCheckpoint(trace, event, fail)
validateCheckpoint(trace, event.data.source, fail)
return undefined
}
if (event.type !== 'compaction/start' && event.type !== 'compaction/summary' && event.type !== 'compaction/end') {
@@ -1,4 +1,5 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import {
@@ -13,6 +14,12 @@ import type { SessionSeq } from '@deepseek-ai/dsh-session'
import type { CompactionAgentContext } from '@deepseek-ai/dsh-compaction'
import type { ManualCompactAgentContext } from '@deepseek-ai/dsh-compaction'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'other': { kind: 'other' } & ContextFormed
}
}
/**
* A trivial concrete CompactionEngine implementing the abstract contract. The
* Service Definition package owns no algorithm — these tests exercise its contract:
@@ -146,7 +153,7 @@ describe('CompactionEngine seam', () => {
&& isCompactCheckpointSource(event.data.source))
expect(checkpoint?.type === 'user/message' && checkpoint.data.source)
.toEqual(compactCheckpointSource(result.compactionId))
expect(isCompactCheckpointSource({ kind: 'plugin', plugin: 'other' })).toBe(false)
expect(isCompactCheckpointSource({ kind: 'other' })).toBe(false)
expect(isCompactCheckpointSource({ kind: 'user' })).toBe(false)
expect(session.snapshotEvents().filter(e => e.type.startsWith('compaction/')).map(e => e.type))
.toEqual(['compaction/start', 'compaction/summary', 'compaction/end'])
@@ -1,9 +1,16 @@
import { describe, expect, it } from 'vitest'
import { createUserMessage, ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import { CompactionId, compactCheckpointSource, toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction'
import { Session, SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionSeq as SessionSeqType } from '@deepseek-ai/dsh-session'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'test': { kind: 'test' } & ContextFormed
}
}
const SURFACE = { surfaceOp: 'append' as const }
function seqOf(session: Session, type: SessionEvent['type'], nth = 0): SessionSeqType {
@@ -138,7 +145,7 @@ describe('tool-pairing boundaries', () => {
}, SURFACE)
midStep.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'background update' }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
}), SURFACE)
midStep.append('tool/result', {
turn: 1, step: 1,
@@ -170,7 +177,7 @@ describe('tool-pairing surface identity', () => {
const nodes = session.surface.nodes
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'checkpoint' }],
source: { kind: 'plugin', plugin: 'compact' },
source: compactCheckpointSource(CompactionId('tool-pairing-compaction')),
}), {
surfaceOp: { op: 'replace', startSeq: nodes[0]!, endSeq: nodes.at(-1)! },
sourceEventSeqs: [...nodes],
@@ -93,7 +93,7 @@ function agentInstructionsHook(text: string, changes: AgentInstructionChange[]):
export function agentInstructionsMessage(text: string): Message {
return createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: name },
source: { kind: name, form: 'instructions', changes: [] },
})
}
@@ -5,11 +5,12 @@ import { afterAll, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import * as AgentInstructions from '@deepseek-ai/dsh-agent-instructions'
import LlmRuntime, { createUserMessage, ToolCallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, ToolCallId, type Message, type MessageSource, type StreamChunk } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, SessionSeq, type SessionEvent, type SurfaceIntent, type UserMessage } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop, { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import type {
FsDirEntry,
@@ -48,6 +49,21 @@ import {
mountAgentLoopTestHarness,
} from '@deepseek-ai/dsh-agent-loop-testkit'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'downstream': { kind: 'downstream' } & ContextFormed
'other': { kind: 'other' } & ContextFormed
'test-skills': { kind: 'test-skills' } & ContextFormed
}
}
type CheckpointSource = Extract<MessageSource, { readonly kind: 'compact-checkpoint' }>
/** Build a typed checkpoint source for an instruction projection fixture. */
function checkpointSource(compactionId: string): CheckpointSource {
return { kind: 'compact-checkpoint', compactionId: compactionId as CheckpointSource['compactionId'] }
}
/** Per-candidate reconciliation scope key: directory paired with the file name. */
const sk = (directory: string, candidateName: string): string => candidateScopeKey(directory, candidateName)
@@ -1746,7 +1762,7 @@ describe('workspace context request injection', () => {
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted summary' }],
source: { kind: 'plugin', plugin: 'compact' },
source: checkpointSource('instruction-compaction-1'),
}), {
surfaceOp: { op: 'replace', startSeq: baseline!.seq, endSeq: baseline!.seq },
sourceEventSeqs: [baseline!.seq],
@@ -1779,7 +1795,7 @@ describe('workspace context request injection', () => {
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted summary' }],
source: { kind: 'plugin', plugin: 'compact' },
source: checkpointSource('instruction-compaction-2'),
}), {
surfaceOp: { op: 'replace', startSeq: baseline!.seq, endSeq: baseline!.seq },
sourceEventSeqs: [baseline!.seq],
@@ -1882,7 +1898,7 @@ describe('workspace context request injection', () => {
...decision,
messages: [
...decision.messages,
createUserMessage({ content: [{ type: 'text', text: '<system-reminder>Available skills</system-reminder>' }], source: { kind: 'plugin', plugin: 'test-skills' } }),
createUserMessage({ content: [{ type: 'text', text: '<system-reminder>Available skills</system-reminder>' }], source: { kind: 'test-skills' } }),
],
}
})
@@ -3710,7 +3726,7 @@ describe('dynamic nested workspace context injection', () => {
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted summary' }],
source: { kind: 'plugin', plugin: 'compact' },
source: checkpointSource('instruction-compaction-3'),
}), {
surfaceOp: { op: 'replace', startSeq: SessionSeq(contextSeq), endSeq: SessionSeq(contextSeq) },
sourceEventSeqs: [SessionSeq(contextSeq)],
@@ -3757,7 +3773,7 @@ describe('dynamic nested workspace context injection', () => {
})
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted summary' }],
source: { kind: 'plugin', plugin: 'compact' },
source: checkpointSource('instruction-compaction-4'),
}), {
surfaceOp: { op: 'replace', startSeq: baseline!.seq, endSeq: baseline!.seq },
sourceEventSeqs: [baseline!.seq],
@@ -3901,7 +3917,7 @@ describe('dynamic nested workspace context injection', () => {
}), { surfaceOp: 'append' })
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'foreign plugin context' }],
source: { kind: 'plugin', plugin: 'other' },
source: { kind: 'other' },
}), { surfaceOp: 'append' })
await ctx.tools.execute({
@@ -4015,7 +4031,7 @@ describe('dynamic nested workspace context injection', () => {
},
additionalContexts: [createUserMessage({
content: [{ type: 'text' as const, text: 'downstream context' }],
source: { kind: 'plugin' as const, plugin: 'downstream' },
source: { kind: 'downstream' as const },
})],
}))
@@ -4049,7 +4065,7 @@ describe('dynamic nested workspace context injection', () => {
id: expect.any(String) as unknown,
role: 'user',
content: [{ type: 'text', text: 'downstream context' }],
source: { kind: 'plugin', plugin: 'downstream' },
source: { kind: 'downstream' },
})
} finally {
await rm(root, { recursive: true, force: true })
@@ -3,6 +3,7 @@ import { Context } from '@deepseek-ai/cordis'
import { agentEvents, installModelSelection, type Agent, type ModelSelectionRef } from '@deepseek-ai/dsh-agent'
import { CompactionId, compactCheckpointSource } from '@deepseek-ai/dsh-compaction'
import LlmRuntime, { createMessage, createSystemMessage, createToolResultMessage, createUserMessage, LlmError, ToolCallId } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SessionQueryEngine from '@deepseek-ai/dsh-session-query'
@@ -19,6 +20,15 @@ import SessionReferenceResolver, {
import { stringifyTagSafeJson } from '../src/serialization.ts'
import { SpillLocator, SpillStore, type SaveTextSpill, type SpillRef } from '@deepseek-ai/dsh-spill'
import type { MessageSource } from '@deepseek-ai/dsh-llm'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'test': { kind: 'test' } & ContextFormed
'workspace': { kind: 'workspace' } & ContextFormed
}
}
class TestSessionQueryEngine extends SessionQueryEngine {
override searchSessions(
..._args: Parameters<SessionQueryEngine['searchSessions']>
@@ -92,7 +102,7 @@ function checkpointSource(id: string) {
function appendConversation(session: Session): void {
session.append(
'system/message',
{ turn: 1, step: 1, message: createSystemMessage('system prompt secret', 'system-prompt') },
{ turn: 1, step: 1, message: createSystemMessage('system prompt secret') },
{ surfaceOp: 'append' },
)
const oldUser = session.append(
@@ -140,7 +150,7 @@ function appendConversation(session: Session): void {
session.append(
'user/message',
createUserMessage({
content: [{ type: 'text', text: 'workspace secret' }], source: { kind: 'plugin', plugin: 'workspace' },
content: [{ type: 'text', text: 'workspace secret' }], source: { kind: 'workspace' },
}),
{ surfaceOp: 'append' },
)
@@ -156,7 +166,7 @@ function appendConversation(session: Session): void {
'user/message',
createUserMessage({
content: [{ type: 'text', text: 'plugin steer' }],
source: { kind: 'plugin', plugin: 'goal' },
source: { kind: 'goal' } as unknown as MessageSource,
}),
{ surfaceOp: 'append' },
)
@@ -192,7 +202,7 @@ function appendConversation(session: Session): void {
session.append(
'user/message',
createUserMessage({
content: [{ type: 'text', text: 'plugin-generated user' }], source: { kind: 'plugin', plugin: 'goal' },
content: [{ type: 'text', text: 'plugin-generated user' }], source: { kind: 'goal' } as unknown as MessageSource,
}),
{ surfaceOp: 'append' },
)
@@ -863,7 +873,7 @@ describe('session reference discovery and preparation', () => {
})
const plugin = createUserMessage({
content: [{ type: 'text', text: formatSessionReferenceMention({ sessionId: source.id, label: 'Ignored' }) }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
})
const signal = new AbortController().signal
+9 -2
View File
@@ -10,6 +10,13 @@ import z from '@deepseek-ai/schemastery'
import { z as zod } from 'zod'
import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'time-context': { kind: 'time-context' } & ContextFormed
}
}
import type { UserMessage } from '@deepseek-ai/dsh-llm'
import { SessionSeq } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-projection'
@@ -160,7 +167,7 @@ export function apply(ctx: Context, config: Config): void {
return state.lastTurnInjectionTime === null ? state : { ...state, lastTurnInjectionTime: null }
}
if (event.type === 'user/message') {
const injected = event.data.source.kind === 'plugin' && event.data.source.plugin === name
const injected = event.data.source.kind === name
const withMessage = state.lastMessageTime === event.time
? state
: { ...state, lastMessageTime: event.time }
@@ -214,7 +221,7 @@ export function apply(ctx: Context, config: Config): void {
...decision.messages,
createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: name, form: 'snapshot', sections: [{ name, text }] },
source: { kind: name, form: 'snapshot', sections: [{ name, text }] },
}),
],
}
@@ -105,7 +105,7 @@ function validateReading(
}
const source = event.data.source
/* v8 ignore next 2 -- replay and dispatch callers select this exact package-owned source before validation. */
if (source.kind !== 'plugin' || source.plugin !== SOURCE_NAME) {
if (source.kind !== SOURCE_NAME) {
fail('time-context source must retain package ownership')
}
const sections: unknown = 'sections' in source ? source.sections : undefined
@@ -113,7 +113,7 @@ function validateReading(
const section = typeof sectionValue === 'object' && sectionValue !== null
? sectionValue as Record<string, unknown>
: undefined
if (Object.keys(source).length !== 4
if (Object.keys(source).length !== 3
|| source.form !== 'snapshot'
|| !Array.isArray(sections)
|| sections.length !== 1
@@ -165,8 +165,7 @@ function validateSession(session: Session, fail: InvariantFailure): void {
const events = session.snapshotEvents()
for (const [index, event] of events.entries()) {
if (event.type !== 'user/message'
|| event.data.source.kind !== 'plugin'
|| event.data.source.plugin !== SOURCE_NAME) continue
|| event.data.source.kind !== SOURCE_NAME) continue
validateReading(events.slice(0, index), event, fail)
}
}
@@ -179,8 +178,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant
if (eventName !== 'session/event') return
const [session, event] = args as [Session, SessionEvent]
if (event.type !== 'user/message'
|| event.data.source.kind !== 'plugin'
|| event.data.source.plugin !== SOURCE_NAME) return
|| event.data.source.kind !== SOURCE_NAME) return
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
validateReading(session.snapshotEvents(), event, fail)
}, { global: true })
@@ -31,12 +31,11 @@ function event(
content: (content ?? [{ type: 'text', text }]) as ContentBlock[],
source: plugin === 'time-context'
? {
kind: 'plugin',
plugin,
kind: 'time-context',
form: 'snapshot',
sections: [{ name: plugin, text }],
}
: { kind: 'plugin', plugin },
: { kind: 'other' },
}),
}
}
@@ -78,8 +77,7 @@ function appendReading(session: Session, text: string): void {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text }],
source: {
kind: 'plugin',
plugin: 'time-context',
kind: 'time-context',
form: 'snapshot',
sections: [{ name: 'time-context', text }],
},
@@ -287,23 +285,20 @@ describe('time-context invariants', () => {
const ctx = await setup()
const base = event(reading())
for (const source of [
{ kind: 'plugin', plugin: 'time-context' },
{ kind: 'time-context' },
{ ...base.data.source, authority: {} },
{
kind: 'plugin',
plugin: 'time-context',
kind: 'time-context',
form: 'snapshot',
sections: [{ name: 'time-context', text: 'different' }],
},
{
kind: 'plugin',
plugin: 'time-context',
kind: 'time-context',
form: 'snapshot',
sections: { 0: { name: 'time-context', text: reading() }, length: 1 },
},
{
kind: 'plugin',
plugin: 'time-context',
kind: 'time-context',
form: 'snapshot',
sections: [{ name: 'time-context', text: reading(), extra: true }],
},
@@ -1,11 +1,18 @@
import { describe, expect, it } from 'vitest'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import {
deriveBrowserTimeZoneContext,
renderBrowserTimeZoneContext,
} from '../src/request-zone.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'test': { kind: 'test' } & ContextFormed
}
}
function browserMessage(timeZone: string): UserMessage {
return createUserMessage({
content: [{ type: 'text', text: timeZone }],
@@ -17,7 +24,7 @@ describe('browser request-zone context', () => {
it('derives missing, unique, and sorted mixed zones from user-rpc messages only', () => {
const plugin = createUserMessage({
content: [{ type: 'text', text: 'plugin' }],
source: { kind: 'plugin', plugin: 'test' },
source: { kind: 'test' },
})
expect(deriveBrowserTimeZoneContext([plugin])).toEqual({ kind: 'missing' })
expect(deriveBrowserTimeZoneContext([
@@ -50,8 +50,7 @@ describe('time-context through the production headless profile', () => {
const contexts = events.filter(
(event): event is SessionEvent<'user/message'> => event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === 'time-context')
&& event.data.source.kind === 'time-context')
const starts = events.filter(event => event.type === 'step/start')
expect(contexts).toHaveLength(2)
expect(starts).toHaveLength(2)
@@ -61,8 +60,7 @@ describe('time-context through the production headless profile', () => {
// `snapshot` form: one named contribution whose text is exactly what the
// model read, so a consumer attributes it without re-splitting prose.
expect(contexts[index]!.data.source).toMatchObject({
kind: 'plugin',
plugin: 'time-context',
kind: 'time-context',
form: 'snapshot',
sections: [{ name: 'time-context' }],
})
@@ -3,6 +3,7 @@ import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import { createUserMessage, ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
@@ -12,6 +13,13 @@ import { unsupportedInbox, mountAgentLoopTestDependencies } from '@deepseek-ai/d
import * as timeContext from '@deepseek-ai/dsh-time-context'
import type { Config } from '@deepseek-ai/dsh-time-context'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'compaction-basic': { kind: 'compaction-basic' } & ContextFormed
'time-context-test': { kind: 'time-context-test' } & ContextFormed
}
}
const BASE = Date.parse('2026-07-14T00:00:00.000Z')
const ORIGINAL_TIME_ZONE = process.env['TZ']
const SIGNAL = new AbortController().signal
@@ -70,8 +78,7 @@ function contextTexts(session: Session): string[] {
const texts: string[] = []
for (const event of session.snapshotEvents()) {
if (event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === 'time-context') {
&& event.data.source.kind === 'time-context') {
texts.push(event.data.content.find(block => block.type === 'text')?.text ?? '')
}
}
@@ -87,7 +94,7 @@ async function fire(
): Promise<void> {
const proposed = createUserMessage({
content: [{ type: 'text', text: 'request proposal' }],
source: { kind: 'plugin', plugin: 'time-context-test' },
source: { kind: 'time-context-test' },
})
const decision = await agentEvents(ctx, agent).waterfall(
'agent/pre-step',
@@ -175,8 +182,7 @@ describe('durable step context', () => {
// text is exactly what the model read, so a consumer attributes it without
// re-splitting prose.
expect(event.data.source).toEqual({
kind: 'plugin',
plugin: 'time-context',
kind: 'time-context',
form: 'snapshot',
sections: [{
name: 'time-context',
@@ -292,11 +298,11 @@ describe('durable step context', () => {
openMessageTurn(original, 1)
await fire(ctx, sessionAgent(original), 1, 1)
const user = original.snapshotEvents().find(event => event.type === 'user/message' && event.data.source.kind === 'user')
const reading = original.snapshotEvents().find(event => event.type === 'user/message' && event.data.source.kind === 'plugin')
const reading = original.snapshotEvents().find(event => event.type === 'user/message' && event.data.source.kind === 'time-context')
if (user === undefined || reading === undefined) throw new Error('missing source surface events')
original.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted history' }],
source: { kind: 'plugin', plugin: 'compaction-basic' },
source: { kind: 'compaction-basic' },
}), {
surfaceOp: { op: 'replace', startSeq: user.seq, endSeq: reading.seq },
sourceEventSeqs: [user.seq, reading.seq],
@@ -421,7 +427,7 @@ describe('time-context projection fold edges', () => {
session.append('turn/start', { turn: 1 })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'reading' }],
source: { kind: 'plugin', plugin: 'time-context' },
source: { kind: 'time-context' },
}), { surfaceOp: 'append' })
expect(typeof ctx.sessionProjections.stateOf(session, 'timeContext')?.lastTurnInjectionTime).toBe('number')
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
@@ -483,15 +489,14 @@ describe('real agent-loop request history', () => {
expect(adapter.requests).toHaveLength(2)
const contexts = agent.session.snapshotEvents().filter(
(event): event is SessionEvent<'user/message'> => event.type === 'user/message' && event.data.source.kind === 'plugin')
(event): event is SessionEvent<'user/message'> => event.type === 'user/message' && event.data.source.kind !== 'user')
const starts = agent.session.snapshotEvents().filter(event => event.type === 'step/start')
expect(contexts).toHaveLength(adapter.requests.length)
expect(starts).toHaveLength(adapter.requests.length)
for (let index = 0; index < contexts.length; index += 1) {
expect(contexts[index]!.seq).toBeGreaterThan(starts[index]!.seq)
}
expect(contexts.every(event => event.data.source.kind === 'plugin'
&& event.data.source.plugin === 'time-context'
expect(contexts.every(event => event.data.source.kind === 'time-context'
&& event.surfaceOp === 'append')).toBe(true)
const firstRequestText = requestText(adapter.requests[0]!)
+13 -3
View File
@@ -25,6 +25,17 @@ import type { PreStepDecision } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-session-projection'
import type { ShellExecutor, ShellRunResult } from '@deepseek-ai/dsh-shell'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
/** Location attribution; readers preserve the content without this producer.
* Its projection uses the kind to avoid repeated injection.
* @persistenceAttribution
*/
'tmux-context': { kind: 'tmux-context' } & ContextFormed
}
}
/** Cordis plugin name used by loader diagnostics. */
export const name = 'tmux-context'
@@ -223,8 +234,7 @@ export function apply(ctx: Context, config: Config): void {
init: () => null,
apply: (state, event) => {
if (event.type !== 'user/message'
|| event.data.source.kind !== 'plugin'
|| event.data.source.plugin !== name) return state
|| event.data.source.kind !== name) return state
const [block] = event.data.content
if (block?.type !== 'text') return state
const newline = block.text.indexOf('\n')
@@ -256,7 +266,7 @@ export function apply(ctx: Context, config: Config): void {
messages: [
createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: name, form: 'snapshot', sections: [{ name, text }] },
source: { kind: name, form: 'snapshot', sections: [{ name, text }] },
}),
...decision.messages,
],
@@ -125,8 +125,7 @@ function contextTexts(session: Session): string[] {
const texts: string[] = []
for (const event of session.snapshotEvents()) {
if (event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === 'tmux-context') {
&& event.data.source.kind === 'tmux-context') {
texts.push(event.data.content.find(block => block.type === 'text')?.text ?? '')
}
}
@@ -176,8 +175,7 @@ describe('tmux-context injection', () => {
// `snapshot` form: one named contribution carrying exactly the reading the
// model saw, so a consumer attributes it without re-splitting prose.
expect(event.data.source).toMatchObject({
kind: 'plugin',
plugin: 'tmux-context',
kind: 'tmux-context',
form: 'snapshot',
sections: [{ name: 'tmux-context' }],
})
@@ -275,7 +273,7 @@ describe('tmux-context prior-reading resilience', () => {
openMessageTurn(session, 1)
session.append('user/message', createUserMessage({
content: [{ type: 'reasoning', text: 'not a location' }],
source: { kind: 'plugin', plugin: 'tmux-context' },
source: { kind: 'tmux-context' },
}), { surfaceOp: 'append' })
await fire(ctx, agent, 1, 1)
@@ -291,7 +289,7 @@ describe('tmux-context prior-reading resilience', () => {
openMessageTurn(session, 1)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'single line, no newline' }],
source: { kind: 'plugin', plugin: 'tmux-context' },
source: { kind: 'tmux-context' },
}), { surfaceOp: 'append' })
await fire(ctx, agent, 1, 1)
+1
View File
@@ -34,6 +34,7 @@ import type { SessionHandle, SessionPersistence } from '@deepseek-ai/dsh-session
import { ReactLoopAgent } from './agent.ts'
import { inboxProjectionDefinition } from './inbox.ts'
import { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from './constants.ts'
import type {} from './runtime-context.ts'
/** Fiber states that cannot own or serve a new lifecycle. */
const INACTIVE_STATES: ReadonlySet<FiberState> = new Set([
@@ -6,16 +6,21 @@
*/
import { createSystemMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContextSnapshotSection, Message } from '@deepseek-ai/dsh-llm'
import type { ContextFormed, ContextSnapshotSection, Message } from '@deepseek-ai/dsh-llm'
import type { Session, SessionEvent, SessionSeq, SurfaceIntent, SystemMessage, UserMessage } from '@deepseek-ai/dsh-session'
import { isReplacementSurfaceEvent } from '@deepseek-ai/dsh-session'
import type { Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'runtime-context': { kind: 'runtime-context' } & ContextFormed
}
}
const SOURCE = '@deepseek-ai/dsh-system-prompt'
const SOURCE = 'runtime-context'
const CLEARED = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'
function isOwned(message: UserMessage): boolean {
return message.source.kind === 'plugin' && message.source.plugin === SOURCE
return message.source.kind === SOURCE
}
function textOf(message: Message): string | undefined {
@@ -84,7 +89,7 @@ export class SystemPromptProjection {
const nodes = this.systemNodes()
const head = nodes[0]
if (head === undefined) {
return [{ message: createSystemMessage(rendered, SOURCE), intent: { surfaceOp: 'append' } }]
return [{ message: createSystemMessage(rendered), intent: { surfaceOp: 'append' } }]
}
const latest = nodes.findLast(node => node.text !== '') ?? head
if (!input.inHistory || input.startsSeries || rendered.length === 0) {
@@ -94,12 +99,12 @@ export class SystemPromptProjection {
return updates
}
if (latest.text === rendered) return []
return [{ message: createSystemMessage(rendered, SOURCE), intent: { surfaceOp: 'append' } }]
return [{ message: createSystemMessage(rendered), intent: { surfaceOp: 'append' } }]
}
private replace(seq: SessionSeq, text: string): SystemPromptCommit {
return {
message: createSystemMessage(text, SOURCE),
message: createSystemMessage(text),
intent: { surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq }, sourceEventSeqs: [seq] },
}
}
@@ -152,8 +157,8 @@ export class RuntimeContextProjection {
content: [{ type: 'text', text: snapshot }],
// The cleared marker has no contributions left to attribute.
source: sections.length === 0
? { kind: 'plugin', plugin: SOURCE }
: { kind: 'plugin', plugin: SOURCE, form: 'snapshot', sections },
? { kind: SOURCE }
: { kind: SOURCE, form: 'snapshot', sections },
})
}
}
Loaded 100 of 298 files, more files were not shown because too many files have changed in this diff. Show more