refactor: remove per-followup result attribution
This commit is contained in:
@@ -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/sdk/sdk-client/README.md
|
||||
README.md: 3ac4de540401f6f40dab3e84f7005f91d024aee8
|
||||
README.zh.md: 95fb6b2887a7bc748a610f7fedae4be1aa2af623
|
||||
README.md: 9c441e8538a62f7139f789eef78cf70d8008418a
|
||||
README.zh.md: fbf638c9160d0356da0eae0566a2775146c47750
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The TypeScript client SDK for driving a DeepSeek Harness runtime as a subprocess over stdio JSON-RPC — the design twin of the [Python SDK](../../../python/README.md) (`deepseek-harness`), sharing the same runtime peer, protocol, and layering: `DeepSeekHarness` is the high-level turns API, `HarnessClient` the lower-level protocol client. The package root enumerates the consumer interface: the two client layers, caller-facing types, and `JsonRpcResponseError`; source modules, normalization helpers, and subscription-delivery machinery are not consumer imports. A pure library: it registers nothing on a Cordis context; the runtime process it spawns is a complete harness whose composition its own `cordis.yml` decides.
|
||||
The TypeScript client SDK for driving a DeepSeek Harness runtime as a subprocess over stdio JSON-RPC — the design twin of the [Python SDK](../../../python/README.md) (`deepseek-harness`), sharing the same runtime peer, protocol, and layering: `DeepSeekHarness` is the high-level owned-run API, `HarnessClient` the lower-level protocol client. The package root enumerates the consumer interface: the two client layers, caller-facing types, and `JsonRpcResponseError`; source modules, normalization helpers, and subscription-delivery machinery are not consumer imports. A pure library: it registers nothing on a Cordis context; the runtime process it spawns is a complete harness whose composition its own `cordis.yml` decides.
|
||||
|
||||
Unlike the Python SDK, the launch spec is fully explicit (`command`/`args`): this package is for repo-adjacent TypeScript consumers — the [`dsh-subagent-dsh-sdk`](../../subagent/subagent-dsh-sdk/README.md) backend, tests, automation — which know which runtime they are launching. Bundled-runtime resolution (finding a packaged executable) remains the Python distribution's concern.
|
||||
|
||||
@@ -18,14 +18,16 @@ await using harness = new DeepSeekHarness({
|
||||
maxTokens: 49_152,
|
||||
})
|
||||
const result = await harness.run('say hi')
|
||||
console.log(result.status, result.finalResponse)
|
||||
console.log(result.finalResponse)
|
||||
```
|
||||
|
||||
The subprocess starts lazily on first use and stays owned by the instance across `run()` calls; `close()` (or `await using`) is required so the child is always reaped. `start()` memoizes the `initialize` handshake (the workspace cwd — resolved absolute before it crosses the wire — plus the provider/model route and optional positive `maxTokens` output cap); a failed handshake reaps the runtime and swaps in a fresh client, so a later call retries with a new subprocess (until `close()`, which is terminal). The cap applies to each root-agent request and is inherited by in-process descendants; compaction plugins own their separate summary limits. `session(id?)` opens a named or fresh session handle; `run(input, { sessionId?, onNotification? })` sends one prompt turn and settles when the paired `session.finished` arrives, returning a `TurnResult`: `status` (`ok`/`error` as the deployment maps it), the structured `reason` (`TurnEndReason`), `finalResponse` (last assistant message text), root-session `events`, and raw `notifications` for that session plus descendants discovered from `subagent.started`, all in wire order. Model-level failure is a `status: 'error'` result, never a rejection; rejections mean transport loss, timeout, or protocol violation.
|
||||
The subprocess starts lazily on first use and stays owned by the instance across `run()` calls; `close()` (or `await using`) is required so the child is always reaped. `start()` memoizes the `initialize` handshake (the workspace cwd — resolved absolute before it crosses the wire — plus the provider/model route and optional positive `maxTokens` output cap); a failed handshake reaps the runtime and swaps in a fresh client, so a later call retries with a new subprocess (until `close()`, which is terminal). The cap applies to each root-agent request and is inherited by in-process descendants; compaction plugins own their separate summary limits. `session(id?)` opens a named or fresh session handle.
|
||||
|
||||
`run(input, { sessionId?, onNotification? })` owns one activity interval: it queues the prompt, waits until its `MessageId` appears in a durable `agent/inbox/spliced` receipt, then collects through the next whole-agent `idle`. It returns `RunResult { sessionId, finalResponse, events, notifications }`. `finalResponse` is the last committed root-session assistant text in that interval, not a response causally assigned to the prompt; steering, injected context, and other queued work may contribute before idle. `events` contains root-session events, while `notifications` also contains descendants discovered from `subagent.started`, all in wire order. The result carries no prompt-level status or turn reason. Transport loss, timeout, and protocol violations reject; model outcomes remain observable in the event stream without being attributed to one input.
|
||||
|
||||
## HarnessClient
|
||||
|
||||
The protocol client under the turns API: explicit `start()`/`initialize()`/`prompt()`/`request()`/`close()`, plus notification subscriptions. `subscribe(filter?)` returns a `NotificationSubscription` (awaitable `next()`, non-blocking `tryNext()`, async iteration); `subscribeSessionTree(id)` scopes to one session and the descendants discovered from `subagent.started` lineage edges — the runtime notifies for every session in its context, and scoping is client-side, exactly like the Python SDK. Error surfaces are typed and exported from this package: `JsonRpcResponseError` (wire error response, code/data preserved), `RequestTimeoutError` (a configured bound elapsed; there is no wire-level cancel, so the request keeps running server-side until close), `SdkProtocolError` (a response outside the documented protocol), `TransportClosedError` (the runtime is gone — message carries the exit code and a bounded stderr tail).
|
||||
The protocol client under the owned-run API: explicit `start()`/`initialize()`/`prompt()`/`request()`/`close()`, plus notification subscriptions. `prompt()` returns the queued message id as soon as the runtime accepts it; it never waits for agent activity. `subscribe(filter?)` returns a `NotificationSubscription` (awaitable `next()`, non-blocking `tryNext()`, async iteration); `subscribeSessionTree(id)` scopes to one session and the descendants discovered from `subagent.started` lineage edges — the runtime notifies for every session in its context, and scoping is client-side, exactly like the Python SDK. Error surfaces are typed and exported from this package: `JsonRpcResponseError` (wire error response, code/data preserved), `RequestTimeoutError` (a configured bound elapsed), `SdkProtocolError` (a response outside the documented protocol), `TransportClosedError` (the runtime is gone — message carries the exit code and a bounded stderr tail).
|
||||
|
||||
`close()` requests protocol `shutdown` (bounded by `shutdownTimeoutMs`, default 1000 ms), then walks a stdin-EOF → SIGTERM → SIGKILL ladder (`disposeEofGraceMs` default 6000, `disposeGraceMs` default 3000) until the process has actually exited. The ladder is private to this client: it runs outside any harness context, so it cannot ride the [`dsh-subprocess`](../../subprocess/README.md) service — the seam's documented exception for SDK-managed transports. It is idempotent, and a closed client refuses reuse.
|
||||
|
||||
@@ -33,7 +35,7 @@ The protocol client under the turns API: explicit `start()`/`initialize()`/`prom
|
||||
|
||||
## Testing
|
||||
|
||||
Keyless unit tests drive a scripted fake runtime subprocess (`tests/fake-runtime.ts`, protocol-only, env-scripted) over real stdio: turn loop, session-tree scoping, timeout/death/malformed-response surfaces, and the dispose ladder. The [SDK snapshot suite](../../../examples/jsonrpc-agent/tests/sdk.snapshot.ts) drives the real `dsh-jsonrpc-agent` runtime through this client keylessly via `llm-replay`, pinning the notification stream, the turn result, and the persisted logs; `DSH_SNAPSHOT=record` re-records against the live API.
|
||||
Keyless unit tests drive a scripted fake runtime subprocess (`tests/fake-runtime.ts`, protocol-only, env-scripted) over real stdio: activity collection, session-tree scoping, timeout/death/malformed-response surfaces, and the dispose ladder. The [SDK snapshot suite](../../../examples/jsonrpc-agent/tests/sdk.snapshot.ts) drives the real `dsh-jsonrpc-agent` runtime through this client keylessly via `llm-replay`, pinning the notification stream, run result, and persisted logs; `DSH_SNAPSHOT=record` re-records against the live API.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -47,5 +49,5 @@ None; this package neither assembles nor sends a provider request.
|
||||
|
||||
- **No bundled-runtime resolution** — callers name the runtime executable explicitly; packaged-executable discovery stays Python-side until a TypeScript distribution consumer exists.
|
||||
- **No mid-turn cancel** — the wire has no prompt-cancel method; abandoning a turn means closing the runtime (see the protocol's [Known Limitations](../sdk-protocol/README.md)).
|
||||
- **One in-flight prompt per session** — a server-side rule this client surfaces as a `JsonRpcResponseError`; independent sessions run concurrently on one runtime.
|
||||
- **No per-prompt result or cancel** — low-level `prompt()` returns only an enqueue receipt; high-level `run()` owns receipt-to-idle collection, and abandoning it means closing the runtime.
|
||||
- **Client→server notifications and server→client requests are unimplemented** on both wire ends; the transport carries them for future approval flows.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
以子进程方式驱动 DeepSeek Harness 运行时、走 stdio JSON-RPC 的 TypeScript 客户端 SDK——[Python SDK](../../../python/README.md)(`deepseek-harness`)的设计孪生,共享同一个运行时对端、协议与分层:`DeepSeekHarness` 是高层轮次 API,`HarnessClient` 是低层协议客户端。包(package)根枚举消费方接口:两层客户端、面向调用方的类型和 `JsonRpcResponseError`;源模块、规范化辅助函数与订阅投递机制不供消费方导入。纯库:不在任何 Cordis 上下文注册;它所 spawn 的运行时进程是一个完整 harness,其组成由自己的 `cordis.yml` 决定。
|
||||
以子进程方式驱动 DeepSeek Harness 运行时、走 stdio JSON-RPC 的 TypeScript 客户端 SDK——[Python SDK](../../../python/README.md)(`deepseek-harness`)的设计孪生,共享同一个运行时对端、协议与分层:`DeepSeekHarness` 是高层自有运行 API,`HarnessClient` 是低层协议客户端。包(package)根枚举消费方接口:两层客户端、面向调用方的类型和 `JsonRpcResponseError`;源模块、规范化辅助函数与订阅投递机制不供消费方导入。纯库:不在任何 Cordis 上下文注册;它所 spawn 的运行时进程是一个完整 harness,其组成由自己的 `cordis.yml` 决定。
|
||||
|
||||
与 Python SDK 不同,启动规格完全显式(`command`/`args`):本包面向仓库近旁的 TypeScript 消费方——[`dsh-subagent-dsh-sdk`](../../subagent/subagent-dsh-sdk/README.md) 后端、测试、自动化——它们知道自己要启动哪个运行时。捆绑运行时解析(寻找打包可执行文件)仍归 Python 发行版负责。
|
||||
|
||||
@@ -18,14 +18,16 @@ await using harness = new DeepSeekHarness({
|
||||
maxTokens: 49_152,
|
||||
})
|
||||
const result = await harness.run('say hi')
|
||||
console.log(result.status, result.finalResponse)
|
||||
console.log(result.finalResponse)
|
||||
```
|
||||
|
||||
子进程在首次使用时惰性启动,并在多次 `run()` 之间持续归实例所有;必须 `close()`(或 `await using`),子进程才总能被回收。`start()` 记忆化 `initialize` 握手(工作区 cwd——在通过协议传输之前解析为绝对路径——加 provider/model 路由和可选的正整数 `maxTokens` 输出上限);握手失败会回收运行时并换入全新客户端,后续调用用新子进程重试(直到终结性的 `close()`)。该上限作用于根 agent(智能体)的每次请求,并由进程内后代继承;压缩(compaction)插件单独持有摘要上限。`session(id?)` 打开具名或全新的会话句柄;`run(input, { sessionId?, onNotification? })` 发送一个提示词轮次,在配对的 `session.finished` 到达时完成,并返回 `TurnResult`:`status`(按部署映射的 `ok`/`error`)、结构化 `reason`(`TurnEndReason`)、`finalResponse`(最后一条助手消息文本)、根会话的 `events`,以及该会话和通过 `subagent.started` 发现的后代的原始 `notifications`,均按协议传输顺序排列。模型层失败会返回 `status: 'error'` 的结果,绝不会导致 Promise 被拒绝;Promise 被拒绝意味着传输丢失、超时或协议违例。
|
||||
子进程在首次使用时惰性启动,并在多次 `run()` 之间持续归实例所有;必须 `close()`(或 `await using`),子进程才总能被回收。`start()` 记忆化 `initialize` 握手(工作区 cwd——在通过协议传输之前解析为绝对路径——加 provider/model 路由和可选的正整数 `maxTokens` 输出上限);握手失败会回收运行时并换入全新客户端,后续调用用新子进程重试(直到终结性的 `close()`)。该上限作用于根 agent(智能体)的每次请求,并由进程内后代继承;压缩(compaction)插件单独持有摘要上限。`session(id?)` 打开具名或全新的会话句柄。
|
||||
|
||||
`run(input, { sessionId?, onNotification? })` 拥有一个活动区间:它将提示词排入队列,等待其 `MessageId` 出现在持久的 `agent/inbox/spliced` 回执中,然后持续收集到整个 agent 下一次进入 `idle`。它返回 `RunResult { sessionId, finalResponse, events, notifications }`。`finalResponse` 是该区间内根会话最后提交的助手文本,并非因果上归属于该提示词的响应;steering(中途引导)、注入的上下文和其他排队工作都可能在 idle 前参与其中。`events` 包含根会话事件,`notifications` 还包含通过 `subagent.started` 发现的后代,均按协议传输顺序排列。结果不携带提示词级状态或轮次原因。传输丢失、超时和协议违例会导致 Promise 被拒绝;模型结果仍可在事件流中观察,但不会归属于某一输入。
|
||||
|
||||
## HarnessClient
|
||||
|
||||
轮次 API 之下的协议客户端:显式 `start()`/`initialize()`/`prompt()`/`request()`/`close()`,外加通知订阅。`subscribe(filter?)` 返回 `NotificationSubscription`(可等待的 `next()`、非阻塞 `tryNext()`、异步迭代);`subscribeSessionTree(id)` 把范围限定到一个会话及从 `subagent.started` 血缘边发现的后代——运行时对上下文内每个会话都发通知,范围限定在客户端完成,与 Python SDK 完全一致。本包导出有明确类型的错误:`JsonRpcResponseError`(协议错误响应,保留 code/data)、`RequestTimeoutError`(配置的时限已到;协议层没有取消机制,请求在服务端继续运行直到 close)、`SdkProtocolError`(响应超出文档化协议)、`TransportClosedError`(运行时已消失——消息携带退出码与有界 stderr 尾部)。
|
||||
自有运行 API 之下的协议客户端:显式 `start()`/`initialize()`/`prompt()`/`request()`/`close()`,外加通知订阅。`prompt()` 在运行时接受排队消息后立即返回该消息的 ID,绝不等待 agent 活动。`subscribe(filter?)` 返回 `NotificationSubscription`(可等待的 `next()`、非阻塞 `tryNext()`、异步迭代);`subscribeSessionTree(id)` 把范围限定到一个会话及从 `subagent.started` 血缘边发现的后代——运行时对上下文内每个会话都发通知,范围限定在客户端完成,与 Python SDK 完全一致。本包导出有明确类型的错误:`JsonRpcResponseError`(协议错误响应,保留 code/data)、`RequestTimeoutError`(配置的时限已到)、`SdkProtocolError`(响应超出文档化协议)、`TransportClosedError`(运行时已消失——消息携带退出码与有界 stderr 尾部)。
|
||||
|
||||
`close()` 先请求协议 `shutdown`(受 `shutdownTimeoutMs` 约束,默认 1000 毫秒),然后走 stdin-EOF → SIGTERM → SIGKILL 阶梯(`disposeEofGraceMs` 默认 6000,`disposeGraceMs` 默认 3000)直到进程真正退出。该阶梯为本客户端私有:它运行在任何 harness 上下文之外,无法搭乘 [`dsh-subprocess`](../../subprocess/README.md) 服务——即该 seam 所记录的 SDK 托管传输例外。幂等,已关闭的客户端拒绝复用。
|
||||
|
||||
@@ -33,7 +35,7 @@ console.log(result.status, result.finalResponse)
|
||||
|
||||
## 测试
|
||||
|
||||
免密钥单元测试通过真实 stdio 驱动一个脚本化伪运行时子进程(`tests/fake-runtime.ts`,纯协议、环境变量脚本化):轮次循环、会话树范围限定、超时、进程死亡和响应畸形场景,以及 dispose(资源释放)阶梯。[SDK 快照套件](../../../examples/jsonrpc-agent/tests/sdk.snapshot.ts) 经由 `llm-replay` 免密钥地通过本客户端驱动真实 `dsh-jsonrpc-agent` 运行时,固定通知流、轮次结果与持久化日志;`DSH_SNAPSHOT=record` 对真实 API 重录。
|
||||
免密钥单元测试通过真实 stdio 驱动一个脚本化伪运行时子进程(`tests/fake-runtime.ts`,纯协议、环境变量脚本化):活动收集、会话树范围限定、超时、进程死亡和响应畸形场景,以及 dispose(资源释放)阶梯。[SDK 快照套件](../../../examples/jsonrpc-agent/tests/sdk.snapshot.ts) 经由 `llm-replay` 免密钥地通过本客户端驱动真实 `dsh-jsonrpc-agent` 运行时,固定通知流、运行结果与持久化日志;`DSH_SNAPSHOT=record` 对真实 API 重录。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -47,5 +49,5 @@ console.log(result.status, result.finalResponse)
|
||||
|
||||
- **无捆绑运行时解析**——调用方显式指定运行时可执行文件;打包可执行文件的发现留在 Python 侧,直到出现 TypeScript 发行版消费方。
|
||||
- **无轮次中取消**——协议层没有提示词取消方法;放弃轮次意味着关闭运行时(见协议的 [已知限制](../sdk-protocol/README.md))。
|
||||
- **每会话同时只有一个在途提示词**——服务端规则,本客户端将其呈现为 `JsonRpcResponseError`;相互独立的会话可在同一运行时上并发。
|
||||
- **没有逐提示词结果或取消**——低层 `prompt()` 只返回入队回执;高层 `run()` 负责从回执收集到 idle,放弃该过程意味着关闭运行时。
|
||||
- **客户端→服务端通知与服务端→客户端请求**在协议两端都未实现;传输层为未来审批流保留了承载能力。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* High-level turns API over {@link HarnessClient}: `DeepSeekHarness` owns one
|
||||
* High-level run API over {@link HarnessClient}: `DeepSeekHarness` owns one
|
||||
* runtime subprocess across many sessions; `HarnessSession.run` sends a
|
||||
* prompt and settles with the final response once `session.finished` arrives.
|
||||
* prompt and settles when the whole agent next becomes idle.
|
||||
* Mirrors the Python SDK's `DeepSeekHarness`/`Session` pair.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-sdk-client/api
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { resolve } from 'node:path'
|
||||
import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { HarnessClient, isRecord, SdkProtocolError } from './client.ts'
|
||||
import type { ContentBlock, DeepSeekHarnessOptions, HarnessClientOptions, HarnessNotification, TurnResult } from './types.ts'
|
||||
import type { ContentBlock, DeepSeekHarnessOptions, HarnessClientOptions, HarnessNotification, RunResult } from './types.ts'
|
||||
|
||||
/**
|
||||
* Reusable SDK for running DeepSeek Harness agent turns in a runtime
|
||||
@@ -93,9 +93,9 @@ export class DeepSeekHarness implements AsyncDisposable {
|
||||
* Run one prompt on a fresh (or named) session.
|
||||
* @param input - prompt text, or content blocks sent verbatim.
|
||||
* @param options - optional session id and per-notification observer.
|
||||
* @returns the settled turn result.
|
||||
* @returns the owned activity interval.
|
||||
*/
|
||||
run(input: string | ContentBlock[], options?: RunOptions): Promise<TurnResult> {
|
||||
run(input: string | ContentBlock[], options?: RunOptions): Promise<RunResult> {
|
||||
return this.session(options?.sessionId).run(input, options)
|
||||
}
|
||||
|
||||
@@ -127,8 +127,7 @@ export interface RunOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* One SDK session: a stable id plus the turn loop that pairs a
|
||||
* `session/prompt` with its `session.finished`.
|
||||
* One SDK session: a stable id plus owned activity intervals.
|
||||
*/
|
||||
export class HarnessSession {
|
||||
/**
|
||||
@@ -138,27 +137,23 @@ export class HarnessSession {
|
||||
constructor(readonly harness: DeepSeekHarness, readonly id: string) {}
|
||||
|
||||
/**
|
||||
* Run one prompt turn to settlement.
|
||||
* Queue one prompt, then observe the whole session through its next idle.
|
||||
* @param input - prompt text, or content blocks sent verbatim.
|
||||
* @param options - optional per-notification observer.
|
||||
* @returns the settled turn result; rejects on transport loss, timeout, or
|
||||
* a protocol error — never on a model-level failure (that is
|
||||
* `status: 'error'` in the result).
|
||||
* @returns the owned activity interval; rejects on transport loss, timeout,
|
||||
* or a protocol error.
|
||||
*/
|
||||
async run(input: string | ContentBlock[], options?: Pick<RunOptions, 'onNotification'>): Promise<TurnResult> {
|
||||
async run(input: string | ContentBlock[], options?: Pick<RunOptions, 'onNotification'>): Promise<RunResult> {
|
||||
await this.harness.start()
|
||||
const client = this.harness.client
|
||||
const contentBlocks = normalizeInput(input)
|
||||
const events: SessionEvent[] = []
|
||||
const notifications: HarnessNotification[] = []
|
||||
let status: TurnResult['status'] = 'error'
|
||||
let reason: TurnEndReason | undefined
|
||||
let finished = false
|
||||
|
||||
const subscription = client.subscribeSessionTree(this.id)
|
||||
const collect = (notification: HarnessNotification): void => {
|
||||
if (notification.method === 'session.event' && notification.params.sessionId === this.id) {
|
||||
// Wire boundary: the envelope feeds the typed TurnResult, so a
|
||||
// Wire boundary: the envelope feeds the typed RunResult, so a
|
||||
// malformed runtime surfaces as a protocol error, not as type-invalid
|
||||
// data (or a TypeError out of finalResponse).
|
||||
const event = validatedSessionEvent(notification.params.event)
|
||||
@@ -167,37 +162,31 @@ export class HarnessSession {
|
||||
events.push(event)
|
||||
return
|
||||
}
|
||||
if (notification.method === 'session.finished' && notification.params.sessionId === this.id) {
|
||||
reason = validatedTurnEndReason(notification.params.reason)
|
||||
notifications.push(notification)
|
||||
options?.onNotification?.(notification)
|
||||
status = notification.params.status === 'ok' ? 'ok' : 'error'
|
||||
finished = true
|
||||
return
|
||||
}
|
||||
notifications.push(notification)
|
||||
options?.onNotification?.(notification)
|
||||
}
|
||||
const accepted = client.prompt(this.id, contentBlocks)
|
||||
// Drain concurrently so observers see progress while the prompt request
|
||||
// is still pending (its response arrives only after settlement).
|
||||
const drain = (async () => {
|
||||
while (!finished) collect(await subscription.next())
|
||||
})()
|
||||
try {
|
||||
await Promise.all([accepted, drain])
|
||||
const messageId = await client.prompt(this.id, contentBlocks)
|
||||
let received = false
|
||||
while (true) {
|
||||
const notification = await subscription.next()
|
||||
if (!received) {
|
||||
if (notification.method !== 'session.event'
|
||||
|| notification.params.sessionId !== this.id
|
||||
|| !isInboxReceipt(notification.params.event, messageId)) continue
|
||||
received = true
|
||||
}
|
||||
collect(notification)
|
||||
if (notification.method === 'session.status'
|
||||
&& notification.params.sessionId === this.id
|
||||
&& notification.params.status === 'idle') break
|
||||
}
|
||||
} finally {
|
||||
// On a prompt rejection the drain is still parked on next(); closing the
|
||||
// subscription settles it, and the swallow keeps that secondary
|
||||
// TransportClosedError from surfacing as an unhandled rejection.
|
||||
subscription.close()
|
||||
await drain.catch(() => {})
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: this.id,
|
||||
status,
|
||||
reason,
|
||||
finalResponse: finalResponse(events),
|
||||
events,
|
||||
notifications,
|
||||
@@ -232,18 +221,16 @@ function validatedSessionEvent(value: unknown): SessionEvent {
|
||||
return value as unknown as SessionEvent
|
||||
}
|
||||
|
||||
/** Validate a wire `session.finished` reason (absent, or a kind-tagged record). */
|
||||
function validatedTurnEndReason(value: unknown): TurnEndReason | undefined {
|
||||
if (value === undefined) return undefined
|
||||
if (!isRecord(value) || typeof value.kind !== 'string') {
|
||||
throw new SdkProtocolError(`session.finished carried a malformed reason: ${JSON.stringify(value)}`)
|
||||
}
|
||||
return value as unknown as TurnEndReason
|
||||
/** Whether a raw session event is the durable enqueue receipt for `messageId`. */
|
||||
function isInboxReceipt(value: unknown, messageId: string): boolean {
|
||||
if (!isRecord(value) || value.type !== 'agent/inbox/spliced' || !isRecord(value.data)) return false
|
||||
const inserted = value.data.inserted
|
||||
return Array.isArray(inserted) && inserted.some(message => isRecord(message) && message.id === messageId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the concatenated text of the last assistant message.
|
||||
* @param events - the turn's `session.event` payloads in wire order.
|
||||
* @param events - the activity interval's `session.event` payloads in wire order.
|
||||
* @returns the final response text, or `''` when no assistant message exists.
|
||||
*/
|
||||
export function finalResponse(events: SessionEvent[]): string {
|
||||
|
||||
@@ -275,17 +275,18 @@ export class HarnessClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one prompt turn to settlement (the response arrives only after the
|
||||
* turn settled; progress streams as notifications meanwhile).
|
||||
* Queue one prompt and return its durable inbox identity.
|
||||
* @param sessionId - target session; an unknown id creates it.
|
||||
* @param contentBlocks - the user message, sent verbatim.
|
||||
* @returns the queued message id.
|
||||
*/
|
||||
async prompt(sessionId: string, contentBlocks: ContentBlock[]): Promise<void> {
|
||||
async prompt(sessionId: string, contentBlocks: ContentBlock[]): Promise<string> {
|
||||
const params: SessionPromptParams = { sessionId, contentBlocks }
|
||||
const result = await this.request('session/prompt', { ...params })
|
||||
if (!isRecord(result) || result.accepted !== true) {
|
||||
throw new SdkProtocolError(`session/prompt was not accepted: ${JSON.stringify(result)}`)
|
||||
if (!isRecord(result) || typeof result.messageId !== 'string') {
|
||||
throw new SdkProtocolError(`session/prompt returned no message id: ${JSON.stringify(result)}`)
|
||||
}
|
||||
return result.messageId
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* TypeScript client SDK for the DeepSeek Harness runtime: spawn the
|
||||
* `dsh-jsonrpc-agent` runtime as a subprocess and drive agent turns over
|
||||
* stdio JSON-RPC. `DeepSeekHarness` is the high-level turns API;
|
||||
* stdio JSON-RPC. `DeepSeekHarness` is the high-level run API;
|
||||
* `HarnessClient` is the lower-level protocol client. A pure library — it
|
||||
* registers nothing on a Cordis context; the runtime process it spawns is a
|
||||
* complete harness configured by its own `cordis.yml`.
|
||||
@@ -25,5 +25,5 @@ export type {
|
||||
HarnessClientOptions,
|
||||
HarnessNotification,
|
||||
NotificationFilter,
|
||||
TurnResult,
|
||||
RunResult,
|
||||
} from './types.ts'
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
/**
|
||||
* Types for the TypeScript SDK client: launch options, notification shapes,
|
||||
* and turn results.
|
||||
* and owned activity results.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-sdk-client/types
|
||||
*/
|
||||
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import type { SdkRunStatus } from '@deepseek-ai/dsh-sdk-protocol'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** One server-to-client notification as received off the wire. */
|
||||
export interface HarnessNotification {
|
||||
/** The JSON-RPC method name (`session.event`, `session.finished`, `subagent.started`, `subagent.finished`). */
|
||||
/** The JSON-RPC notification method name. */
|
||||
method: string
|
||||
/** The raw params object; see `HarnessSdkNotificationMap` for the shapes per method. */
|
||||
params: Record<string, unknown>
|
||||
@@ -59,15 +58,11 @@ export interface DeepSeekHarnessOptions {
|
||||
maxTokens?: number
|
||||
}
|
||||
|
||||
/** The settled outcome of one {@link HarnessSession.run} turn. */
|
||||
export interface TurnResult {
|
||||
/** The session the turn ran on. */
|
||||
/** One owned session activity interval, from enqueue receipt through idle. */
|
||||
export interface RunResult {
|
||||
/** The session the activity ran on. */
|
||||
sessionId: string
|
||||
/** Deployment-mapped turn outcome from `session.finished`. */
|
||||
status: SdkRunStatus
|
||||
/** Why the last message-triggered turn ended; `undefined` when no turn ran. */
|
||||
reason: TurnEndReason | undefined
|
||||
/** Concatenated text of the session's last assistant message (empty when none). */
|
||||
/** Concatenated text of the interval's last assistant message (empty when none). */
|
||||
finalResponse: string
|
||||
/** Every `session.event` payload for the root session, in wire order. */
|
||||
events: SessionEvent[]
|
||||
|
||||
@@ -142,13 +142,6 @@ function runTurn(sessionId: string): void {
|
||||
lastAssistantMessage: [{ type: 'text', text: 'child says hi' }],
|
||||
})
|
||||
}
|
||||
notify('session.finished', {
|
||||
sessionId,
|
||||
status: env.FAKE_STATUS ?? 'ok',
|
||||
...(env.FAKE_MALFORMED_REASON !== undefined
|
||||
? { reason: 'not-a-record' }
|
||||
: reasonKind === 'none' ? {} : { reason: { kind: reasonKind } }),
|
||||
})
|
||||
}
|
||||
|
||||
function sessionIdOf(params: Record<string, unknown> | undefined): string {
|
||||
@@ -197,8 +190,20 @@ reader.on('line', (line) => {
|
||||
respond({ serverInfo: { name: 'deepseek-harness-sdk-runtime', version: '0.0.1' } })
|
||||
return
|
||||
case 'session/prompt': {
|
||||
const sessionId = sessionIdOf(frame.params)
|
||||
const messageId = `fake-user-${seq}`
|
||||
event(sessionId, 'agent/inbox/spliced', {
|
||||
target: 'next-turn',
|
||||
start: 0,
|
||||
inserted: [{
|
||||
id: messageId,
|
||||
role: 'user',
|
||||
content: [],
|
||||
source: { kind: 'user' },
|
||||
}],
|
||||
})
|
||||
notify('session.status', { sessionId, status: 'running' })
|
||||
if (env.FAKE_STREAM_THEN_MALFORMED !== undefined) {
|
||||
const sessionId = sessionIdOf(frame.params)
|
||||
event(sessionId, 'assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text: 'streamed then cut short' } })
|
||||
respond({})
|
||||
return
|
||||
@@ -208,9 +213,9 @@ reader.on('line', (line) => {
|
||||
respond({})
|
||||
return
|
||||
}
|
||||
const sessionId = sessionIdOf(frame.params)
|
||||
runTurn(sessionId)
|
||||
respond({ accepted: true })
|
||||
notify('session.status', { sessionId, status: 'idle' })
|
||||
respond({ messageId })
|
||||
return
|
||||
}
|
||||
case 'shutdown':
|
||||
|
||||
@@ -56,14 +56,13 @@ describe('DeepSeekHarness', () => {
|
||||
it('runs a turn end to end and reuses the runtime across sessions', async () => {
|
||||
const harness = harnessWith({ FAKE_TEXT: 'turn answer' })
|
||||
const first = await harness.run('say hi')
|
||||
expect(first.status).toBe('ok')
|
||||
expect(first.reason).toEqual({ kind: 'completed' })
|
||||
expect(first.finalResponse).toBe('turn answer')
|
||||
expect(first.events.map(event => event.type)).toEqual(['turn/start', 'assistant/chunk', 'assistant/message', 'turn/end'])
|
||||
expect(first.events.map(event => event.type)).toEqual([
|
||||
'agent/inbox/spliced', 'turn/start', 'assistant/chunk', 'assistant/message', 'turn/end',
|
||||
])
|
||||
|
||||
// Same subprocess, second session: ids differ, protocol state is reusable.
|
||||
const second = await harness.run([{ type: 'text', text: 'again' }])
|
||||
expect(second.status).toBe('ok')
|
||||
expect(second.sessionId).not.toBe(first.sessionId)
|
||||
await harness.close()
|
||||
})
|
||||
@@ -76,13 +75,12 @@ describe('DeepSeekHarness', () => {
|
||||
onNotification: (n) => { seen.push(n) },
|
||||
})
|
||||
|
||||
expect(result.status).toBe('ok')
|
||||
// The child session's events arrive through subagent.started lineage.
|
||||
expect(seen.map(n => n.method)).toContain('subagent.started')
|
||||
expect(seen.map(n => n.method)).toContain('subagent.finished')
|
||||
const childEvents = seen.filter(n => n.method === 'session.event' && n.params.sessionId === 'parent-1-child')
|
||||
expect(childEvents.length).toBeGreaterThan(0)
|
||||
// TurnResult.events is the root session's typed stream; descendants retain
|
||||
// RunResult.events is the root session's typed stream; descendants retain
|
||||
// their session ids in the raw notification stream above.
|
||||
expect(result.events.every(event => event.type !== 'assistant/message'
|
||||
|| event.data.message.content[0]?.type !== 'text'
|
||||
@@ -90,22 +88,6 @@ describe('DeepSeekHarness', () => {
|
||||
await harness.close()
|
||||
})
|
||||
|
||||
it('reports an error status with the turn-end reason', async () => {
|
||||
const harness = harnessWith({ FAKE_STATUS: 'error', FAKE_REASON_KIND: 'max-tokens' })
|
||||
const result = await harness.run('overflow')
|
||||
expect(result.status).toBe('error')
|
||||
expect(result.reason).toEqual({ kind: 'max-tokens' })
|
||||
await harness.close()
|
||||
})
|
||||
|
||||
it('omits the reason when the runtime settled without one', async () => {
|
||||
const harness = harnessWith({ FAKE_STATUS: 'error', FAKE_REASON_KIND: 'none' })
|
||||
const result = await harness.run('no turn')
|
||||
expect(result.status).toBe('error')
|
||||
expect(result.reason).toBeUndefined()
|
||||
await harness.close()
|
||||
})
|
||||
|
||||
it('sends the configured cwd/provider/model/maxTokens in the handshake exactly once', async () => {
|
||||
const dir = await tempDir('sdk-client-init-')
|
||||
const recordFile = join(dir, 'init.jsonl')
|
||||
@@ -311,14 +293,14 @@ describe('HarnessClient', () => {
|
||||
await client.initialize({ cwd: process.cwd(), provider: 'p', model: 'm' })
|
||||
|
||||
const all = client.subscribe()
|
||||
const finishedOnly = client.subscribe(n => n.method === 'session.finished')
|
||||
const idleOnly = client.subscribe(n => n.method === 'session.status' && n.params.status === 'idle')
|
||||
await client.prompt('sub-test', normalizeInput('go'))
|
||||
|
||||
const first = await all.next()
|
||||
expect(first.method).toBe('session.event')
|
||||
const finished = await finishedOnly.next()
|
||||
expect(finished.method).toBe('session.finished')
|
||||
expect(finishedOnly.tryNext()).toBeUndefined()
|
||||
const idle = await idleOnly.next()
|
||||
expect(idle.method).toBe('session.status')
|
||||
expect(idleOnly.tryNext()).toBeUndefined()
|
||||
|
||||
// A bare unbounded request with omitted params sends `{}` on the wire.
|
||||
const identity = await client.request('initialize') as { serverInfo: { name: string } }
|
||||
@@ -328,12 +310,12 @@ describe('HarnessClient', () => {
|
||||
const collected: string[] = []
|
||||
for await (const notification of all) {
|
||||
collected.push(notification.method)
|
||||
if (notification.method === 'session.finished') break
|
||||
if (notification.method === 'session.status' && notification.params.status === 'idle') break
|
||||
}
|
||||
expect(collected.at(-1)).toBe('session.finished')
|
||||
expect(collected.at(-1)).toBe('session.status')
|
||||
|
||||
all.close()
|
||||
finishedOnly.close()
|
||||
idleOnly.close()
|
||||
await expect(all.next()).rejects.toThrow('notification subscription closed')
|
||||
await client.close()
|
||||
})
|
||||
@@ -346,11 +328,11 @@ describe('HarnessClient', () => {
|
||||
const broken = client.subscribe(() => { throw new Error('filter exploded') })
|
||||
// A non-Error throw is normalized rather than crashing dispatch.
|
||||
const brokenNonError = client.subscribe(() => { throw 'string boom' })
|
||||
const healthy = client.subscribe(n => n.method === 'session.finished')
|
||||
const healthy = client.subscribe(n => n.method === 'session.status' && n.params.status === 'idle')
|
||||
await client.prompt('filter-contain', normalizeInput('go'))
|
||||
|
||||
// The sibling subscription and the read loop are undisturbed.
|
||||
expect((await healthy.next()).method).toBe('session.finished')
|
||||
expect((await healthy.next()).method).toBe('session.status')
|
||||
// Each broken subscription failed with ITS OWN error and detached.
|
||||
await expect(broken.next()).rejects.toThrow('filter exploded')
|
||||
await expect(brokenNonError.next()).rejects.toThrow('string boom')
|
||||
@@ -445,10 +427,6 @@ describe('wire payload validation', () => {
|
||||
await expect(harness.run('no-data')).rejects.toThrow(SdkProtocolError)
|
||||
})
|
||||
|
||||
it('rejects a malformed session.finished reason as a protocol error', async () => {
|
||||
const harness = harnessWith({ FAKE_MALFORMED_REASON: '1' })
|
||||
await expect(harness.run('bad-reason')).rejects.toThrow(SdkProtocolError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('stderr tail bound', () => {
|
||||
|
||||
@@ -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/sdk/sdk-protocol/README.md
|
||||
README.md: 62b26d4a82d358fa4efcb7ab84036e5f4848057f
|
||||
README.zh.md: 11677c6119c7da407d95ee38ad9f8f7a552c15de
|
||||
README.md: 2120e6090fcc5d5f4a543424e9c5647e6009bad6
|
||||
README.zh.md: 70b046cf1dedbf01031e3e0a4441f522d6153fbc
|
||||
|
||||
@@ -15,14 +15,14 @@ The shared wire protocol for the DeepSeek Harness SDK runtime: one newline-delim
|
||||
| Direction | Method | Types |
|
||||
|---|---|---|
|
||||
| client→server | `initialize` | `InitializeParams` → `InitializeResult` |
|
||||
| client→server | `session/prompt` | `SessionPromptParams` → `SessionPromptResult` (answered only after turn settlement) |
|
||||
| client→server | `session/prompt` | `SessionPromptParams` → `SessionPromptResult` (durable enqueue receipt) |
|
||||
| client→server | `shutdown` | no params → `{}` |
|
||||
| server→client | `session.event` | `SessionEventNotification` (every session in the runtime, unfiltered) |
|
||||
| server→client | `session.finished` | `SessionFinishedNotification` (one per accepted prompt) |
|
||||
| server→client | `session.status` | `SessionStatusNotification` (whole-agent `running`/`idle` transition) |
|
||||
| server→client | `subagent.started` | `SubagentStartedNotification` |
|
||||
| server→client | `subagent.finished` | `SubagentFinishedNotification` (in-process runs only) |
|
||||
|
||||
`HarnessSdkRequestMap` and `HarnessSdkNotificationMap` index these by method name. `InitializeParams.maxTokens` is an optional positive safe integer that caps each conversation-model output for SDK-created agents and their in-process descendants; omission leaves the provider default in control. The notification payload types depend on `SessionEvent` (`dsh-session`), `ContentBlock` (`dsh-llm`), and `SubagentStopReason` (`dsh-subagent`) — the protocol streams full session-log envelopes, so the session vocabulary is part of the wire contract. `serverInfo.name` stays the wire-stable `deepseek-harness-sdk-runtime`.
|
||||
`HarnessSdkRequestMap` and `HarnessSdkNotificationMap` index these by method name. `SessionPromptResult.messageId` identifies the queued `UserMessage`; it does not identify a later assistant message, turn ending, or prompt result. Clients combine the open-ended `session.event` stream with agent-wide `session.status` according to their own activity ownership. `InitializeParams.maxTokens` is an optional positive safe integer that caps each conversation-model output for SDK-created agents and their in-process descendants; omission leaves the provider default in control. The notification payload types depend on `SessionEvent` (`dsh-session`), `ContentBlock` (`dsh-llm`), and `SubagentStopReason` (`dsh-subagent`) — the protocol streams full session-log envelopes, so the session vocabulary is part of the wire contract. `serverInfo.name` stays the wire-stable `deepseek-harness-sdk-runtime`.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@ DeepSeek Harness SDK 运行时的共享协议格式(wire format):一个按
|
||||
| 方向 | 方法 | 类型 |
|
||||
|---|---|---|
|
||||
| client→server | `initialize` | `InitializeParams` → `InitializeResult` |
|
||||
| client→server | `session/prompt` | `SessionPromptParams` → `SessionPromptResult`(仅在轮次结算完成后应答) |
|
||||
| client→server | `session/prompt` | `SessionPromptParams` → `SessionPromptResult`(持久入队回执) |
|
||||
| client→server | `shutdown` | 无参数 → `{}` |
|
||||
| server→client | `session.event` | `SessionEventNotification`(运行时内每个会话,不过滤) |
|
||||
| server→client | `session.finished` | `SessionFinishedNotification`(每个获准的提示词请求一条) |
|
||||
| server→client | `session.status` | `SessionStatusNotification`(整个 agent(智能体)的 `running`/`idle` 转换) |
|
||||
| server→client | `subagent.started` | `SubagentStartedNotification` |
|
||||
| server→client | `subagent.finished` | `SubagentFinishedNotification`(仅进程内运行) |
|
||||
|
||||
`HarnessSdkRequestMap` 与 `HarnessSdkNotificationMap` 按方法名索引这些类型。`InitializeParams.maxTokens` 是可选的正的安全整数,用于限制 SDK 创建的 agent(智能体)及其进程内后代的每次对话模型输出;省略时由提供方默认值控制。通知载荷类型依赖 `SessionEvent`(`dsh-session`)、`ContentBlock`(`dsh-llm`)与 `SubagentStopReason`(`dsh-subagent`)——协议以完整会话日志封套进行流式传输,因此会话词汇是协议格式契约的一部分。`serverInfo.name` 的协议值固定为 `deepseek-harness-sdk-runtime`。
|
||||
`HarnessSdkRequestMap` 与 `HarnessSdkNotificationMap` 按方法名索引这些类型。`SessionPromptResult.messageId` 标识已排队的 `UserMessage`;它不标识后续的助手消息、轮次结束或提示词结果。客户端根据自己对活动区间的所有权,组合持续开放的 `session.event` 流与 agent 级的 `session.status`。`InitializeParams.maxTokens` 是可选的正的安全整数,用于限制 SDK 创建的 agent 及其进程内后代的每次对话模型输出;省略时由提供方默认值控制。通知载荷类型依赖 `SessionEvent`(`dsh-session`)、`ContentBlock`(`dsh-llm`)与 `SubagentStopReason`(`dsh-subagent`)——协议以完整会话日志封套进行流式传输,因此会话词汇是协议格式契约的一部分。`serverInfo.name` 的协议值固定为 `deepseek-harness-sdk-runtime`。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export type {
|
||||
InitializeResult,
|
||||
SdkRunStatus,
|
||||
SessionEventNotification,
|
||||
SessionFinishedNotification,
|
||||
SessionStatusNotification,
|
||||
SessionPromptParams,
|
||||
SessionPromptResult,
|
||||
SubagentFinishedNotification,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { SubagentStopReason } from '@deepseek-ai/dsh-subagent'
|
||||
|
||||
/** Parameters for the process-wide SDK handshake. */
|
||||
@@ -38,10 +38,10 @@ export interface SessionPromptParams {
|
||||
contentBlocks: ContentBlock[]
|
||||
}
|
||||
|
||||
/** Prompt acceptance after turn settlement; outcome rides on `session.finished`. */
|
||||
/** Durable enqueue receipt for one prompt. */
|
||||
export interface SessionPromptResult {
|
||||
/** Always `true`; the turn outcome is the paired `session.finished` notification. */
|
||||
accepted: true
|
||||
/** Identity of the queued user message. */
|
||||
messageId: string
|
||||
}
|
||||
|
||||
/** Deployment-mapped SDK outcome: `ok` for an accepted result, `error` otherwise. */
|
||||
@@ -55,14 +55,12 @@ export interface SessionEventNotification {
|
||||
event: SessionEvent
|
||||
}
|
||||
|
||||
/** `session.finished` payload: one per accepted prompt, after turn settlement. */
|
||||
export interface SessionFinishedNotification {
|
||||
/** The settled session. */
|
||||
/** Whole-agent lifecycle state for one session. */
|
||||
export interface SessionStatusNotification {
|
||||
/** Session whose live agent changed status. */
|
||||
sessionId: string
|
||||
/** Deployment-mapped turn outcome (see `maxTokensAsSuccess` on the server). */
|
||||
status: SdkRunStatus
|
||||
/** Why the last message-triggered turn ended; absent when no turn ran. */
|
||||
reason: TurnEndReason | undefined
|
||||
/** The whole-agent state after the transition. */
|
||||
status: 'idle' | 'running'
|
||||
}
|
||||
|
||||
/** `subagent.started` payload: an in-runtime child session was created. */
|
||||
@@ -94,7 +92,7 @@ export interface SubagentFinishedNotification {
|
||||
/** Server-to-client notifications by JSON-RPC method name. */
|
||||
export interface HarnessSdkNotificationMap {
|
||||
'session.event': SessionEventNotification
|
||||
'session.finished': SessionFinishedNotification
|
||||
'session.status': SessionStatusNotification
|
||||
'subagent.started': SubagentStartedNotification
|
||||
'subagent.finished': SubagentFinishedNotification
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ describe('JsonRpcLineTransport', () => {
|
||||
const response = await b.request('echo', { value: 42 })
|
||||
expect(response).toEqual({ echoed: { value: 42 } })
|
||||
|
||||
a.notify('session.finished', { sessionId: 'main', status: 'ok' })
|
||||
a.notify('session.status', { sessionId: 'main', status: 'idle' })
|
||||
a.notify('heartbeat')
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
expect(notifications).toEqual([
|
||||
{ method: 'session.finished', params: { sessionId: 'main', status: 'ok' } },
|
||||
{ method: 'session.status', params: { sessionId: 'main', status: 'idle' } },
|
||||
{ method: 'heartbeat', params: {} },
|
||||
])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user