Revert "feat(agent): admit Web prompts through the agent controller"

This commit is contained in:
Turtle authored and GitHub committed 2026-10-20 19:09:02 +08:00
1 parent 92d48e0579
commit aa8bdfc79e
65 files changed
+111 -2172

No files matched your search

@@ -387,27 +387,6 @@ export class ApiSessionAgentController {
return result
}
/**
* Take the image admission turn for one Agent: wait until earlier image
* admissions and model selections settled, then hold the turn until the
* returned release runs.
* @param agent - live Agent that owns the serialization chain.
* @param signal - the waiting caller's cancellation; a caller cancelled by its turn passes the turn on.
* @returns the release, which the caller runs once its image prompt settled.
* @throws the cancellation reason when the caller was cancelled by its turn.
*/
acquireImageAdmission(agent: Agent, signal: AbortSignal): Promise<() => void> {
const previous = this.imageAdmissionChains.get(agent) ?? Promise.resolve()
const held = Promise.withResolvers<undefined>()
this.imageAdmissionChains.set(agent, previous.then(() => held.promise))
const release = (): void => { held.resolve(undefined) }
return previous.then(() => {
if (signal.aborted) release()
signal.throwIfAborted()
return release
})
}
/**
* Resolve the preset id and pre-publication Agent setup for a create or resume.
* @param presetId - requested preset or the configured default when omitted.
+42 -89
View File
@@ -5,15 +5,14 @@ import { randomUUID } from 'node:crypto'
import type { Context } from '@deepseek-ai/cordis'
import { brandString } from '@deepseek-ai/dsh-brand'
import type { Agent, ModelSelection as AgentModelSelection } from '@deepseek-ai/dsh-agent'
import type { ADMISSION_AGENT_GONE, ADMISSION_REJECTED_CODE, AgentAdmissionRejected } from '@deepseek-ai/dsh-agent-controller'
import { AttachmentError } from '@deepseek-ai/dsh-attachment'
import type {
AttachmentAdmissionPart, FileAttachmentRef, ImageAttachmentRef,
} from '@deepseek-ai/dsh-attachment'
import type { FileUploadReceiptId } from '@deepseek-ai/dsh-client-file-upload/types'
import type { PromptFileBinding } from '@deepseek-ai/dsh-client-file-upload'
import type {} from '@deepseek-ai/dsh-client-file-upload'
import {
ReasoningEffortId, assistantStreamChunks, freezeMessage,
ReasoningEffortId, assistantStreamChunks, createUserMessage, freezeMessage,
} from '@deepseek-ai/dsh-llm'
import type { MessageSource } from '@deepseek-ai/dsh-llm'
import { buildForkSeed } from '@deepseek-ai/dsh-session/fork'
@@ -311,72 +310,56 @@ export class SessionCommandController {
}
/**
* Deliver one prompt through the Agent controller, whose admission stage
* may hold or refuse it; the Remote method has already validated the
* content and built the durable message source. The controller runs this
* Session's content preparation (image model check, upload receipts,
* attachment storage) in the Agent's delivery order and binds the upload
* receipts immediately before the message enters the inbox. A repeated
* request identity is acknowledged without delivering it again. An image
* prompt takes the Session's image admission turn when its preparation
* starts and keeps it until the prompt settles, so a model selection lands
* either before its image check or after it entered the inbox; a selection
* waits while a listener holds that image prompt.
* Admit one prompt after Agent and attachment validation; the Remote method
* has already validated the content and built the durable message source.
* @param request - Session identity, prompt content, and delivery mode.
* @param source - the `user-rpc` source recorded on the accepted message.
* @param signal - the requesting client's cancellation; it stops a prompt that waits for its turn or is held at the admission stage.
* @returns acknowledgement that the Agent accepted the prompt.
* @throws `session/prompt-rejected` when an admission listener refuses the prompt or fails.
*/
async prompt(request: SessionPromptRequest, source: MessageSource, signal: AbortSignal): Promise<SessionPromptValue> {
async prompt(request: SessionPromptRequest, source: MessageSource): Promise<SessionPromptValue> {
const agent = await this.resolveAgent(request.sessionId)
if (hasPromptRequest(agent, request.requestId)) return { accepted: true }
const hasImage = request.content.some(part => part.type === 'image')
const uploads: { receiptIds: readonly FileUploadReceiptId[]; binding?: PromptFileBinding } = { receiptIds: [] }
const imageTurn: { release?: () => void } = {}
try {
await this.ctx.agentController.deliver(agent, {
mode: request.mode === 'steer' ? 'steer' : 'followup',
source,
signal,
prepare: async (preparing) => {
if (hasImage) {
// Taken in the Agent's delivery order, so a waiting image prompt keeps its place among the others.
imageTurn.release = await this.agents.acquireImageAdmission(agent, preparing)
await this.requireImageInput(agent)
const admit = async (): Promise<SessionPromptValue> => {
try {
if (hasImage) {
const current = this.agents.selectionFor(agent).current
const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model)
if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
throw new RemoteError(
'session/attachment-invalid',
`Model "${current.model}" does not support image input.`,
{ reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' },
)
}
const admission = resolvePromptFileReceipts(
request.content,
receiptId => this.ctx.fileUploads.resolve(agent, receiptId),
}
const admission = resolvePromptFileReceipts(
request.content,
receiptId => this.ctx.fileUploads.resolve(agent, receiptId),
)
const content = await this.ctx.attachments.admitPromptContent(admission.content)
const message: UserMessage = createUserMessage({ content, source })
if (this.ctx.agents.get(agent.id) !== agent) {
throw new RemoteError(
'session/not-found',
`session "${agent.id}" was disposed during prompt admission`,
{ sessionId: agent.id },
)
uploads.receiptIds = admission.receiptIds
return await this.ctx.attachments.admitPromptContent(admission.content)
},
beforeInsert: () => {
uploads.binding = this.ctx.fileUploads.bindPrompt(agent, uploads.receiptIds, request.requestId)
},
})
uploads.binding?.commit()
} catch (error) {
throw promptFailure(agent, error, signal)
} finally {
uploads.binding?.[Symbol.dispose]()
imageTurn.release?.()
}
return { accepted: true }
}
/** Refuse an image prompt when the Session's current model does not declare image input. */
private async requireImageInput(agent: Agent): Promise<void> {
const current = this.agents.selectionFor(agent).current
const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model)
if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
throw new RemoteError(
'session/attachment-invalid',
`Model "${current.model}" does not support image input.`,
{ reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' },
)
}
using binding = this.ctx.fileUploads.bindPrompt(agent, admission.receiptIds, request.requestId)
if (request.mode === 'steer') agent.steer(message)
else agent.followup(message)
binding.commit()
} catch (error) {
if (remoteErrorOf(error) !== undefined) throw error
if (error instanceof AttachmentError) {
throw new RemoteError('session/attachment-invalid', error.message, { reason: error.code })
}
throw new RemoteError('session/agent-busy', 'prompt rejected', { reason: String(error) })
}
return { accepted: true }
}
return hasImage ? this.agents.serializeImageAdmission(agent, admit) : admit()
}
private async requireModel(selection: Pick<AgentModelSelection, 'provider' | 'model'>): Promise<void> {
@@ -577,36 +560,6 @@ export class SessionCommandController {
}
}
// Typed by the agent-controller constants, so a changed value fails this module's build while it imports no agent-controller value.
const ADMISSION_REJECTED: typeof ADMISSION_REJECTED_CODE = 'AGENT_ADMISSION_REJECTED'
const AGENT_GONE: typeof ADMISSION_AGENT_GONE = 'agent-controller/agent-gone'
/** Recognize an admission refusal by the code that the error of every copy of the agent-controller module carries. */
function isAdmissionRejected(error: unknown): error is AgentAdmissionRejected {
return error instanceof Error && 'code' in error && error.code === ADMISSION_REJECTED
}
/**
* Map a failed prompt admission onto the Session Remote's failures: Remote
* failures and the client's own cancellation pass unchanged, and an admission
* refusal keeps its reason and whether its plugin presents it.
*/
function promptFailure(agent: Agent, error: unknown, signal: AbortSignal): unknown {
if (remoteErrorOf(error) !== undefined || signal.aborted) return error
if (isAdmissionRejected(error)) {
if (error.reason === AGENT_GONE) {
return new RemoteError('session/not-found', `session "${agent.id}" was disposed during prompt admission`, {
sessionId: agent.id,
})
}
return new RemoteError('session/prompt-rejected', error.message, { reason: error.reason, presented: error.presented })
}
if (error instanceof AttachmentError) {
return new RemoteError('session/attachment-invalid', error.message, { reason: error.code })
}
return new RemoteError('session/agent-busy', 'prompt rejected', { reason: String(error) })
}
function resolvePromptFileReceipts(
content: SessionPromptRequest['content'],
stagedFile: (receiptId: FileUploadReceiptId) => FileAttachmentRef | undefined,
+3 -8
View File
@@ -117,7 +117,6 @@ export interface SessionControllerInternals {
/** Host service backing the generated `ctx.remote.session` namespace. */
export class SessionController extends TypertRemoteService {
static inject = [
'agentController',
'agentDefaultModel',
'agents',
'attachments',
@@ -459,14 +458,10 @@ export class SessionController extends TypertRemoteService {
}
/**
* Admit one prompt after explicitly resuming its Session. The prompt is
* delivered through `ctx.agentController`, whose `agent-controller/admission`
* stage runs before it enters the inbox, so the call can wait for a listener
* that holds it.
* Admit one prompt after explicitly resuming its Session.
* @param request - Session identity, prompt content, source metadata, and delivery mode.
* @param signal - caller cancellation; it also stops a prompt waiting for or held at the admission stage.
* @param signal - caller cancellation before prompt admission begins.
* @returns acknowledgement that the Agent accepted the prompt.
* @throws `session/prompt-rejected` when an admission listener refuses the prompt or fails.
*/
@Remote('prompt')
async prompt(request: SessionPromptRequest, signal: AbortSignal): Promise<SessionPromptValue> {
@@ -492,7 +487,7 @@ export class SessionController extends TypertRemoteService {
...(clientTimeZone === undefined ? {} : { clientTimeZone }),
...(principal === undefined ? {} : { principal }),
}
return this.commands.prompt(request, source, signal)
return this.commands.prompt(request, source)
}
/**
@@ -212,12 +212,6 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
'session/projections-unavailable': Record<string, never>
'session/writer-held': { readonly sessionId: SessionId }
'session/agent-busy': { readonly reason: string }
'session/prompt-rejected': {
/** Code of the refusal, owned by the plugin that refused, such as `forgePanel/worktree-missing`. */
readonly reason: string
/** Whether the refusing plugin shows its own explanation, so the client adds no notice. */
readonly presented: boolean
}
'session/invalid-time-zone': { readonly value: string }
'session/workspace-attach-failed': { readonly sessionId: SessionId; readonly workspaceId: string }
'agent-preset/conflict': {