types.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import type { Branded, CallId, ContentBlock, MessageSource, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm'
  2. /** Identifies one session in the store (and its persistence artifacts). */
  3. export type SessionId = Branded<'SessionId'>
  4. /** Brand a string as a {@link SessionId}. */
  5. export function SessionId(id: string): SessionId {
  6. return id as SessionId
  7. }
  8. /**
  9. * Immutable session metadata — written once at creation and never rewritten.
  10. *
  11. * Kept SEPARATE from the event log deliberately: format-version, cwd, and
  12. * lineage are storage concerns, not conversation events, so they stay out of
  13. * {@link SessionEventMap} and never reach `deriveMessages()`. Every reference
  14. * system (pi's `version: 3` header, Codex's `SessionMeta`, Claude Code's tail
  15. * metadata) writes such a header.
  16. */
  17. export interface SessionHeader {
  18. /** On-disk format version; a persistence backend rejects unknown versions. */
  19. version: number
  20. /** The session's id (mirrors the {@link Session}'s id). */
  21. id: SessionId
  22. /** Unix epoch milliseconds when the session was created. */
  23. createdAt: number
  24. /** Absolute working directory the session was created in (if any). */
  25. cwd?: string
  26. /** The session this one was forked from (seed lineage), if any. */
  27. parentSession?: SessionId
  28. }
  29. /**
  30. * Options for creating a {@link Session} via the store. `seed` replays/forks
  31. * an existing event log; `meta` carries the caller-supplied storage fields the
  32. * store folds into a {@link SessionHeader}.
  33. */
  34. export interface CreateSessionOptions {
  35. /** Events to seed the new session with (replay/fork). */
  36. seed?: SessionEvent[]
  37. /**
  38. * Creation metadata. The store fills in `version`/`id` and defaults
  39. * `createdAt` to now; the caller supplies the storage-level fields (validated
  40. * absolute `cwd`, `parentSession` lineage, and — when reconstructing a
  41. * persisted session — the original `createdAt` to preserve it).
  42. */
  43. meta?: { cwd?: string; parentSession?: SessionId; createdAt?: number }
  44. }
  45. /**
  46. * What started a turn.
  47. * Merge-extensible sum type (same pattern as MessageSourceMap).
  48. */
  49. export interface TurnTriggerMap {
  50. message: { kind: 'message'; source: MessageSource }
  51. continuation: { kind: 'continuation' }
  52. /**
  53. * An out-of-band context injection (`agent.inject()`) made while the agent
  54. * was idle. The loop wraps the injected `context/message` in a one-shot turn
  55. * (`turn/start` → `context/message` → `turn/end`) so every event in the log
  56. * stays turn-enclosed — the durability/replay boundary is the turn, and a
  57. * bare event between turns would otherwise be indistinguishable from a crash
  58. * tail on reload.
  59. */
  60. injection: { kind: 'injection'; source: MessageSource }
  61. }
  62. export type TurnTrigger = TurnTriggerMap[keyof TurnTriggerMap]
  63. /**
  64. * Why a turn ended.
  65. * Merge-extensible sum type.
  66. *
  67. * `max-tokens` mirrors the model-call `FinishReasonMap` variant (DeepSeek's
  68. * `length`): the turn ended because a step hit the output-token ceiling, not
  69. * because the model chose to stop. The agent-loop surfaces it via the rule
  70. * "any `max-tokens` step in the turn makes the turn end `max-tokens`" (a
  71. * continuation plugin can run further steps after one, but the cut-short fact
  72. * still wins). It is distinct from `completed` so a consumer (e.g. the ACP
  73. * bridge mapping to `StopReason: 'max_tokens'`) can tell a clean stop from a
  74. * truncated one. The next variants to add — when an adapter/loop first emits
  75. * them — are `refusal` and `max_turn_requests` (both named by the ACP RFC as ACP
  76. * stop reasons); no current adapter produces a `refusal` finish (unknown
  77. * DeepSeek finish reasons collapse to `error`), so it is deliberately omitted
  78. * until one does.
  79. */
  80. export interface TurnEndReasonMap {
  81. completed: { kind: 'completed' }
  82. aborted: { kind: 'aborted'; reason?: string }
  83. error: { kind: 'error'; message: string; code?: string }
  84. disposed: { kind: 'disposed' }
  85. 'max-tokens': { kind: 'max-tokens' }
  86. /**
  87. * The turn never ended on its own: the process crashed mid-turn and a
  88. * persistence backend later closed the orphaned (open) turn on reload so the
  89. * log stays balanced. SYNTHESIZED by the backend's crash-recovery repair — no
  90. * loop ever emits this. Its events are real (they were durably appended before
  91. * the crash) and are PRESERVED, not discarded: a single turn can be huge in a
  92. * long-horizon task (many steps, large tool output), so truncating it would
  93. * lose real work. The marker records that the turn was cut short, not that the
  94. * model completed it. See the session-persistence RFC.
  95. */
  96. interrupted: { kind: 'interrupted' }
  97. }
  98. export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap]
  99. /**
  100. * The session event vocabulary — the append-only source of truth for an
  101. * agent's whole interaction history. The LLM message history is *derived*
  102. * from this log; nothing else is authoritative. Replay = re-derive from the
  103. * same events; trace/telemetry = subscribe to the log.
  104. *
  105. * Merge-extensible: plugins declare extra event types via declaration merging
  106. * (e.g. a compaction plugin adds `'compaction/marker'`).
  107. *
  108. * Durability contract (what a persistence backend relies on): the durable log
  109. * persists every event verbatim, INCLUDING `assistant/chunk` — `seq` must stay
  110. * contiguous (`seq = log.length`), so chunks cannot be filtered out of the
  111. * canonical log. All `event.data` must be JSON-serializable — `Session.append`
  112. * (and the seed path in the constructor) enforces this at the source (throwing
  113. * on non-serializable data), so a bad event never enters the log and
  114. * `session.events` always equals what a backend can persist. Adding a new event
  115. * type that carries non-serializable data, or that breaks the turn/step nesting
  116. * the invariants plugin checks, is a breaking change to the on-disk format.
  117. */
  118. export interface SessionEventMap {
  119. 'turn/start': { turn: number; trigger: TurnTrigger }
  120. 'turn/end': { turn: number; reason: TurnEndReason }
  121. 'step/start': { turn: number; step: number }
  122. 'step/end': { turn: number; step: number }
  123. /** A user-visible prompt (queued message drained at turn start). */
  124. 'user/message': { content: ContentBlock[]; source: MessageSource }
  125. /**
  126. * In-session context injection (file-change notices, subdir AGENTS.md,
  127. * skill content, cron notifications, …). Rendered into the derived history
  128. * as tagged synthetic context — NOT a user prompt.
  129. */
  130. 'context/message': { content: ContentBlock[]; source: MessageSource }
  131. /** Raw stream chunk — token-level replay fidelity. */
  132. 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }
  133. /** Assembled assistant message for one step (derived history uses this). */
  134. 'assistant/message': { turn: number; step: number; content: ContentBlock[] }
  135. 'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string }
  136. 'tool/result': { turn: number; step: number; callId: CallId; content: ContentBlock[]; isError: boolean; error?: { name: string; code: string } }
  137. /** Steering content injected between steps of a running turn. */
  138. 'steering/message': { turn: number; content: ContentBlock[]; source: MessageSource }
  139. 'usage': { turn: number; step: number; usage: TokenUsage }
  140. 'error': { turn: number; step: number; message: string; code?: string }
  141. }
  142. export type SessionEventType = keyof SessionEventMap
  143. /**
  144. * One immutable entry in the session log.
  145. *
  146. * A proper discriminated union over `type` (not independent `type`/`data`
  147. * unions), so `switch (event.type)` narrows `event.data` without casts.
  148. */
  149. export type SessionEvent<T extends SessionEventType = SessionEventType> = {
  150. [K in SessionEventType]: {
  151. type: K
  152. /** Monotonic sequence number within the session. */
  153. seq: number
  154. /** Unix epoch milliseconds. */
  155. time: number
  156. data: SessionEventMap[K]
  157. }
  158. }[T]