Browse Source

fix(client): consume typed business session events

imccyu 2 months ago
parent
commit
10464d155d
47 changed files with 405 additions and 359 deletions
  1. 1 1
      .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md
  2. 1 1
      .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md
  3. 2 0
      packages/client/runtime/package.json
  4. 10 2
      packages/client/runtime/src/client/sessions/conversation-location-index.ts
  5. 32 76
      packages/client/runtime/src/client/sessions/request-inspection.ts
  6. 3 2
      packages/client/runtime/src/client/sessions/steering-history.ts
  7. 5 17
      packages/client/runtime/src/client/sessions/tool-call-tree.ts
  8. 6 0
      packages/client/runtime/tsconfig.json
  9. 8 0
      packages/client/ui-conversation/package.json
  10. 5 5
      packages/client/ui-conversation/src/client/conversation-nodes/assistant.ts
  11. 29 43
      packages/client/ui-conversation/src/client/conversation-nodes/command.ts
  12. 7 7
      packages/client/ui-conversation/src/client/conversation-nodes/compaction.ts
  13. 7 6
      packages/client/ui-conversation/src/client/conversation-nodes/inbox.ts
  14. 17 41
      packages/client/ui-conversation/src/client/conversation-nodes/retry.ts
  15. 17 14
      packages/client/ui-conversation/src/client/conversation-nodes/tool.ts
  16. 4 3
      packages/client/ui-conversation/src/client/conversation-nodes/turn-error.ts
  17. 4 5
      packages/client/ui-conversation/src/client/conversation-nodes/turn-tail.ts
  18. 3 1
      packages/client/ui-conversation/tests/chat-apply.spec.tsx
  19. 22 0
      packages/client/ui-conversation/tests/conversation-node-definitions.spec.ts
  20. 12 0
      packages/client/ui-conversation/tsconfig.json
  21. 4 4
      packages/compact/compact/package.json
  22. 3 1
      packages/compact/compact/src/types.ts
  23. 2 2
      packages/core/agent/package.json
  24. 1 1
      packages/core/agent/src/dispatch.ts
  25. 1 3
      packages/core/agent/src/inbox.ts
  26. 3 2
      packages/core/agent/src/index.ts
  27. 3 19
      packages/core/agent/src/runtime-types.ts
  28. 27 0
      packages/core/agent/src/session-types.ts
  29. 1 1
      packages/core/session/tests/gen-persistence-catalog.spec.ts
  30. 4 0
      packages/core/tools/package.json
  31. 1 35
      packages/core/tools/src/code-mode.ts
  32. 1 0
      packages/core/tools/src/index.ts
  33. 58 0
      packages/core/tools/src/types.ts
  34. 4 0
      packages/interaction/commands/package.json
  35. 1 41
      packages/interaction/commands/src/index.ts
  36. 48 0
      packages/interaction/commands/src/types.ts
  37. 0 4
      packages/llm/llm-retry/package.json
  38. 3 1
      packages/llm/llm-retry/src/types.ts
  39. 1 1
      packages/llm/llm-retry/tests/invariant.spec.ts
  40. 1 1
      packages/llm/llm-retry/tests/persistence.spec.ts
  41. 1 1
      packages/llm/token-meter/tests/context-breakdown-projection.spec.ts
  42. 1 1
      packages/llm/token-meter/tests/token-usage-projection.spec.ts
  43. 1 1
      packages/support/llm-replay/tests/llm-replay.spec.ts
  44. 18 0
      pnpm-lock.yaml
  45. 11 8
      scripts/gen-persistence-catalog.ts
  46. 8 8
      scripts/type-equiv.manifest.json
  47. 3 0
      tsconfig.base.json

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md

@@ -16,7 +16,7 @@ Business events also use different correlation models. Tool has call IDs, Assist
 
 Client Runtime provides a target-neutral Conversation Node assembly engine. Business plugins register Event Definitions, and view plugins register per-Session View Builders. `ui-conversation` registers the first built-in Definitions and the `chat` builder; Session only submits the current contiguous Event window to the engine and publishes its snapshot instead of interpreting individual conversation businesses.
 
-The complete derivation, business-by-business validation, and file-level implementation plan remain in [`business-node assembler version one`](../../../../docs/client-conversation-node-engine-rfc.md), [`follow-up design differences`](../../../../docs/client-conversation-node-engine-follow-up-differences.md), [`business-node and dual-view adaptation analysis`](../../../../docs/client-conversation-node-adaptation-analysis.md), and the [`Chat implementation design`](../../../../docs/client-conversation-node-chat-implementation-plan.md). Those design documents retain the full discussion; this Note fixes the responsibilities, algorithms, and trade-offs that remain relevant after implementation.
+This Note retains the derivation, business-by-business validation, responsibilities, algorithms, and trade-offs that remain relevant after implementation.
 
 ### Responsibility layers
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md

@@ -16,7 +16,7 @@ Client Session 既维护传输窗口、连接状态和待处理交互,也在
 
 Client Runtime 提供 target-neutral 的 Conversation Node 组装引擎,业务插件注册 Event Definition,视图插件注册 per-Session View Builder。`ui-conversation` 注册第一批内建 Definition 和 `chat` builder;Session 只负责把当前连续事件窗口送入引擎并发布它的 snapshot,不再解释具体 conversation 业务。
 
-详细的方案推导、逐业务适配和逐文件实施设计保留在 [`业务节点组装器第一版`](../../../../docs/client-conversation-node-engine-rfc.md)、[`后续方案差异`](../../../../docs/client-conversation-node-engine-follow-up-differences.md)、[`业务节点与双视图适配论证`](../../../../docs/client-conversation-node-adaptation-analysis.md) 和 [`Chat 链路工程实施设计`](../../../../docs/client-conversation-node-chat-implementation-plan.md)。这些设计稿保留完整讨论过程;本 Note 固定实现后仍需长期维护的职责、算法和取舍。
+本 Note 保留实现后仍有价值的方案推导、逐业务适配、职责、算法和取舍。
 
 ### 责任分层
 

+ 2 - 0
packages/client/runtime/package.json

@@ -32,6 +32,7 @@
   },
   "license": "BSD-3-Clause",
   "dependencies": {
+    "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
@@ -41,6 +42,7 @@
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-session-title": "workspace:^",
+    "@deepseek-ai/dsh-tools": "workspace:^",
     "immer": "^10.1.1",
     "react": "^18.2.0",
     "zustand": "~4.4.7"

+ 10 - 2
packages/client/runtime/src/client/sessions/conversation-location-index.ts

@@ -139,7 +139,11 @@ export class ConversationLocationIndex {
     return this.timeline
   }
 
-  /** Replace all Definition-owned Location values while preserving reader identities. */
+  /**
+   * Replace all Definition-owned Location values while preserving reader identities.
+   * @param entries - complete current set of Definition-owned Location values.
+   * @returns whether any published Location data changed.
+   */
   replaceData(entries: readonly { readonly owner: string; readonly data: ConversationLocationData }[]): boolean {
     const turns = new Map<number, Map<string, OwnedLocationData>>()
     const steps = new Map<string, Map<string, OwnedLocationData>>()
@@ -165,7 +169,11 @@ export class ConversationLocationIndex {
     return changed
   }
 
-  /** Apply changed Context publications without rebuilding Turn/Step membership. */
+  /**
+   * Apply changed Context publications without rebuilding Turn/Step membership.
+   * @param changes - incremental removals and replacements from published Contexts.
+   * @returns whether any published Location data changed.
+   */
   applyData(changes: readonly ConversationLocationDataChange[]): boolean {
     let changed = false
     for (const change of changes) {

+ 32 - 76
packages/client/runtime/src/client/sessions/request-inspection.ts

@@ -5,6 +5,9 @@
 import type { ContentBlock, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm/types'
 import type { HistoryEntry } from '@deepseek-ai/dsh-client-connection/client'
 import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
+import type {} from '@deepseek-ai/dsh-compact/types'
+import type {} from '@deepseek-ai/dsh-llm-retry/types'
+import type {} from '@deepseek-ai/dsh-tools/types'
 import type {
   AssistantProvenanceView, AssistantRequestConfig,
 } from './conversation.ts'
@@ -109,48 +112,6 @@ export function inspectRequests(
   }
 }
 
-interface RetryEvent {
-  type: 'llm/retry'
-  seq: number
-  time: number
-  data: {
-    turn: number
-    step: number
-    retry: number
-    maxRetries: number
-    delayMs: number
-    failure: { message: string }
-  }
-}
-
-interface CompactionStartEvent {
-  type: 'compact/start'
-  seq: number
-  time: number
-  data: { turn: number | null }
-}
-
-interface CompactionSummaryEvent {
-  type: 'compact/summary'
-  seq: number
-  time: number
-  data: {
-    summary: readonly ContentBlock[]
-    rawOutput?: readonly ContentBlock[]
-    provider: string
-    model: string
-    maxTokens?: number
-    usage?: unknown
-  }
-}
-
-interface CompactionEndEvent {
-  type: 'compact/end'
-  seq: number
-  time: number
-  data: { turn: number | null; error?: string }
-}
-
 function requestKey(turn: number, step: number): string {
   return `${turn}\u0000${step}`
 }
@@ -205,10 +166,8 @@ function deriveCallSchemas(
       capture(String(event.data.callId), event.data.name)
       continue
     }
-    const type = event.type as string
-    if (type === 'tool/code-dispatch-start' || type === 'tool/code-dispatch') {
-      const data = event.data as unknown as { subCallId: string; name: string }
-      capture(data.subCallId, data.name)
+    if (event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch') {
+      capture(String(event.data.subCallId), event.data.name)
     }
   }
   return calls
@@ -351,14 +310,14 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
       if (activeStep === key) activeStep = undefined
       continue
     }
-    if ((sourceEvent.type as string) === 'llm/retry') {
-      const event = sourceEvent as unknown as RetryEvent
-      updateAssistant(ordinaryByStep.get(requestKey(event.data.turn, event.data.step)), {
+    if (sourceEvent.type === 'llm/retry') {
+      const data = sourceEvent.data
+      updateAssistant(ordinaryByStep.get(requestKey(data.turn, data.step)), {
         status: 'error',
-        error: displayFailureMessage(event.data.failure),
-        retry: event.data.retry,
-        maxRetries: event.data.maxRetries,
-        retryDelayMs: event.data.delayMs,
+        error: displayFailureMessage(data.failure),
+        retry: data.retry,
+        ...data.mode === 'normal' ? { maxRetries: data.maxRetries } : {},
+        retryDelayMs: data.delayMs,
       })
       continue
     }
@@ -374,8 +333,7 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
       continue
     }
 
-    const type = sourceEvent.type as string
-    if (type === 'session/end-seed' && activeCompaction !== undefined) {
+    if (sourceEvent.type === 'session/end-seed' && activeCompaction !== undefined) {
       updateCompaction(activeCompaction, {
         completedAt: sourceEvent.time,
         status: 'error',
@@ -384,37 +342,36 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
       activeCompaction = undefined
       continue
     }
-    if (type === 'compact/start') {
-      const event = sourceEvent as unknown as CompactionStartEvent
+    if (sourceEvent.type === 'compact/start') {
       activeCompaction = requests.length
       requests.push({
         purpose: 'compaction',
-        startSeq: event.seq,
-        turn: event.data.turn,
+        startSeq: sourceEvent.seq,
+        turn: sourceEvent.data.turn,
         step: 0,
-        startedAt: event.time,
+        startedAt: sourceEvent.time,
         completedAt: null,
         status: 'running',
       })
       continue
     }
-    if (type === 'compact/summary' && activeCompaction !== undefined) {
-      const event = sourceEvent as unknown as CompactionSummaryEvent
+    if (sourceEvent.type === 'compact/summary' && activeCompaction !== undefined) {
+      const data = sourceEvent.data
       updateCompaction(activeCompaction, {
-        resultSeq: event.seq,
-        summary: event.data.summary,
-        ...(event.data.rawOutput === undefined ? {} : { rawOutput: event.data.rawOutput }),
+        resultSeq: sourceEvent.seq,
+        summary: data.summary,
+        ...(data.rawOutput === undefined ? {} : { rawOutput: data.rawOutput }),
         provenance: {
-          provider: event.data.provider,
-          model: event.data.model,
+          provider: data.provider,
+          model: data.model,
         },
         requestConfig: {
-          provider: event.data.provider,
-          model: event.data.model,
+          provider: data.provider,
+          model: data.model,
           purpose: 'compaction',
-          ...(event.data.maxTokens === undefined ? {} : { maxTokens: event.data.maxTokens }),
+          ...(data.maxTokens === undefined ? {} : { maxTokens: data.maxTokens }),
         },
-        ...(event.data.usage === undefined ? {} : { usage: event.data.usage }),
+        ...(data.usage === undefined ? {} : { usage: data.usage }),
       })
       continue
     }
@@ -426,12 +383,11 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
       updateCompaction(activeCompaction, { replacementSeq: sourceEvent.seq })
       continue
     }
-    if (type !== 'compact/end' || activeCompaction === undefined) continue
-    const event = sourceEvent as unknown as CompactionEndEvent
+    if (sourceEvent.type !== 'compact/end' || activeCompaction === undefined) continue
     updateCompaction(activeCompaction, {
-      completedAt: event.time,
-      status: event.data.error === undefined ? 'complete' : 'error',
-      ...(event.data.error === undefined ? {} : { error: event.data.error }),
+      completedAt: sourceEvent.time,
+      status: sourceEvent.data.error === undefined ? 'complete' : 'error',
+      ...(sourceEvent.data.error === undefined ? {} : { error: sourceEvent.data.error }),
     })
     activeCompaction = undefined
   }

+ 3 - 2
packages/client/runtime/src/client/sessions/steering-history.ts

@@ -1,6 +1,7 @@
 /** Reconstruct durable steering identity from the event-sourced agent inbox. */
 
 import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
+import type {} from '@deepseek-ai/dsh-agent/types'
 
 type InboxTarget = 'next-turn' | 'next-step'
 
@@ -45,8 +46,8 @@ export class SteeringHistory {
    * @returns true only for a user-origin message previously claimed from `next-step`.
    */
   apply(event: SessionEvent): boolean {
-    if ((event.type as string) === 'agent/inbox/spliced') {
-      this.applySplice(event.data as unknown as InboxSplice)
+    if (event.type === 'agent/inbox/spliced') {
+      this.applySplice(event.data)
       return false
     }
     if (event.type !== 'user/message') return false

+ 5 - 17
packages/client/runtime/src/client/sessions/tool-call-tree.ts

@@ -1,5 +1,5 @@
-import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
 import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
+import type {} from '@deepseek-ai/dsh-tools/types'
 import type {
   ConversationNode, RunningToolCall, ToolCallBlock, ToolResultNode,
 } from './conversation.ts'
@@ -55,13 +55,8 @@ export class ToolCallTree {
    * @returns Whether the event was consumed as a child-call lifecycle event.
    */
   apply(event: SessionEvent): boolean {
-    if ((event.type as string) === 'tool/code-dispatch-start') {
-      const data = event.data as unknown as {
-        parentCallId: string
-        subCallId: string
-        name: string
-        arguments: unknown
-      }
+    if (event.type === 'tool/code-dispatch-start') {
+      const data = event.data
       const running: RunningToolCall = {
         callId: data.subCallId,
         name: data.name,
@@ -78,15 +73,8 @@ export class ToolCallTree {
       this.revision++
       return true
     }
-    if ((event.type as string) !== 'tool/code-dispatch') return false
-    const data = event.data as unknown as {
-      parentCallId: string
-      subCallId: string
-      name: string
-      arguments: unknown
-      isError: boolean
-      content: ContentBlock[]
-    }
+    if (event.type !== 'tool/code-dispatch') return false
+    const data = event.data
     const siblings = this.childrenByParent.get(data.parentCallId) ?? []
     const at = siblings.findIndex(sub => sub.callId === data.subCallId)
     if (at === -1 && !this.acceptEdge(data.parentCallId, data.subCallId)) return true

+ 6 - 0
packages/client/runtime/tsconfig.json

@@ -26,6 +26,12 @@
     {
       "path": "../../interaction/commands"
     },
+    {
+      "path": "../../core/agent"
+    },
+    {
+      "path": "../../core/tools"
+    },
     {
       "path": "../../compact/compact"
     },

+ 8 - 0
packages/client/ui-conversation/package.json

@@ -39,22 +39,28 @@
     "clsx": "^2.0.0"
   },
   "peerDependencies": {
+    "@deepseek-ai/dsh-agent": "^0.0.1",
     "@deepseek-ai/dsh-client-locale": "^0.0.1",
     "@deepseek-ai/dsh-client-runtime": "^0.0.1",
     "@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
     "@deepseek-ai/dsh-client-ui-slash": "^0.0.1",
     "@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
     "@deepseek-ai/dsh-compact": "^0.0.1",
+    "@deepseek-ai/dsh-commands": "^0.0.1",
     "@deepseek-ai/dsh-invariants": "^0.0.1",
+    "@deepseek-ai/dsh-llm-retry": "^0.0.1",
     "@deepseek-ai/dsh-token-meter": "^0.0.1",
+    "@deepseek-ai/dsh-tools": "^0.0.1",
     "cordis": "^4.0.0-rc.7",
     "react": "^18.2.0"
   },
   "devDependencies": {
+    "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
     "@deepseek-ai/dsh-compact": "workspace:^",
+    "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-goal": "workspace:^",
     "@deepseek-ai/dsh-plan-mode": "workspace:^",
     "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
@@ -62,9 +68,11 @@
     "@deepseek-ai/dsh-client-ui-slash": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@deepseek-ai/dsh-llm-retry": "workspace:^",
     "@deepseek-ai/dsh-permission": "workspace:^",
     "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-token-meter": "workspace:^",
+    "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-tool-todo": "workspace:^",
     "@types/react": "~18.3.1",
     "cordis": "^4.0.0-rc.7",

+ 5 - 5
packages/client/ui-conversation/src/client/conversation-nodes/assistant.ts

@@ -6,6 +6,7 @@ import type {
 import {
   emptyAssistantBlock, isAppendSurfaceEvent, isTokenDelta, toAssistantBlock, toAssistantBlocks,
 } from '@deepseek-ai/dsh-client-runtime/client'
+import type {} from '@deepseek-ai/dsh-llm-retry/types'
 import type { AssistantChatData } from '../contract/chat-nodes.ts'
 import { CHAT_SYNTHETIC_SEQ_OFFSETS, chatNode } from './common.ts'
 
@@ -197,7 +198,7 @@ function fallbackState(context: ConversationNodeContext<AssistantState>): Assist
       }
       continue
     }
-    if ((match.event.type as string) === 'llm/retry' && state !== undefined) {
+    if (match.event.type === 'llm/retry' && state !== undefined) {
       state = resetForRetry(state)
     }
   }
@@ -247,9 +248,8 @@ export const assistantDefinition: ConversationNodeDefinition<AssistantState> = {
       || (event.type === 'assistant/message' && isAppendSurfaceEvent(event))) {
       return { id: `${event.data.turn}:${event.data.step}`, role: 'update' }
     }
-    if ((event.type as string) === 'llm/retry') {
-      const data = event.data as unknown as { turn: number; step: number }
-      return { id: `${data.turn}:${data.step}`, role: 'update' }
+    if (event.type === 'llm/retry') {
+      return { id: `${event.data.turn}:${event.data.step}`, role: 'update' }
     }
     return null
   },
@@ -268,7 +268,7 @@ export const assistantDefinition: ConversationNodeDefinition<AssistantState> = {
         usage: match.event.data.usage,
       }
     }
-    if ((match.event.type as string) === 'llm/retry') {
+    if (match.event.type === 'llm/retry') {
       return resetForRetry(context.state)
     }
     return context.state

+ 29 - 43
packages/client/ui-conversation/src/client/conversation-nodes/command.ts

@@ -5,6 +5,8 @@ import type {
 } from '@deepseek-ai/dsh-client-runtime/client'
 import { isReplacementSurfaceEvent } from '@deepseek-ai/dsh-client-runtime/client'
 import type { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact/checkpoint'
+import type {} from '@deepseek-ai/dsh-compact/types'
+import type {} from '@deepseek-ai/dsh-commands/types'
 import type { ManualCompactionChatData } from '../contract/chat-nodes.ts'
 import { chatNode } from './common.ts'
 
@@ -32,21 +34,9 @@ interface CompactionEvidence {
   readonly checkpoint?: ConversationMatch
 }
 
-interface CommandRunData {
-  readonly commandId: CommandId
-  readonly name: string
-  readonly args?: string
-}
-
-interface CommandDoneData {
-  readonly commandId: CommandId
-  readonly kind: 'success' | 'error'
-  readonly text?: string
-  readonly sourceEventSeq?: number
-}
-
 function commandFromRun(match: ConversationMatch): CommandNode {
-  const data = match.event.data as unknown as CommandRunData
+  if (match.event.type !== 'command/run') throw new Error('command start requires command/run')
+  const data = match.event.data
   return {
     kind: 'command',
     seq: match.event.seq,
@@ -59,10 +49,12 @@ function commandFromRun(match: ConversationMatch): CommandNode {
 }
 
 function commandFromDone(match: ConversationMatch, previous?: CommandNode): CommandNode {
-  const data = match.event.data as unknown as CommandDoneData
+  if (match.event.type !== 'command/done') throw new Error('command update requires command/done')
+  const data = match.event.data
   const sourceEventSeq = data.kind === 'success'
-    && Number.isSafeInteger(data.sourceEventSeq) && (data.sourceEventSeq as number) >= 0
-    ? data.sourceEventSeq as number
+    && data.sourceEventSeq !== undefined
+    && Number.isSafeInteger(data.sourceEventSeq) && data.sourceEventSeq >= 0
+    ? data.sourceEventSeq
     : undefined
   return {
     kind: 'command',
@@ -112,28 +104,21 @@ function compactSummary(match: ConversationMatch | undefined, checkpoint: Conver
   let summary: string | null = null
   let shadowedItemCount: number | null = null
   let shadowedTokenCount: number | null = null
-  if (match !== undefined) {
-    const data = match.event.data as unknown as {
-      summary?: unknown
-      shadowedSeqs?: unknown
-      shadowedTokenCount?: unknown
-    }
+  if (match?.event.type === 'compact/summary') {
+    const data = match.event.data
     if (Array.isArray(data.summary)) {
       const text = data.summary
-        .map((block: unknown) => {
-          const value = block as { type?: unknown; text?: unknown }
-          return value.type === 'text' && typeof value.text === 'string' ? value.text : ''
-        })
+        .map(block => block.type === 'text' ? block.text : '')
         .join('')
       summary = text.trim() === '' ? null : text
     }
     shadowedItemCount = Array.isArray(data.shadowedSeqs)
-      && data.shadowedSeqs.every(seq => Number.isSafeInteger(seq) && (seq as number) >= 0)
+      && data.shadowedSeqs.every(seq => Number.isSafeInteger(seq) && seq >= 0)
       ? data.shadowedSeqs.length
       : null
     shadowedTokenCount = Number.isSafeInteger(data.shadowedTokenCount)
-      && (data.shadowedTokenCount as number) >= 0
-      ? data.shadowedTokenCount as number
+      && data.shadowedTokenCount >= 0
+      ? data.shadowedTokenCount
       : null
   }
   return {
@@ -148,9 +133,9 @@ function compactSummary(match: ConversationMatch | undefined, checkpoint: Conver
 }
 
 function fallbackState(context: ConversationNodeContext<CommandState>): CommandState | undefined {
-  const done = context.matches.find(match => (match.event.type as string) === 'command/done')
+  const done = context.matches.find(match => match.event.type === 'command/done')
   const checkpoint = context.matches.find(match => compactSource(match.event) !== undefined)
-  const summary = context.matches.find(match => (match.event.type as string) === 'compact/summary')
+  const summary = context.matches.find(match => match.event.type === 'compact/summary')
   if (checkpoint === undefined) return done === undefined ? undefined : { command: commandFromDone(done) }
   const source = compactSource(checkpoint.event)
   if (source?.sourceCommandId === undefined) return done === undefined ? undefined : { command: commandFromDone(done) }
@@ -182,7 +167,7 @@ export function updateCompactionState<State extends CompactionEvidence>(
   state: State,
   match: ConversationMatch,
 ): State {
-  if ((match.event.type as string) === 'compact/summary') return { ...state, summary: match }
+  if (match.event.type === 'compact/summary') return { ...state, summary: match }
   if (compactSource(match.event) !== undefined) return { ...state, checkpoint: match }
   return state
 }
@@ -191,27 +176,28 @@ export function updateCompactionState<State extends CompactionEvidence>(
 export const commandDefinition: ConversationNodeDefinition<CommandState> = {
   kind: 'command',
   match: (event) => {
-    if ((event.type as string) === 'command/run') {
-      return { id: String((event.data as unknown as CommandRunData).commandId), role: 'start' }
+    if (event.type === 'command/run') {
+      return { id: String(event.data.commandId), role: 'start' }
     }
-    if ((event.type as string) === 'command/done') {
-      return { id: String((event.data as unknown as CommandDoneData).commandId), role: 'update' }
+    if (event.type === 'command/done') {
+      return { id: String(event.data.commandId), role: 'update' }
     }
     const checkpoint = compactSource(event)
     if (checkpoint?.sourceCommandId !== undefined) {
       return { id: String(checkpoint.sourceCommandId), role: 'update' }
     }
-    if ((event.type as string) === 'compact/start'
-      || (event.type as string) === 'compact/summary'
-      || (event.type as string) === 'compact/end') {
-      const data = event.data as unknown as { sourceCommandId?: CommandId }
-      if (data.sourceCommandId !== undefined) return { id: String(data.sourceCommandId), role: 'update' }
+    if (event.type === 'compact/start'
+      || event.type === 'compact/summary'
+      || event.type === 'compact/end') {
+      if (event.data.sourceCommandId !== undefined) {
+        return { id: String(event.data.sourceCommandId), role: 'update' }
+      }
     }
     return null
   },
   start: (_context, match) => ({ command: commandFromRun(match) }),
   update: (context, match) => {
-    if ((match.event.type as string) === 'command/done') {
+    if (match.event.type === 'command/done') {
       return { ...context.state, command: commandFromDone(match, context.state.command) }
     }
     return updateCompactionState(context.state, match)

+ 7 - 7
packages/client/ui-conversation/src/client/conversation-nodes/compaction.ts

@@ -2,6 +2,7 @@ import type { Context } from 'cordis'
 import type {
   CompactionSummaryNode, ConversationMatch, ConversationNodeContext, ConversationNodeDefinition,
 } from '@deepseek-ai/dsh-client-runtime/client'
+import type {} from '@deepseek-ai/dsh-compact/types'
 import { chatNode } from './common.ts'
 import { compactSource, compactSummary, updateCompactionState } from './command.ts'
 
@@ -18,7 +19,7 @@ interface CompactionState {
 }
 
 function fallbackState(context: ConversationNodeContext<CompactionState>): CompactionState {
-  const summary = context.matches.find(match => (match.event.type as string) === 'compact/summary')
+  const summary = context.matches.find(match => match.event.type === 'compact/summary')
   const checkpoint = context.matches.find(match => compactSource(match.event) !== undefined)
   return {
     ...summary === undefined ? {} : { summary },
@@ -34,12 +35,11 @@ export const compactionDefinition: ConversationNodeDefinition<CompactionState> =
     if (checkpoint !== undefined && checkpoint.sourceCommandId === undefined) {
       return { id: checkpoint.compactionId, role: 'update' }
     }
-    if ((event.type as string) === 'compact/start'
-      || (event.type as string) === 'compact/summary'
-      || (event.type as string) === 'compact/end') {
-      const data = event.data as unknown as { compactionId?: unknown; sourceCommandId?: unknown }
-      if (typeof data.compactionId !== 'string' || data.sourceCommandId !== undefined) return null
-      return { id: data.compactionId, role: (event.type as string) === 'compact/start' ? 'start' : 'update' }
+    if (event.type === 'compact/start'
+      || event.type === 'compact/summary'
+      || event.type === 'compact/end') {
+      if (event.data.sourceCommandId !== undefined) return null
+      return { id: String(event.data.compactionId), role: event.type === 'compact/start' ? 'start' : 'update' }
     }
     return null
   },

+ 7 - 6
packages/client/ui-conversation/src/client/conversation-nodes/inbox.ts

@@ -2,6 +2,7 @@ import type { Context } from 'cordis'
 import type {
   ConversationNodeDefinition, ConversationPreviousContext,
 } from '@deepseek-ai/dsh-client-runtime/client'
+import type {} from '@deepseek-ai/dsh-agent/types'
 
 type InboxTarget = 'next-turn' | 'next-step'
 
@@ -41,14 +42,14 @@ function inboxDefinition(target: InboxTarget): ConversationNodeDefinition<InboxS
   const kind = `inbox-${target}`
   return {
     kind,
-    match: event => (event.type as string) === 'agent/inbox/spliced'
-      && (event.data as unknown as { target?: unknown }).target === target
+    match: event => event.type === 'agent/inbox/spliced'
+      && event.data.target === target
       ? { id: String(event.seq), role: 'start' }
       : null,
-    start: (_context, match, reader) => applySplice(
-      reader.previous<InboxState>(kind),
-      match.event.data as unknown as InboxSplice,
-    ),
+    start: (_context, match, reader) => {
+      if (match.event.type !== 'agent/inbox/spliced') throw new Error(`${kind} start requires agent/inbox/spliced`)
+      return applySplice(reader.previous<InboxState>(kind), match.event.data)
+    },
     update: context => context.state,
     publication: () => 'none',
     buildViewNode: () => null,

+ 17 - 41
packages/client/ui-conversation/src/client/conversation-nodes/retry.ts

@@ -2,6 +2,7 @@ import type { Context } from 'cordis'
 import type {
   ConversationLocation, ConversationNodeDefinition, ModelRetryNode,
 } from '@deepseek-ai/dsh-client-runtime/client'
+import type {} from '@deepseek-ai/dsh-llm-retry/types'
 import type { RetryChatData } from '../contract/chat-nodes.ts'
 import { chatNode } from './common.ts'
 
@@ -12,11 +13,6 @@ declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
   }
 }
 
-type WithoutRetryProjection<Node> = Node extends unknown
-  ? Omit<Node, 'kind' | 'seq' | 'time' | 'retryState'>
-  : never
-type RetryEventData = WithoutRetryProjection<ModelRetryNode>
-
 /** Accumulated retry attempts sharing one producer-owned RetryId. */
 export interface RetryState {
   readonly turn: number
@@ -24,31 +20,14 @@ export interface RetryState {
   readonly attempts: readonly ModelRetryNode[]
 }
 
-function retryData(value: unknown): RetryEventData | undefined {
-  if (value === null || typeof value !== 'object') return undefined
-  const data = value as Record<string, unknown>
-  if (typeof data.retryId !== 'string' || data.retryId === ''
-    || !Number.isSafeInteger(data.turn) || (data.turn as number) < 0
-    || !Number.isSafeInteger(data.step) || (data.step as number) < 0
-    || !Number.isSafeInteger(data.retry) || (data.retry as number) <= 0
-    || typeof data.delayMs !== 'number' || !Number.isFinite(data.delayMs) || data.delayMs < 0
-    || typeof data.provider !== 'string' || typeof data.policyKey !== 'string'
-    || (data.mode !== 'normal' && data.mode !== 'always')
-    || data.failure === null || typeof data.failure !== 'object') return undefined
-  if (data.mode === 'normal' && (!Number.isSafeInteger(data.maxRetries) || (data.maxRetries as number) <= 0)) {
-    return undefined
-  }
-  return data as unknown as RetryEventData
-}
-
-function scheduledNode(event: { seq: number; time: number; data: unknown }): ModelRetryNode | undefined {
-  const data = retryData(event.data)
-  return data === undefined ? undefined : {
+function scheduledNode(match: Parameters<ConversationNodeDefinition['start']>[1]): ModelRetryNode | undefined {
+  if (match.event.type !== 'llm/retry') return undefined
+  return {
     kind: 'model-retry',
-    seq: event.seq,
-    time: event.time,
+    seq: match.event.seq,
+    time: match.event.time,
     retryState: 'scheduled',
-    ...data,
+    ...match.event.data,
   }
 }
 
@@ -61,33 +40,30 @@ function isClosed(location: ConversationLocation): boolean {
 export const retryDefinition: ConversationNodeDefinition<RetryState> = {
   kind: 'model-retry',
   match: (event) => {
-    if ((event.type as string) === 'llm/retry') {
-      const data = retryData(event.data)
-      if (data === undefined) return null
-      return { id: String(data.retryId), role: data.retry === 1 ? 'start' : 'update' }
+    if (event.type === 'llm/retry') {
+      return { id: String(event.data.retryId), role: event.data.retry === 1 ? 'start' : 'update' }
     }
-    if ((event.type as string) === 'llm/retry-started') {
-      const data = event.data as unknown as { retryId?: unknown }
-      return typeof data.retryId === 'string' ? { id: data.retryId, role: 'update' } : null
+    if (event.type === 'llm/retry-started') {
+      return { id: String(event.data.retryId), role: 'update' }
     }
     return null
   },
   start: (_context, match) => {
-    const node = scheduledNode(match.event)
+    const node = scheduledNode(match)
     if (node === undefined) throw new Error('model-retry start requires a valid llm/retry event')
     return { turn: node.turn, step: node.step, attempts: [node] }
   },
   update: (context, match) => {
-    if ((match.event.type as string) === 'llm/retry') {
-      const node = scheduledNode(match.event)
+    if (match.event.type === 'llm/retry') {
+      const node = scheduledNode(match)
       return node === undefined ? context.state : { ...context.state, attempts: [...context.state.attempts, node] }
     }
-    if ((match.event.type as string) !== 'llm/retry-started') return context.state
-    const data = match.event.data as unknown as { retry: number }
+    if (match.event.type !== 'llm/retry-started') return context.state
+    const retry = match.event.data.retry
     return {
       ...context.state,
       attempts: context.state.attempts.map(attempt =>
-        attempt.retry === data.retry ? { ...attempt, retryState: 'started' } : attempt),
+        attempt.retry === retry ? { ...attempt, retryState: 'started' } : attempt),
     }
   },
   buildViewNode: (context, target) => {

+ 17 - 14
packages/client/ui-conversation/src/client/conversation-nodes/tool.ts

@@ -4,6 +4,7 @@ import type {
   RunningToolCall, ToolCallBlock, ToolResultNode,
 } from '@deepseek-ai/dsh-client-runtime/client'
 import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-client-runtime/client'
+import type {} from '@deepseek-ai/dsh-tools/types'
 import type { ToolChatData } from '../contract/chat-nodes.ts'
 import { CHAT_SYNTHETIC_SEQ_OFFSETS, chatNode } from './common.ts'
 
@@ -141,27 +142,30 @@ function acceptsEdge(state: ToolState, parent: string, child: string): boolean {
 }
 
 function updateDispatch(state: ToolState, match: ConversationMatch): ToolState {
-  const data = match.event.data as unknown as DispatchData
-  const siblings = state.children.get(data.parentCallId) ?? []
-  const index = siblings.findIndex(candidate => candidate.callId === data.subCallId)
-  if ((match.event.type as string) === 'tool/code-dispatch-start') {
-    if (index >= 0 || !acceptsEdge(state, data.parentCallId, data.subCallId)) return state
+  const event = match.event
+  if (event.type !== 'tool/code-dispatch-start' && event.type !== 'tool/code-dispatch') return state
+  const data = event.data
+  const parentCallId = String(data.parentCallId)
+  const subCallId = String(data.subCallId)
+  const siblings = state.children.get(parentCallId) ?? []
+  const index = siblings.findIndex(candidate => candidate.callId === subCallId)
+  if (event.type === 'tool/code-dispatch-start') {
+    if (index >= 0 || !acceptsEdge(state, parentCallId, subCallId)) return state
     const children = new Map(state.children)
-    children.set(data.parentCallId, [...siblings, childCall(match, data)])
+    children.set(parentCallId, [...siblings, childCall(match, data)])
     const parents = new Map(state.parents)
-    parents.set(data.subCallId, data.parentCallId)
+    parents.set(subCallId, parentCallId)
     return { ...state, children, parents }
   }
-  if ((match.event.type as string) !== 'tool/code-dispatch') return state
-  if (index < 0 && !acceptsEdge(state, data.parentCallId, data.subCallId)) return state
+  if (index < 0 && !acceptsEdge(state, parentCallId, subCallId)) return state
   const previous = index < 0 ? undefined : siblings[index]
   const settled = childResult(match, data, previous)
   const children = new Map(state.children)
-  children.set(data.parentCallId, index < 0
+  children.set(parentCallId, index < 0
     ? [...siblings, settled]
     : siblings.map((child, at) => at === index ? settled : child))
   const parents = new Map(state.parents)
-  if (index < 0) parents.set(data.subCallId, data.parentCallId)
+  if (index < 0) parents.set(subCallId, parentCallId)
   return { ...state, children, parents }
 }
 
@@ -236,9 +240,8 @@ export const toolDefinition: ConversationNodeDefinition<ToolState> = {
     if (event.type === 'tool/result' && isAppendSurfaceEvent(event)) {
       return { id: String(event.data.message.source.callId), role: 'update' }
     }
-    if ((event.type as string) === 'tool/code-dispatch-start' || (event.type as string) === 'tool/code-dispatch') {
-      const data = event.data as unknown as { rootCallId: string }
-      return { id: data.rootCallId, role: 'update' }
+    if (event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch') {
+      return { id: String(event.data.rootCallId), role: 'update' }
     }
     return null
   },

+ 4 - 3
packages/client/ui-conversation/src/client/conversation-nodes/turn-error.ts

@@ -3,6 +3,7 @@ import type {
   ConversationMatch, ConversationNodeContext, ConversationNodeDefinition, TurnErrorNode,
 } from '@deepseek-ai/dsh-client-runtime/client'
 import { displayFailureMessage } from '@deepseek-ai/dsh-client-runtime/client'
+import type {} from '@deepseek-ai/dsh-llm-retry/types'
 import { chatNode } from './common.ts'
 
 declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
@@ -30,9 +31,9 @@ function lastStep(context: ConversationNodeContext<TurnErrorState>): number {
 }
 
 function retryTurn(event: Parameters<ConversationNodeDefinition['match']>[0]): number | undefined {
-  if ((event.type as string) !== 'llm/retry' && (event.type as string) !== 'llm/retry-started') return undefined
-  const turn = (event.data as unknown as { turn?: unknown }).turn
-  return Number.isSafeInteger(turn) && (turn as number) >= 0 ? turn as number : undefined
+  return event.type === 'llm/retry' || event.type === 'llm/retry-started'
+    ? event.data.turn
+    : undefined
 }
 
 function failureFrom(match: ConversationMatch): TurnErrorState['failure'] | undefined {

+ 4 - 5
packages/client/ui-conversation/src/client/conversation-nodes/turn-tail.ts

@@ -3,6 +3,7 @@ import type {
   ConversationMatch, ConversationNodeContext, ConversationNodeDefinition, TurnLocation,
 } from '@deepseek-ai/dsh-client-runtime/client'
 import { isAppendSurfaceEvent, toAssistantBlocks } from '@deepseek-ai/dsh-client-runtime/client'
+import type {} from '@deepseek-ai/dsh-llm-retry/types'
 import type {
   AssistantChatData, FinalAssistantChatData, TurnTailChatData,
 } from '../contract/chat-nodes.ts'
@@ -58,9 +59,7 @@ function turnCoordinates(event: Parameters<ConversationNodeDefinition['match']>[
     || event.type === 'step/end') {
     return { turn: event.data.turn, step: event.data.step }
   }
-  if ((event.type as string) === 'llm/retry') {
-    return event.data as unknown as { turn: number; step: number }
-  }
+  if (event.type === 'llm/retry') return { turn: event.data.turn, step: event.data.step }
   return undefined
 }
 
@@ -90,7 +89,7 @@ function closingAnchor(context: ConversationNodeContext<TurnTailState>): number
       }
       continue
     }
-    if ((event.type as string) === 'llm/retry') {
+    if (event.type === 'llm/retry') {
       steps.set(coordinates.step, { streamedText: false, finalized: false })
       continue
     }
@@ -130,7 +129,7 @@ function tailData(context: ConversationNodeContext<TurnTailState>): TurnTailChat
     const candidate = event.type === 'tool/call'
       || (event.type === 'tool/result' && isAppendSurfaceEvent(event))
       || (event.type === 'turn/end' && event.data.reason.kind === 'error')
-      || (event.type as string) === 'llm/retry'
+      || event.type === 'llm/retry'
       ? event.seq
       : undefined
     if (candidate !== undefined && (latestTranscriptSeq === undefined || candidate > latestTranscriptSeq)) {

+ 3 - 1
packages/client/ui-conversation/tests/chat-apply.spec.tsx

@@ -62,7 +62,9 @@ describe('apply wiring', () => {
     expect(entries[0]?.options.order).toBe(0)
     // Declaring is claiming: the chat entry's registration put the hole on
     // the ledger with the contract's kind/scope.
-    expect(b.slots.spec('conversation.chat.node')).toEqual({ kind: 'keyed', scope: 'session' })
+    const nodeSlot = b.slots.spec('conversation.chat.node')
+    expect(nodeSlot).toMatchObject({ kind: 'keyed', scope: 'session' })
+    expect(nodeSlot?.inject?.hooks?.turnData).toBeTypeOf('function')
     await b.runtime.dispose()
   })
 

+ 22 - 0
packages/client/ui-conversation/tests/conversation-node-definitions.spec.ts

@@ -644,6 +644,28 @@ describe('built-in conversation node Definitions', () => {
     })
   })
 
+  it('renders a historical compaction when its start remains outside the loaded window', () => {
+    const value = assembler([
+      at(10, 'compact/summary', {
+        compactionId: 'compact-windowed',
+        summary: [{ type: 'text', text: 'loaded summary' }],
+        shadowedSeqs: [1, 2, 3],
+        shadowedTokenCount: 42,
+      }),
+      at(11, 'user/message', {
+        ...textMessage('checkpoint-windowed', 'checkpoint'),
+        source: { kind: 'plugin', plugin: 'compact', compactionId: 'compact-windowed' },
+      }, { surfaceOp: { op: 'replace', start: 1, end: 3 } }),
+    ], true)
+
+    expect(node(snapshot(value), 'compaction')?.data).toMatchObject({
+      summary: 'loaded summary',
+      summaryEventSeq: 10,
+      shadowedItemCount: 3,
+      shadowedTokenCount: 42,
+    })
+  })
+
   it('suppresses a turn error when the loaded tail contains only a later retry attempt', () => {
     const value = assembler([
       at(5, 'llm/retry', {

+ 12 - 0
packages/client/ui-conversation/tsconfig.json

@@ -23,12 +23,24 @@
     {
       "path": "../runtime"
     },
+    {
+      "path": "../../core/agent"
+    },
+    {
+      "path": "../../core/tools"
+    },
+    {
+      "path": "../../interaction/commands"
+    },
     {
       "path": "../../session/session-projection"
     },
     {
       "path": "../../llm/token-meter"
     },
+    {
+      "path": "../../llm/llm-retry"
+    },
     {
       "path": "../../plan/plan-mode"
     },

+ 4 - 4
packages/compact/compact/package.json

@@ -15,14 +15,14 @@
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"
     },
+    "./types": {
+      "types": "./lib/types/types.d.ts",
+      "default": "./lib/types/types.js"
+    },
     "./checkpoint": {
       "types": "./lib/types/checkpoint.d.ts",
       "default": "./lib/types/checkpoint.js"
     },
-    "./brand": {
-      "types": "./lib/types/brand.d.ts",
-      "default": "./lib/types/brand.js"
-    },
     "./src/*": "./src/*",
     "./package.json": "./package.json"
   },

+ 3 - 1
packages/compact/compact/src/types.ts

@@ -9,7 +9,9 @@
 
 import type { ContentBlock, TokenUsage } from '@deepseek-ai/dsh-llm'
 import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
-import type { CompactionId } from './brand.ts'
+import { CompactionId } from './brand.ts'
+
+export { CompactionId }
 
 declare module '@deepseek-ai/dsh-session/types' {
   interface SessionEventMap {

+ 2 - 2
packages/core/agent/package.json

@@ -16,8 +16,8 @@
       "default": "./lib/invariant.js"
     },
     "./types": {
-      "types": "./lib/types/types.d.ts",
-      "default": "./lib/types/types.js"
+      "types": "./lib/types/session-types.d.ts",
+      "default": "./lib/types/session-types.js"
     },
     "./src/*": "./src/*",
     "./package.json": "./package.json"

+ 1 - 1
packages/core/agent/src/dispatch.ts

@@ -10,7 +10,7 @@ import type { Context, Events } from 'cordis'
 import { scopeTarget } from '@deepseek-ai/dsh-scope'
 import type { Scoped } from '@deepseek-ai/dsh-scope'
 import type { AssembleContext } from '@deepseek-ai/dsh-system-prompt'
-import type { Agent } from './types.ts'
+import type { Agent } from './runtime-types.ts'
 
 /** Extract the parameter tuple from an event handler type (its `this` is not part of the tuple). */
 type Params<F> = F extends (...args: infer P) => unknown ? P : never

+ 1 - 3
packages/core/agent/src/inbox.ts

@@ -6,9 +6,7 @@
 
 import type { MessageId } from '@deepseek-ai/dsh-llm'
 import type { Session, SessionEventMap, UserMessage } from '@deepseek-ai/dsh-session'
-
-/** One of the two ordered pending-message lists owned by an agent. */
-export type InboxTarget = 'next-turn' | 'next-step'
+import type { InboxTarget } from './session-types.ts'
 
 /** Mutable state privately owned by an {@link Inbox}. */
 type InboxState = Record<InboxTarget, UserMessage[]>

+ 3 - 2
packages/core/agent/src/index.ts

@@ -13,9 +13,10 @@ import { scopeTarget } from '@deepseek-ai/dsh-scope'
 import type { Scoped } from '@deepseek-ai/dsh-scope'
 import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
 import type { TypeRTContext, TypeRTLookup } from '@deepseek-ai/dsh-type-meta'
-import type { Agent, AgentOptions } from './types.ts'
+import type { Agent, AgentOptions } from './runtime-types.ts'
 
-export * from './types.ts'
+export * from './runtime-types.ts'
+export * from './session-types.ts'
 export * from './inbox.ts'
 export * from './model-selection.ts'
 export { agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from './dispatch.ts'

+ 3 - 19
packages/core/agent/src/types.ts → packages/core/agent/src/runtime-types.ts

@@ -2,7 +2,7 @@
  * Public agent types and live-runtime events. Durable transcript facts and
  * turn/step boundaries remain `@deepseek-ai/dsh-session` events.
  *
- * @module @deepseek-ai/dsh-agent/types
+ * @module @deepseek-ai/dsh-agent
  */
 
 import type { Context } from 'cordis'
@@ -10,7 +10,8 @@ import type { Scoped } from '@deepseek-ai/dsh-scope'
 import type { LlmCallConfig, LlmFailure, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
 import type { AgentCancelCause, Session, SessionId, UserMessage } from '@deepseek-ai/dsh-session'
 export type { AgentCancelCause } from '@deepseek-ai/dsh-session'
-import type { Inbox, InboxTarget } from './inbox.ts'
+import type { Inbox } from './inbox.ts'
+import type { InboxTarget } from './session-types.ts'
 import type {} from '@deepseek-ai/dsh-system-prompt'
 declare module '@deepseek-ai/dsh-system-prompt' {
   interface AssembleContext {
@@ -289,20 +290,3 @@ declare module 'cordis' {
     'agent/error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; error: unknown }): void
   }
 }
-
-declare module '@deepseek-ai/dsh-session/types' {
-  interface SessionEventMap {
-    /**
-     * One normalized mutation of an agent's durable pending-message lists.
-     * Live dispatch precedes projection mutation, so synchronous observers may
-     * read the pre-splice inbox to recover the removed messages.
-     */
-    'agent/inbox/spliced': {
-      target: InboxTarget
-      start: number
-      removedCount?: number
-      inserted: UserMessage[]
-      outcome?: 'canceled'
-    }
-  }
-}

+ 27 - 0
packages/core/agent/src/session-types.ts

@@ -0,0 +1,27 @@
+/**
+ * Durable agent session-event vocabulary shared with type-only consumers.
+ *
+ * @module @deepseek-ai/dsh-agent/types
+ */
+
+import type { UserMessage } from '@deepseek-ai/dsh-llm/types'
+
+/** One of the two ordered pending-message lists owned by an agent. */
+export type InboxTarget = 'next-turn' | 'next-step'
+
+declare module '@deepseek-ai/dsh-session/types' {
+  interface SessionEventMap {
+    /**
+     * One normalized mutation of an agent's durable pending-message lists.
+     * Live dispatch precedes projection mutation, so synchronous observers may
+     * read the pre-splice inbox to recover the removed messages.
+     */
+    'agent/inbox/spliced': {
+      target: InboxTarget
+      start: number
+      removedCount?: number
+      inserted: UserMessage[]
+      outcome?: 'canceled'
+    }
+  }
+}

+ 1 - 1
packages/core/session/tests/gen-persistence-catalog.spec.ts

@@ -89,7 +89,7 @@ describe('gen-persistence-catalog collectLogEvents', () => {
   it('hard-errors on an extends clause (inherited keys would escape the catalog)', () => {
     expect(() => collectLogEvents(make({
       'packages/group/fix/src/types.ts':
-        'interface Extra { \'fix/hidden\': { turn: number } }\ndeclare module \'@deepseek-ai/dsh-session\' {\n  interface SessionEventMap extends Extra {\n    /** Declared directly. */\n    \'fix/direct\': { turn: number }\n  }\n}\n',
+        'interface Extra { \'fix/hidden\': { turn: number } }\ndeclare module \'@deepseek-ai/dsh-session/types\' {\n  interface SessionEventMap extends Extra {\n    /** Declared directly. */\n    \'fix/direct\': { turn: number }\n  }\n}\n',
     }))).toThrow(/uses extends; inherited keys would join keyof SessionEventMap without a catalog row/)
   })
 

+ 4 - 0
packages/core/tools/package.json

@@ -15,6 +15,10 @@
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"
     },
+    "./types": {
+      "types": "./lib/types/types.d.ts",
+      "default": "./lib/types/types.js"
+    },
     "./presentation": {
       "types": "./lib/types/presentation.d.ts",
       "default": "./lib/types/presentation.js"

+ 1 - 35
packages/core/tools/src/code-mode.ts

@@ -14,41 +14,7 @@ import type { JsonValue } from '@deepseek-ai/dsh-session'
 import { defineTool, parameterSchemaSpecToJsonSchema } from './schema.ts'
 import { TOOL_REGISTRY_SCHEDULER } from './index.ts'
 import type { CodeDispatchLog, ToolDefinition, ToolExecutionResult, ToolRegistry, ToolRunContext } from './index.ts'
-
-declare module '@deepseek-ai/dsh-session/types' {
-  interface SessionEventMap {
-    /**
-     * One sub-dispatch STARTING inside a `run_code` program: the parent
-     * `run_code` call id, the deterministic sub-call id (`<parent>:code:<n>`,
-     * numbered in submission order), and the tool `name` with its
-     * JSON-normalized `arguments` — the exact value dispatched, normalized
-     * BEFORE dispatch, so this append can never fail on payload shape.
-     * Appended when the scheduler actually starts the call (not at
-     * submission), so a start means the tool body pipeline was entered; a
-     * call abandoned in the queue logs nothing. Log-only: `deriveMessages()`
-     * ignores it; UIs use it for live per-sub-call running state and pair it
-     * with `tool/code-dispatch` by `subCallId` (timing = the two events'
-     * `time` fields).
-     */
-    'tool/code-dispatch-start': { rootCallId: CallId; parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown }
-    /**
-     * One bridged sub-dispatch SETTLING: the pairing ids (matching the
-     * `tool/code-dispatch-start` with the same `subCallId`), the tool `name`
-     * with the same JSON-normalized `arguments`, and the sub-call's complete
-     * model-facing outcome in `tool/result`'s own vocabulary
-     * (`content` + `isError`), so UIs render a sub-call through the exact
-     * code path that renders a native call. Every started sub-call settles
-     * with exactly one of these (abort included: the aborted pipeline result
-     * is an `isError` outcome).
-     * Log-only: `deriveMessages()` ignores it, so sub-calls never re-enter
-     * model context; persistence and UIs get every call. Appended inside the
-     * parent `run_code`'s execution (the bridge drains in-flight dispatches
-     * before returning), so its execution-enclosure relation holds by
-     * construction.
-     */
-    'tool/code-dispatch': { rootCallId: CallId; parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; content: ContentBlock[] }
-  }
-}
+import type {} from './types.ts'
 
 /** The model-facing name of the Code Mode tool. */
 export const RUN_CODE_NAME = 'run_code'

+ 1 - 0
packages/core/tools/src/index.ts

@@ -85,6 +85,7 @@ export {
 } from './json-schema.ts'
 
 export type { JsonValue } from '@deepseek-ai/dsh-session'
+export type { CodeDispatchEventData, CodeDispatchStartEventData } from './types.ts'
 
 export { CodeRunFailedError, RUN_CODE_NAME } from './code-mode.ts'
 export { jsonSchemaToTs, renderToolsSdk } from './ts-types.ts'

+ 58 - 0
packages/core/tools/src/types.ts

@@ -0,0 +1,58 @@
+/**
+ * Durable Tool event vocabulary shared with type-only consumers.
+ *
+ * @module @deepseek-ai/dsh-tools/types
+ */
+
+import type { CallId } from '@deepseek-ai/dsh-llm/brand'
+import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
+
+/** Payload recorded when one nested Code Mode Tool dispatch starts. */
+export interface CodeDispatchStartEventData {
+  rootCallId: CallId
+  parentCallId: CallId
+  subCallId: CallId
+  name: string
+  arguments: unknown
+}
+
+/** Payload recorded when one nested Code Mode Tool dispatch settles. */
+export interface CodeDispatchEventData extends CodeDispatchStartEventData {
+  isError: boolean
+  content: ContentBlock[]
+}
+
+declare module '@deepseek-ai/dsh-session/types' {
+  interface SessionEventMap {
+    /**
+     * One sub-dispatch STARTING inside a `run_code` program: the parent
+     * `run_code` call id, the deterministic sub-call id (`<parent>:code:<n>`,
+     * numbered in submission order), and the tool `name` with its
+     * JSON-normalized `arguments` — the exact value dispatched, normalized
+     * BEFORE dispatch, so this append can never fail on payload shape.
+     * Appended when the scheduler actually starts the call (not at
+     * submission), so a start means the tool body pipeline was entered; a
+     * call abandoned in the queue logs nothing. Log-only: `deriveMessages()`
+     * ignores it; UIs use it for live per-sub-call running state and pair it
+     * with `tool/code-dispatch` by `subCallId` (timing = the two events'
+     * `time` fields).
+     */
+    'tool/code-dispatch-start': CodeDispatchStartEventData
+    /**
+     * One bridged sub-dispatch SETTLING: the pairing ids (matching the
+     * `tool/code-dispatch-start` with the same `subCallId`), the tool `name`
+     * with the same JSON-normalized `arguments`, and the sub-call's complete
+     * model-facing outcome in `tool/result`'s own vocabulary
+     * (`content` + `isError`), so UIs render a sub-call through the exact
+     * code path that renders a native call. Every started sub-call settles
+     * with exactly one of these (abort included: the aborted pipeline result
+     * is an `isError` outcome).
+     * Log-only: `deriveMessages()` ignores it, so sub-calls never re-enter
+     * model context; persistence and UIs get every call. Appended inside the
+     * parent `run_code`'s execution (the bridge drains in-flight dispatches
+     * before returning), so its execution-enclosure relation holds by
+     * construction.
+     */
+    'tool/code-dispatch': CodeDispatchEventData
+  }
+}

+ 4 - 0
packages/interaction/commands/package.json

@@ -15,6 +15,10 @@
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"
     },
+    "./types": {
+      "types": "./lib/types/types.d.ts",
+      "default": "./lib/types/types.js"
+    },
     "./brand": {
       "types": "./lib/types/brand.d.ts",
       "default": "./lib/types/brand.js"

+ 1 - 41
packages/interaction/commands/src/index.ts

@@ -11,24 +11,12 @@ import type { Session, SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-se
 import { CommandId } from './brand.ts'
 
 export { CommandId } from './brand.ts'
+export type { CommandSource, CommandSourceMap } from './types.ts'
 
 export const name = 'commands'
 
 const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u
 
-/**
- * Producer record for one command invocation (the `command/run` event's
- * source slot). Merge-extensible sum type mirroring `MessageSourceMap`'s
- * shape; minimal today because every executor caller is a human-facing UI
- * surface dispatching a human-typed line, so the sole variant is `user`.
- */
-export interface CommandSourceMap {
-  user: { kind: 'user' }
-}
-
-/** The union over {@link CommandSourceMap} — who issued a command line. */
-export type CommandSource = CommandSourceMap[keyof CommandSourceMap]
-
 /** Immutable metadata for a command's optional unstructured input. */
 export interface CommandInputDescriptor {
   /** Placeholder shown before the user supplies free-form input. */
@@ -131,34 +119,6 @@ class CommandLayer implements ScopeLayer {
   }
 }
 
-declare module '@deepseek-ai/dsh-session/types' {
-  interface SessionEventMap {
-    /**
-     * A resolved slash command entered its handler. Log-only (never model
-     * surface); paired with `command/done` by `commandId`, mirroring the
-     * `tool/call`↔`tool/result` pairing. The payload is structured — `name`
-     * and `args` are `parseCommand`'s own split (name and verbatim rawInput,
-     * separator whitespace included), so a consumer (a projection unit
-     * folding its own command records, a rich command card) never re-parses
-     * a line. `args` is absent when the definition sets `recordInput: false`
-     * because an authoritative domain event owns the input payload.
-     */
-    'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource }
-    /**
-     * The paired command settled. `kind`/`text` carry the handler's verbatim
-     * outcome (a thrown/aborted handler settles as `kind: 'error'` with the
-     * rendered failure). A successful command may identify the earlier
-     * authoritative domain event for a richer client-computed presentation.
-     */
-    'command/done': {
-      commandId: CommandId
-      kind: 'success' | 'error'
-      text?: string
-      sourceEventSeq?: number
-    }
-  }
-}
-
 declare module 'cordis' {
   interface Context {
     commands: CommandService

+ 48 - 0
packages/interaction/commands/src/types.ts

@@ -0,0 +1,48 @@
+/**
+ * Durable command event vocabulary shared with type-only consumers.
+ *
+ * @module @deepseek-ai/dsh-commands/types
+ */
+
+import type { CommandId } from './brand.ts'
+
+/**
+ * Producer record for one command invocation (the `command/run` event's
+ * source slot). Merge-extensible sum type mirroring `MessageSourceMap`'s
+ * shape; minimal today because every executor caller is a human-facing UI
+ * surface dispatching a human-typed line, so the sole variant is `user`.
+ */
+export interface CommandSourceMap {
+  user: { kind: 'user' }
+}
+
+/** The union over {@link CommandSourceMap} — who issued a command line. */
+export type CommandSource = CommandSourceMap[keyof CommandSourceMap]
+
+declare module '@deepseek-ai/dsh-session/types' {
+  interface SessionEventMap {
+    /**
+     * A resolved slash command entered its handler. Log-only (never model
+     * surface); paired with `command/done` by `commandId`, mirroring the
+     * `tool/call`↔`tool/result` pairing. The payload is structured — `name`
+     * and `args` are `parseCommand`'s own split (name and verbatim rawInput,
+     * separator whitespace included), so a consumer (a projection unit
+     * folding its own command records, a rich command card) never re-parses
+     * a line. `args` is absent when the definition sets `recordInput: false`
+     * because an authoritative domain event owns the input payload.
+     */
+    'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource }
+    /**
+     * The paired command settled. `kind`/`text` carry the handler's verbatim
+     * outcome (a thrown/aborted handler settles as `kind: 'error'` with the
+     * rendered failure). A successful command may identify the earlier
+     * authoritative domain event for a richer client-computed presentation.
+     */
+    'command/done': {
+      commandId: CommandId
+      kind: 'success' | 'error'
+      text?: string
+      sourceEventSeq?: number
+    }
+  }
+}

+ 0 - 4
packages/llm/llm-retry/package.json

@@ -19,10 +19,6 @@
       "types": "./lib/types/types.d.ts",
       "default": "./lib/types/types.js"
     },
-    "./brand": {
-      "types": "./lib/types/brand.d.ts",
-      "default": "./lib/types/brand.js"
-    },
     "./package.json": "./package.json"
   },
   "files": [

+ 3 - 1
packages/llm/llm-retry/src/types.ts

@@ -1,5 +1,7 @@
 import type { LlmFailure } from '@deepseek-ai/dsh-llm/types'
-import type { RetryId } from './brand.ts'
+import { RetryId } from './brand.ts'
+
+export { RetryId }
 
 declare module '@deepseek-ai/dsh-session/types' {
   interface SessionEventMap {

+ 1 - 1
packages/llm/llm-retry/tests/invariant.spec.ts

@@ -5,7 +5,7 @@ import { createUserMessage, ProviderRequestId } from '@deepseek-ai/dsh-llm'
 import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
 import InvariantService from '@deepseek-ai/dsh-invariants'
 import * as RetryInvariant from '@deepseek-ai/dsh-llm-retry/invariant'
-import { RetryId } from '@deepseek-ai/dsh-llm-retry/brand'
+import { RetryId } from '@deepseek-ai/dsh-llm-retry/types'
 import { providerForOpenStep } from '../src/history.ts'
 
 async function setup(): Promise<Context> {

+ 1 - 1
packages/llm/llm-retry/tests/persistence.spec.ts

@@ -6,7 +6,7 @@ import { Context } from 'cordis'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
 import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
 import SessionPersistenceSqlite from '@deepseek-ai/dsh-session-persistence-sqlite'
-import { RetryId } from '@deepseek-ai/dsh-llm-retry/brand'
+import { RetryId } from '@deepseek-ai/dsh-llm-retry/types'
 import type {} from '../src/index.ts'
 
 const dirs: string[] = []

+ 1 - 1
packages/llm/token-meter/tests/context-breakdown-projection.spec.ts

@@ -10,7 +10,7 @@ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import TokenMeterService from '@deepseek-ai/dsh-token-meter'
 import type { ContextBreakdownProjection } from '@deepseek-ai/dsh-token-meter/client'
-import { CompactionId } from '@deepseek-ai/dsh-compact/brand'
+import { CompactionId } from '@deepseek-ai/dsh-compact/types'
 import { contextBreakdownProjectionDefinition } from '../src/breakdown-projection.ts'
 import {
   estimateContent,

+ 1 - 1
packages/llm/token-meter/tests/token-usage-projection.spec.ts

@@ -7,7 +7,7 @@ import type { Session } from '@deepseek-ai/dsh-session'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import TokenMeterService from '@deepseek-ai/dsh-token-meter'
 import type { ContextPressureProjection, TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client'
-import { CompactionId } from '@deepseek-ai/dsh-compact/brand'
+import { CompactionId } from '@deepseek-ai/dsh-compact/types'
 
 const ZERO: TokenUsageProjection = {
   uncachedInputTokens: 0,

+ 1 - 1
packages/support/llm-replay/tests/llm-replay.spec.ts

@@ -4,7 +4,7 @@ import { join } from 'node:path'
 import { afterEach, beforeEach, describe, expect, it } from 'vitest'
 import { Context } from 'cordis'
 import type { SessionEvent } from '@deepseek-ai/dsh-session'
-import { CompactionId } from '@deepseek-ai/dsh-compact/brand'
+import { CompactionId } from '@deepseek-ai/dsh-compact/types'
 import LlmService, { CallId, createUserMessage, GenerateOptions, LlmAdapter, StreamChunk } from '@deepseek-ai/dsh-llm'
 import {
   type ReplayEntry,

+ 18 - 0
pnpm-lock.yaml

@@ -1495,6 +1495,9 @@ importers:
 
   packages/client/runtime:
     dependencies:
+      '@deepseek-ai/dsh-agent':
+        specifier: workspace:^
+        version: link:../../core/agent
       '@deepseek-ai/dsh-client-connection':
         specifier: workspace:^
         version: link:../connection
@@ -1522,6 +1525,9 @@ importers:
       '@deepseek-ai/dsh-session-title':
         specifier: workspace:^
         version: link:../../session/session-title
+      '@deepseek-ai/dsh-tools':
+        specifier: workspace:^
+        version: link:../../core/tools
       immer:
         specifier: ^10.1.1
         version: 10.2.0
@@ -1656,6 +1662,9 @@ importers:
         specifier: ^2.0.0
         version: 2.1.1
     devDependencies:
+      '@deepseek-ai/dsh-agent':
+        specifier: workspace:^
+        version: link:../../core/agent
       '@deepseek-ai/dsh-client-locale':
         specifier: workspace:^
         version: link:../locale
@@ -1677,6 +1686,9 @@ importers:
       '@deepseek-ai/dsh-client-ui-slots':
         specifier: workspace:^
         version: link:../ui-slots
+      '@deepseek-ai/dsh-commands':
+        specifier: workspace:^
+        version: link:../../interaction/commands
       '@deepseek-ai/dsh-compact':
         specifier: workspace:^
         version: link:../../compact/compact
@@ -1686,6 +1698,9 @@ importers:
       '@deepseek-ai/dsh-invariants':
         specifier: workspace:^
         version: link:../../support/invariants
+      '@deepseek-ai/dsh-llm-retry':
+        specifier: workspace:^
+        version: link:../../llm/llm-retry
       '@deepseek-ai/dsh-permission':
         specifier: workspace:^
         version: link:../../interaction/permission
@@ -1701,6 +1716,9 @@ importers:
       '@deepseek-ai/dsh-tool-todo':
         specifier: workspace:^
         version: link:../../todo/tool-todo
+      '@deepseek-ai/dsh-tools':
+        specifier: workspace:^
+        version: link:../../core/tools
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31

+ 11 - 8
scripts/gen-persistence-catalog.ts

@@ -18,8 +18,11 @@ const OUT = 'docs/persistence-catalog.md'
  * doc-typecheck, since their imported types are not standalone-compilable). */
 const FENCE = 'ts persistence-catalog'
 
-/** The package whose module id plugin merges augment (`declare module '…'`). */
-const SESSION_MODULE = '@deepseek-ai/dsh-session'
+/** The package that owns the durable event vocabulary. */
+const SESSION_PACKAGE = '@deepseek-ai/dsh-session'
+
+/** The type-only module that plugin declaration merges augment. */
+const SESSION_TYPES_MODULE = '@deepseek-ai/dsh-session/types'
 
 /** Event-envelope declarations rendered before the per-event vocabulary. */
 const EVENT_ENVELOPE_TYPE_NAMES = [
@@ -115,7 +118,7 @@ function declarationText(text: string, sf: ts.SourceFile, node: ts.Node): string
 /**
  * Every `interface SessionEventMap` declaration in a source file: the owning
  * top-level declaration (in `@deepseek-ai/dsh-session`) and any declaration
- * merge inside a `declare module '@deepseek-ai/dsh-session'` block. Both forms
+ * merge inside a `declare module '@deepseek-ai/dsh-session/types'` block. Both forms
  * declare members of the SAME merged interface, so both are catalogued
  * uniformly. `topLevel` distinguishes the owning form so the caller can verify
  * it actually lives in the owning package — an unrelated local interface that
@@ -125,7 +128,7 @@ function sessionEventMapDecls(sf: ts.SourceFile): { decl: ts.InterfaceDeclaratio
   const decls: { decl: ts.InterfaceDeclaration; topLevel: boolean }[] = []
   for (const stmt of sf.statements) {
     if (ts.isInterfaceDeclaration(stmt) && stmt.name.text === 'SessionEventMap') decls.push({ decl: stmt, topLevel: true })
-    if (ts.isModuleDeclaration(stmt) && ts.isStringLiteral(stmt.name) && stmt.name.text === SESSION_MODULE
+    if (ts.isModuleDeclaration(stmt) && ts.isStringLiteral(stmt.name) && stmt.name.text === SESSION_TYPES_MODULE
       && stmt.body && ts.isModuleBlock(stmt.body)) {
       for (const inner of stmt.body.statements) {
         if (ts.isInterfaceDeclaration(inner) && inner.name.text === 'SessionEventMap') decls.push({ decl: inner, topLevel: false })
@@ -174,8 +177,8 @@ export function collectLogEvents(scanRoot: string = root): LogEventEntry[] {
         // the owning package. Same-named interfaces elsewhere are different
         // types and must not enter the on-disk catalog.
         const pkg = packageNameFor(rel, scanRoot)
-        if (pkg !== SESSION_MODULE) {
-          violations.push(`top-level interface SessionEventMap (${declSrc}) is outside ${SESSION_MODULE} (package ${pkg ?? 'unknown'}). Rename the interface, or contribute events via declare module '${SESSION_MODULE}'.`)
+        if (pkg !== SESSION_PACKAGE) {
+          violations.push(`top-level interface SessionEventMap (${declSrc}) is outside ${SESSION_PACKAGE} (package ${pkg ?? 'unknown'}). Rename the interface, or contribute events via declare module '${SESSION_TYPES_MODULE}'.`)
           continue
         }
         const exported = decl.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false
@@ -243,7 +246,7 @@ export function collectEventEnvelopeTypes(scanRoot: string = root): EventEnvelop
     const abs = resolve(scanRoot, rel)
     const text = readFileSync(abs, 'utf8')
     if (!EVENT_ENVELOPE_TYPE_NAMES.some(name => text.includes(name))) continue
-    if (packageNameFor(rel, scanRoot) !== SESSION_MODULE) continue
+    if (packageNameFor(rel, scanRoot) !== SESSION_PACKAGE) continue
     const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true)
     for (const stmt of sf.statements) {
       if (!ts.isTypeAliasDeclaration(stmt) || !wanted.has(stmt.name.text)) continue
@@ -352,7 +355,7 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv
     '',
     '# Session Persistence Event Catalog',
     '',
-    'Every event type that can appear in a session\'s durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](subsystems/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](subsystems/persistence.md) (how the log is made durable), and the generated region of [session.md](subsystems/session.md#cordis-surface) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit).',
+    'Every event type that can appear in a session\'s durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge into `@deepseek-ai/dsh-session/types` in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](subsystems/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](subsystems/persistence.md) (how the log is made durable), and the generated region of [session.md](subsystems/session.md#cordis-surface) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit).',
     '',
     'This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md).',
     '',

+ 8 - 8
scripts/type-equiv.manifest.json

@@ -114,12 +114,12 @@
     {
       "doc": "docs/subsystems/core.md",
       "symbol": "InboxTarget",
-      "source": "packages/core/agent/src/inbox.ts"
+      "source": "packages/core/agent/src/session-types.ts"
     },
     {
       "doc": "docs/subsystems/core.md",
       "symbol": "CancelOptions",
-      "source": "packages/core/agent/src/types.ts"
+      "source": "packages/core/agent/src/runtime-types.ts"
     },
     {
       "doc": "docs/subsystems/core.md",
@@ -129,22 +129,22 @@
     {
       "doc": "docs/subsystems/core.md",
       "symbol": "Agent",
-      "source": "packages/core/agent/src/types.ts"
+      "source": "packages/core/agent/src/runtime-types.ts"
     },
     {
       "doc": "docs/subsystems/core.md",
       "symbol": "PreStepDecision",
-      "source": "packages/core/agent/src/types.ts"
+      "source": "packages/core/agent/src/runtime-types.ts"
     },
     {
       "doc": "docs/subsystems/core.md",
       "symbol": "RequestErrorAction",
-      "source": "packages/core/agent/src/types.ts"
+      "source": "packages/core/agent/src/runtime-types.ts"
     },
     {
       "doc": "docs/subsystems/core.md",
       "symbol": "SessionStartSource",
-      "source": "packages/core/agent/src/types.ts"
+      "source": "packages/core/agent/src/runtime-types.ts"
     },
     {
       "doc": "docs/subsystems/scope.md",
@@ -1698,12 +1698,12 @@
     {
       "doc": "docs/subsystems/core.md",
       "symbol": "AgentStatus",
-      "source": "packages/core/agent/src/types.ts"
+      "source": "packages/core/agent/src/runtime-types.ts"
     },
     {
       "doc": "docs/subsystems/core.md",
       "symbol": "AgentOptions",
-      "source": "packages/core/agent/src/types.ts"
+      "source": "packages/core/agent/src/runtime-types.ts"
     }
   ]
 }

+ 3 - 0
tsconfig.base.json

@@ -71,8 +71,11 @@
       "@deepseek-ai/dsh-llm-retry/types": ["./packages/llm/llm-retry/src/types.ts"],
       "@deepseek-ai/dsh-llm/message": ["./packages/llm/llm/src/message.ts"],
       "@deepseek-ai/dsh-commands/brand": ["./packages/interaction/commands/src/brand.ts"],
+      "@deepseek-ai/dsh-commands/types": ["./packages/interaction/commands/src/types.ts"],
       "@deepseek-ai/dsh-compact/checkpoint": ["./packages/compact/compact/src/checkpoint.ts"],
+      "@deepseek-ai/dsh-compact/types": ["./packages/compact/compact/src/types.ts"],
       "@deepseek-ai/dsh-tools/presentation": ["./packages/core/tools/src/presentation.ts"],
+      "@deepseek-ai/dsh-tools/types": ["./packages/core/tools/src/types.ts"],
       "@deepseek-ai/dsh-tool-subagent-control/list-agents": ["./packages/subagent/tool-subagent-control/src/list-agents.ts"],
       "@deepseek-ai/dsh-user-approval/types": ["./packages/interaction/user-approval/src/types.ts"],
       "@deepseek-ai/dsh-user-interaction/types": ["./packages/interaction/user-interaction/src/types.ts"],