Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions integration-tests/cli/qwen-serve-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/serve/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Comment references narrowDaemonEvent falling back to kind: 'unknown', but the actual API is asKnownDaemonEvent which returns undefined for unknown/unrecognized events.

Suggested change
// pinning against this SDK release — `narrowDaemonEvent` falls back
// SDK consumers can detect `KnownDaemonEvent` schema support without
// pinning against this SDK release — `asKnownDaemonEvent` returns
// `undefined` for daemons that don't advertise the tag or send
// malformed payloads, so the tag is purely informational.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

// 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' },
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/serve/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const EXPECTED_STAGE1_FEATURES = [
'session_prompt',
'session_cancel',
'session_events',
'typed_event_schema',
'session_set_model',
'client_identity',
'permission_vote',
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk-typescript/src/daemon/DaemonSessionClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
103 changes: 103 additions & 0 deletions packages/sdk-typescript/test/unit/daemon-public-surface.test.ts
Original file line number Diff line number Diff line change
@@ -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<KnownDaemonEvent>().not.toBeNever();
expectTypeOf<DaemonSessionEvent>().not.toBeNever();
expectTypeOf<DaemonControlEvent>().not.toBeNever();
expectTypeOf<DaemonStreamLifecycleEvent>().not.toBeNever();
expectTypeOf<DaemonSessionViewState>().not.toBeNever();
expectTypeOf<DaemonKnownEventType>().not.toBeNever();
expectTypeOf<DaemonEventEnvelope<'foo', { x: 1 }>>().not.toBeNever();

expectTypeOf<DaemonSessionUpdateEvent>().not.toBeNever();
expectTypeOf<DaemonPermissionRequestEvent>().not.toBeNever();
expectTypeOf<DaemonPermissionResolvedEvent>().not.toBeNever();
expectTypeOf<DaemonModelSwitchedEvent>().not.toBeNever();
expectTypeOf<DaemonModelSwitchFailedEvent>().not.toBeNever();
expectTypeOf<DaemonSessionDiedEvent>().not.toBeNever();
expectTypeOf<DaemonClientEvictedEvent>().not.toBeNever();
expectTypeOf<DaemonStreamErrorEvent>().not.toBeNever();

expectTypeOf<DaemonSessionUpdateData>().not.toBeNever();
expectTypeOf<DaemonPermissionRequestData>().not.toBeNever();
expectTypeOf<DaemonPermissionResolvedData>().not.toBeNever();
expectTypeOf<DaemonModelSwitchedData>().not.toBeNever();
expectTypeOf<DaemonModelSwitchFailedData>().not.toBeNever();
expectTypeOf<DaemonSessionDiedData>().not.toBeNever();
expectTypeOf<DaemonClientEvictedData>().not.toBeNever();
expectTypeOf<DaemonStreamErrorData>().not.toBeNever();
expectTypeOf<DaemonPermissionOption>().not.toBeNever();
});
});
Loading