mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user