mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge branch 'master' into worktree/web-session-titles
This commit is contained in:
@@ -34,9 +34,12 @@ export type {
|
||||
} from './contract/store.ts'
|
||||
export type {
|
||||
AssistantBlock, AssistantMessageNode, ContextMessageNode, ConversationNode, ConversationSnapshot,
|
||||
PendingInteraction, RunningToolCall, SteeringMessageNode,
|
||||
RunningToolCall, SteeringMessageNode,
|
||||
ToolResultNode, UnknownSurfaceNode, UserMessageNode,
|
||||
} from './sessions/conversation.ts'
|
||||
// PendingWait is a value export: tests construct fixture waits directly.
|
||||
export { PendingWait } from './sessions/pending.ts'
|
||||
export type { PendingInteraction, PendingKind, PendingPayloads } from './sessions/pending.ts'
|
||||
export type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
|
||||
// ---- Narrowed aliases (the single narrowing point of the slot type chain:
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
// string here (narrow to real brands when convenient).
|
||||
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { RpcError, RpcId, SessionId, ToolCallView, ToolResultView } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { RpcError, SessionId, ToolCallView, ToolResultView } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { PendingInteraction } from './pending.ts'
|
||||
|
||||
/** Assistant content blocks sorted by what the UI cares about
|
||||
* (text body / collapsible reasoning / tool-call card head / other fallback). */
|
||||
@@ -121,11 +122,6 @@ export interface RunningToolCall {
|
||||
callView: ToolCallView | null
|
||||
}
|
||||
|
||||
/** Approval/question placeholder cards (visible, not answerable;
|
||||
* rpcId = the requested frame's envelope id, the future respond backfill key). */
|
||||
export type PendingInteraction =
|
||||
| { kind: 'approval'; rpcId: RpcId; approvalId: string; toolName: string; callId?: string; reason?: string }
|
||||
| { kind: 'question'; rpcId: RpcId; questions: readonly unknown[] }
|
||||
|
||||
/** In-progress assistant output (chunk accumulator product). */
|
||||
export interface PartialAssistant {
|
||||
|
||||
79
packages/client/runtime/src/client/sessions/pending.ts
Normal file
79
packages/client/runtime/src/client/sessions/pending.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// PendingWait: the carrier-protocol half of a pending host interaction. The runtime owns only
|
||||
// envelope knowledge (rpcId backfill into a client-response); domain result encoding belongs to
|
||||
// the interaction's consumer package.
|
||||
|
||||
import type {
|
||||
ClientResponse, MuxFrame, RpcId, RpcReceipt, SessionId,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
|
||||
/** Kind-keyed payload map: the requested frame's domain fields (envelope fields stripped). */
|
||||
export interface PendingPayloads {
|
||||
approval: Omit<Extract<MuxFrame, { type: 'approval/requested' }>, 'type' | 'sessionId'>
|
||||
question: Omit<Extract<MuxFrame, { type: 'question/requested' }>, 'type' | 'sessionId'>
|
||||
}
|
||||
|
||||
/** Pending-interaction discriminant (the keys of PendingPayloads). */
|
||||
export type PendingKind = keyof PendingPayloads
|
||||
|
||||
/** Kind-discriminated union of concrete waits: narrowing on `kind` types `payload`. */
|
||||
export type PendingInteraction = { [K in PendingKind]: PendingWait<K> }[PendingKind]
|
||||
|
||||
/** Key prefixes, one per kind (the key doubles as the Session pending-map key). */
|
||||
const KEY_PREFIX: Record<PendingKind, string> = { approval: 'a', question: 'q' }
|
||||
|
||||
/**
|
||||
* One pending host-owned interaction wait: an immutable render face
|
||||
* (kind/key/sessionId/payload) plus the response carrier. respond() backfills
|
||||
* the requested frame's rpcId into a client-response envelope — no consumer
|
||||
* ever sees the raw rpcId. Settlement is expressed only by pending-list
|
||||
* membership (the settled flag is a fail-loud guard, not a render input).
|
||||
*/
|
||||
export class PendingWait<K extends PendingKind = PendingKind> {
|
||||
/** Interaction kind (union discriminant). */
|
||||
readonly kind: K
|
||||
/** Opaque render identity, `<prefix>:<rpcId>` — stable across baseline replay, usable as a React key. */
|
||||
readonly key: string
|
||||
/** Owning session. */
|
||||
readonly sessionId: SessionId
|
||||
/** The requested frame's domain fields, verbatim. */
|
||||
readonly payload: PendingPayloads[K]
|
||||
#settled = false
|
||||
readonly #rpcId: RpcId
|
||||
readonly #respond: (message: ClientResponse) => Promise<RpcReceipt>
|
||||
|
||||
/**
|
||||
* Minted by Session on a requested frame (public construction is the test-fixture path).
|
||||
* @param kind - interaction kind.
|
||||
* @param rpcId - the requested frame's stable envelope id (kept private; respond echoes it).
|
||||
* @param sessionId - owning session.
|
||||
* @param payload - the requested frame's domain fields.
|
||||
* @param respond - the client-response carrier (api.respond).
|
||||
*/
|
||||
constructor(
|
||||
kind: K, rpcId: RpcId, sessionId: SessionId, payload: PendingPayloads[K],
|
||||
respond: (message: ClientResponse) => Promise<RpcReceipt>,
|
||||
) {
|
||||
this.kind = kind
|
||||
this.key = `${KEY_PREFIX[kind]}:${rpcId}`
|
||||
this.sessionId = sessionId
|
||||
this.payload = payload
|
||||
this.#rpcId = rpcId
|
||||
this.#respond = respond
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a result for this wait: wraps it into the client-response envelope
|
||||
* with the rpcId backfilled. Throws synchronously once settled.
|
||||
* @param result - the result shell (ok value / error envelope), domain-encoded by the caller.
|
||||
* @returns the carrier receipt.
|
||||
*/
|
||||
respond(result: ClientResponse['result']): Promise<RpcReceipt> {
|
||||
if (this.#settled) throw new Error(`pending wait ${this.key} is already settled`)
|
||||
return this.#respond({ type: 'client-response', rpcId: this.#rpcId, result })
|
||||
}
|
||||
|
||||
/** Session-only settlement mark (the authoritative resolved frame arrived); respond() throws afterwards. */
|
||||
markSettled(): void {
|
||||
this.#settled = true
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,17 @@
|
||||
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import type { HistoryEntry, IApiClient, MuxFrame, RpcError, RpcId, RpcResult, SessionId, ToolEventView } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type {
|
||||
HistoryEntry, IApiClient, MuxFrame, RpcError, RpcId, RpcResult,
|
||||
SessionId, ToolEventView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { transportError } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ObservableSnapshot } from '../contract/store.ts'
|
||||
import type {
|
||||
ConversationNode, ConversationSnapshot, OpenState, PendingInteraction, PromptError, RunningToolCall,
|
||||
ConversationNode, ConversationSnapshot, OpenState, PromptError, RunningToolCall,
|
||||
} from './conversation.ts'
|
||||
import type { PendingInteraction } from './pending.ts'
|
||||
import { PendingWait } from './pending.ts'
|
||||
import { FoldAdapter } from './fold-adapter.ts'
|
||||
import { Notifier } from './notifier.ts'
|
||||
import { PartialAccumulator } from './partial.ts'
|
||||
@@ -183,7 +188,9 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
this.events = []
|
||||
this.views = []
|
||||
this.baseSeq = 0
|
||||
this.pending.clear() // the subscribed baseline replay re-sends still-pending requested frames verbatim
|
||||
// Superseded, not settled: the baseline replay re-sends still-pending requested frames verbatim
|
||||
// (same rpcId), re-minting fresh waits; a stale reference's respond() still reaches the host.
|
||||
this.pending.clear()
|
||||
this.pendingRev++
|
||||
this.subscribedLastSeq = null
|
||||
this.liveBuffer = []
|
||||
@@ -229,33 +236,27 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
return // pure baseline bookkeeping, no visible change
|
||||
}
|
||||
case 'approval/requested': {
|
||||
this.pending.set(`a:${rpcId}`, {
|
||||
kind: 'approval', rpcId, approvalId: frame.approvalId, toolName: frame.toolName,
|
||||
...(frame.callId !== undefined ? { callId: frame.callId } : {}),
|
||||
...(frame.reason !== undefined ? { reason: frame.reason } : {}),
|
||||
})
|
||||
this.pendingRev++
|
||||
const { type: _type, sessionId: _sid, ...payload } = frame
|
||||
this.mint(new PendingWait('approval', rpcId, this.sessionId, payload, m => this.api.respond(m)))
|
||||
this.notifier.markDirty()
|
||||
return
|
||||
}
|
||||
case 'approval/resolved': {
|
||||
for (const [key, item] of this.pending) {
|
||||
if (item.kind === 'approval' && item.approvalId === frame.approvalId) {
|
||||
this.pending.delete(key)
|
||||
this.pendingRev++
|
||||
}
|
||||
for (const item of this.pending.values()) {
|
||||
if (item.kind === 'approval' && item.payload.approvalId === frame.approvalId) this.settle(item)
|
||||
}
|
||||
this.notifier.markDirty()
|
||||
return
|
||||
}
|
||||
case 'question/requested': {
|
||||
this.pending.set(`q:${rpcId}`, { kind: 'question', rpcId, questions: frame.questions })
|
||||
this.pendingRev++
|
||||
const { type: _type, sessionId: _sid, ...payload } = frame
|
||||
this.mint(new PendingWait('question', rpcId, this.sessionId, payload, m => this.api.respond(m)))
|
||||
this.notifier.markDirty()
|
||||
return
|
||||
}
|
||||
case 'question/resolved': {
|
||||
if (this.pending.delete(`q:${frame.questionRpcId}`)) this.pendingRev++
|
||||
const item = this.pending.get(`q:${frame.questionRpcId}`)
|
||||
if (item !== undefined) this.settle(item)
|
||||
this.notifier.markDirty()
|
||||
return
|
||||
}
|
||||
@@ -295,6 +296,19 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
|
||||
// ---- 私有 ----
|
||||
|
||||
/** Requested-frame arrival: the wait enters the pending map under its own key. */
|
||||
private mint(wait: PendingInteraction): void {
|
||||
this.pending.set(wait.key, wait)
|
||||
this.pendingRev++
|
||||
}
|
||||
|
||||
/** Authoritative resolved-frame settlement: mark, then drop from the pending map. */
|
||||
private settle(wait: PendingInteraction): void {
|
||||
wait.markSettled()
|
||||
this.pending.delete(wait.key)
|
||||
this.pendingRev++
|
||||
}
|
||||
|
||||
/** @param generation - openGeneration at launch; every await re-checks it and a stale pass
|
||||
* drops all writes (resync superseded this open — its outcome belongs to a dead connection). */
|
||||
private async doOpen(generation: number): Promise<void> {
|
||||
|
||||
@@ -66,6 +66,10 @@ interface ErasedRegisterOptions {
|
||||
id?: string
|
||||
order?: number
|
||||
label?: string
|
||||
/** Chain-slot routing selector (pure; the core validates presence for chain targets). */
|
||||
select?: (owner: never) => unknown
|
||||
/** Chain-slot explicit ordering override (ascending; registration order otherwise). */
|
||||
priority?: number
|
||||
registrant?: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user