Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui

The Client API carrier's `agentPresets` member was the one member of its class
without an `IApiClient[...]` annotation. Inferring it inlined `AgentPresetEntry`
into the emitted declaration by the specifier TS picks — the host `index.ts` —
dragging the whole gateway, and with it the host `Context` merges, into every
Client program importing the carrier. Annotated like its siblings.

`ApiRemoteAgentOptions.setup` now takes the inspected session rather than its
header alone: this layer resolves a resumed session's preset from the LOG,
because a session that switched while blank ran its turns under the newer
composition and the header is written once at creation.

Conflicts:
	apps/web/tests/snapshots/*/*.expected.md
	packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
	packages/host/apiproxy/src/api-proxy.ts
	scripts/doc-budgets.manifest.json
This commit is contained in:
Yichen Jiang
2026-08-08 15:00:31 +08:00
649 files changed
+21091 -2838

No files matched your search

+211
View File
@@ -0,0 +1,211 @@
/** Host BFF policy for resolving Remote Agent and Session identities. */
import type { Context } from 'cordis'
import type { Agent, AgentOptions, AgentSetup } from '@deepseek-ai/dsh-agent'
import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-persistence'
import { TypeRTLookupFailure } from '@deepseek-ai/dsh-type-meta'
import type {} from '@deepseek-ai/dsh-typert-registry'
/** Caller-facing failures preserved by the Gateway's RPC adapter. */
export type ApiRemoteLookupError =
| { readonly code: 'agent-busy'; readonly message: string; readonly details: { readonly reason: string } }
| { readonly code: 'session-not-found'; readonly message: string; readonly details: { readonly sessionId: SessionId } }
| { readonly code: 'internal'; readonly message: string; readonly details: Record<never, never> }
/** Result of resolving one session identity to its live Agent. */
export type ApiRemoteAgentResult =
| { readonly agent: Agent }
| { readonly error: ApiRemoteLookupError }
/** Resume configuration supplied by the owning Host composition. */
export interface ApiRemoteAgentOptions {
/** Read the per-Agent defaults when a cold identity must resume. */
readonly agentOptions?: () => AgentOptions
/**
* Build the Host-specific Agent-scope composition completed before
* publication. Keyed by the resumed session itself because what a Host
* installs may depend on what that session recorded: an agent preset fixes
* the tools its history was produced under, so rebuilding it under another
* composition would replay tool calls the agent can no longer make. The
* events come along because a session's own record of such a choice may be
* an event rather than a header field.
* @param session - the resumed session's persisted header and event log.
* @returns the Agent-scope setup to run before publication.
*/
readonly setup?: (
session: { meta: SessionHeader; events: readonly SessionEvent[] },
) => AgentSetup | Promise<AgentSetup>
}
/** Cold identity absent from the durable session store. */
export class ApiRemoteSessionNotFound extends Error {}
/** Session identity whose lifecycle belongs to subagent routing. */
export class ApiRemoteSubagentSessionOwnership extends Error {
/**
* Construct the ownership fence.
* @param sessionId - identity reserved to subagent routing.
*/
constructor(readonly sessionId: SessionId) {
super(`session "${sessionId}" is a subagent session; use subagent delivery`)
}
}
/**
* Test whether generic Host routing must leave an identity to subagent routing.
* @param ctx - Host Context carrying the live Agent registry.
* @param session - attached or live Session metadata.
* @param agent - live Agent when one is registered.
* @returns whether generic Remote and legacy API calls must reject the identity.
*/
export function hasApiRemoteSubagentOwner(
ctx: Context,
session: Pick<Session, 'header'>,
agent: Agent | undefined,
): boolean {
if (session.header.origin === 'subagent') return true
const parentId = session.header.parentSession
if (parentId === undefined || agent === undefined) return false
const parent = ctx.agents.get(parentId)
return parent !== undefined && ctx.agents.isOwnedBy(agent.id, parent)
}
/**
* Build the stable caller-facing ownership rejection.
* @param sessionId - identity reserved to subagent routing.
* @returns the existing `agent-busy` RPC shape.
*/
export function apiRemoteSubagentOwnershipError(sessionId: SessionId): ApiRemoteLookupError {
return {
code: 'agent-busy',
message: `session "${sessionId}" is owned by subagent routing`,
details: { reason: 'use subagent delivery for this child session' },
}
}
/**
* Inspect one cold served session without repairing, resuming, or publishing it.
* @param ctx - Host Context carrying the optional persistence provider.
* @param sessionId - durable identity to inspect.
* @returns detached metadata and events for a servable session.
* @throws {@link ApiRemoteSessionNotFound} when the identity has no project-backed session.
*/
export async function inspectApiRemoteSession(
ctx: Context,
sessionId: SessionId,
): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
const persistence = ctx.get('sessionPersistence')
if (persistence === undefined) {
throw new Error('session persistence is not configured (load a dsh-session-persistence backend)')
}
const meta = (await persistence.list()).find(candidate => candidate.id === sessionId)
if (meta === undefined || meta.cwd === undefined) {
throw new ApiRemoteSessionNotFound(`session "${sessionId}" not found`)
}
const inspected = await persistence.inspect(sessionId)
if (inspected.meta.cwd === undefined) {
throw new ApiRemoteSessionNotFound(`session "${sessionId}" not found`)
}
return { meta: inspected.meta, events: [...inspected.events] }
}
/**
* Create the Host's shared Agent resolver and configure Agent/Session TypeRT lookups.
* Live Agents are reused, ordinary cold sessions resume once per identity, and
* subagent-owned identities retain the legacy `agent-busy` fence.
* @param ctx - owning Host Context.
* @param options - defaults and Agent-scope setup used only for cold resume.
* @returns resolver shared by legacy API Proxy methods and TypeRT lookups.
*/
export function createApiRemoteAgentResolver(
ctx: Context,
options: ApiRemoteAgentOptions,
): (sessionId: SessionId) => Promise<ApiRemoteAgentResult> {
const resumes = new Map<SessionId, Promise<Agent>>()
const fencedLiveAgent = (sessionId: SessionId): ApiRemoteAgentResult | undefined => {
const live = ctx.agents.get(sessionId)
if (live === undefined) return undefined
if (hasApiRemoteSubagentOwner(ctx, live.session, live)) {
return { error: apiRemoteSubagentOwnershipError(sessionId) }
}
return { agent: live }
}
const agentFor = async (sessionId: SessionId): Promise<ApiRemoteAgentResult> => {
const fenced = fencedLiveAgent(sessionId)
if (fenced !== undefined) return fenced
const attached = ctx.sessions.get(sessionId)
if (attached !== undefined && hasApiRemoteSubagentOwner(ctx, attached, undefined)) {
return { error: apiRemoteSubagentOwnershipError(sessionId) }
}
let resume = resumes.get(sessionId)
if (resume === undefined) {
resume = (async () => {
try {
const inspected = await inspectApiRemoteSession(ctx, sessionId)
if (hasApiRemoteSubagentOwner(ctx, { header: inspected.meta }, undefined)) {
throw new ApiRemoteSubagentSessionOwnership(sessionId)
}
// Built from the inspected session before the published re-checks
// below, so those stay adjacent to `resume` and a Host setup that
// awaits (composing a preset, say) does not widen the collision
// window.
const setup = options.setup === undefined ? undefined : await options.setup(inspected)
const publishedSession = ctx.sessions.get(sessionId)
const publishedAgent = ctx.agents.get(sessionId)
if (publishedSession !== undefined
&& hasApiRemoteSubagentOwner(ctx, publishedSession, publishedAgent)) {
throw new ApiRemoteSubagentSessionOwnership(sessionId)
}
const handle = await ctx.agents.resume({
resumeSessionId: sessionId,
...options.agentOptions === undefined ? {} : { agentOptions: options.agentOptions() },
...setup === undefined ? {} : { setup },
})
return handle.agent
} finally {
resumes.delete(sessionId)
}
})()
resumes.set(sessionId, resume)
}
try {
return { agent: await resume }
} catch (error: unknown) {
if (error instanceof ApiRemoteSessionNotFound) {
return { error: { code: 'session-not-found', message: error.message, details: { sessionId } } }
}
if (error instanceof ApiRemoteSubagentSessionOwnership) {
return { error: apiRemoteSubagentOwnershipError(error.sessionId) }
}
const fenced = fencedLiveAgent(sessionId)
if (fenced !== undefined) return fenced
const attached = ctx.sessions.get(sessionId)
if (attached !== undefined && hasApiRemoteSubagentOwner(ctx, attached, undefined)) {
return { error: apiRemoteSubagentOwnershipError(sessionId) }
}
return {
error: {
code: 'internal',
message: `resume failed for session "${sessionId}": ${String(error)}`,
details: {},
},
}
}
}
ctx.inject(['typert'], (typeCtx) => {
const resolveAgent = async (sessionId: SessionId): Promise<Agent> => {
const found = await agentFor(sessionId)
if ('error' in found) throw new TypeRTLookupFailure(found.error)
return found.agent
}
typeCtx.typert.lookups.configure('agent', resolveAgent)
typeCtx.typert.lookups.configure('session', async sessionId => (await resolveAgent(sessionId)).session)
typeCtx.typert.contexts.configureHost('agent', async sessionId => (await resolveAgent(sessionId)).ctx)
})
return agentFor
}
+27
View File
@@ -0,0 +1,27 @@
/** Platform-neutral assembly of generated Host Remote contributions. */
import type { Context } from 'cordis'
import goalsRemote from '@deepseek-ai/dsh-goal/remote'
import type { TypeRTClientRemote } from '@deepseek-ai/dsh-type-meta'
export type { TypeRTClientRemote as ClientRemote } from '@deepseek-ai/dsh-type-meta'
export type {} from '@deepseek-ai/dsh-goal/remote'
declare module 'cordis' {
interface Context {
/** Generated Remote namespaces selected by this Client assembly. */
remote: TypeRTClientRemote
}
}
/** Required service: the typed Client Remote contribution mount. */
export const inject = ['remote']
/**
* Mount the Host capabilities explicitly selected for this Client assembly.
* @param ctx - Client Cordis root carrying the typed API service.
* @returns disposer after every selected Remote namespace is ready.
*/
export async function apply(ctx: Context): Promise<() => Promise<void>> {
return await ctx.remote.$mount(goalsRemote)
}
+18
View File
@@ -0,0 +1,18 @@
/** Host BFF entry and Loader shell for the Remote contribution assembly. */
export {
ApiRemoteSessionNotFound,
ApiRemoteSubagentSessionOwnership,
apiRemoteSubagentOwnershipError,
createApiRemoteAgentResolver,
hasApiRemoteSubagentOwner,
inspectApiRemoteSession,
} from './agent-lookup.ts'
export type {
ApiRemoteAgentOptions,
ApiRemoteAgentResult,
ApiRemoteLookupError,
} from './agent-lookup.ts'
/** Host plugin body; the selected contributions mount only in Client environments. */
export function apply(): void {}
+24
View File
@@ -0,0 +1,24 @@
/** Package-owned invariant companion for `@deepseek-ai/dsh-api-remotes`. */
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-api-remotes'
/** Cordis companion plugin name. */
export const name = 'api-remotes-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** No runtime invariant: TypeRT and the Agent/Session registries own the observed relationships. */
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */