146 lines
6.1 KiB
TypeScript
146 lines
6.1 KiB
TypeScript
|
|
/**
|
||
|
|
* Telemetry seam for the DeepSeek Harness.
|
||
|
|
*
|
||
|
|
* The seam owns the CAPTURE side of session-event reporting — which records
|
||
|
|
* exist (the chunk projection), what they carry (the logical record), when
|
||
|
|
* they are handed over (adoption, the per-append firehose, lifecycle
|
||
|
|
* forwarding), and the HMR handoff cursor. Everything downstream of
|
||
|
|
* {@link Telemetry.emit} — batching, retry, queueing, loss policy — is the
|
||
|
|
* reporting SDK's territory and is deliberately not modelled here. The
|
||
|
|
* design and its trade-offs are pinned in
|
||
|
|
* .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md.
|
||
|
|
*
|
||
|
|
* @module @deepseek-ai/dsh-session-telemetry
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { Context, Service } from 'cordis'
|
||
|
|
|
||
|
|
declare module 'cordis' {
|
||
|
|
interface Context {
|
||
|
|
telemetry: Telemetry
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Events {
|
||
|
|
/**
|
||
|
|
* Redact one outbound record before it reaches the backend. The innermost
|
||
|
|
* `next()` applies the seam's conservative default rule set
|
||
|
|
* (credential-shape scrubbing); listeners stack stricter rules by
|
||
|
|
* transforming its return value, and returning without `next()` replaces
|
||
|
|
* the default — the exported record is then only as clean as the
|
||
|
|
* replacing rule. Dispatched synchronously on the capture hot path inside
|
||
|
|
* the coordinator's containment: a throwing listener withholds that one
|
||
|
|
* record (fail-closed) and never reaches the agent loop. Redaction
|
||
|
|
* applies to the exported copy only; the canonical session log is never
|
||
|
|
* rewritten.
|
||
|
|
* @param record - the candidate record, already the coordinator's own deep
|
||
|
|
* copy; listeners return a (possibly new) record and must not mutate it.
|
||
|
|
* @mode waterfall
|
||
|
|
*/
|
||
|
|
'telemetry/redact'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Severity of a telemetry record, pre-mapped at capture so a receiver can
|
||
|
|
* alert with zero configuration: `error` for events whose own outcome flag
|
||
|
|
* says so (`tool/result.isError`, `turn/end` error reasons, `compact/end`
|
||
|
|
* errors) and for `agent-error` operational records, `warn` for
|
||
|
|
* `prompt/blocked`, `info` for everything else.
|
||
|
|
*/
|
||
|
|
export type TelemetrySeverity = 'info' | 'warn' | 'error'
|
||
|
|
|
||
|
|
/**
|
||
|
|
* One logical record handed to a backend — the seam's whole outbound
|
||
|
|
* vocabulary. Ledger records mirror session-log events one-to-one;
|
||
|
|
* operational records (`channel: 'ops'`) carry the two signals with no log
|
||
|
|
* home (`agent-error`, `shutdown`) and deliberately omit `event.seq`-style
|
||
|
|
* identity so they can never be mistaken for ledger rows.
|
||
|
|
*/
|
||
|
|
export interface TelemetryRecord {
|
||
|
|
/** Ledger (session-log mirror) or ops (operational signal) channel; backends keep the two under separate instrumentation scopes. */
|
||
|
|
channel: 'ledger' | 'ops'
|
||
|
|
/** Unix epoch milliseconds — the source event's append time for ledger records, the emission time for ops records. */
|
||
|
|
time: number
|
||
|
|
/** Pre-mapped alerting severity; see {@link TelemetrySeverity}. */
|
||
|
|
severity: TelemetrySeverity
|
||
|
|
/**
|
||
|
|
* Identity attributes, deliberately minimal: ledger records carry
|
||
|
|
* `session.id`, `event.type`, `event.seq`, plus `session.cwd` /
|
||
|
|
* `session.parent_id` when the header has them; ops records carry
|
||
|
|
* `telemetry.op`, `session.id`, and (for `agent-error`) `agent.id`,
|
||
|
|
* `turn`, `step`, `error.name`. Anything recoverable from the body is
|
||
|
|
* intentionally NOT duplicated here.
|
||
|
|
*/
|
||
|
|
attributes: Record<string, string | number>
|
||
|
|
/**
|
||
|
|
* The complete payload: a deep copy of the session event's `data` for
|
||
|
|
* ledger records (JSON-serializable by `Session.append`'s own
|
||
|
|
* validation), or the op payload for ops records. Never mutated after
|
||
|
|
* handoff.
|
||
|
|
*/
|
||
|
|
body: unknown
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The backend contract the coordinator hands records to — the minimum any
|
||
|
|
* reporting SDK satisfies with zero bending. {@link Telemetry} is its
|
||
|
|
* service-registered form; tests compose the coordinator with a bare
|
||
|
|
* implementation of this interface.
|
||
|
|
*/
|
||
|
|
export interface TelemetryBackend {
|
||
|
|
/**
|
||
|
|
* Hand one record to the backend's pipeline. MUST be a non-blocking
|
||
|
|
* enqueue — the coordinator calls this synchronously from the
|
||
|
|
* `session/event` hot path, so anything slower than a queue push would tax
|
||
|
|
* the agent loop. Errors thrown here are contained by the coordinator and
|
||
|
|
* logged; they never reach the loop.
|
||
|
|
* @param record - the logical record to report; owned by the backend after the call.
|
||
|
|
*/
|
||
|
|
emit(record: TelemetryRecord): void
|
||
|
|
/**
|
||
|
|
* Optional hint that a natural boundary (turn end) passed — a backend may
|
||
|
|
* forward it to its SDK's flush so records land at turn boundaries. Called
|
||
|
|
* fire-and-forget; implementations must not block and must not throw
|
||
|
|
* meaningfully (the coordinator contains exceptions).
|
||
|
|
*/
|
||
|
|
flush?(): void
|
||
|
|
/**
|
||
|
|
* Forward the fiber's disposal to the SDK: flush whatever is queued and
|
||
|
|
* reach quiescence, per the SDK's own shutdown contract. Awaited by the
|
||
|
|
* coordinator's dispose; a rejection is logged as a warning and never
|
||
|
|
* fails application teardown.
|
||
|
|
* @returns resolves when the backend's pipeline has quiesced.
|
||
|
|
*/
|
||
|
|
shutdown(): Promise<void>
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The backend contract in its loadable form: one implementation per context —
|
||
|
|
* the cordis `Service` registration under the `telemetry` key throws on a
|
||
|
|
* duplicate, cordis' standard behavior. A backend composes a
|
||
|
|
* {@link TelemetryCoordinator} in its constructor to install the capture side.
|
||
|
|
*/
|
||
|
|
export abstract class Telemetry extends Service implements TelemetryBackend {
|
||
|
|
constructor(ctx: Context) {
|
||
|
|
super(ctx, 'telemetry')
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* See {@link TelemetryBackend.emit} — the seam declaration is the contract's one home.
|
||
|
|
* @param record - the logical record to report; owned by the backend after the call.
|
||
|
|
*/
|
||
|
|
abstract emit(record: TelemetryRecord): void
|
||
|
|
|
||
|
|
/** See {@link TelemetryBackend.flush}. */
|
||
|
|
flush?(): void
|
||
|
|
|
||
|
|
/**
|
||
|
|
* See {@link TelemetryBackend.shutdown}.
|
||
|
|
* @returns resolves when the backend's pipeline has quiesced.
|
||
|
|
*/
|
||
|
|
abstract shutdown(): Promise<void>
|
||
|
|
}
|
||
|
|
|
||
|
|
export { TelemetryCoordinator } from './coordinator.ts'
|
||
|
|
export { applyDefaultRedaction, REDACTION_PLACEHOLDER } from './redact.ts'
|