diff --git a/integration-tests/cli/qwen-serve-routes.test.ts b/integration-tests/cli/qwen-serve-routes.test.ts index 71c020b309b..910f2263fb2 100644 --- a/integration-tests/cli/qwen-serve-routes.test.ts +++ b/integration-tests/cli/qwen-serve-routes.test.ts @@ -198,6 +198,7 @@ describe('qwen serve — capabilities envelope', () => { 'session_prompt', 'session_cancel', 'session_events', + 'typed_event_schema', 'session_set_model', 'client_identity', 'permission_vote', diff --git a/packages/cli/src/serve/capabilities.ts b/packages/cli/src/serve/capabilities.ts index e022e27b74f..294f4dffa16 100644 --- a/packages/cli/src/serve/capabilities.ts +++ b/packages/cli/src/serve/capabilities.ts @@ -36,6 +36,11 @@ export const SERVE_CAPABILITY_REGISTRY = { session_prompt: { since: 'v1' }, session_cancel: { since: 'v1' }, session_events: { since: 'v1' }, + // SDK consumers can detect `KnownDaemonEvent` schema support without + // pinning against this SDK release — `narrowDaemonEvent` falls back + // to `kind: 'unknown'` for daemons that don't advertise the tag, + // so the tag is purely informational. + typed_event_schema: { since: 'v1' }, session_set_model: { since: 'v1' }, client_identity: { since: 'v1' }, permission_vote: { since: 'v1' }, diff --git a/packages/cli/src/serve/server.test.ts b/packages/cli/src/serve/server.test.ts index 01fa01e94e9..728c94a3d7d 100644 --- a/packages/cli/src/serve/server.test.ts +++ b/packages/cli/src/serve/server.test.ts @@ -72,6 +72,7 @@ const EXPECTED_STAGE1_FEATURES = [ 'session_prompt', 'session_cancel', 'session_events', + 'typed_event_schema', 'session_set_model', 'client_identity', 'permission_vote', diff --git a/packages/sdk-typescript/src/daemon/DaemonSessionClient.ts b/packages/sdk-typescript/src/daemon/DaemonSessionClient.ts index 5cac0e40dbe..d8bc1dc604b 100644 --- a/packages/sdk-typescript/src/daemon/DaemonSessionClient.ts +++ b/packages/sdk-typescript/src/daemon/DaemonSessionClient.ts @@ -51,7 +51,8 @@ export interface DaemonSessionSubscribeOptions extends SubscribeOptions { * IDE, and web backends: it binds one daemon session, forwards the existing * Stage 1 routes, and preserves SSE replay state. It intentionally does not * interpret daemon event payloads; typed event reducers belong to the protocol - * schema layer. + * schema layer — see `asKnownDaemonEvent` and `reduceDaemonSessionEvent` in + * `./events.js` for the typed consumption surface. */ export class DaemonSessionClient { readonly client: DaemonClient; diff --git a/packages/sdk-typescript/test/unit/daemon-public-surface.test.ts b/packages/sdk-typescript/test/unit/daemon-public-surface.test.ts new file mode 100644 index 00000000000..c669f614a91 --- /dev/null +++ b/packages/sdk-typescript/test/unit/daemon-public-surface.test.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, expectTypeOf } from 'vitest'; +import * as Public from '../../src/index.js'; +// Type-only imports also exercise the public entry: any name missing +// from `src/index.ts` is a tsc compile error and the suite refuses to +// build, which is the regression fence for the kind of "exists in +// `src/daemon/index.ts` but not re-exported by the published entry" +// gap that two-layer SDK re-exports are easy to drift on. +import type { + DaemonClientEvictedData, + DaemonClientEvictedEvent, + DaemonControlEvent, + DaemonEvent, + DaemonEventEnvelope, + DaemonKnownEventType, + DaemonModelSwitchedData, + DaemonModelSwitchedEvent, + DaemonModelSwitchFailedData, + DaemonModelSwitchFailedEvent, + DaemonPermissionOption, + DaemonPermissionRequestData, + DaemonPermissionRequestEvent, + DaemonPermissionResolvedData, + DaemonPermissionResolvedEvent, + DaemonSessionDiedData, + DaemonSessionDiedEvent, + DaemonSessionEvent, + DaemonSessionUpdateData, + DaemonSessionUpdateEvent, + DaemonSessionViewState, + DaemonStreamErrorData, + DaemonStreamErrorEvent, + DaemonStreamLifecycleEvent, + KnownDaemonEvent, +} from '../../src/index.js'; + +describe('public SDK entry — typed daemon event surface (#4217)', () => { + it('exports the runtime narrow + reducer surface', () => { + expect(typeof Public.asKnownDaemonEvent).toBe('function'); + expect(typeof Public.isKnownDaemonEvent).toBe('function'); + expect(typeof Public.isDaemonEventType).toBe('function'); + expect(typeof Public.reduceDaemonSessionEvent).toBe('function'); + expect(typeof Public.reduceDaemonSessionEvents).toBe('function'); + expect(typeof Public.createDaemonSessionViewState).toBe('function'); + }); + + it('round-trips a raw DaemonEvent through the public narrow helper', () => { + // Pin the user-facing contract: `import { asKnownDaemonEvent } + // from '@qwen-code/sdk'` must work end-to-end via the published + // entry, not just exist as a re-export inside src/daemon/index.ts. + const evt: DaemonEvent = { + id: 1, + v: 1, + type: 'model_switched', + data: { sessionId: 'sess-1', modelId: 'qwen-plus' }, + }; + const narrowed = Public.asKnownDaemonEvent(evt); + if (narrowed?.type === 'model_switched') { + expect(narrowed.data.modelId).toBe('qwen-plus'); + } else { + expect.fail('expected typed model_switched'); + } + }); + + it('exposes the typed event schema types at the public entry (compile-time)', () => { + // The type-only imports at the top of this file would fail to + // compile if any of these names were absent from src/index.ts. + // The runtime expectations below document the surface set the + // SDK promises to ship and give tooling that ignores type-only + // imports a runtime assertion trail. + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf>().not.toBeNever(); + + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + expectTypeOf().not.toBeNever(); + }); +});