feat(subagent): migrate browser control to Remote

This commit is contained in:
imccyu
2026-08-26 14:52:53 +08:00
parent 1d00f5b4c0
commit 377f3b4f1d
23 changed files with 507 additions and 467 deletions
+2
View File
@@ -76,6 +76,7 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/dsh-user-questions": "workspace:^"
},
@@ -97,6 +98,7 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/dsh-user-questions": "workspace:^"
}
+5 -2
View File
@@ -9,6 +9,7 @@ import fileReferencesRemote from '@deepseek-ai/dsh-file-reference/remote'
import pluginInventoryRemote from '@deepseek-ai/dsh-host-plugin-inventory/remote'
import messageFeedbackRemote from '@deepseek-ai/dsh-message-feedback/remote'
import sessionReferencesRemote from '@deepseek-ai/dsh-session-reference/remote'
import subagentsRemote from '@deepseek-ai/dsh-subagent/remote'
import sessionRemote from '@deepseek-ai/dsh-api-session-controller/remote'
import workspaceRemote from '@deepseek-ai/dsh-api-workspace-controller/remote'
import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
@@ -22,6 +23,8 @@ export type {} from '@deepseek-ai/dsh-goal/remote'
export type {} from '@deepseek-ai/dsh-host-plugin-inventory/remote'
export type {} from '@deepseek-ai/dsh-message-feedback/remote'
export type {} from '@deepseek-ai/dsh-session-reference/remote'
export type {} from '@deepseek-ai/dsh-subagent/remote'
export type * from '@deepseek-ai/dsh-subagent/client'
export type {} from '@deepseek-ai/dsh-api-session-controller/remote'
export type * from '@deepseek-ai/dsh-api-session-controller/types'
export type {} from '@deepseek-ai/dsh-api-workspace-controller/remote'
@@ -54,7 +57,6 @@ export type {
MessageId, ModelCatalog, ModelCatalogFailure, ModelProviderGroup, ModelReasoningEffort, ModelSelection,
RpcError, RpcId, RpcRequest, RpcResponse, RpcResult, SessionId,
SettingsNamespaceView, SettingsPathOpView, SkillEntry, StreamChunk,
SubagentAddress, SubagentCatalog,
} from '@deepseek-ai/dsh-client-connection/client'
export type {} from '@deepseek-ai/dsh-api-gateway/client'
export type {} from '@deepseek-ai/dsh-cordis-host-runner/remote'
@@ -110,6 +112,7 @@ export type ClientFailure =
| import('@deepseek-ai/dsh-client-connection/client').RpcError
| import('@deepseek-ai/dsh-agent-presets/types').AgentPresetError
| import('@deepseek-ai/dsh-api-session-controller/types').SessionError
| import('@deepseek-ai/dsh-subagent/client').SubagentControlError
| import('@deepseek-ai/dsh-api-workspace-controller/types').WorkspaceError
/** Success or failure returned by Client operations spanning both API families. */
@@ -138,7 +141,7 @@ export async function apply(ctx: Context): Promise<() => Promise<void>> {
for (const contribution of [
agentPresetsRemote, commandsRemote, goalsRemote, dynamicRemote, fileReferencesRemote,
pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote,
sessionRemote, workspaceRemote,
subagentsRemote, sessionRemote, workspaceRemote,
]) {
disposers.push(await ctx.remote.$mount(contribution))
}
@@ -54,6 +54,9 @@
{
"path": "../../settings/settings"
},
{
"path": "../../subagent/subagent"
},
{
"path": "../../interaction/user-approval"
},
-1
View File
@@ -61,7 +61,6 @@
"@deepseek-ai/dsh-session-query": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-util-crypto": "workspace:^",
"@deepseek-ai/schemastery": "workspace:^",
"fflate": "^0.8.2",
-163
View File
@@ -9,7 +9,6 @@ import type { Context } from '@deepseek-ai/cordis'
import type { ModelSelection } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-agent-presets/types'
import type { SessionId } from '@deepseek-ai/dsh-session'
import { SubagentError } from '@deepseek-ai/dsh-subagent'
import { isUserInvocable } from '@deepseek-ai/dsh-skill'
import {
InvalidPresetIdError, PresetExistsError,
@@ -19,7 +18,6 @@ import type {
ApiProxy, ConfigurableProviderView, CredentialView,
SettingsNamespaceView,
} from './api/index.ts'
import type { SessionRequestId } from '@deepseek-ai/dsh-api-session-controller/types'
import { buildModelCatalog } from '@deepseek-ai/dsh-api-session-controller'
import { SessionQueryError } from '@deepseek-ai/dsh-session-query'
import {
@@ -46,25 +44,6 @@ import type { RpcError, RpcRequest, RpcResponse } from './api/rpc.ts'
import { DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker'
import { canOpenNativePath, openNativePath, openNativeTextFile } from './native-path-opener.ts'
/** Strict browser-zone profile: UTC or an IANA Area/Location-style identifier. */
const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/
/** Validate and canonicalize one browser-supplied IANA zone at the wire boundary. */
function canonicalClientTimeZone(value: string): string | undefined {
if (value.length === 0 || value.trim() !== value
|| (value !== 'UTC' && !IANA_TIME_ZONE.test(value))) return undefined
try {
const canonical = new Intl.DateTimeFormat('en-US', { timeZone: value })
.resolvedOptions().timeZone
/* v8 ignore next -- Intl returns UTC or a canonical IANA Area/Location for accepted input. */
if (canonical !== 'UTC' && !IANA_TIME_ZONE.test(canonical)) return undefined
return canonical
} catch {
// Intl rejects unsupported zone names; the RPC maps that parser rejection below.
return undefined
}
}
/** Read live abort state across awaits without treating it as synchronously immutable. */
function isAborted(signal: AbortSignal): boolean {
return signal.aborted
@@ -110,55 +89,6 @@ export interface ApiProxyDefaults {
canOpenPath?: () => boolean
}
/** Map continuation admission failures without exposing provider details. */
function subagentPromptError(
request: RpcRequest<{ childSessionId: SessionId }>,
error: unknown,
signal: AbortSignal,
): RpcResponse<never> {
const childSessionId = request.payload.childSessionId
if (signal.aborted) {
return err(request, { code: 'cancelled', message: 'subagent prompt was cancelled', details: {} })
}
if (error instanceof SubagentError) {
switch (error.code) {
case 'NOT_RESUMABLE':
return err(request, {
code: 'subagent-not-resumable',
message: 'subagent cannot be resumed',
details: { childSessionId },
})
case 'UNAUTHORIZED':
return err(request, {
code: 'subagent-unauthorized',
message: 'subagent does not belong to this parent',
details: { childSessionId },
})
case 'DRAINING':
case 'ACTIVATION_CLOSING':
case 'CONTINUATION_UNAVAILABLE':
case 'PERSISTENCE_UNAVAILABLE':
return err(request, {
code: 'subagent-delivery-unavailable',
message: 'subagent follow-up is temporarily unavailable',
details: { childSessionId },
})
default:
break
}
}
return err(request, { code: 'internal', message: 'subagent prompt failed', details: {} })
}
/** Stable RPC face of the missing projections capability, shared by every catalog read path. */
function projectionsUnavailableError(): RpcError {
return {
code: 'internal',
message: 'subagent catalog is unavailable: this deployment does not mount the sessionProjections registry (load @deepseek-ai/dsh-session-projection)',
details: {},
}
}
/** The roster is absent: this deployment composes no agent presets at all. */
function noRoster(agentPreset: string): RpcError {
return {
@@ -341,99 +271,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
}
return {
subagents: {
async list(request, signal) {
try {
const entries = await ctx.subagents.listChildren(request.payload.parentSessionId, signal)
return ok(request, {
entries: entries.map(entry => entry.kind === 'child'
? {
...entry,
activity: ctx.agents.get(entry.id)?.status === 'running' ? 'running' : 'inactive',
}
: entry),
parentAvailable: ctx.agents.get(request.payload.parentSessionId) !== undefined,
})
} catch (error: unknown) {
if (signal?.aborted || (error instanceof SubagentError && error.code === 'CANCELLED')) {
return err(request, {
code: 'cancelled',
message: 'subagent catalog read was cancelled',
details: {},
})
}
if (error instanceof SubagentError && error.code === 'SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE') {
return err(request, projectionsUnavailableError())
}
return err(request, {
code: 'internal',
message: 'subagent catalog read failed',
details: {},
})
}
},
async prompt(request, signal) {
const { parentSessionId, childSessionId, content, clientTimeZone } = request.payload
const canonicalTimeZone = clientTimeZone === undefined
? undefined
: canonicalClientTimeZone(clientTimeZone)
if (clientTimeZone !== undefined && canonicalTimeZone === undefined) {
return err(request, {
code: 'invalid-time-zone',
message: 'clientTimeZone must be UTC or a valid IANA Area/Location name',
details: { value: clientTimeZone },
})
}
const parent = ctx.agents.get(parentSessionId)
if (parent === undefined) {
return err(request, {
code: 'subagent-parent-unavailable',
message: `parent session "${parentSessionId}" is not live`,
details: { parentSessionId },
})
}
try {
const messageId = await ctx.subagents.followup(parent, childSessionId, content, {
source: {
kind: 'user',
rpcId: request.rpcId as unknown as SessionRequestId,
...(canonicalTimeZone === undefined ? {} : { clientTimeZone: canonicalTimeZone }),
},
signal,
})
return ok(request, { messageId })
} catch (error: unknown) {
return subagentPromptError(request, error, signal)
}
},
// Deliberately no catalog, history, persistence, or parent Agent lookup:
// the core primitive alone authorizes the durable address against the
// live Activation, which is what keeps a live child interruptible while
// its parent Agent is offline. Absent targets are accepted no-ops there.
interrupt(request) {
const { parentSessionId, childSessionId } = request.payload
try {
ctx.subagents.interrupt(childSessionId, { kind: 'user', parentSessionId })
} catch (error: unknown) {
if (error instanceof SubagentError && error.code === 'UNAUTHORIZED') {
return Promise.resolve(err(request, {
code: 'subagent-unauthorized',
message: 'subagent does not belong to this parent',
details: { childSessionId },
}))
}
return Promise.resolve(err(request, {
code: 'internal',
message: 'subagent interrupt failed',
details: {},
}))
}
return Promise.resolve(ok(request, { accepted: true as const }))
},
},
host: {
describe(request) {
// TODO(apiproxy-version): read the version from apps/cli/package.json.
-6
View File
@@ -7,7 +7,6 @@
import type { HostApi } from './host.ts'
import type { AgentPresetsApi } from './agent-presets.ts'
import type { SkillsApi } from './skills.ts'
import type { SubagentsApi } from './subagents.ts'
import type { SettingsApi } from './settings.ts'
import type { CredentialsApi } from './credentials.ts'
import type { LlmApi } from './llm.ts'
@@ -15,7 +14,6 @@ import type { DownloadsApi } from './downloads.ts'
/** Root interface of the unified API. New client-request domain = one new file pair + one field here + one map row. */
export interface ApiProxy {
subagents: SubagentsApi
host: HostApi
skills: SkillsApi
agentPresets: AgentPresetsApi
@@ -32,10 +30,6 @@ export type {
ModelReasoningEffort, ModelSelection,
} from '@deepseek-ai/dsh-api-session-controller/types'
export type { DirectoryEntry, DirectoryListing, HostApi } from './host.ts'
export type {
SubagentAddress, SubagentCatalog, SubagentInterruptReceipt, SubagentListEntry,
SubagentPromptReceipt, SubagentsApi,
} from './subagents.ts'
export type { SkillsApi, SkillEntry } from './skills.ts'
export type { AgentPresetsApi } from './agent-presets.ts'
export type { SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView } from './settings.ts'
@@ -9,7 +9,6 @@ import type { SkillsApi } from './skills.ts'
import type { SettingsApi } from './settings.ts'
import type { CredentialsApi } from './credentials.ts'
import type { LlmApi } from './llm.ts'
import type { SubagentsApi } from './subagents.ts'
import type { RpcResponse } from './rpc.ts'
/**
@@ -18,9 +17,6 @@ import type { RpcResponse } from './rpc.ts'
* request; the carrier passes its request signal, never a wire field.
*/
export interface RpcMethodMap {
'subagent.list': SubagentsApi['list']
'subagent.prompt': SubagentsApi['prompt']
'subagent.interrupt': SubagentsApi['interrupt']
'host.describe': HostApi['describe']
'host.pickDirectory': HostApi['pickDirectory']
'host.listDirectory': HostApi['listDirectory']
@@ -49,16 +49,6 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
z.object({ code: z.literal('settings-conflict'), message: z.string(), details: z.object({ ns: z.string(), expected: z.number(), actual: z.number() }) }),
z.object({ code: z.literal('credential-rejected'), message: z.string(), details: z.object({ ref: z.string() }) }),
z.object({ code: z.literal('model-discovery-failed'), message: z.string(), details: z.object({ settingsNs: z.string(), baseURL: z.string().optional() }) }),
z.object({ code: z.literal('subagent-parent-unavailable'), message: z.string(), details: z.object({ parentSessionId: z.string() }) }),
z.object({ code: z.literal('subagent-not-found'), message: z.string(), details: z.object({ parentSessionId: z.string(), childSessionId: z.string() }) }),
z.object({ code: z.literal('subagent-catalog-diagnostic'), message: z.string(), details: z.object({
parentSessionId: z.string(),
childSessionId: z.string(),
reason: z.union([z.literal('corrupt'), z.literal('unsupported'), z.literal('unavailable')]),
}) }),
z.object({ code: z.literal('subagent-not-resumable'), message: z.string(), details: z.object({ childSessionId: z.string() }) }),
z.object({ code: z.literal('subagent-unauthorized'), message: z.string(), details: z.object({ childSessionId: z.string() }) }),
z.object({ code: z.literal('subagent-delivery-unavailable'), message: z.string(), details: z.object({ childSessionId: z.string() }) }),
z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }),
]) as unknown as z.ZodType<RpcError>
-10
View File
@@ -62,16 +62,6 @@ export interface RpcErrorDetailsMap {
* details name the endpoint asked, never the credential offered.
*/
'model-discovery-failed': { settingsNs: string; baseURL?: string }
'subagent-parent-unavailable': { parentSessionId: SessionId }
'subagent-not-found': { parentSessionId: SessionId; childSessionId: SessionId }
'subagent-catalog-diagnostic': {
parentSessionId: SessionId
childSessionId: SessionId
reason: 'corrupt' | 'unsupported' | 'unavailable'
}
'subagent-not-resumable': { childSessionId: SessionId }
'subagent-unauthorized': { childSessionId: SessionId }
'subagent-delivery-unavailable': { childSessionId: SessionId }
'internal': {}
}
@@ -1,74 +0,0 @@
/** Zod schemas for the browser-safe subagent domain. */
import { z } from 'zod'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
import { sessionIdSchema } from './ids.schema.ts'
import type { SubagentListEntry } from './subagents.ts'
const contentBlockSchema = z.looseObject({ type: z.string() })
/** Healthy and diagnostic durable catalog rows. */
export const subagentListEntrySchema = z.union([
z.object({
kind: z.literal('child'),
id: sessionIdSchema,
mode: z.literal('one-shot'),
activity: z.union([z.literal('running'), z.literal('inactive')]),
hasChildren: z.boolean(),
label: z.string().optional(),
}),
z.object({
kind: z.literal('child'),
id: sessionIdSchema,
mode: z.literal('continuable'),
activity: z.union([z.literal('running'), z.literal('inactive')]),
hasChildren: z.boolean(),
label: z.string(),
}),
z.object({
kind: z.literal('diagnostic'),
id: sessionIdSchema,
reason: z.union([z.literal('corrupt'), z.literal('unsupported'), z.literal('unavailable')]),
}),
]) satisfies z.ZodType<Wire<SubagentListEntry>>
/** subagent.list request payload. */
export const subagentListRequestSchema = z.object({
parentSessionId: sessionIdSchema,
}) satisfies z.ZodType<Wire<RequestPayload<'subagent.list'>>>
/** subagent.list response value. */
export const subagentListValueSchema = z.object({
entries: z.array(subagentListEntrySchema),
parentAvailable: z.boolean(),
}) satisfies z.ZodType<Wire<ResponseValue<'subagent.list'>>>
/** subagent.prompt request payload. */
export const subagentPromptRequestSchema = z.object({
parentSessionId: sessionIdSchema,
childSessionId: sessionIdSchema,
mode: z.literal('continuable'),
content: z.array(contentBlockSchema),
clientTimeZone: z.string().optional(),
}) as unknown as z.ZodType<RequestPayload<'subagent.prompt'>>
/** subagent.interrupt request payload. */
export const subagentInterruptRequestSchema = z.object({
parentSessionId: sessionIdSchema,
childSessionId: sessionIdSchema,
mode: z.literal('continuable'),
}) satisfies z.ZodType<Wire<RequestPayload<'subagent.interrupt'>>>
/** subagent.interrupt response value. */
export const subagentInterruptValueSchema = z.object({
accepted: z.literal(true),
}) satisfies z.ZodType<Wire<ResponseValue<'subagent.interrupt'>>>
const messageIdSchema = z.string() as unknown as z.ZodType<MessageId>
/** subagent.prompt response value. */
export const subagentPromptValueSchema = z.object({
messageId: messageIdSchema,
}) satisfies z.ZodType<Wire<ResponseValue<'subagent.prompt'>>>
-101
View File
@@ -1,101 +0,0 @@
/** Browser-safe subagent catalog, continuation, and interrupt contract. */
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { RpcRequest, RpcResponse } from './rpc.ts'
/** Complete durable direct-child catalog row. */
export type SubagentListEntry =
| {
kind: 'child'
id: SessionId
/** Whether the child Agent driver is running at the Host sampling boundary. */
activity: 'running' | 'inactive'
/** Whether a direct descendant has durable `origin: 'subagent'`. */
hasChildren: boolean
} & (
| {
mode: 'one-shot'
label?: string
}
| {
mode: 'continuable'
label: string
}
)
| {
kind: 'diagnostic'
id: SessionId
reason: 'corrupt' | 'unsupported' | 'unavailable'
}
/** Inbox identity returned once the continuation accepts one human message. */
export interface SubagentPromptReceipt {
messageId: MessageId
}
/** Uniform acknowledgement that one interrupt request was admitted. */
export interface SubagentInterruptReceipt {
accepted: true
}
/** Durable parent/child address that selects subagent transport in the client. */
export type SubagentAddress =
& {
parentSessionId: SessionId
childSessionId: SessionId
}
& (
| { mode: 'one-shot' }
| { mode: 'continuable' }
)
/** Complete direct-child catalog plus the delivery-time parent availability hint. */
export interface SubagentCatalog {
entries: SubagentListEntry[]
parentAvailable: boolean
}
/** Subagent-domain unary methods. */
export interface SubagentsApi {
/**
* Lists direct session-backed children without loading either side. Parent
* availability is a hint; continuable prompt performs the authoritative
* check.
*/
list(
request: RpcRequest<{ parentSessionId: SessionId }>,
signal?: AbortSignal,
): Promise<RpcResponse<SubagentCatalog>>
/**
* Delivers human content to a continuable child through the exact live
* parent's continuation owner. Success identifies the message accepted by
* the child's FIFO inbox; later execution is independent of this request.
* Optional browser-zone provenance is validated and logged on that message.
*/
prompt(
request: RpcRequest<
Extract<SubagentAddress, { mode: 'continuable' }> & {
content: ContentBlock[]
/** Optional browser zone sampled for this exact human prompt. */
clientTimeZone?: string
}
>,
signal: AbortSignal,
): Promise<RpcResponse<SubagentPromptReceipt>>
/**
* Interrupts a live continuable child's current turn under the address's
* durable direct-parent authority, without requiring a live parent Agent,
* consulting the catalog, or resuming anything. Fire-and-return: `accepted`
* acknowledges the admitted cancel signal, not target quiescence, so the
* child may remain visibly running briefly. Unclaimed queued follow-ups are
* kept and parked; an absent, idle, or already-completed target is likewise
* `accepted`.
*/
interrupt(
request: RpcRequest<Extract<SubagentAddress, { mode: 'continuable' }>>,
): Promise<RpcResponse<SubagentInterruptReceipt>>
}
@@ -28,11 +28,6 @@ import {
credentialsDescribeValueSchema, credentialsSetValueSchema, credentialsUnsetValueSchema,
} from '../api/credentials.schema.ts'
import { llmDiscoverModelsValueSchema, llmModelsValueSchema, llmProvidersValueSchema } from '../api/llm.schema.ts'
import {
subagentInterruptValueSchema,
subagentListValueSchema,
subagentPromptValueSchema,
} from '../api/subagents.schema.ts'
/**
* Client consumption face of the contract (shape a): same domain tree as ApiProxy, but unary
@@ -47,11 +42,6 @@ import {
* Derived per method key from RpcMethodMap so a map row addition updates this mechanically.
*/
export interface IApiClient {
subagents: {
list(payload: RequestPayload<'subagent.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'subagent.list'>>>
prompt(payload: RequestPayload<'subagent.prompt'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'subagent.prompt'>>>
interrupt(payload: RequestPayload<'subagent.interrupt'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'subagent.interrupt'>>>
}
host: {
describe(payload: RequestPayload<'host.describe'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'host.describe'>>>
pickDirectory(payload: RequestPayload<'host.pickDirectory'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'host.pickDirectory'>>>
@@ -89,9 +79,6 @@ export interface IApiClient {
* mirror of the handler's request table; key coverage compiler-enforced against RpcMethodMap).
*/
const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseValue<K>>> } = {
'subagent.list': subagentListValueSchema,
'subagent.prompt': subagentPromptValueSchema,
'subagent.interrupt': subagentInterruptValueSchema,
'host.describe': hostDescribeValueSchema,
'host.pickDirectory': hostPickDirectoryValueSchema,
'host.listDirectory': hostListDirectoryValueSchema,
@@ -240,12 +227,6 @@ export abstract class AbstractApiClient implements IApiClient {
// ---- IApiClient API (arrow properties so destructured/passed references stay bound) ----
readonly subagents: IApiClient['subagents'] = {
list: (payload, signal) => this.callUnary('subagent.list', payload, signal),
prompt: (payload, signal) => this.callUnary('subagent.prompt', payload, signal),
interrupt: (payload, signal) => this.callUnary('subagent.interrupt', payload, signal),
}
readonly host: IApiClient['host'] = {
describe: (payload, signal) => this.callUnary('host.describe', payload, signal),
// A native system dialog is user-paced and may legitimately stay open
@@ -31,11 +31,6 @@ import {
credentialsDescribeRequestSchema, credentialsSetRequestSchema, credentialsUnsetRequestSchema,
} from '../api/credentials.schema.ts'
import { llmDiscoverModelsRequestSchema, llmModelsRequestSchema, llmProvidersRequestSchema } from '../api/llm.schema.ts'
import {
subagentInterruptRequestSchema,
subagentListRequestSchema,
subagentPromptRequestSchema,
} from '../api/subagents.schema.ts'
/**
* Unary dispatch table, keyed by (and compiler-locked to) RpcMethodMap: a map row without a
@@ -54,9 +49,6 @@ type UnaryRoutes = {
}
const UNARY_ROUTES: UnaryRoutes = {
'subagent.list': { schema: subagentListRequestSchema, invoke: (api, r, signal) => api.subagents.list(r, signal) },
'subagent.prompt': { schema: subagentPromptRequestSchema, invoke: (api, r, signal) => api.subagents.prompt(r, signal) },
'subagent.interrupt': { schema: subagentInterruptRequestSchema, invoke: (api, r) => api.subagents.interrupt(r) },
'host.describe': { schema: hostDescribeRequestSchema, invoke: (api, r) => api.host.describe(r) },
'host.pickDirectory': { schema: hostPickDirectoryRequestSchema, invoke: (api, r, signal) => api.host.pickDirectory(r, signal) },
'host.listDirectory': { schema: hostListDirectoryRequestSchema, invoke: (api, r, signal) => api.host.listDirectory(r, signal) },
+1 -3
View File
@@ -62,7 +62,7 @@ export interface Config {
*/
export class ApiProxyService extends Service implements ApiProxy {
static inject = [
'agentDefaultModel', 'agents', 'attachments', 'directoryPicker', 'llm', 'sessions', 'subagents', 'sessionQuery',
'agentDefaultModel', 'agents', 'attachments', 'directoryPicker', 'llm', 'sessions', 'sessionQuery',
'sessionController',
]
@@ -72,7 +72,6 @@ export class ApiProxyService extends Service implements ApiProxy {
.default(DEFAULT_SESSION_LOG_COMPRESSION_LEVEL) as z<SessionLogCompressionLevel>,
})
readonly subagents: ApiProxy['subagents']
readonly host: ApiProxy['host']
readonly skills: ApiProxy['skills']
readonly agentPresets: ApiProxy['agentPresets']
@@ -91,7 +90,6 @@ export class ApiProxyService extends Service implements ApiProxy {
? {}
: { sessionExportCompressionLevel: config.sessionExportCompressionLevel }),
})
this.subagents = api.subagents
this.host = api.host
this.skills = api.skills
this.agentPresets = api.agentPresets
-3
View File
@@ -53,9 +53,6 @@
{
"path": "../../session-query/session-query"
},
{
"path": "../../subagent/subagent"
},
{
"path": "../../skill/skill"
},
+15 -1
View File
@@ -26,6 +26,14 @@
"types": "./lib/types/client.d.ts",
"default": "./lib/types/client.js"
},
"./typert": {
"types": "./lib/typert.host.d.ts",
"default": "./lib/typert.host.js"
},
"./remote": {
"types": "./lib/typert.remote-client.d.ts",
"default": "./lib/typert.remote-client.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
@@ -33,7 +41,11 @@
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
"lib/types/**/*.d.ts",
"lib/typert.host.js",
"lib/typert.host.d.ts",
"lib/typert.remote-client.js",
"lib/typert.remote-client.d.ts"
],
"license": "MIT",
"dependencies": {
@@ -56,6 +68,7 @@
"@deepseek-ai/dsh-session-query": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
@@ -108,6 +121,7 @@
"@deepseek-ai/dsh-storage-json": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
+2 -1
View File
@@ -1,7 +1,8 @@
/**
* Browser-safe subagent projection vocabulary.
* Browser-safe subagent projection and control vocabulary.
*
* @module @deepseek-ai/dsh-subagent/client
*/
export type * from './control-types.ts'
export type { SubagentIdentityProjection, SubagentTimingProjection } from './projection-types.ts'
@@ -0,0 +1,147 @@
/**
* Client-safe subagent catalog and control vocabulary: the durable direct-child
* row both the listing and the browser catalog answer with, plus the
* browser-facing control surface's prompt, receipts, and failures.
*
* @module @deepseek-ai/dsh-subagent/control-types
*/
import type { Branded } from '@deepseek-ai/dsh-brand'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { z as zCore } from 'zod'
/**
* Client-minted identity of one browser prompt, persisted on the exact accepted
* message. It carries the Session Controller's `session-request-id` brand so a
* subagent prompt and an ordinary Session prompt share one identity
* vocabulary; that package depends on this one, so the brand is spelled here
* rather than imported.
*/
export type SubagentPromptRequestId = Branded<'session-request-id'>
/**
* One durable direct-child row, ordered by header `createdAt` with ties broken
* on id. Only a candidate whose durable header has `origin: 'subagent'` is
* interpreted. A served `subagent` projection value produces a `child`; a
* settled candidate whose fold served no identity produces a `diagnostic`; a
* running candidate without one is omitted — its descriptor may not be
* appended yet (the creation window). Diagnostics relay the projection fold's
* outcome or a failed read, never a per-child event scan, and never expose
* model-hidden descriptor content.
*/
export type SubagentListEntry =
| {
readonly kind: 'child'
/** The durable child session id, stable across Activations. */
readonly id: SessionId
/**
* Whether the child is live at the moment its reader sampled it: the
* durable listing reads the Session store (`running` means the logical
* record is resident, `inactive` that it exists only in persistence),
* while the browser catalog re-samples the child's Agent driver. Neither
* encodes a durable outcome, and a continuable child may still reject
* delivery as an ownership conflict.
*/
readonly activity: 'running' | 'inactive'
/** Whether a direct descendant has durable `origin: 'subagent'`. */
readonly hasChildren: boolean
} & (
| {
/** A terminal one-shot child. */
readonly mode: 'one-shot'
/** Optional durable creation label from the child's descriptor. */
readonly label?: string
}
| {
/** A resumable conversation. */
readonly mode: 'continuable'
/** Durable creation label from the child's descriptor. */
readonly label: string
}
)
| {
readonly kind: 'diagnostic'
/** The candidate's session id. */
readonly id: SessionId
/**
* Why the candidate has no `child` row: `corrupt` for a settled candidate
* whose projection fold served no identity (a missing, malformed, or
* unrecognized-version descriptor — deliberately undistinguished), and
* for any candidate whose log makes a registered unit's fold or schema
* throw (deterministic data damage, contained per child); `unavailable`
* when the candidate's Session observation was absent or transiently
* unreadable (retried on the next listing). `unsupported` is never produced; it remains in the
* union for consumers that route on it.
*/
readonly reason: 'corrupt' | 'unsupported' | 'unavailable'
}
/** Complete direct-child catalog plus the delivery-time parent availability hint. */
export interface SubagentCatalog {
readonly entries: readonly SubagentListEntry[]
readonly parentAvailable: boolean
}
/** Durable parent/child address that selects subagent transport in the client. */
export type SubagentAddress =
& {
readonly parentSessionId: SessionId
readonly childSessionId: SessionId
}
& (
| { readonly mode: 'one-shot' }
| { readonly mode: 'continuable' }
)
/** One human message addressed to a continuable direct child. */
export interface SubagentPromptRequest {
/** Identity persisted on the accepted message, minted before the call. */
readonly requestId: SubagentPromptRequestId
readonly parentSessionId: SessionId
readonly childSessionId: SessionId
/** Required discriminator retained from the browser control address. */
readonly mode: 'continuable'
/** Content delivered as the child's user message. */
readonly content: ContentBlock[]
/** Optional browser zone sampled for this exact human prompt. */
readonly clientTimeZone?: string
}
/** Inbox identity returned once the continuation accepts one human message. */
export interface SubagentPromptReceipt {
readonly messageId: MessageId
}
/** Uniform acknowledgement that one interrupt request was admitted. */
export interface SubagentInterruptReceipt {
readonly accepted: true
}
/**
* Failure details the control surface answers with. The catalog read, the
* prompt, and the interrupt produce these codes; a Client fabricates
* `subagent-not-resumable` and `subagent-delivery-unavailable` for a one-shot
* address it refuses before the call, so both planes read one vocabulary.
*/
export interface SubagentControlErrorDetailsMap {
'bad-request': { readonly issues: zCore.core.$ZodIssue[] }
cancelled: Record<never, never>
'invalid-time-zone': { readonly value: string }
'subagent-parent-unavailable': { readonly parentSessionId: SessionId }
'subagent-not-resumable': { readonly childSessionId: SessionId }
'subagent-unauthorized': { readonly childSessionId: SessionId }
'subagent-delivery-unavailable': { readonly childSessionId: SessionId }
'subagent-projections-unavailable': Record<never, never>
internal: Record<never, never>
}
/** One subagent control failure, returned without a carrier error. */
export type SubagentControlError = {
[Code in keyof SubagentControlErrorDetailsMap]: {
readonly code: Code
readonly message: string
readonly details: SubagentControlErrorDetailsMap[Code]
}
}[keyof SubagentControlErrorDetailsMap]
+180
View File
@@ -0,0 +1,180 @@
/**
* Browser-facing subagent control assembly: the catalog view sampled against
* the live Agent registry, one browser zone's validation, and the stable
* failure codes the Remote surface answers with.
*
* @module @deepseek-ai/dsh-subagent
*/
import type { Context } from '@deepseek-ai/cordis'
import type { SessionId } from '@deepseek-ai/dsh-session'
import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
import { z } from 'zod'
import type {
SubagentCatalog, SubagentControlErrorDetailsMap, SubagentListEntry,
} from './control-types.ts'
import { SubagentError } from './error.ts'
/** Strict browser-zone profile: UTC or an IANA Area/Location-style identifier. */
const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/
const SESSION_ID_SCHEMA = z.string().min(1)
const CONTROL_ID_SCHEMAS = {
'subagent.list': z.object({ parentSessionId: SESSION_ID_SCHEMA }),
'subagent.prompt': z.object({
parentSessionId: SESSION_ID_SCHEMA,
childSessionId: SESSION_ID_SCHEMA,
mode: z.literal('continuable'),
}),
'subagent.interrupt': z.object({
parentSessionId: SESSION_ID_SCHEMA,
childSessionId: SESSION_ID_SCHEMA,
mode: z.literal('continuable'),
}),
} as const
/**
* Validate and canonicalize one browser-supplied IANA zone at the wire boundary.
* @param value - the browser's reported zone name.
* @returns the canonical zone, or `undefined` when the name is unusable.
*/
export function canonicalClientTimeZone(value: string): string | undefined {
if (value.length === 0 || value.trim() !== value
|| (value !== 'UTC' && !IANA_TIME_ZONE.test(value))) return undefined
try {
const canonical = new Intl.DateTimeFormat('en-US', { timeZone: value })
.resolvedOptions().timeZone
/* v8 ignore next -- Intl returns UTC or a canonical IANA Area/Location for accepted input. */
if (canonical !== 'UTC' && !IANA_TIME_ZONE.test(canonical)) return undefined
return canonical
} catch {
// Intl rejects unsupported zone names; the caller maps that parser rejection.
return undefined
}
}
/**
* Refuse one Remote call with a stable business failure the carrier preserves.
* @param code - declared caller-facing code.
* @param message - human-readable refusal.
* @param details - that code's declared detail payload.
* @returns Never — the failure is thrown.
* @throws {TypertRemoteFailure} always.
*/
export function rejectControl<Code extends keyof SubagentControlErrorDetailsMap>(
code: Code,
message: string,
details: SubagentControlErrorDetailsMap[Code],
): never {
throw new TypertRemoteFailure({ code, message, details })
}
/**
* Apply the subagent payload checks that are stricter than generated
* branded-string codecs.
* @param method - method name carried in the failure message.
* @param payload - decoded control fields to validate.
* @throws {TypertRemoteFailure} `bad-request` with the original Zod issues.
*/
export function validateControlRequest(
method: keyof typeof CONTROL_ID_SCHEMAS,
payload: unknown,
): void {
const parsed = CONTROL_ID_SCHEMAS[method].safeParse(payload)
if (!parsed.success) {
return rejectControl('bad-request', `invalid payload for ${method}`, {
issues: parsed.error.issues,
})
}
}
/**
* Project one durable listing onto the catalog view, replacing each row's
* store-derived activity with the live Agent driver's status and reporting
* whether the exact parent Agent is live. Without an Agent registry no driver
* runs at all, so every row is inactive and the parent is unavailable.
* @param ctx - Host context that may carry the Agent registry.
* @param parentSessionId - the listed parent.
* @param entries - the durable direct-child listing.
* @returns the catalog view answered to one browser.
*/
export function catalogView(
ctx: Context,
parentSessionId: SessionId,
entries: readonly SubagentListEntry[],
): SubagentCatalog {
const agents = ctx.get('agents')
return {
entries: entries.map((entry): SubagentListEntry => entry.kind === 'child'
? { ...entry, activity: agents?.get(entry.id)?.status === 'running' ? 'running' : 'inactive' }
: entry),
parentAvailable: agents?.get(parentSessionId) !== undefined,
}
}
/**
* Refuse one catalog read while preserving cancellation and a missing
* projections registry as distinct failures.
* @param error - the thrown value.
* @param signal - the caller's cancellation.
* @returns Never — the refusal is thrown.
* @throws {TypertRemoteFailure} always.
*/
export function rejectCatalogRead(error: unknown, signal: AbortSignal): never {
if (isCancellation(error, signal)) {
return rejectControl('cancelled', 'subagent catalog read was cancelled', {})
}
if (error instanceof SubagentError && error.code === 'SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE') {
return rejectControl(
'subagent-projections-unavailable',
'subagent catalog is unavailable: this deployment does not mount the sessionProjections registry (load @deepseek-ai/dsh-session-projection)',
{},
)
}
return rejectControl('internal', 'subagent catalog read failed', {})
}
/**
* Refuse one continuation prompt without exposing provider detail: admission
* failures the caller can act on keep their own code, everything else is
* internal.
* @param error - the thrown value.
* @param childSessionId - the addressed child.
* @param signal - the caller's cancellation.
* @returns Never — the refusal is thrown.
* @throws {TypertRemoteFailure} always.
*/
export function rejectPrompt(error: unknown, childSessionId: SessionId, signal: AbortSignal): never {
if (isCancellation(error, signal)) {
return rejectControl('cancelled', 'subagent prompt was cancelled', {})
}
if (error instanceof SubagentError) {
switch (error.code) {
case 'NOT_RESUMABLE':
return rejectControl('subagent-not-resumable', 'subagent cannot be resumed', { childSessionId })
case 'UNAUTHORIZED':
return rejectControl(
'subagent-unauthorized',
'subagent does not belong to this parent',
{ childSessionId },
)
case 'DRAINING':
case 'ACTIVATION_CLOSING':
case 'CONTINUATION_UNAVAILABLE':
case 'PERSISTENCE_UNAVAILABLE':
return rejectControl(
'subagent-delivery-unavailable',
'subagent follow-up is temporarily unavailable',
{ childSessionId },
)
// A code outside the admission vocabulary is not the caller's move to make.
default:
break
}
}
return rejectControl('internal', 'subagent prompt failed', {})
}
function isCancellation(error: unknown, signal: AbortSignal): boolean {
return signal.aborted || (error instanceof SubagentError && error.code === 'CANCELLED')
}
+137 -3
View File
@@ -29,13 +29,25 @@
* @module @deepseek-ai/dsh-subagent
*/
import { Context, Service } from '@deepseek-ai/cordis'
import { Context } from '@deepseek-ai/cordis'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import type { Scoped } from '@deepseek-ai/dsh-scope'
import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools'
import type { ContentBlock, MessageId } from '@deepseek-ai/dsh-llm'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { SessionId } from '@deepseek-ai/dsh-session'
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import {
canonicalClientTimeZone, catalogView, rejectCatalogRead, rejectControl, rejectPrompt,
validateControlRequest,
} from './control.ts'
import type {
SubagentCatalog,
SubagentInterruptReceipt,
SubagentPromptReceipt,
SubagentPromptRequest,
SubagentPromptRequestId,
} from './control-types.ts'
import type {
ContinuableCreateRequest,
ContinuableCreateSpec,
@@ -121,7 +133,8 @@ export type {
SubagentSettledMessageSource,
} from './continuation.ts'
export type { ContinuableSetupContribution } from './activation-setup-registry.ts'
export type { SubagentDescendantListEntry, SubagentListEntry } from './list-children.ts'
export type * from './control-types.ts'
export type { SubagentDescendantListEntry } from './list-children.ts'
export type { SubagentRunEndInfo, SubagentRunInfo } from './types.ts'
export type { SubagentIdentityProjection, SubagentTimingProjection } from './projection-types.ts'
@@ -166,8 +179,21 @@ declare module '@deepseek-ai/cordis' {
}
}
/**
* Durable attribution of one browser-authored follow-up. The Session
* Controller declares this `user-rpc` message source and depends on this
* package, so the fields are spelled here: `MessageSource`'s `user` member
* accepts the record and the correlation id rides the durable message the
* Client reconciles its optimistic prompt against.
*/
interface BrowserPromptSource {
readonly kind: 'user'
readonly rpcId: SubagentPromptRequestId
readonly clientTimeZone?: string
}
/** Named provider registry with one-shot runs, durable discovery, and continuable-child operations. */
export class SubagentRuntime extends Service {
export class SubagentRuntime extends TypertRemoteService {
private providers = new Map<string, SubagentProvider>()
private continuations: SubagentContinuationManager | undefined
/** Deployment contributions composed into unpublished continuable children. */
@@ -363,6 +389,114 @@ export class SubagentRuntime extends Service {
return listSubagentDescendants(this.ctx, rootSessionId, signal)
}
/**
* Remote face of {@link listChildren} for one browser: the durable listing
* plus live Agent activity and the delivery-time parent availability hint.
* Parent availability is a hint; {@link prompt} performs the authoritative
* check. Named apart from the provider-name {@link list}, which owns the
* member.
* @param parentSessionId - parent session whose direct children are listed.
* @param signal - carrier cancellation forwarded to Session queries.
* @returns the catalog view for that parent.
* @throws {TypertRemoteFailure} `bad-request` for an empty parent id,
* `cancelled` for an aborted read, `subagent-projections-unavailable` when
* the deployment has no projection registry, otherwise `internal`.
*/
@Remote('list')
async remoteExportList(parentSessionId: SessionId, signal: AbortSignal): Promise<SubagentCatalog> {
validateControlRequest('subagent.list', { parentSessionId })
try {
return catalogView(this.ctx, parentSessionId, await this.listChildren(parentSessionId, signal))
} catch (error: unknown) {
return rejectCatalogRead(error, signal)
}
}
/**
* Deliver one browser-authored message to a continuable child through the
* exact live direct parent, retaining the caller-minted request identity and
* validated browser zone on the accepted message. Success identifies the
* message the child's FIFO inbox accepted; later execution is independent of
* this call.
* @param request - durable address, minted identity, content, and optional browser zone.
* @param signal - carrier cancellation, owning the call until inbox acceptance.
* @returns the accepted message's inbox identity.
* @throws {TypertRemoteFailure} `bad-request`, `invalid-time-zone`,
* `subagent-parent-unavailable`, `subagent-not-resumable`,
* `subagent-unauthorized`, `subagent-delivery-unavailable`, `cancelled`, or
* `internal`.
*/
@Remote('prompt')
async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptReceipt> {
const { parentSessionId, childSessionId, clientTimeZone } = request
validateControlRequest('subagent.prompt', request)
const canonicalTimeZone = clientTimeZone === undefined
? undefined
: canonicalClientTimeZone(clientTimeZone)
if (clientTimeZone !== undefined && canonicalTimeZone === undefined) {
return rejectControl(
'invalid-time-zone',
'clientTimeZone must be UTC or a valid IANA Area/Location name',
{ value: clientTimeZone },
)
}
const parent = this.ctx.get('agents')?.get(parentSessionId)
if (parent === undefined) {
return rejectControl(
'subagent-parent-unavailable',
`parent session "${parentSessionId}" is not live`,
{ parentSessionId },
)
}
const source: BrowserPromptSource = {
kind: 'user',
rpcId: request.requestId,
...(canonicalTimeZone === undefined ? {} : { clientTimeZone: canonicalTimeZone }),
}
const content: ContentBlock[] = [...request.content]
try {
return { messageId: await this.followup(parent, childSessionId, content, { source, signal }) }
} catch (error: unknown) {
return rejectPrompt(error, childSessionId, signal)
}
}
/**
* Remote face of {@link interrupt} under one durable parent address. No
* catalog, history, persistence, or parent Agent lookup runs: the core
* primitive alone authorizes the address against the live Activation, which
* is what keeps a live child interruptible while its parent Agent is offline.
* Absent, idle, and already-completed targets are accepted no-ops there.
* @param childSessionId - durable child session id to interrupt.
* @param parentSessionId - durable direct parent whose authority is claimed.
* @param mode - required continuable-address discriminator.
* @returns acknowledgement that the cancel signal was admitted, not that the target is quiescent.
* @throws {TypertRemoteFailure} `bad-request` for an empty id,
* `subagent-unauthorized` when the address does not own the live target,
* otherwise `internal`.
*/
@Remote('interruptByParent')
interruptByParent(
childSessionId: SessionId,
parentSessionId: SessionId,
mode: 'continuable',
): SubagentInterruptReceipt {
validateControlRequest('subagent.interrupt', { childSessionId, parentSessionId, mode })
try {
this.interrupt(childSessionId, { kind: 'user', parentSessionId })
} catch (error: unknown) {
if (error instanceof SubagentError && error.code === 'UNAUTHORIZED') {
return rejectControl(
'subagent-unauthorized',
'subagent does not belong to this parent',
{ childSessionId },
)
}
return rejectControl('internal', 'subagent interrupt failed', {})
}
return { accepted: true }
}
/**
* Register a provider under its name. Registration is effect-scoped and HMR
* safe; removing a provider blocks new starts but does not revoke runs that
@@ -21,9 +21,12 @@ import type { Session, SessionHeader, SessionId } from '@deepseek-ai/dsh-session
import type { SessionProjectionRegistry } from '@deepseek-ai/dsh-session-projection'
import type { SessionProjectionCache } from '@deepseek-ai/dsh-session-projection-cache'
import type { SessionObservation, SessionQueryEngine } from '@deepseek-ai/dsh-session-query'
import type { SubagentListEntry } from './control-types.ts'
import { SubagentError } from './error.ts'
import type { SubagentIdentityProjection } from './projection-types.ts'
export type { SubagentListEntry } from './control-types.ts'
/**
* Concurrent cold observations per explicit catalog listing. Current Session
* persistence providers are local; a networked provider must promote this to
@@ -31,61 +34,6 @@ import type { SubagentIdentityProjection } from './projection-types.ts'
*/
const COLD_READ_CONCURRENCY = 4
/**
* One entry of a {@link listChildren} result, ordered by header `createdAt`
* with ties broken on id. Only a candidate whose durable header has
* `origin: 'subagent'` is interpreted. A served `subagent` projection value
* produces a `child`; a settled candidate whose fold served no identity
* produces a `diagnostic`; a running candidate without one is omitted — its
* descriptor may not be appended yet (the creation window). Diagnostics
* relay the projection fold's outcome or a failed read, never a per-child
* event scan, and never expose model-hidden descriptor content.
*/
export type SubagentListEntry =
| {
readonly kind: 'child'
/** The durable child session id, stable across Activations. */
readonly id: SessionId
/**
* Store snapshot activity: `running` means the logical record is live in
* `ctx.sessions`; `inactive` means it exists only in persistence. Neither
* encodes a durable outcome, and a continuable child may still reject
* delivery as an ownership conflict.
*/
readonly activity: 'running' | 'inactive'
/** Whether a direct descendant has durable `origin: 'subagent'`. */
readonly hasChildren: boolean
} & (
| {
/** A terminal one-shot child. */
readonly mode: 'one-shot'
/** Optional durable creation label from the child's descriptor. */
readonly label?: string
}
| {
/** A resumable conversation. */
readonly mode: 'continuable'
/** Durable creation label from the child's descriptor. */
readonly label: string
}
)
| {
readonly kind: 'diagnostic'
/** The candidate's session id. */
readonly id: SessionId
/**
* Why the candidate has no `child` row: `corrupt` for a settled candidate
* whose projection fold served no identity (a missing, malformed, or
* unrecognized-version descriptor — deliberately undistinguished), and
* for any candidate whose log makes a registered unit's fold or schema
* throw (deterministic data damage, contained per child); `unavailable`
* when the candidate's Session observation was absent or transiently
* unreadable (retried on the next listing). `unsupported` is never produced; it remains in the
* union for consumers that route on it.
*/
readonly reason: 'corrupt' | 'unsupported' | 'unavailable'
}
/**
* One entry of a descendant listing: the interpreted subagent facts plus its
* position in the complete session tree. `parentId` is the durable direct
+3
View File
@@ -56,6 +56,9 @@
{
"path": "../../jobs/jobs"
},
{
"path": "../../typert/protocol"
},
{
"path": "../../runtime-diagnostics/invariants"
}
+9 -3
View File
@@ -722,6 +722,9 @@ importers:
'@deepseek-ai/dsh-settings':
specifier: workspace:^
version: link:../../settings/settings
'@deepseek-ai/dsh-subagent':
specifier: workspace:^
version: link:../../subagent/subagent
'@deepseek-ai/dsh-user-approval':
specifier: workspace:^
version: link:../../interaction/user-approval
@@ -5715,9 +5718,6 @@ importers:
'@deepseek-ai/dsh-skill':
specifier: workspace:^
version: link:../../skill/skill
'@deepseek-ai/dsh-subagent':
specifier: workspace:^
version: link:../../subagent/subagent
'@deepseek-ai/dsh-util-crypto':
specifier: workspace:^
version: link:../../util/crypto
@@ -8241,6 +8241,9 @@ importers:
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../../core/tools
'@deepseek-ai/dsh-typert-protocol':
specifier: workspace:^
version: link:../../typert/protocol
'@deepseek-ai/dsh-user-approval':
specifier: workspace:^
version: link:../../interaction/user-approval
@@ -9049,6 +9052,9 @@ importers:
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
'@deepseek-ai/dsh-subagent':
specifier: workspace:^
version: link:../../subagent/subagent
'@types/react':
specifier: ~18.3.1
version: 18.3.31