Merge branch 'stack/agent-profiles-1-seam' into stack/agent-profiles-3-wire
master extracted this layer's inline cold-resume resolver into @deepseek-ai/dsh-api-remotes, whose `setup` was a fixed AgentSetup. A resumed session composes the preset ITS header recorded, so the option becomes a function of that header; the resolver builds the setup before the published re-checks so those stay adjacent to `resume`. Conflicts: docs/cordis-catalog/services.md docs/module-graph.md packages/host/apiproxy/package.json packages/host/apiproxy/src/api-proxy.ts pnpm-lock.yaml
This commit is contained in:
649 files changed
+21067
-2810
No files matched your search
@@ -0,0 +1,206 @@
|
||||
/** 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's own header 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.
|
||||
* @param meta - the resumed session's persisted header.
|
||||
* @returns the Agent-scope setup to run before publication.
|
||||
*/
|
||||
readonly setup?: (meta: SessionHeader) => 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 header 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.meta)
|
||||
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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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 */
|
||||
Reference in New Issue
Block a user