2026-07-11 14:08:12 +08:00
|
|
|
/**
|
2026-07-19 02:18:59 +08:00
|
|
|
* JSON-RPC method and notification surface for out-of-process harness SDKs.
|
|
|
|
|
* The surrounding context owns plugins, persistence, and configured adapters.
|
2026-07-11 14:08:12 +08:00
|
|
|
*
|
|
|
|
|
* @module @deepseek-ai/dsh-jsonrpc/server
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type { Context } from 'cordis'
|
2026-07-13 16:34:03 +08:00
|
|
|
import { resolve } from 'node:path'
|
2026-07-14 08:41:31 +08:00
|
|
|
import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
|
|
|
|
|
import { carrierKeyOf, type Scoped } from '@deepseek-ai/dsh-scope'
|
2026-07-21 15:49:37 +08:00
|
|
|
import { findLastMessageTurnEnd, SessionId, type TurnEndReason } from '@deepseek-ai/dsh-session'
|
2026-07-14 08:41:31 +08:00
|
|
|
import type SubagentService from '@deepseek-ai/dsh-subagent'
|
2026-07-11 14:08:12 +08:00
|
|
|
import type { SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent'
|
|
|
|
|
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
2026-07-27 03:21:06 +08:00
|
|
|
import type {
|
|
|
|
|
InitializeParams,
|
|
|
|
|
InitializeResult,
|
|
|
|
|
JsonRpcTransportPeer,
|
|
|
|
|
SessionEventNotification,
|
|
|
|
|
SessionFinishedNotification,
|
|
|
|
|
SessionPromptParams,
|
|
|
|
|
SessionPromptResult,
|
|
|
|
|
SubagentFinishedNotification,
|
|
|
|
|
SubagentStartedNotification,
|
|
|
|
|
} from '@deepseek-ai/dsh-sdk-protocol'
|
2026-07-11 14:08:12 +08:00
|
|
|
|
|
|
|
|
interface SessionRecord {
|
|
|
|
|
handle: AgentHandle
|
|
|
|
|
lastTurnEnd: TurnEndReason | undefined
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
activePrompt: boolean
|
2026-07-11 14:08:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-19 02:18:59 +08:00
|
|
|
/** Recover the delegating parent from the service-owned scoped carrier. */
|
2026-07-14 08:41:31 +08:00
|
|
|
function subagentParentOf(carrier: Scoped<SubagentService>): Agent {
|
|
|
|
|
return carrierKeyOf(carrier) as Agent
|
2026-07-11 14:08:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-17 17:29:38 +08:00
|
|
|
/** Deployment-specific status mapping for SDK turn and subagent outcomes. */
|
|
|
|
|
export interface HarnessSdkServerOptions {
|
|
|
|
|
/** Report max-token termination as an accepted result instead of an infrastructure error. */
|
|
|
|
|
maxTokensAsSuccess?: boolean
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 14:53:03 +08:00
|
|
|
function successStatus(reason: string, options: HarnessSdkServerOptions): 'ok' | 'error' {
|
|
|
|
|
if (reason === 'completed') return 'ok'
|
|
|
|
|
return reason === 'max-tokens' && options.maxTokensAsSuccess === true ? 'ok' : 'error'
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 14:08:12 +08:00
|
|
|
/**
|
2026-07-14 00:40:36 +08:00
|
|
|
* SDK server over one booted harness context and transport peer. Construction
|
|
|
|
|
* subscribes to session, agent, and subagent lifecycle events until shutdown;
|
|
|
|
|
* reinitialization is unsupported.
|
2026-07-11 14:08:12 +08:00
|
|
|
*/
|
|
|
|
|
export class HarnessSdkServer {
|
|
|
|
|
private cwd = process.cwd()
|
2026-07-14 21:57:52 +08:00
|
|
|
private provider = 'deepseek'
|
2026-07-11 14:08:12 +08:00
|
|
|
private model = 'deepseek'
|
|
|
|
|
private llmFiber: { dispose(): Promise<void> } | undefined
|
|
|
|
|
private readonly sessions = new Map<string, SessionRecord>()
|
2026-07-13 15:57:12 +08:00
|
|
|
private readonly sessionCreations = new Map<string, Promise<SessionRecord>>()
|
2026-07-11 14:08:12 +08:00
|
|
|
private readonly disposers: (() => void)[] = []
|
2026-07-13 15:57:12 +08:00
|
|
|
private shutdownTask: Promise<Record<string, never>> | undefined
|
|
|
|
|
private shuttingDown = false
|
2026-07-11 14:08:12 +08:00
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly ctx: Context,
|
|
|
|
|
private readonly transport: JsonRpcTransportPeer,
|
2026-07-17 17:29:38 +08:00
|
|
|
private readonly options: HarnessSdkServerOptions = {},
|
2026-07-11 14:08:12 +08:00
|
|
|
) {
|
2026-07-19 14:53:03 +08:00
|
|
|
const serverOptions = this.options
|
2026-07-11 14:08:12 +08:00
|
|
|
this.disposers.push(ctx.on('session/event', (session, event) => {
|
|
|
|
|
if (event.type === 'turn/end') {
|
|
|
|
|
const rec = this.sessions.get(String(session.id))
|
2026-07-21 15:49:37 +08:00
|
|
|
if (rec && findLastMessageTurnEnd(session.events)?.seq === event.seq) {
|
|
|
|
|
rec.lastTurnEnd = event.data.reason
|
|
|
|
|
}
|
2026-07-11 14:08:12 +08:00
|
|
|
}
|
2026-07-27 03:21:06 +08:00
|
|
|
const payload: SessionEventNotification = { sessionId: String(session.id), event }
|
|
|
|
|
this.transport.notify('session.event', payload)
|
2026-07-11 14:08:12 +08:00
|
|
|
}))
|
|
|
|
|
this.disposers.push(ctx.on('session/created', (session) => {
|
|
|
|
|
const parentSession = session.header.parentSession
|
|
|
|
|
if (parentSession === undefined) return
|
2026-07-27 03:21:06 +08:00
|
|
|
const payload: SubagentStartedNotification = {
|
2026-07-11 14:08:12 +08:00
|
|
|
parentSessionId: String(parentSession),
|
|
|
|
|
childSessionId: String(session.id),
|
2026-07-27 03:21:06 +08:00
|
|
|
}
|
|
|
|
|
this.transport.notify('subagent.started', payload)
|
2026-07-11 14:08:12 +08:00
|
|
|
}))
|
2026-07-14 08:41:31 +08:00
|
|
|
this.disposers.push(ctx.on('subagent/end', function (this: Scoped<SubagentService>, info: SubagentRunEndInfo) {
|
|
|
|
|
const parent = subagentParentOf(this)
|
2026-07-18 14:26:14 +08:00
|
|
|
// This protocol reports only in-process child sessions. The service
|
|
|
|
|
// snapshots the provider's exact run provenance through child disposal;
|
|
|
|
|
// matching ids or parent lineage alone never establishes locality.
|
|
|
|
|
if (!info.local) return
|
2026-07-27 03:21:06 +08:00
|
|
|
const payload: SubagentFinishedNotification = {
|
2026-07-11 14:08:12 +08:00
|
|
|
provider: info.provider,
|
|
|
|
|
agentId: String(info.id),
|
2026-07-14 08:41:31 +08:00
|
|
|
parentSessionId: String(parent.session.id),
|
2026-07-14 02:09:09 +08:00
|
|
|
childSessionId: String(info.id),
|
2026-07-19 14:53:03 +08:00
|
|
|
status: successStatus(info.stopReason, serverOptions),
|
2026-07-11 14:08:12 +08:00
|
|
|
stopReason: info.stopReason,
|
|
|
|
|
...(info.lastAssistantMessage === undefined ? {} : { lastAssistantMessage: info.lastAssistantMessage }),
|
2026-07-27 03:21:06 +08:00
|
|
|
}
|
|
|
|
|
transport.notify('subagent.finished', payload)
|
2026-07-11 14:08:12 +08:00
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-19 02:18:59 +08:00
|
|
|
* Configure the SDK route, mounting the DeepSeek fallback only when unowned.
|
|
|
|
|
* @param params - SDK handshake parameters.
|
|
|
|
|
* @returns server identity for the handshake.
|
2026-07-11 14:08:12 +08:00
|
|
|
*/
|
|
|
|
|
async initialize(params: InitializeParams): Promise<InitializeResult> {
|
2026-07-13 16:34:03 +08:00
|
|
|
this.cwd = resolve(params.cwd)
|
2026-07-14 21:57:52 +08:00
|
|
|
this.provider = params.provider
|
2026-07-11 14:08:12 +08:00
|
|
|
this.model = params.model
|
2026-07-14 21:57:52 +08:00
|
|
|
if (!this.hasAdapterFor(this.provider)) {
|
|
|
|
|
if (this.provider !== 'deepseek') throw new Error(`no adapter registered for provider "${this.provider}"`)
|
|
|
|
|
this.llmFiber = await this.ctx.plugin(LlmDeepSeek, {})
|
2026-07-11 14:08:12 +08:00
|
|
|
}
|
|
|
|
|
return { serverInfo: { name: 'deepseek-harness-sdk-runtime', version: '0.0.1' } }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-19 02:18:59 +08:00
|
|
|
* Run one prompt to settlement; overlap on the same session fails.
|
|
|
|
|
* @param params - target session and user content.
|
|
|
|
|
* @returns acceptance after the turn settled.
|
2026-07-11 14:08:12 +08:00
|
|
|
*/
|
|
|
|
|
async prompt(params: SessionPromptParams): Promise<SessionPromptResult> {
|
2026-07-13 15:57:12 +08:00
|
|
|
const rec = await this.getOrCreateSession(params.sessionId)
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
if (rec.activePrompt) throw new Error(`session already has an active prompt: ${params.sessionId}`)
|
2026-07-27 02:00:12 +08:00
|
|
|
// An agent-loop-only reload disposes the loop's agents while this record
|
|
|
|
|
// survives; a retained agent accepts followup() silently, so validate the
|
|
|
|
|
// record against the live registry before delivery (as the ACP bridge does).
|
|
|
|
|
if (this.ctx.agents.get(rec.handle.agent.id) !== rec.handle.agent) {
|
|
|
|
|
throw new Error(`session agent was disposed outside the server: ${params.sessionId}`)
|
|
|
|
|
}
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
rec.activePrompt = true
|
|
|
|
|
try {
|
|
|
|
|
rec.lastTurnEnd = undefined
|
2026-07-24 22:38:50 +08:00
|
|
|
rec.handle.agent.followup({ content: params.contentBlocks, source: { kind: 'user' } })
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
await rec.handle.agent.whenIdle()
|
2026-07-27 03:21:06 +08:00
|
|
|
const payload: SessionFinishedNotification = {
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
sessionId: params.sessionId,
|
2026-07-27 03:21:06 +08:00
|
|
|
status: this.finishedStatus(rec.lastTurnEnd),
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
reason: rec.lastTurnEnd,
|
2026-07-27 03:21:06 +08:00
|
|
|
}
|
|
|
|
|
this.transport.notify('session.finished', payload)
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
return { accepted: true }
|
|
|
|
|
} finally {
|
|
|
|
|
rec.activePrompt = false
|
|
|
|
|
}
|
2026-07-11 14:08:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-19 02:18:59 +08:00
|
|
|
* Dispose server-owned agents, adapter, and subscriptions to quiescence.
|
|
|
|
|
* The surrounding context remains running.
|
|
|
|
|
* @returns empty JSON-RPC result.
|
2026-07-11 14:08:12 +08:00
|
|
|
*/
|
2026-07-13 15:57:12 +08:00
|
|
|
shutdown(): Promise<Record<string, never>> {
|
|
|
|
|
this.shutdownTask ??= this.performShutdown()
|
|
|
|
|
return this.shutdownTask
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async performShutdown(): Promise<Record<string, never>> {
|
|
|
|
|
this.shuttingDown = true
|
|
|
|
|
const pendingCreations = [...this.sessionCreations.values()]
|
|
|
|
|
await Promise.allSettled(pendingCreations)
|
2026-07-13 16:34:03 +08:00
|
|
|
this.sessionCreations.clear()
|
2026-07-11 14:08:12 +08:00
|
|
|
const records = [...this.sessions.values()]
|
|
|
|
|
this.sessions.clear()
|
2026-07-13 16:34:03 +08:00
|
|
|
const failures: unknown[] = []
|
|
|
|
|
while (this.disposers.length > 0) {
|
|
|
|
|
try {
|
|
|
|
|
this.disposers.pop()?.()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
failures.push(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const teardownResults = await Promise.allSettled([
|
|
|
|
|
...records.map(rec => Promise.resolve().then(() => rec.handle.dispose())),
|
|
|
|
|
...(this.llmFiber === undefined ? [] : [Promise.resolve().then(() => this.llmFiber?.dispose())]),
|
|
|
|
|
])
|
2026-07-11 14:08:12 +08:00
|
|
|
this.llmFiber = undefined
|
2026-07-13 16:34:03 +08:00
|
|
|
failures.push(...teardownResults
|
|
|
|
|
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
|
|
|
.map(result => result.reason as unknown))
|
|
|
|
|
if (failures.length === 1) throw failures[0]
|
|
|
|
|
if (failures.length > 1) throw new AggregateError(failures, 'SDK server teardown failed')
|
2026-07-11 14:08:12 +08:00
|
|
|
return {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Dispatch one incoming JSON-RPC request to its typed handler. Throws (→ a
|
|
|
|
|
* JSON-RPC error response) on an unknown method.
|
|
|
|
|
* @param method - the JSON-RPC method name.
|
|
|
|
|
* @param params - the raw params object from the wire.
|
|
|
|
|
* @returns the handler's result, to be serialized as the response.
|
|
|
|
|
*/
|
|
|
|
|
async handleRequest(method: string, params: Record<string, unknown> | undefined): Promise<unknown> {
|
|
|
|
|
switch (method) {
|
|
|
|
|
case 'initialize':
|
|
|
|
|
return this.initialize(params as unknown as InitializeParams)
|
|
|
|
|
case 'session/prompt':
|
|
|
|
|
return this.prompt(params as unknown as SessionPromptParams)
|
|
|
|
|
case 'shutdown':
|
|
|
|
|
return this.shutdown()
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`unknown DeepSeek Harness SDK runtime method: ${method}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 15:57:12 +08:00
|
|
|
private async getOrCreateSession(sessionId: string): Promise<SessionRecord> {
|
|
|
|
|
if (this.shuttingDown) throw new Error('SDK server is shutting down')
|
2026-07-11 14:08:12 +08:00
|
|
|
const existing = this.sessions.get(sessionId)
|
|
|
|
|
if (existing) return existing
|
2026-07-13 15:57:12 +08:00
|
|
|
const pending = this.sessionCreations.get(sessionId)
|
|
|
|
|
if (pending) return pending
|
|
|
|
|
const creation = this.createSession(sessionId)
|
|
|
|
|
this.sessionCreations.set(sessionId, creation)
|
|
|
|
|
void creation.then(
|
|
|
|
|
() => { this.sessionCreations.delete(sessionId) },
|
|
|
|
|
() => { this.sessionCreations.delete(sessionId) },
|
|
|
|
|
)
|
|
|
|
|
return creation
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async createSession(sessionId: string): Promise<SessionRecord> {
|
|
|
|
|
const handle = await this.ctx.agents.create({
|
2026-07-11 14:08:12 +08:00
|
|
|
sessionId: SessionId(sessionId),
|
|
|
|
|
meta: { cwd: this.cwd },
|
2026-07-14 21:57:52 +08:00
|
|
|
agentOptions: { provider: this.provider, model: this.model },
|
2026-07-11 14:08:12 +08:00
|
|
|
})
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
const rec: SessionRecord = { handle, lastTurnEnd: undefined, activePrompt: false }
|
2026-07-11 14:08:12 +08:00
|
|
|
this.sessions.set(sessionId, rec)
|
|
|
|
|
return rec
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private finishedStatus(reason: TurnEndReason | undefined): 'ok' | 'error' {
|
|
|
|
|
if (!reason) return 'error'
|
2026-07-19 14:53:03 +08:00
|
|
|
return successStatus(reason.kind, this.options)
|
2026-07-11 14:08:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-14 21:57:52 +08:00
|
|
|
private hasAdapterFor(provider: string): boolean {
|
2026-07-15 13:33:42 +08:00
|
|
|
return this.ctx.get('llm')?.listProviders().some(entry => entry.id === provider) ?? false
|
2026-07-11 14:08:12 +08:00
|
|
|
}
|
|
|
|
|
}
|