mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
feat(subagent): migrate browser control to Remote
This commit is contained in:
@@ -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:^"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'>>>
|
||||
@@ -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) },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -53,9 +53,6 @@
|
||||
{
|
||||
"path": "../../session-query/session-query"
|
||||
},
|
||||
{
|
||||
"path": "../../subagent/subagent"
|
||||
},
|
||||
{
|
||||
"path": "../../skill/skill"
|
||||
},
|
||||
|
||||
@@ -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:^"
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
{
|
||||
"path": "../../jobs/jobs"
|
||||
},
|
||||
{
|
||||
"path": "../../typert/protocol"
|
||||
},
|
||||
{
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
|
||||
Generated
+9
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user