Merge remote-tracking branch 'origin/master' into xtr/explicit-agent-context

# Conflicts:
#	packages/bundle/headless/tests/headless.spec.ts
#	packages/core/agent-loop/src/agent.ts
#	packages/subagent/subagent/tests/continuation.spec.ts
This commit is contained in:
_Kerman
2026-09-08 17:12:51 +08:00
1849 changed files with 65767 additions and 7427 deletions
@@ -58,6 +58,14 @@ interface SessionReadState {
readonly events: readonly SessionEvent[]
}
type PromptContentCandidate =
| SessionPromptRequest['content'][number]
| Extract<SessionUpdateQueueRequest['action'], { readonly kind: 'edit' }>['content'][number]
function hasPromptContent(content: readonly PromptContentCandidate[]): boolean {
return content.some(part => part.type !== 'text' || part.text.trim().length > 0)
}
/** Implements Session business commands delegated by the Session Controller Remote service. */
export class SessionCommandController {
/**
@@ -287,11 +295,18 @@ export class SessionCommandController {
}
/**
* Admit one browser prompt after explicit Agent resume and image validation.
* Reject empty content, then admit one prompt after Agent and attachment validation.
* @param request - Session identity, prompt content, source metadata, and delivery mode.
* @returns acknowledgement that the Agent accepted the prompt.
*/
async prompt(request: SessionPromptRequest): Promise<SessionPromptValue> {
if (!hasPromptContent(request.content)) {
throw new RemoteError(
'gateway/bad-request',
'prompt content must include non-whitespace text or an attachment',
{},
)
}
const clientTimeZone = request.clientTimeZone === undefined
? undefined
: canonicalClientTimeZone(request.clientTimeZone)
@@ -407,13 +422,21 @@ export class SessionCommandController {
* @returns acknowledgement that the queue mutation was applied.
*/
updateQueue(request: SessionUpdateQueueRequest): SessionUpdateQueueValue {
if (request.action.kind === 'edit'
&& request.action.content.some(block => block.type !== 'text')) {
throw new RemoteError(
'session/attachment-invalid',
'queue edits accept text content only',
{ reason: 'QUEUE_EDIT_NON_TEXT' },
)
if (request.action.kind === 'edit') {
if (request.action.content.some(block => block.type !== 'text')) {
throw new RemoteError(
'session/attachment-invalid',
'queue edits accept text content only',
{ reason: 'QUEUE_EDIT_NON_TEXT' },
)
}
if (!hasPromptContent(request.action.content)) {
throw new RemoteError(
'gateway/bad-request',
'queue edit content must include non-whitespace text',
{},
)
}
}
const agent = this.ctx.agents.get(request.sessionId)
if (agent === undefined) {
+20 -26
View File
@@ -1,11 +1,11 @@
/** Live Session queue, jobs, and projection state with reconnect baselines. */
import type { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { Agent, InboxState } from '@deepseek-ai/dsh-agent'
import { Deque } from '@deepseek-ai/dsh-deque'
import type { JobSnapshot } from '@deepseek-ai/dsh-jobs'
import type {
Session, SessionEvent, SessionEventMap, SessionId, UserMessage,
Session, SessionId, UserMessage,
} from '@deepseek-ai/dsh-session'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import type {
@@ -23,7 +23,6 @@ export class SessionControlController {
/** @param ctx - Host context carrying live Agent, projection, and jobs services. */
constructor(private readonly ctx: Context) {
ctx.on('session/event', (session, event) => { this.onSessionEvent(session, event) })
ctx.sessionProjections.onChanged((session, key, value, seq) => {
this.broadcast({
type: 'projection',
@@ -32,6 +31,14 @@ export class SessionControlController {
value: value as JsonValue,
seq,
})
if (key !== 'inbox') return
const agent = this.ctx.agents.get(session.id)
if (agent?.session !== session) return
this.broadcast({
type: 'queue',
sessionId: session.id,
items: queueItemsFromInbox(value as InboxState),
})
})
ctx.inject(['jobs'], (jobsCtx) => {
jobsCtx.jobs.onJobsChanged((owner) => { this.onJobsChanged(owner) })
@@ -95,17 +102,6 @@ export class SessionControlController {
return blocks
}
private onSessionEvent(session: Session, event: SessionEvent): void {
if (event.type !== 'agent/inbox/spliced') return
const agent = this.ctx.agents.get(session.id)
if (agent?.session !== session) return
this.broadcast({
type: 'queue',
sessionId: session.id,
items: queueItems(agent, event.data),
})
}
private onJobsChanged(owner: Agent | undefined): void {
if (owner !== undefined) {
this.broadcast({ type: 'jobs', sessionId: owner.id, jobs: this.jobsFor(owner) })
@@ -171,24 +167,22 @@ class ControlQueue {
}
}
function queueItems(
agent: Agent,
splice?: SessionEventMap['agent/inbox/spliced'],
): SessionQueuedItem[] {
const project = (target: 'next-turn' | 'next-step'): readonly UserMessage[] => {
const messages = target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep
return splice?.target === target
? messages.toSpliced(splice.start, splice.removedCount ?? 0, ...splice.inserted)
: messages
}
function queueItems(agent: Agent): SessionQueuedItem[] {
return queueItemsFromInbox({
'next-turn': agent.inbox.nextTurn,
'next-step': agent.inbox.nextStep,
})
}
function queueItemsFromInbox(inbox: InboxState): SessionQueuedItem[] {
return [
...project('next-turn').map(message => ({
...inbox['next-turn'].map(message => ({
id: message.id,
placement: 'queued' as const,
...promptRpcId(message),
message: { id: message.id, content: message.content as unknown as JsonValue[] },
})),
...project('next-step').map(message => ({
...inbox['next-step'].map(message => ({
id: message.id,
placement: message.source.kind === 'user' ? 'steering' as const : 'context' as const,
...promptRpcId(message),
+6 -1
View File
@@ -151,7 +151,11 @@ export interface ModelCatalog {
/** One client-requested mutation of a still-pending queue item. */
export type QueueAction =
| { readonly kind: 'edit'; readonly content: readonly ContentBlock[] }
| {
readonly kind: 'edit'
/** Non-empty text-only replacement content. */
readonly content: readonly ContentBlock[]
}
| { readonly kind: 'remove' }
| { readonly kind: 'steer' }
@@ -308,6 +312,7 @@ export interface SessionPromptRequest {
readonly requestId: SessionRequestId
readonly sessionId: SessionId
readonly mode: 'queue' | 'steer'
/** At least one non-whitespace text part or attachment. */
readonly content: readonly PromptContentPart[]
readonly clientTimeZone?: string
}