mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-14 04:01:35 +00:00
Merge remote-tracking branch 'origin/master' into dshw/pr-deepseek-harness-deepseek-harness-2672
# Conflicts: # docs/event-producer-consumer.i18n.yaml # docs/event-producer-consumer.md # docs/event-producer-consumer.zh.md # packages/acp/acp/tests/harness.ts # packages/api/session-controller/README.i18n.yaml # packages/api/session-controller/README.md # packages/api/session-controller/README.zh.md # packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts # packages/context/agent-instructions/tests/agent-instructions.e2e.ts # packages/fs/tool-fs/tests/harness.ts # packages/preset/agent-presets/tests/invariant.spec.ts # packages/preset/agent-presets/tests/mount.spec.ts # packages/preset/agent-presets/tests/remote.spec.ts # packages/test-support/agent-loop-testkit/package.json
This commit is contained in:
@@ -7,22 +7,25 @@
|
||||
* must stub); implementation-internal entry points (history staging, wire-frame
|
||||
* dispatch) stay on the class, invisible out here.
|
||||
*/
|
||||
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
import type { AttachmentIdType, FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types'
|
||||
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
|
||||
import type { PromptContentPart, QueueAction, SessionRequestId } from '../../types.ts'
|
||||
import type { PendingSubmissionImage, SessionSnapshot } from './snapshot.ts'
|
||||
import type { PendingSubmissionAttachment, SessionSnapshot } from './snapshot.ts'
|
||||
|
||||
/**
|
||||
* Why a local submission echo left the snapshot: `observed` when its durable
|
||||
* `user/message` event or host queue occurrence arrived (with the admitted
|
||||
* image references in prompt order), `failed` when the prompt was rejected,
|
||||
* attachment references in prompt order), `failed` when the prompt was rejected,
|
||||
* threw, or was aborted before acceptance.
|
||||
*/
|
||||
export type PendingSubmissionRetirement =
|
||||
| { readonly reason: 'observed'; readonly attachments: readonly ImageAttachmentRef[] }
|
||||
| {
|
||||
readonly reason: 'observed'
|
||||
readonly attachments: readonly (ImageAttachmentRef | FileAttachmentRef)[]
|
||||
}
|
||||
| { readonly reason: 'failed' }
|
||||
|
||||
/** Input registering one local submission echo ahead of its prompt call. */
|
||||
@@ -31,8 +34,8 @@ export interface BeginSubmissionInput {
|
||||
readonly mode: 'queue' | 'steer'
|
||||
/** Prompt text exactly as the upcoming prompt will send it. */
|
||||
readonly text: string
|
||||
/** Ordered image previews matching the upcoming prompt's image parts. */
|
||||
readonly images: readonly PendingSubmissionImage[]
|
||||
/** Ordered image previews and durable file metadata matching the upcoming prompt attachments. */
|
||||
readonly attachments: readonly PendingSubmissionAttachment[]
|
||||
/** Settlement callback fired exactly once when the echo retires. */
|
||||
readonly onRetire?: (retirement: PendingSubmissionRetirement) => void
|
||||
}
|
||||
@@ -95,7 +98,7 @@ export interface ISession {
|
||||
attachmentId: AttachmentIdType,
|
||||
): Promise<RemoteResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>>
|
||||
/**
|
||||
* Apply one edit, remove, or strict steer action to a still-pending queue occurrence.
|
||||
* Apply one edit, remove, or Steer action to a still-pending queue occurrence.
|
||||
* @param itemId - agent-owned inbox occurrence identity.
|
||||
* @param action - requested queue operation.
|
||||
* @returns acceptance, or a business/transport error.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Session-owned observable state excluding Conversation target data. */
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
|
||||
@@ -30,6 +31,23 @@ export interface PendingSubmissionImage {
|
||||
readonly height?: number
|
||||
}
|
||||
|
||||
/** Image branch of a local submission echo attachment. */
|
||||
export interface PendingSubmissionImageAttachment {
|
||||
readonly type: 'image'
|
||||
readonly value: PendingSubmissionImage
|
||||
}
|
||||
|
||||
/** File branch of a local submission echo attachment. */
|
||||
export interface PendingSubmissionFileAttachment {
|
||||
readonly type: 'file'
|
||||
readonly value: FileAttachmentRef
|
||||
}
|
||||
|
||||
/** One attachment displayed by a local submission echo, in prompt order. */
|
||||
export type PendingSubmissionAttachment =
|
||||
| PendingSubmissionImageAttachment
|
||||
| PendingSubmissionFileAttachment
|
||||
|
||||
/** Client surface selected when a local submission begins. */
|
||||
export type PendingSubmissionPlacement = 'transcript' | 'queued' | 'steering'
|
||||
|
||||
@@ -48,8 +66,8 @@ export interface PendingSubmission {
|
||||
readonly time: number
|
||||
/** Prompt text exactly as it will be sent (one text block). */
|
||||
readonly text: string
|
||||
/** Ordered image previews matching the prompt's image parts. */
|
||||
readonly images: readonly PendingSubmissionImage[]
|
||||
/** Ordered image previews and durable file metadata matching the prompt attachments. */
|
||||
readonly attachments: readonly PendingSubmissionAttachment[]
|
||||
}
|
||||
|
||||
/** History-open lifecycle of a Session event window. */
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-agent/types'
|
||||
import type {} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-file-upload/client'
|
||||
import { createSessionControlStream } from './transport.ts'
|
||||
import { ClientSessions } from './sessions/service.ts'
|
||||
import type { SessionRemotes } from './sessions/remotes.ts'
|
||||
@@ -62,7 +64,10 @@ export type {
|
||||
export type {
|
||||
OpenState,
|
||||
PendingSubmission,
|
||||
PendingSubmissionAttachment,
|
||||
PendingSubmissionFileAttachment,
|
||||
PendingSubmissionImage,
|
||||
PendingSubmissionImageAttachment,
|
||||
PendingSubmissionPlacement,
|
||||
PromptError,
|
||||
QueuedMessage,
|
||||
@@ -78,6 +83,8 @@ declare module '@deepseek-ai/cordis' {
|
||||
|
||||
/** Required Remote and Context projection services. */
|
||||
export const inject = [
|
||||
'connection',
|
||||
'fileUpload',
|
||||
'typert',
|
||||
'remote',
|
||||
'remote.commands',
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { QueuedMessage } from '../contract/snapshot.ts'
|
||||
|
||||
const QUEUE_PREVIEW_CHARS = 200
|
||||
|
||||
// Image blocks are excluded: queue presentation renders them as thumbnails
|
||||
// from `content`, so the text preview covers only what has no visual form.
|
||||
// Attachment blocks are excluded: queue presentation renders them from
|
||||
// `content`, so the text preview covers only what has no visual form.
|
||||
function previewOf(content: readonly ContentBlock[]): string {
|
||||
const flat = content
|
||||
.filter(block => block.type !== 'image')
|
||||
.filter(block => block.type !== 'image' && block.type !== 'file')
|
||||
.map(block => (block.type === 'text' ? block.text : `[${block.type}]`))
|
||||
.join(' ').replace(/\s+/g, ' ').trim()
|
||||
const chars = Array.from(flat)
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* @module @deepseek-ai/dsh-api-session-controller/client/sessions/remotes
|
||||
*/
|
||||
|
||||
import type { EncodedImageAttachment } from '@deepseek-ai/dsh-attachment/types'
|
||||
import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import type { CommandSubmitAttachment } from '@deepseek-ai/dsh-commands/types'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type {
|
||||
SubagentCatalog, SubagentInterruptReceipt, SubagentPromptReceipt, SubagentPromptRequest,
|
||||
@@ -19,7 +19,7 @@ export interface SessionCommandsRemote {
|
||||
execute(
|
||||
agentId: SessionId,
|
||||
line: string,
|
||||
images: readonly EncodedImageAttachment[],
|
||||
attachments: readonly CommandSubmitAttachment[],
|
||||
signal?: AbortSignal,
|
||||
): Promise<RemoteResult<object | undefined>>
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
|
||||
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
import type { AttachmentIdType, FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
|
||||
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import { SessionLogOffset, SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
} from '../contract/events.ts'
|
||||
import { Notifier } from './notifier.ts'
|
||||
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { SessionRemotes } from './remotes.ts'
|
||||
import { ProjectionValueStore } from './projection-store.ts'
|
||||
@@ -210,7 +211,7 @@ export class Session implements SessionFace {
|
||||
: 'transcript',
|
||||
time: Date.now(),
|
||||
text: input.text,
|
||||
images: input.images,
|
||||
attachments: input.attachments,
|
||||
}]
|
||||
this.submissionSettlements.set(requestId, { onRetire: input.onRetire, retiring: false })
|
||||
// The blank → engaging edge flips here, ahead of prompt(): the composer
|
||||
@@ -222,7 +223,7 @@ export class Session implements SessionFace {
|
||||
|
||||
/**
|
||||
* Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
|
||||
* @param content - text plus browser-owned temporary image uploads.
|
||||
* @param content - text, browser-owned temporary image uploads, and staged-file receipts.
|
||||
* @param mode - queue appends after the current turn; steer interrupts it.
|
||||
* @param signal - optional caller cancellation for the complete admission round-trip.
|
||||
* @param requestId - identity from {@link beginSubmission}; a failed identified prompt retires its echo.
|
||||
@@ -252,13 +253,26 @@ export class Session implements SessionFace {
|
||||
content,
|
||||
clientTimeZone,
|
||||
}, signal)
|
||||
} else if (content.some(part => part.type === 'file')) {
|
||||
result = {
|
||||
ok: false,
|
||||
error: new RemoteError(
|
||||
'subagent/attachment-invalid',
|
||||
'subagent continuation does not accept files',
|
||||
{ reason: 'SUBAGENT_FILE_UNSUPPORTED' },
|
||||
),
|
||||
}
|
||||
} else {
|
||||
// The preceding branch rejects file parts before the narrower subagent
|
||||
// wire type is used; this array is not filtered or reordered.
|
||||
const routedContent = content as Exclude<PromptContentPart, { readonly type: 'file' }>[]
|
||||
const routed = await this.remote.subagents.prompt({
|
||||
requestId: randomUUID() as SessionRequestId,
|
||||
parentSessionId: this.address.parentSessionId,
|
||||
childSessionId: this.address.childSessionId,
|
||||
mode: 'continuable',
|
||||
content,
|
||||
delivery: mode,
|
||||
content: routedContent,
|
||||
clientTimeZone: resolvedClientTimeZone(),
|
||||
}, signal)
|
||||
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
|
||||
@@ -717,7 +731,7 @@ export class Session implements SessionFace {
|
||||
const data = event.data as { readonly source?: unknown; readonly content?: unknown } | undefined
|
||||
const source = data?.source as { readonly kind?: unknown; readonly rpcId?: unknown } | undefined
|
||||
if (source?.kind !== 'user' || typeof source.rpcId !== 'string') return
|
||||
this.scheduleObservedRetirement(source.rpcId as SessionRequestId, imageRefsIn(data?.content))
|
||||
this.scheduleObservedRetirement(source.rpcId as SessionRequestId, attachmentRefsIn(data?.content))
|
||||
}
|
||||
|
||||
/** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
|
||||
@@ -725,7 +739,7 @@ export class Session implements SessionFace {
|
||||
if (this.submissionSettlements.size === 0) return
|
||||
for (const item of items) {
|
||||
if (item.rpcId !== undefined) {
|
||||
this.scheduleObservedRetirement(item.rpcId, imageRefsIn(item.message.content))
|
||||
this.scheduleObservedRetirement(item.rpcId, attachmentRefsIn(item.message.content))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -738,7 +752,7 @@ export class Session implements SessionFace {
|
||||
*/
|
||||
private scheduleObservedRetirement(
|
||||
requestId: SessionRequestId,
|
||||
attachments: readonly ImageAttachmentRef[],
|
||||
attachments: readonly (ImageAttachmentRef | FileAttachmentRef)[],
|
||||
): void {
|
||||
const settlement = this.submissionSettlements.get(requestId)
|
||||
if (settlement === undefined || settlement.retiring) return
|
||||
@@ -816,15 +830,16 @@ function scheduleFrame(fn: () => void): void {
|
||||
else setTimeout(fn, 0)
|
||||
}
|
||||
|
||||
/** Image attachment references in one structurally-read content block list, in block order. */
|
||||
function imageRefsIn(content: unknown): readonly ImageAttachmentRef[] {
|
||||
/** Attachment references in one structurally-read content block list, in block order. */
|
||||
function attachmentRefsIn(content: unknown): readonly (ImageAttachmentRef | FileAttachmentRef)[] {
|
||||
if (!Array.isArray(content)) return []
|
||||
const refs: ImageAttachmentRef[] = []
|
||||
const refs: Array<ImageAttachmentRef | FileAttachmentRef> = []
|
||||
for (const block of content) {
|
||||
if (typeof block !== 'object' || block === null) continue
|
||||
const candidate = block as { readonly type?: unknown; readonly attachment?: unknown }
|
||||
if (candidate.type === 'image' && typeof candidate.attachment === 'object' && candidate.attachment !== null) {
|
||||
refs.push(candidate.attachment as ImageAttachmentRef)
|
||||
if ((candidate.type === 'image' || candidate.type === 'file')
|
||||
&& typeof candidate.attachment === 'object' && candidate.attachment !== null) {
|
||||
refs.push(candidate.attachment as ImageAttachmentRef | FileAttachmentRef)
|
||||
}
|
||||
}
|
||||
return refs
|
||||
|
||||
@@ -4,10 +4,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 { AttachmentError, admitPromptContent } from '@deepseek-ai/dsh-attachment'
|
||||
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
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 {} from '@deepseek-ai/dsh-client-file-upload'
|
||||
import {
|
||||
ReasoningEffortId, createUserMessage, expandAssistantStream, freezeMessage,
|
||||
ReasoningEffortId, assistantStreamChunks, createUserMessage, freezeMessage,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import type { MessageSource } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
|
||||
@@ -15,6 +19,7 @@ import type { SessionEvent, SessionHeader, SessionId, UserMessage } from '@deeps
|
||||
import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
|
||||
import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title'
|
||||
import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time'
|
||||
import { assertNever } from '@deepseek-ai/dsh-util-values'
|
||||
import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { Workspace } from '@deepseek-ai/dsh-workspace'
|
||||
import {
|
||||
@@ -44,6 +49,7 @@ import type {
|
||||
SessionSelectModelValue,
|
||||
SessionUpdateQueueRequest,
|
||||
SessionUpdateQueueValue,
|
||||
SessionRequestId,
|
||||
} from './types.ts'
|
||||
|
||||
interface SessionReadState {
|
||||
@@ -297,6 +303,7 @@ export class SessionCommandController {
|
||||
)
|
||||
}
|
||||
const agent = await this.resolveAgent(request.sessionId)
|
||||
if (hasPromptRequest(agent, request.requestId)) return { accepted: true }
|
||||
const selection = this.agents.selectionFor(agent).current
|
||||
if (!routeServed(this.ctx, selection.provider)) {
|
||||
throw new RemoteError(
|
||||
@@ -324,10 +331,23 @@ export class SessionCommandController {
|
||||
)
|
||||
}
|
||||
}
|
||||
const content = await admitPromptContent(this.ctx.attachments, request.content)
|
||||
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 },
|
||||
)
|
||||
}
|
||||
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) {
|
||||
@@ -396,12 +416,18 @@ export class SessionCommandController {
|
||||
)
|
||||
}
|
||||
const agent = this.ctx.agents.get(request.sessionId)
|
||||
if (agent !== undefined && hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
||||
throw apiSessionSubagentOwnershipError(request.sessionId)
|
||||
}
|
||||
if (agent === undefined) {
|
||||
throw new RemoteError('session/queue-item-not-found', 'queued item is no longer pending', { itemId: request.itemId })
|
||||
}
|
||||
if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
||||
const identity = this.ctx.sessionProjections
|
||||
.snapshot(agent.session, ['subagent'])
|
||||
.values.subagent
|
||||
if (identity?.mode !== 'continuable'
|
||||
|| !agent.session.isOwnSeq(identity.seq)) {
|
||||
throw apiSessionSubagentOwnershipError(request.sessionId)
|
||||
}
|
||||
}
|
||||
const nextTurn = agent.inbox.nextTurn.find(message => message.id === request.itemId)
|
||||
const nextStep = agent.inbox.nextStep.find(message => message.id === request.itemId)
|
||||
const located = nextTurn === undefined
|
||||
@@ -414,14 +440,28 @@ export class SessionCommandController {
|
||||
if (request.action.kind === 'steer' && (target !== 'next-turn' || agent.status !== 'running')) {
|
||||
throw new RemoteError('session/steer-unavailable', 'current turn no longer accepts steering', { itemId: request.itemId })
|
||||
}
|
||||
if (request.action.kind === 'edit') {
|
||||
agent.inbox.replace(request.itemId, freezeMessage<UserMessage>({
|
||||
...message,
|
||||
content: [...request.action.content],
|
||||
}))
|
||||
} else {
|
||||
agent.inbox.remove(request.itemId)
|
||||
if (request.action.kind === 'steer') agent.steer(message)
|
||||
switch (request.action.kind) {
|
||||
case 'edit':
|
||||
agent.inbox.replace(request.itemId, freezeMessage<UserMessage>({
|
||||
...message,
|
||||
content: [...request.action.content],
|
||||
}))
|
||||
break
|
||||
case 'remove': {
|
||||
agent.inbox.remove(request.itemId)
|
||||
const source = message.source
|
||||
if (source.kind === 'user' && 'rpcId' in source) {
|
||||
this.ctx.fileUploads.retirePrompt(agent, source.rpcId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'steer':
|
||||
agent.inbox.remove(request.itemId)
|
||||
agent.steer(message)
|
||||
break
|
||||
/* v8 ignore next 2 -- closed-union exhaustiveness guard */
|
||||
default:
|
||||
assertNever(request.action, 'queue action')
|
||||
}
|
||||
return { accepted: true }
|
||||
}
|
||||
@@ -497,6 +537,39 @@ export class SessionCommandController {
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePromptFileReceipts(
|
||||
content: SessionPromptRequest['content'],
|
||||
stagedFile: (receiptId: FileUploadReceiptId) => FileAttachmentRef | undefined,
|
||||
): { readonly content: AttachmentAdmissionPart[]; readonly receiptIds: readonly FileUploadReceiptId[] } {
|
||||
const receiptIds = new Set<FileUploadReceiptId>()
|
||||
const resolved = content.map((part): AttachmentAdmissionPart => {
|
||||
if (part.type !== 'file') return part
|
||||
const attachment = stagedFile(part.receiptId)
|
||||
if (attachment === undefined) {
|
||||
throw new RemoteError(
|
||||
'session/attachment-invalid',
|
||||
'File was not uploaded for this session.',
|
||||
{ reason: 'FILE_NOT_STAGED' },
|
||||
)
|
||||
}
|
||||
receiptIds.add(part.receiptId)
|
||||
return { type: 'file', attachment }
|
||||
})
|
||||
return { content: resolved, receiptIds: [...receiptIds] }
|
||||
}
|
||||
|
||||
function hasPromptRequest(agent: Agent, requestId: SessionRequestId): boolean {
|
||||
const matches = (message: UserMessage): boolean => {
|
||||
const source = message.source
|
||||
return source.kind === 'user' && 'rpcId' in source && source.rpcId === requestId
|
||||
}
|
||||
if (agent.inbox.nextTurn.some(matches) || agent.inbox.nextStep.some(matches)) return true
|
||||
return agent.session.snapshotEvents().some((event) => {
|
||||
if (event.type !== 'user/message') return false
|
||||
const source = event.data.source
|
||||
return source.kind === 'user' && 'rpcId' in source && source.rpcId === requestId
|
||||
})
|
||||
}
|
||||
function imageBlockIn(
|
||||
content: unknown,
|
||||
match: (ref: ImageAttachmentRef) => boolean,
|
||||
@@ -535,8 +608,7 @@ function imageInEvent(
|
||||
if (found !== undefined) return found
|
||||
}
|
||||
if (event.type === 'assistant/message' || event.type === 'assistant/attempt') {
|
||||
for (const { chunk } of expandAssistantStream(event.data.stream)) {
|
||||
if (chunk.type !== 'block-end') continue
|
||||
for (const chunk of assistantStreamChunks(event.data.stream, 'block-end')) {
|
||||
const found = imageBlockIn([chunk.block], match)
|
||||
if (found !== undefined) return found
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { errorChain } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-client-file-upload'
|
||||
import { canOpenNativePath, openNativePath } from '@deepseek-ai/dsh-native-command'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence'
|
||||
@@ -84,6 +85,7 @@ export class SessionController extends TypertRemoteService {
|
||||
'agentDefaultModel',
|
||||
'agents',
|
||||
'attachments',
|
||||
'fileUploads',
|
||||
'llm',
|
||||
'sessions',
|
||||
'sessionProjections',
|
||||
@@ -115,6 +117,11 @@ export class SessionController extends TypertRemoteService {
|
||||
installModelSelectionProjection(ctx)
|
||||
this.agents = new ApiSessionAgentController(ctx)
|
||||
this.commands = new SessionCommandController(ctx, this.agents, process.cwd())
|
||||
ctx.effect(() => ctx.fileUploads.registerAgentResolver(async (sessionId) => {
|
||||
const result = await this.agents.resolveAgent(sessionId)
|
||||
if ('error' in result) throw result.error
|
||||
return result.agent
|
||||
}), 'session-controller: file-upload Agent resolver')
|
||||
this.controlState = new SessionControlController(ctx)
|
||||
// Registered before history so reverse-order teardown closes every
|
||||
// follower before waiting for already-admitted promotions.
|
||||
|
||||
@@ -67,7 +67,11 @@ export interface SessionProjectionBaseline {
|
||||
export type SessionProjectionValues = Partial<SessionProjectionMap>
|
||||
& Readonly<Record<string, SessionProjectionValue>>
|
||||
|
||||
/** Browser-submitted prompt content; the Host promotes image bytes to durable references. */
|
||||
/**
|
||||
* Browser-submitted prompt content; the Host promotes image bytes to durable
|
||||
* references. File parts carry the opaque receipt returned by a preceding
|
||||
* `uploadFile` call on the same Session.
|
||||
*/
|
||||
export type PromptContentPart =
|
||||
| { readonly type: 'text'; readonly text: string }
|
||||
| {
|
||||
@@ -76,6 +80,7 @@ export type PromptContentPart =
|
||||
readonly data: string
|
||||
readonly name?: string
|
||||
}
|
||||
| { readonly type: 'file'; readonly receiptId: Branded<'file-upload-receipt-id'> }
|
||||
|
||||
/** Complete model selection for one Session. */
|
||||
export interface ModelSelection {
|
||||
|
||||
Reference in New Issue
Block a user