refactor(api-session-controller): route subagent calls through Remote

This commit is contained in:
imccyu
2026-08-26 14:52:53 +08:00
parent 377f3b4f1d
commit cbe5d76e5c
13 changed files with 47 additions and 34 deletions
@@ -1,10 +1,11 @@
/** Client operation results spanning Session Remote calls and the legacy subagent carrier. */
/** Client operation results spanning the Session and subagent Remote calls. */
import type { RpcError } from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentControlError } from '@deepseek-ai/dsh-subagent/client'
import type { SessionError } from '../../types.ts'
/** Failure surfaced by the Client Session object layer. */
export type ClientFailure = RpcError | SessionError
export type ClientFailure = RpcError | SessionError | SubagentControlError
/** Success or failure returned by a Client Session operation. */
export type ClientResult<T> =
@@ -13,7 +14,7 @@ export type ClientResult<T> =
/**
* Fold a rejected carrier operation into the Client Session failure vocabulary.
* @param error - rejection from a legacy subagent or local carrier call.
* @param error - rejection from a Remote or local carrier call.
* @returns the failure branch of a Client Session result.
*/
export function transportResult<T>(error: unknown): ClientResult<T> {
@@ -5,7 +5,7 @@
* explicit act of widening what features may do to the sessions domain.
*/
import type { Context } from '@deepseek-ai/cordis'
import type { SubagentAddress } from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import type { AgentContext } from '../scope.ts'
@@ -2,7 +2,7 @@
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { SubagentAddress } from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { ClientFailure } from './result.ts'
/** One transient inbox occurrence from the authoritative queue snapshot. */
@@ -73,6 +73,7 @@ export const inject = [
'remote',
'remote.commands',
'remote.session',
'remote.subagents',
]
/**
@@ -82,7 +83,7 @@ export const inject = [
export function apply(ctx: Context): void {
const connection = ctx.get('connection') as ConnectionHandle
const remotes = ctx.remote as unknown as SessionRemotes
const sessions = new ClientSessions(ctx, connection.api, remotes)
const sessions = new ClientSessions(ctx, remotes)
ctx.remote.$on('api-session/added', (summary) => { sessions.handleSessionAdded(summary) })
ctx.remote.$on('api-session/removed', (sessionId) => { sessions.handleSessionRemoved(sessionId) })
ctx.remote.$on('api-session/status', (sessionId, running) => {
@@ -2,9 +2,7 @@
// dispatch entry + list state, constructed and held by ClientSessions (one per browser client).
// List data never enters zustand; React connects via subscribe/getListSnapshot.
import type {
IApiClient, SubagentAddress, SubagentCatalog,
} from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress, SubagentCatalog } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import type {
@@ -145,11 +143,10 @@ export class SessionManager {
})
/**
* @param api - shared wire client.
* @param remote - generated Remote namespaces the Session cluster calls.
* @param restoredSelection - persisted real-Session selection candidate.
*/
constructor(
private readonly api: IApiClient,
private readonly remote: SessionRemotes,
restoredSelection?: SessionId,
restoredAddress?: SubagentAddress,
@@ -321,7 +318,7 @@ export class SessionManager {
const parentAvailable = address === undefined
? undefined
: this.catalogs.get(address.parentSessionId)?.parentAvailable
return new Session(sessionId, this.api, this.remote, {
return new Session(sessionId, this.remote, {
...(address === undefined ? {} : {
address,
...catalogAvailability(parentAvailable),
@@ -369,7 +366,7 @@ export class SessionManager {
this.notifier.markDirty()
const operation = (async () => {
try {
const { result } = await this.api.subagents.list({ parentSessionId })
const result = toSessionResult(await this.remote.subagents.list(parentSessionId))
if (result.ok) {
const parentAvailable = this.catalogInflight.get(parentSessionId)?.parentAvailableOverride
?? result.value.parentAvailable
@@ -8,6 +8,9 @@
import type { EncodedImageAttachment } from '@deepseek-ai/dsh-attachment/types'
import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type {
SubagentCatalog, SubagentInterruptReceipt, SubagentPromptReceipt, SubagentPromptRequest,
} from '@deepseek-ai/dsh-subagent/client'
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import type { SessionRemote } from '../transport.ts'
@@ -21,9 +24,24 @@ export interface SessionCommandsRemote {
): Promise<RemoteResult<object | undefined>>
}
/** Narrow subagent namespace consumed by a Client Session and its manager. */
export interface SessionSubagentsRemote {
list(parentSessionId: SessionId, signal?: AbortSignal): Promise<RemoteResult<SubagentCatalog>>
prompt(
request: SubagentPromptRequest,
signal?: AbortSignal,
): Promise<RemoteResult<SubagentPromptReceipt>>
interruptByParent(
childSessionId: SessionId,
parentSessionId: SessionId,
mode: 'continuable',
): Promise<RemoteResult<SubagentInterruptReceipt>>
}
/** Generated Remote namespaces consumed by the Client Session object layer. */
export interface SessionRemotes {
readonly $stream: ClientRemote['$stream']
readonly commands: SessionCommandsRemote
readonly session: SessionRemote
readonly subagents: SessionSubagentsRemote
}
@@ -15,9 +15,7 @@
* survives frozen (read-only view) until the stage moves on.
*/
import type { Context, Fiber } from '@deepseek-ai/cordis'
import type {
IApiClient, SubagentAddress,
} from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { workspaceTitleOf } from '@deepseek-ai/dsh-util-workspace-path'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
@@ -218,12 +216,10 @@ export class ClientSessions implements ISessions {
/**
* @param ctx - client root context (scope fibers mount under it).
* @param api - wire client shared with every Session.
* @param remote - generated Remote namespaces shared with every Session.
*/
constructor(
private readonly rootCtx: Context,
api: IApiClient,
remote: SessionRemotes,
) {
this.selection = createSnapshotStore<SessionSelection>(
@@ -231,7 +227,6 @@ export class ClientSessions implements ISessions {
{ persist: { name: 'dsh.sessions.current' } })
const restored = this.selection.getSnapshot()
this.manager = new SessionManager(
api,
remote,
restored.sessionId,
restored.subagentAddress,
@@ -3,9 +3,7 @@
import type { Context } from '@deepseek-ai/cordis'
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type {
IApiClient, SubagentAddress,
} from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import {
@@ -133,13 +131,11 @@ export class Session implements SessionFace {
/**
* @param sessionId - Host session identity (client sessions are always Host-born).
* @param api - shared wire client.
* @param remote - generated Remote namespaces this session calls.
* @param options - optional manager-owned state observers.
*/
constructor(
readonly sessionId: SessionId,
private readonly api: IApiClient,
private readonly remote: SessionRemotes,
private readonly options: SessionOptions = {},
) {
@@ -222,13 +218,16 @@ export class Session implements SessionFace {
},
}
} else {
const routed = (await this.api.subagents.prompt({
...this.address,
const routed = toSessionResult(await this.remote.subagents.prompt({
requestId: randomUUID() as SessionRequestId,
parentSessionId: this.address.parentSessionId,
childSessionId: this.address.childSessionId,
mode: this.address.mode,
content: content.flatMap(part => part.type === 'text'
? [{ type: 'text' as const, text: part.text }]
: []),
clientTimeZone: resolvedClientTimeZone(),
}, signal)).result
}, signal))
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
}
}
@@ -290,7 +289,7 @@ export class Session implements SessionFace {
/**
* Stop the active turn while the Host preserves pending inbox work; failures
* land in promptError (same error-strip display slot). A continuable
* subagent address routes through `subagent.interrupt`, whose durable
* subagent address routes through `subagents.interruptByParent`, whose durable
* parent-address authority works without a live parent Agent; a one-shot
* address stays uncancellable (the UI offers no stop action, so this arm is
* defensive).
@@ -314,7 +313,11 @@ export class Session implements SessionFace {
let result: ClientResult<{ accepted: true }>
try {
result = address !== undefined
? (await this.api.subagents.interrupt(address)).result
? toSessionResult(await this.remote.subagents.interruptByParent(
address.childSessionId,
address.parentSessionId,
address.mode,
))
: toSessionResult(await this.remote.session.cancel({ sessionId: this.sessionId }))
} catch (error) {
result = transportResult(error)
@@ -21,6 +21,7 @@
{ "path": "../../llm/llm" },
{ "path": "../../session/session-projection" },
{ "path": "../../session/session-title" },
{ "path": "../../subagent/subagent" },
{ "path": "../../util/brand" },
{ "path": "../../util/crypto" },
{ "path": "../../util/workspace-path" },
@@ -14,7 +14,6 @@ export type {
ModelReasoningEffort, ModelSelection,
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi,
SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt,
} from '@deepseek-ai/dsh-host-apiproxy/api'
export type {
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
@@ -35,7 +35,6 @@ export type {
SkillsApi, SkillEntry,
ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
MessageId, ModelReasoningEffort, ModelSelection,
SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt,
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
ClientRequest, ServerResponse, RpcMessage,
HostDescription, IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
@@ -6,7 +6,7 @@ import {
type SessionListState, type SessionProjectionMap, type SessionSummary,
type SubagentCatalogSnapshot,
} from '@deepseek-ai/dsh-api-session-controller/client'
import type { SubagentAddress } from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import {
IconChevronDownOutline14, IconChevronRightOutline14, IconRefreshOutline14, StateDot,
@@ -14,7 +14,6 @@ import {
import type { PropsLocale, PropsRuntime, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
import { NS } from './locales.ts'
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {} from '@deepseek-ai/dsh-subagent/client'
import type {} from '@deepseek-ai/dsh-token-meter/client'
import css from './SubagentHeaderLineage.module.css'
import { indexSubagentDescendants } from './subagent-lineage.ts'
@@ -1,6 +1,6 @@
/** Web subagent catalog, navigation, and addressed-session composer owner. */
import type { Context as ClientContext } from '@deepseek-ai/cordis'
import type { SubagentAddress } from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { ComposerChainProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
import { SubagentHeaderLineage, type SubagentCatalogInjected } from './SubagentHeaderLineage.tsx'