refactor(packages): dissolve ui/ and rename sdk/ to scaffold/

git mv per the regrouping RFC: the five human-collaboration seams and
tui join packages/interaction/, app-boot becomes packages/boot/, and
jsonrpc joins the renamed scaffold/ (formerly sdk/) as its server half
beside client/protocol/create-sdk/helper/scripts/telemetry, whose
folders drop the legacy sdk- prefix. Three new group README triplets
replace the ui/ and sdk/ ones; tsconfig references/paths/globs,
knip keys, vitest globs, gate scripts, catalogs, docs, and the
lockfile follow. Adds the four settled FIXME rename markers
(dsh-sdk-server, dsh-sdk-telemetry, dsh-sdk-helper, dsh-sdk-scripts).

The scaffold folders diverge from their npm names until those renames
land, so tsconfig.base.json maps the three affected names explicitly
beside the group wildcard. Also repairs two pre-existing stale-path
classes the strengthened sweep surfaced: docs/web-styling.md's retired
web-ui host package and type-model spec fixture-literal joins.

app-boot's three Loader-composition specs time out at the default 5s
under full-suite parallel load on this filesystem (pre-existing;
pass isolated with --testTimeout=30000); interaction/scaffold/boot
suites otherwise green (687 passed).
This commit is contained in:
Tianyi Cui
2026-07-30 03:13:49 +08:00
parent 7e445c3a67
commit 3fc35c91ff
351 changed files with 368 additions and 311 deletions
+25
View File
@@ -0,0 +1,25 @@
/**
* Shared wire protocol for the DeepSeek Harness SDK runtime: the
* newline-delimited JSON-RPC stdio transport plus the named request, result,
* and notification types both wire ends speak. The runtime server plugin
* (`@deepseek-ai/dsh-jsonrpc`) serves this protocol; SDK clients
* (`@deepseek-ai/dsh-sdk-client`, the Python SDK) drive it.
*
* @module @deepseek-ai/dsh-sdk-protocol
*/
export { JsonRpcLineTransport, JsonRpcResponseError } from './transport.ts'
export type { JsonRpcTransportPeer } from './transport.ts'
export type {
HarnessSdkNotificationMap,
HarnessSdkRequestMap,
InitializeParams,
InitializeResult,
SdkRunStatus,
SessionEventNotification,
SessionStatusNotification,
SessionPromptParams,
SessionPromptResult,
SubagentFinishedNotification,
SubagentStartedNotification,
} from './types.ts'
@@ -0,0 +1,31 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-sdk-protocol`.
* @module @deepseek-ai/dsh-sdk-protocol/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-sdk-protocol'
/** Cordis companion plugin name. */
export const name = 'sdk-protocol-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: a pure wire library (transport class + type
* declarations) with no event stream or mutable data relation of its own;
* both wire ends own their protocol behavior.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
+279
View File
@@ -0,0 +1,279 @@
/**
* Newline-delimited JSON-RPC 2.0 over byte streams. Frames with `id` and
* `method` are requests, `id` alone is a response, and `method` alone is a
* notification. Malformed lines are ignored; handler failures become error frames.
*
* @module @deepseek-ai/dsh-sdk-protocol/transport
*/
import { randomUUID } from 'node:crypto'
import type { Readable, Writable } from 'node:stream'
import { StringDecoder } from 'node:string_decoder'
type JsonRpcId = string | number
type RequestHandler = (method: string, params: Record<string, unknown>) => Promise<unknown>
type NotificationHandler = (method: string, params: Record<string, unknown>) => void
/** A JSON-RPC error response, preserving the wire `code` and optional `data`. */
export class JsonRpcResponseError extends Error {
/**
* @param code - the wire error code, or `undefined` when the peer sent none.
* @param message - the wire error message.
* @param data - the optional structured error payload, verbatim.
*/
constructor(readonly code: number | undefined, message: string, readonly data?: unknown) {
super(message)
this.name = 'JsonRpcResponseError'
}
}
/**
* Outbound request and notification surface used by the runtime server and
* SDK clients.
*/
export interface JsonRpcTransportPeer {
/**
* Send a request and await its response.
* @param method - the JSON-RPC method name.
* @param params - the request parameters object.
* @returns the result; rejects with {@link JsonRpcResponseError} on an error
* response, and with a plain `Error` on a write failure or closure.
*/
request(method: string, params: object): Promise<unknown>
/**
* Send a notification; omitted params produce no `params` member.
* @param method - the JSON-RPC method name.
* @param params - the optional notification parameters object.
*/
notify(method: string, params?: object): void
}
interface PendingRequest {
resolve: (value: unknown) => void
reject: (error: Error) => void
}
/**
* Line-delimited endpoint over caller-owned streams. {@link start} attaches
* listeners; {@link close} detaches them and rejects pending requests without
* destroying the streams. Missing request handlers return `-32601`; handler
* failures return `-32603`. Notifications without a handler are dropped.
*/
export class JsonRpcLineTransport implements JsonRpcTransportPeer {
private buffer = ''
private readonly decoder = new StringDecoder('utf8')
private started = false
private requestHandler: RequestHandler | undefined
private notificationHandler: NotificationHandler | undefined
private readonly pending = new Map<JsonRpcId, PendingRequest>()
constructor(
private readonly input: Readable,
private readonly output: Writable,
) {}
/** Attach the input listeners and begin reading frames. Idempotent. */
start(): void {
if (this.started) return
this.started = true
this.input.on('data', this.onData)
this.input.on('error', this.onInputError)
this.input.on('end', this.onInputEnd)
}
/**
* Detach listeners and reject pending requests. Safe before {@link start}.
*/
close(): void {
this.input.off('data', this.onData)
this.input.off('error', this.onInputError)
this.input.off('end', this.onInputEnd)
this.failPending(new Error('JSON-RPC transport closed'))
}
/**
* Install the request handler, replacing any prior handler.
* @param handler - resolves to the response `result`; a rejection becomes a
* `-32603` error response carrying the message.
*/
onRequest(handler: RequestHandler): void {
this.requestHandler = handler
}
/**
* Install the notification handler, replacing any prior handler.
* @param handler - invoked per notification with the method and normalized
* params object.
*/
onNotification(handler: NotificationHandler): void {
this.notificationHandler = handler
}
/**
* Send a request and await its response.
* @param method - the JSON-RPC method name.
* @param params - the request parameters object.
* @param signal - optional abandonment signal: aborting removes the pending
* entry (no state is retained for a response that may never come) and
* rejects with the signal's reason.
* @returns the result; rejects per {@link JsonRpcTransportPeer.request}.
*/
request(method: string, params: object, signal?: AbortSignal): Promise<unknown> {
const id = `req_${randomUUID().replaceAll('-', '')}`
const message = { jsonrpc: '2.0', id, method, params }
return new Promise((resolve, reject) => {
let detach = (): void => {}
if (signal !== undefined) {
if (signal.aborted) {
reject(abortError(signal.reason))
return
}
const onAbort = (): void => {
this.pending.delete(id)
reject(abortError(signal.reason))
}
signal.addEventListener('abort', onAbort, { once: true })
detach = () => { signal.removeEventListener('abort', onAbort) }
}
this.pending.set(id, {
resolve: (value) => {
detach()
resolve(value)
},
reject: (error) => {
detach()
reject(error)
},
})
try {
this.write(message)
} catch (error) {
this.pending.delete(id)
detach()
reject(error instanceof Error ? error : new Error(String(error)))
}
})
}
notify(method: string, params?: object): void {
this.write(params === undefined ? { jsonrpc: '2.0', method } : { jsonrpc: '2.0', method, params })
}
/**
* Wait for prior frame write callbacks. The empty barrier emits no bytes.
* @returns a promise that settles with the output write callback.
*/
flush(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.output.write('', (error) => {
if (error) reject(error)
else resolve()
})
})
}
private readonly onData = (chunk: Buffer | string): void => {
this.buffer += typeof chunk === 'string' ? chunk : this.decoder.write(chunk)
this.drainLines()
}
private drainLines(): void {
for (;;) {
const newline = this.buffer.indexOf('\n')
if (newline < 0) break
const line = this.buffer.slice(0, newline).trim()
this.buffer = this.buffer.slice(newline + 1)
if (!line) continue
void this.handleLine(line)
}
}
private readonly onInputError = (error: Error): void => {
this.failPending(error)
}
private readonly onInputEnd = (): void => {
this.buffer += this.decoder.end()
this.drainLines()
this.failPending(new Error('JSON-RPC input closed'))
}
private async handleLine(line: string): Promise<void> {
let message: unknown
try {
message = JSON.parse(line)
} catch {
// Only JSON syntax errors reach this catch; malformed peer lines are ignored.
return
}
if (!message || typeof message !== 'object') return
const frame = message as Record<string, unknown>
const id = frame.id
const method = frame.method
if ((typeof id === 'string' || typeof id === 'number') && typeof method === 'string') {
await this.handleIncomingRequest(id, method, objectParams(frame.params))
return
}
if (typeof id === 'string' || typeof id === 'number') {
this.handleIncomingResponse(id, frame)
return
}
if (typeof method === 'string') {
this.notificationHandler?.(method, objectParams(frame.params))
}
}
private async handleIncomingRequest(id: JsonRpcId, method: string, params: Record<string, unknown>): Promise<void> {
const handler = this.requestHandler
if (!handler) {
this.writeError(id, -32601, `method not found: ${method}`)
return
}
try {
const result = await handler(method, params)
this.write({ jsonrpc: '2.0', id, result })
} catch (error) {
this.writeError(id, -32603, error instanceof Error ? error.message : String(error))
}
}
private handleIncomingResponse(id: JsonRpcId, frame: Record<string, unknown>): void {
const pending = this.pending.get(id)
if (!pending) return
this.pending.delete(id)
if (frame.error && typeof frame.error === 'object') {
const error = frame.error as Record<string, unknown>
pending.reject(new JsonRpcResponseError(
typeof error.code === 'number' ? error.code : undefined,
typeof error.message === 'string' ? error.message : 'JSON-RPC error',
error.data,
))
return
}
pending.resolve(frame.result)
}
private writeError(id: JsonRpcId, code: number, message: string): void {
this.write({ jsonrpc: '2.0', id, error: { code, message } })
}
private write(message: Record<string, unknown>): void {
this.output.write(`${JSON.stringify(message)}\n`)
}
private failPending(error: Error): void {
const pending = [...this.pending.values()]
this.pending.clear()
for (const waiter of pending) waiter.reject(error)
}
}
/** Normalize JSON-RPC `params` to a plain object (arrays and scalars collapse to `{}`). */
function objectParams(params: unknown): Record<string, unknown> {
return params && typeof params === 'object' && !Array.isArray(params) ? params as Record<string, unknown> : {}
}
/** Normalize an abort reason into the rejection Error (a non-Error reason is stringified). */
function abortError(reason: unknown): Error {
return reason instanceof Error ? reason : new Error(`JSON-RPC request aborted: ${String(reason)}`)
}
+105
View File
@@ -0,0 +1,105 @@
/**
* Named wire types for the DeepSeek Harness SDK runtime protocol: the three
* request/result pairs and the four server-to-client notification payloads
* exchanged over the newline-delimited JSON-RPC stdio transport. The server
* plugin (`@deepseek-ai/dsh-jsonrpc`) and SDK clients share these shapes;
* `serverInfo.name` stays the wire-stable `deepseek-harness-sdk-runtime`.
*
* @module @deepseek-ai/dsh-sdk-protocol/types
*/
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { SubagentStopReason } from '@deepseek-ai/dsh-subagent'
/** Parameters for the process-wide SDK handshake. */
export interface InitializeParams {
/** Working directory recorded on every SDK-created session's header. */
cwd: string
/** Provider route every SDK-created agent runs on. */
provider: string
/** Model name every SDK-created agent runs on (the server may mount a fallback adapter; see `HarnessSdkServer.initialize`). */
model: string
/** Optional positive output-token cap inherited by SDK-created agents and their in-process descendants. */
maxTokens?: number
}
/** Wire-stable server identity returned by initialization. */
export interface InitializeResult {
/** Wire-stable server identity (`deepseek-harness-sdk-runtime`) and version. */
serverInfo: { name: string; version: string }
}
/** One user turn on one SDK session. */
export interface SessionPromptParams {
/** The SDK-side session id; an unknown id lazily creates the agent+session pair. */
sessionId: string
/** The prompt content blocks, sent verbatim as the user message. */
contentBlocks: ContentBlock[]
}
/** Durable enqueue receipt for one prompt. */
export interface SessionPromptResult {
/** Identity of the queued user message. */
messageId: string
}
/** Deployment-mapped SDK outcome: `ok` for an accepted result, `error` otherwise. */
export type SdkRunStatus = 'ok' | 'error'
/** `session.event` payload: one session-log event, streamed as it is recorded. */
export interface SessionEventNotification {
/** Session the event belongs to (every session in the runtime, not only SDK-created ones). */
sessionId: string
/** The full session-log event envelope. */
event: SessionEvent
}
/** Whole-agent lifecycle state for one session. */
export interface SessionStatusNotification {
/** Session whose live agent changed status. */
sessionId: string
/** The whole-agent state after the transition. */
status: 'idle' | 'running'
}
/** `subagent.started` payload: an in-runtime child session was created. */
export interface SubagentStartedNotification {
/** The delegating session. */
parentSessionId: string
/** The new child session. */
childSessionId: string
}
/** `subagent.finished` payload: an in-process subagent run ended (remote runs are not reported). */
export interface SubagentFinishedNotification {
/** Subagent provider name that ran the child. */
provider: string
/** The child agent's id (equals {@link childSessionId} for local runs). */
agentId: string
/** The delegating session. */
parentSessionId: string
/** The child session. */
childSessionId: string
/** Deployment-mapped run outcome. */
status: SdkRunStatus
/** The provider-reported stop reason. */
stopReason: SubagentStopReason
/** The child's final assistant message, when it produced one. */
lastAssistantMessage?: ContentBlock[]
}
/** Server-to-client notifications by JSON-RPC method name. */
export interface HarnessSdkNotificationMap {
'session.event': SessionEventNotification
'session.status': SessionStatusNotification
'subagent.started': SubagentStartedNotification
'subagent.finished': SubagentFinishedNotification
}
/** Client-to-server request methods with their param and result shapes. */
export interface HarnessSdkRequestMap {
'initialize': { params: InitializeParams; result: InitializeResult }
'session/prompt': { params: SessionPromptParams; result: SessionPromptResult }
'shutdown': { params: undefined; result: Record<string, never> }
}