index.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. /**
  2. * Telemetry Service Definition for the DeepSeek Harness.
  3. *
  4. * This package owns the CAPTURE side of session-event reporting — which records
  5. * exist (the chunk projection), what they carry (the logical record), when
  6. * they are captured (adoption, the per-append firehose, lifecycle
  7. * forwarding), live versus on-demand canonical-log capture, and the HMR
  8. * cursor. Everything downstream of
  9. * {@link Telemetry.emit} — batching, retry, queueing, and loss policy — is the
  10. * reporting SDK's territory and is deliberately not modelled here. The
  11. * design and its trade-offs are pinned in
  12. * .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md.
  13. *
  14. * @module @deepseek-ai/dsh-session-telemetry
  15. */
  16. import { Context, Service } from '@deepseek-ai/cordis'
  17. declare module '@deepseek-ai/cordis' {
  18. interface Context {
  19. telemetry: Telemetry
  20. }
  21. interface Events {
  22. /**
  23. * Transform one outbound record before it reaches the backend. This
  24. * waterfall is the Service Definition's redaction extension point. It ships NO rules
  25. * of its own: the
  26. * innermost `next()` passes the record through unchanged, and with no
  27. * listener mounted records reach the backend as captured, so exported
  28. * data is exactly as clean as the rules a deployment mounts. Listeners
  29. * stack by transforming `next()`'s return value; returning without
  30. * `next()` replaces everything beneath. Dispatched synchronously on the
  31. * capture hot path inside the coordinator's containment: a throwing
  32. * listener withholds that one record (fail-closed) and never reaches the
  33. * agent loop. Live capture dispatches at append time; on-demand capture
  34. * dispatches while reading the canonical log. Redaction applies to the
  35. * exported copy only; the canonical session log is never rewritten.
  36. * @param record - the candidate record, already the coordinator's own deep
  37. * copy; listeners return a (possibly new) record and must not mutate it.
  38. * @mode waterfall
  39. */
  40. 'telemetry/record'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord
  41. }
  42. }
  43. /**
  44. * Severity of a telemetry record, pre-mapped at capture so a receiver can
  45. * alert with zero configuration: `error` for events whose own outcome flag
  46. * says so (the tool-result block's `isError`, `turn/end` error reasons) and for
  47. * `agent-error` operational records. Captured events otherwise default to
  48. * `info`; `warn` remains available to `telemetry/record` policies and
  49. * backends.
  50. */
  51. export type TelemetrySeverity = 'info' | 'warn' | 'error'
  52. /**
  53. * One logical record handed to a backend — the capture contract's whole outbound
  54. * vocabulary. Ledger records mirror session-log events one-to-one;
  55. * operational records (`channel: 'ops'`) carry the two signals with no log
  56. * home (`agent-error`, `shutdown`) and deliberately omit `event.seq`-style
  57. * identity so they can never be mistaken for ledger rows.
  58. */
  59. export interface TelemetryRecord {
  60. /** Ledger (session-log mirror) or ops (operational signal) channel; backends keep the two under separate instrumentation scopes. */
  61. channel: 'ledger' | 'ops'
  62. /** Unix epoch milliseconds — the source event's append time for ledger records, the emission time for ops records. */
  63. time: number
  64. /** Pre-mapped alerting severity; see {@link TelemetrySeverity}. */
  65. severity: TelemetrySeverity
  66. /**
  67. * Identity attributes, deliberately minimal: ledger records carry
  68. * `session.id`, `event.type`, `event.seq`, plus `session.cwd` /
  69. * `session.parent_id` / `session.seed_length` when the header has them;
  70. * ops records carry `telemetry.op`, `session.id`, and (for `agent-error`)
  71. * `agent.id`, `turn`, `step`, `error.name`. Anything recoverable from the
  72. * body is intentionally NOT duplicated here.
  73. */
  74. attributes: Record<string, string | number>
  75. /**
  76. * The complete payload: a deep copy of the session event's `data` for
  77. * ledger records (JSON-serializable by `Session.append`'s own
  78. * validation), or the op payload for ops records. Never mutated after
  79. * handoff.
  80. */
  81. body: unknown
  82. }
  83. /**
  84. * The minimum backend contract the coordinator requires. {@link Telemetry} is
  85. * its service-registered form; tests compose the coordinator with a bare
  86. * implementation of this interface.
  87. */
  88. export interface TelemetryBackend {
  89. /**
  90. * Hand one record to the backend's pipeline. MUST be a non-blocking
  91. * enqueue — the coordinator calls this synchronously from the
  92. * `session/event` hot path or an explicit canonical-log capture, so anything
  93. * slower than a queue push would tax the agent loop or feedback handling.
  94. * Errors thrown here are contained by the coordinator and logged; they
  95. * never reach the loop.
  96. * @param record - the logical record to report; owned by the backend after the call.
  97. */
  98. emit(record: TelemetryRecord): void
  99. /**
  100. * Optional hint that a turn ended. A backend may forward it to its SDK's
  101. * flush so records are exported after each turn. Called
  102. * fire-and-forget; implementations must not block and must not throw
  103. * meaningfully (the coordinator contains exceptions). Most backends should
  104. * leave this unimplemented and let their SDK's own batching cadence govern
  105. * export timing: a backend that does implement it owns the interaction
  106. * between its concurrent flushes and {@link shutdown}'s drain (the OTel
  107. * backend leaves it unimplemented for exactly that hazard — see the
  108. * revival Agent Note).
  109. */
  110. flush?(): void
  111. /**
  112. * Forward the fiber's disposal to the SDK: flush whatever is queued and
  113. * reach quiescence, per the SDK's own shutdown contract. Everything
  114. * emitted before this call must still be delivered — including records
  115. * enqueued while a {@link flush} hint is in flight, so a backend whose SDK
  116. * guards against concurrent flushes orders behind the outstanding one (the
  117. * coordinator emits its dispose-time `shutdown` markers immediately before
  118. * calling this). Awaited by the coordinator's dispose; a rejection is
  119. * logged as a warning and never fails application teardown.
  120. * The coordinator captures dispose-time shutdown markers immediately before
  121. * this call for live capture; on-demand capture creates no ops records.
  122. * @returns resolves when the backend's pipeline has quiesced.
  123. */
  124. shutdown(): Promise<void>
  125. }
  126. /**
  127. * Deployment-selected session-sharing policy disclosed by a mounted
  128. * {@link Telemetry} backend to human-facing acknowledgement surfaces (the
  129. * `/feedback` command's confirmation text). The seam owns the vocabulary so
  130. * any backend can disclose a policy without depending on the OTel package;
  131. * the values mirror the OTel backend's serialized `TelemetryMode` choices.
  132. */
  133. export type TelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled'
  134. /**
  135. * Loadable form of the backend contract: one implementation per context —
  136. * the cordis `Service` registration under the `telemetry` key throws on a
  137. * duplicate, cordis' standard behavior. A backend composes a
  138. * {@link TelemetryCoordinator} in its constructor to install the capture side.
  139. */
  140. export abstract class Telemetry extends Service implements TelemetryBackend {
  141. constructor(ctx: Context) {
  142. super(ctx, 'telemetry')
  143. }
  144. /**
  145. * Deployment-selected session-sharing policy, disclosed for acknowledgement
  146. * surfaces that report whether recorded feedback leaves the process. Every
  147. * backend must disclose its policy; a consumer renders "not configured" only
  148. * when no telemetry service is mounted. The seam owns this vocabulary so the
  149. * disclosure is backend-independent.
  150. */
  151. abstract readonly sharing: TelemetrySharingStatus
  152. /**
  153. * See {@link TelemetryBackend.emit} — that declaration is the contract's one home.
  154. * @param record - the logical record to report; owned by the backend after the call.
  155. */
  156. abstract emit(record: TelemetryRecord): void
  157. /** See {@link TelemetryBackend.flush}. */
  158. flush?(): void
  159. /**
  160. * See {@link TelemetryBackend.shutdown}.
  161. * @returns resolves when the backend's pipeline has quiesced.
  162. */
  163. abstract shutdown(): Promise<void>
  164. }
  165. export { TelemetryCoordinator, type TelemetryCapture } from './coordinator.ts'