mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
refactor(telemetry): strongly type sharing mode
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md
|
||||
2026-08-05-feedback-gated-session-telemetry.md: 25cc17f75629f72d7351eb0537d72b700c84411f
|
||||
2026-08-05-feedback-gated-session-telemetry.zh.md: b0e84e60e27fa20f66113c11db62026583a27a19
|
||||
2026-08-05-feedback-gated-session-telemetry.md: 00a8f23fa6bf69f10277ad0d9f2513a0df73de16
|
||||
2026-08-05-feedback-gated-session-telemetry.zh.md: 888ce48abe7a5ce2212c152730f8203f5747ef41
|
||||
|
||||
@@ -10,7 +10,7 @@ Session telemetry originally has one mounted behavior: every accepted record ent
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-session-telemetry-otel` exposes three uppercase `mode` values:
|
||||
`@deepseek-ai/dsh-session-telemetry-otel` exposes the string-valued `TelemetryMode` enum to TypeScript callers and accepts the same three uppercase `mode` values in serialized configuration:
|
||||
|
||||
- `FULL` is the default and preserves immediate delivery to the configured OTel pipeline.
|
||||
- `FEEDBACK_ONLY` reads the canonical session log when `feedback/record` is appended and hands over the unreleased prefix through that exact event. Records appended after that boundary remain local until another feedback event.
|
||||
|
||||
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
`@deepseek-ai/dsh-session-telemetry-otel` 公开三个大写的 `mode` 值:
|
||||
`@deepseek-ai/dsh-session-telemetry-otel` 向 TypeScript 调用方公开以字符串为值的 `TelemetryMode` 枚举,并在序列化配置中接受相同的三个大写 `mode` 值:
|
||||
|
||||
- `FULL` 是默认值,保留向已配置 OTel 流水线的即时投递。
|
||||
- `FEEDBACK_ONLY` 在追加 `feedback/record` 时读取权威会话日志,并交接截至该事件的未释放前缀。该边界后追加的记录会留在本地,直到另一个反馈事件。
|
||||
|
||||
@@ -1184,12 +1184,16 @@ export interface Config {
|
||||
}
|
||||
|
||||
/** Session-sharing policy selected by {@link Config.mode}. */
|
||||
export type TelemetryMode = typeof TELEMETRY_MODES[number]
|
||||
export enum TelemetryMode {
|
||||
FULL = 'FULL',
|
||||
FEEDBACK_ONLY = 'FEEDBACK_ONLY',
|
||||
DISABLED = 'DISABLED',
|
||||
}
|
||||
```
|
||||
|
||||
Depends on: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`)
|
||||
|
||||
Source: [`packages/telemetry/session-telemetry-otel/src/index.ts:55`](../packages/telemetry/session-telemetry-otel/src/index.ts)
|
||||
Source: [`packages/telemetry/session-telemetry-otel/src/index.ts:63`](../packages/telemetry/session-telemetry-otel/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-session-title`
|
||||
|
||||
|
||||
@@ -160,6 +160,29 @@ export function apply(ctx: Context, config: Config): void {}
|
||||
expect(entries[0]?.refs).toEqual([{ alias: 'Remote', imported: 'Remote', specifier: '@fix/dep' }])
|
||||
})
|
||||
|
||||
it('pastes an enum referenced by the config type', () => {
|
||||
const entries = collectConfigCatalog(make({
|
||||
'src/index.ts': `import type { Context } from 'cordis'
|
||||
/** Fixture mode. */
|
||||
export enum Mode {
|
||||
A = 'a',
|
||||
B = 'b',
|
||||
}
|
||||
/** Fixture config. */
|
||||
export interface Config {
|
||||
/** The mode. */
|
||||
mode?: Mode
|
||||
}
|
||||
/** Load. */
|
||||
export function apply(ctx: Context, config: Config): void {}
|
||||
`,
|
||||
}))
|
||||
expect(entries[0]?.pastes?.map(p => p.text)).toEqual([
|
||||
'/** Fixture config. */\nexport interface Config {\n /** The mode. */\n mode?: Mode\n}',
|
||||
"/** Fixture mode. */\nexport enum Mode {\n A = 'a',\n B = 'b',\n}",
|
||||
])
|
||||
})
|
||||
|
||||
it('hard-errors on a referenced type name that resolves nowhere', () => {
|
||||
expect(() => collectConfigCatalog(make({
|
||||
'src/index.ts': `import type { Context } from 'cordis'
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/telemetry/session-telemetry-otel/README.md
|
||||
README.md: 7fc5572614a5bdba312ba97b52606032ef8f5394
|
||||
README.zh.md: 3160b67c8225fb87d5e7be2e43453ef40496fba9
|
||||
README.md: 01d803236329afbe65e2d92960928441aaff301c
|
||||
README.zh.md: 8adf4a3c11b95dc302f8afd0dc79e99433e50f22
|
||||
|
||||
@@ -24,6 +24,8 @@ The OpenTelemetry backend for [the telemetry seam](../session-telemetry/) — th
|
||||
| `FEEDBACK_ONLY` | Each `feedback/record` replays, projects, and redacts the canonical session-log suffix through that event. Later records wait for another feedback event and remain local if none arrives. |
|
||||
| `DISABLED` | No coordinator, provider, processor, or exporter is constructed. No telemetry record leaves the process. A `feedback/record` logs `session telemetry is DISABLED; nothing will be shared and this feedback remains local`; the event remains in the local session log. |
|
||||
|
||||
Programmatic TypeScript configuration uses the exported `TelemetryMode` enum (`TelemetryMode.FULL`, `TelemetryMode.FEEDBACK_ONLY`, or `TelemetryMode.DISABLED`); raw string literals are not assignable. Serialized Cordis configuration continues to use the string values shown above.
|
||||
|
||||
`exporter.url` is required in `FULL` and `FEEDBACK_ONLY`, has no default, and must parse as `http(s)`; it is optional and unused in `DISABLED`. Uploading modes also reject a non-positive-integer `processor.maxExportBatchSize`, which the SDK accepts but then hangs on at shutdown. Everything else is the SDK's option shape, owned and documented by the SDK, and both blocks pass through whole: every `OTLPExporterNodeConfigBase` field (`headers`, `timeoutMillis`, `compression`, `keepAlive`, …) reaches the exporter, and batching, export cadence (`scheduledDelayMillis`), retry, queue bounds, and loss policy under sustained failure are the SDK's documented behavior, tuned through the `processor` passthrough. The backend deliberately implements no `flush()`: the batch processor is the only flusher in the process, which is what makes `shutdown()`'s drain complete.
|
||||
|
||||
## What leaves the machine
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
| `FEEDBACK_ONLY` | 每个 `feedback/record` 都会回放权威会话日志中截至该事件的后缀,并进行投影与脱敏。后续记录等待下一个反馈事件;如果没有后续反馈,则留在本地。 |
|
||||
| `DISABLED` | 不构造协调器、提供方、处理器或导出器。没有遥测记录会离开进程。`feedback/record` 会记录 `session telemetry is DISABLED; nothing will be shared and this feedback remains local`;该事件留在本地会话日志中。 |
|
||||
|
||||
程序化 TypeScript 配置使用导出的 `TelemetryMode` 枚举(`TelemetryMode.FULL`、`TelemetryMode.FEEDBACK_ONLY` 或 `TelemetryMode.DISABLED`);原始字符串字面量不可赋值。序列化后的 Cordis 配置继续使用上表所示的字符串值。
|
||||
|
||||
`exporter.url` 在 `FULL` 与 `FEEDBACK_ONLY` 中必填,无默认值,且必须能解析为 `http(s)`;在 `DISABLED` 中可省略且不使用。上传模式也会拒绝不是正整数的 `processor.maxExportBatchSize`,SDK 虽会接受该值,但随后会在关闭时挂起。其余全部是 SDK 自己的选项形态,由 SDK 拥有并在 SDK 文档中说明,两个配置块都整体透传(passthrough):`OTLPExporterNodeConfigBase` 的每个字段(`headers`、`timeoutMillis`、`compression`、`keepAlive` 等)都会到达导出器;批处理、导出节奏(`scheduledDelayMillis`)、重试、队列上限,以及持续失败下的丢失策略,都是 SDK 的文档化行为,经 `processor` 透传调优。该后端刻意不实现 `flush()`:批处理器是进程内唯一执行 flush 的组件,`shutdown()` 的排空正因如此才是完整的。
|
||||
|
||||
## 哪些数据会离开本机
|
||||
|
||||
@@ -39,11 +39,19 @@ import { resourceFromAttributes } from '@opentelemetry/resources'
|
||||
// version (same pattern as dsh-llm's attribution identity).
|
||||
const { version } = createRequire(import.meta.url)('../package.json') as { version: string }
|
||||
|
||||
/** Supported session-sharing policies for the OTel backend. */
|
||||
export const TELEMETRY_MODES = ['FULL', 'FEEDBACK_ONLY', 'DISABLED'] as const
|
||||
|
||||
/** Session-sharing policy selected by {@link Config.mode}. */
|
||||
export type TelemetryMode = typeof TELEMETRY_MODES[number]
|
||||
export enum TelemetryMode {
|
||||
FULL = 'FULL',
|
||||
FEEDBACK_ONLY = 'FEEDBACK_ONLY',
|
||||
DISABLED = 'DISABLED',
|
||||
}
|
||||
|
||||
/** Supported session-sharing policies for runtime configuration validation. */
|
||||
export const TELEMETRY_MODES = [
|
||||
TelemetryMode.FULL,
|
||||
TelemetryMode.FEEDBACK_ONLY,
|
||||
TelemetryMode.DISABLED,
|
||||
] as const
|
||||
|
||||
const DISABLED_FEEDBACK_WARNING = 'session telemetry is DISABLED; nothing will be shared and this feedback remains local'
|
||||
|
||||
@@ -81,7 +89,7 @@ export interface Config {
|
||||
* axiom (and silently drop every field not re-declared).
|
||||
*/
|
||||
export const Config: z<Config> = z.object({
|
||||
mode: z.union(TELEMETRY_MODES).default('FULL'),
|
||||
mode: z.union(TELEMETRY_MODES).default(TelemetryMode.FULL),
|
||||
exporter: z.any(),
|
||||
processor: z.any(),
|
||||
})
|
||||
@@ -109,8 +117,8 @@ export class TelemetryOtel extends Telemetry {
|
||||
|
||||
constructor(ctx: Context, config: Config) {
|
||||
super(ctx)
|
||||
const mode = config.mode ?? 'FULL'
|
||||
if (mode === 'DISABLED') {
|
||||
const mode = config.mode ?? TelemetryMode.FULL
|
||||
if (mode === TelemetryMode.DISABLED) {
|
||||
this.provider = undefined
|
||||
this.ledger = undefined
|
||||
this.ops = undefined
|
||||
@@ -162,9 +170,9 @@ export class TelemetryOtel extends Telemetry {
|
||||
})
|
||||
this.ledger = this.provider.getLogger('@deepseek-ai/dsh-session-telemetry-otel', version)
|
||||
this.ops = this.provider.getLogger('@deepseek-ai/dsh-session-telemetry-otel/ops', version)
|
||||
const capture: TelemetryCapture = mode === 'FULL' ? 'live' : 'on-demand'
|
||||
const capture: TelemetryCapture = mode === TelemetryMode.FULL ? 'live' : 'on-demand'
|
||||
const coordinator = new TelemetryCoordinator(ctx, this, capture)
|
||||
if (mode === 'FEEDBACK_ONLY') {
|
||||
if (mode === TelemetryMode.FEEDBACK_ONLY) {
|
||||
// Session.append commits before publishing `session/event`, so the
|
||||
// canonical log already includes this feedback record when replay begins.
|
||||
ctx.on('session/event', (session, event) => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* for the default-exported Service class.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
|
||||
import { createServer, type Server } from 'node:http'
|
||||
import { once } from 'node:events'
|
||||
import { gunzipSync } from 'node:zlib'
|
||||
@@ -13,7 +13,7 @@ import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import { recordFeedback } from '@deepseek-ai/dsh-command-feedback'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import TelemetryOtel, { Config } from '../src/index.ts'
|
||||
import TelemetryOtel, { Config, TelemetryMode } from '../src/index.ts'
|
||||
|
||||
interface Capture {
|
||||
headers: import('node:http').IncomingHttpHeaders
|
||||
@@ -211,7 +211,7 @@ describe('TelemetryOtel wire', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const fiber = await ctx.plugin(TelemetryOtel, {
|
||||
mode: 'FEEDBACK_ONLY',
|
||||
mode: TelemetryMode.FEEDBACK_ONLY,
|
||||
exporter: { url },
|
||||
})
|
||||
const session = ctx.sessions.create(SessionId('feedback-only'), { meta: {} })
|
||||
@@ -236,7 +236,7 @@ describe('TelemetryOtel wire', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const fiber = await ctx.plugin(TelemetryOtel, {
|
||||
mode: 'FEEDBACK_ONLY',
|
||||
mode: TelemetryMode.FEEDBACK_ONLY,
|
||||
exporter: { url },
|
||||
})
|
||||
const session = ctx.sessions.create(SessionId('no-feedback'), { meta: {} })
|
||||
@@ -249,7 +249,7 @@ describe('TelemetryOtel wire', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
|
||||
const fiber = await ctx.plugin(TelemetryOtel, { mode: 'DISABLED' })
|
||||
const fiber = await ctx.plugin(TelemetryOtel, { mode: TelemetryMode.DISABLED })
|
||||
const session = ctx.sessions.create(SessionId('disabled'), { meta: {} })
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
recordFeedback(session, 'local report')
|
||||
@@ -284,12 +284,18 @@ describe('TelemetryOtel wire', () => {
|
||||
})
|
||||
|
||||
describe('TelemetryOtel config fails loud', () => {
|
||||
it('exposes modes through the nominal enum', () => {
|
||||
expectTypeOf<Config['mode']>().toEqualTypeOf<TelemetryMode | undefined>()
|
||||
expectTypeOf<'FULL'>().not.toExtend<TelemetryMode>()
|
||||
expectTypeOf<TelemetryMode.FULL>().toExtend<TelemetryMode>()
|
||||
})
|
||||
|
||||
it.each([
|
||||
[{}, /exporter\.url is required/],
|
||||
[{ exporter: { url: '' } }, /exporter\.url is required/],
|
||||
[{ exporter: { url: 'not a url' } }, /not a valid URL/],
|
||||
[{ exporter: { url: 'ftp://collector' } }, /must be http\(s\)/],
|
||||
[{ mode: 'FEEDBACK_ONLY' }, /exporter\.url is required/],
|
||||
[{ mode: TelemetryMode.FEEDBACK_ONLY }, /exporter\.url is required/],
|
||||
[{ mode: 'INVALID' }, /INVALID/],
|
||||
// The SDK accepts a non-positive batch size but its shutdown drain then
|
||||
// splices empty batches forever — dispose would hang, so reject at load.
|
||||
|
||||
@@ -126,12 +126,13 @@ function loadFile(abs: string, rel: string, cache: Map<string, FileCtx>): FileCt
|
||||
}
|
||||
|
||||
/** A type declaration a paste can contain. */
|
||||
type TypeDecl = ts.InterfaceDeclaration | ts.TypeAliasDeclaration
|
||||
type TypeDecl = ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.EnumDeclaration
|
||||
|
||||
/** Find an interface/type-alias declaration by name in a file, or null. */
|
||||
/** Find a pasteable type declaration by name in a file, or null. */
|
||||
function findTypeDecl(ctx: FileCtx, name: string): TypeDecl | null {
|
||||
for (const stmt of ctx.sf.statements) {
|
||||
if ((ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) && stmt.name.text === name) return stmt
|
||||
if ((ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt) || ts.isEnumDeclaration(stmt))
|
||||
&& stmt.name.text === name) return stmt
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -207,7 +208,7 @@ function checkMemberDocs(ctx: FileCtx, decl: TypeDecl, violations: string[]): vo
|
||||
else ts.forEachChild(type, (n) => { walkNested(n, path) })
|
||||
}
|
||||
if (ts.isInterfaceDeclaration(decl)) walkMembers(decl.members, decl.name.text)
|
||||
else walkNested(decl.type, decl.name.text)
|
||||
else if (ts.isTypeAliasDeclaration(decl)) walkNested(decl.type, decl.name.text)
|
||||
}
|
||||
|
||||
/** Cross-file resolution context for the schema-path check. */
|
||||
|
||||
Reference in New Issue
Block a user