mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
All agent/* and agent-loop/config-start-failed events take one payload object carrying the agent subject; waterfall/serial payloads require a signal and keep next as the final argument. PreStepContext and RequestFailureContext are unfolded into payloads and retired. goal/changed follows the same shape so agentEvents keeps its listener error containment. ReactLoopAgent builds its scope carrier once in the constructor. Regenerates scope resolvers, tool-cordis api catalog, and docs catalogs; updates all affected listeners, tests, and the core-data-structures docs (en + zh).
236 lines
8.3 KiB
TypeScript
236 lines
8.3 KiB
TypeScript
/**
|
|
* Shared driver for in-process ONE-SHOT subagent providers. The agent factory's
|
|
* creation transaction owns unpublished setup and rollback; after publication
|
|
* the returned AgentHandle is the one quiescent lifecycle owner held by the
|
|
* provider's caller.
|
|
*
|
|
* Continuable children never come through here: the continuation manager
|
|
* composes and drives them directly, so this driver owns exactly one turn with
|
|
* one result.
|
|
*
|
|
* @module @deepseek-ai/dsh-subagent-inprocess
|
|
*/
|
|
|
|
import { randomUUID } from 'node:crypto'
|
|
import type { Context } from 'cordis'
|
|
import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
|
|
import { findLastMessageTurnEnd, SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
|
|
import { createUserMessage, type ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
import {
|
|
applyChildComposition,
|
|
assertSubagentMaxDepth,
|
|
childSessionMeta,
|
|
resolveChildAgentOptions,
|
|
resolveChildDepth,
|
|
} from '@deepseek-ai/dsh-subagent'
|
|
import type {
|
|
ResolvedSubagentStartRequest,
|
|
SubagentDescriptorData,
|
|
SubagentResult,
|
|
SubagentRun,
|
|
SubagentStopReason,
|
|
} from '@deepseek-ai/dsh-subagent'
|
|
// Type-only: make `ctx.get('sandboxPolicy')` / `ctx.get('approval')` resolve
|
|
// to the policy services when composed — the driver consumes both
|
|
// opportunistically (the documented `ctx.get` pattern), never as a hard dep.
|
|
import type {} from '@deepseek-ai/dsh-sandbox-policy'
|
|
import type {} from '@deepseek-ai/dsh-user-approval'
|
|
import {
|
|
attachStructuredRuntime,
|
|
type StructuredAttachment,
|
|
} from './structured.ts'
|
|
|
|
export {
|
|
STRUCTURED_OUTPUT_TOOL,
|
|
STRUCTURED_OUTPUT_INSTRUCTION,
|
|
} from './structured.ts'
|
|
|
|
/** Map a session turn outcome to the subagent seam's terminal vocabulary. */
|
|
function toStopReason(reason: TurnEndReason | undefined): SubagentStopReason {
|
|
switch (reason?.kind) {
|
|
case 'completed':
|
|
return 'completed'
|
|
case 'max-tokens':
|
|
return 'max-tokens'
|
|
case 'aborted':
|
|
return 'aborted'
|
|
case 'error':
|
|
case 'interrupted':
|
|
default:
|
|
return 'error'
|
|
}
|
|
}
|
|
|
|
/** Extra inputs the spawn and fork providers supply to the shared driver. */
|
|
export interface InProcessRunOptions {
|
|
/** Completed-turn seed for fork, or undefined for a fresh spawn. */
|
|
readonly seed?: SessionEvent[]
|
|
}
|
|
|
|
/** Error used when cancellation wins before the child publication boundary. */
|
|
function prePublicationAbort(): Error {
|
|
return new Error('subagent request was aborted before child publication')
|
|
}
|
|
|
|
/** Append one one-shot descriptor inside the child's initial turn before its first request. */
|
|
function attachDescriptorAppend(childCtx: Context, descriptor: SubagentDescriptorData): void {
|
|
let appended = false
|
|
childCtx.on('agent/pre-step', async ({ agent }, next) => {
|
|
const decision = await next()
|
|
if (!appended && decision.kind === 'enter') {
|
|
appended = true
|
|
agent.session.append('subagent/descriptor', descriptor)
|
|
}
|
|
return decision
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Establish and drive one in-process one-shot child. Fulfillment means the agent
|
|
* is already published in the registry and transfers its turn, cancellation,
|
|
* and disposal work through the returned run. Rejection means the agent
|
|
* factory's unpublished creation transaction reached quiescence without
|
|
* publishing a child. Every start appends its resolved descriptor inside the
|
|
* child's initial turn.
|
|
* @param request - the trusted typed start request, including its required signal.
|
|
* @param options - the optional fork seed.
|
|
* @returns a published holder-owned run.
|
|
*/
|
|
export async function startInProcessRun(
|
|
request: ResolvedSubagentStartRequest,
|
|
options: InProcessRunOptions,
|
|
): Promise<SubagentRun> {
|
|
assertSubagentMaxDepth(request.maxDepth)
|
|
if (request.signal.aborted) throw prePublicationAbort()
|
|
const parent = request.parent
|
|
const childDepth = resolveChildDepth(parent, request.maxDepth)
|
|
|
|
const childId = SessionId(randomUUID())
|
|
const seed = options.seed
|
|
const activationBoundary = seed?.length ?? 0
|
|
|
|
// Capture before the first await: a later parent switch belongs to the
|
|
// parent's future.
|
|
const inheritedMode = parent.ctx.get('sandboxPolicy')?.overrideOf(parent.session)
|
|
const inheritedPolicy = parent.ctx.get('approval')?.overrideOf(parent.session)
|
|
|
|
let structured: StructuredAttachment | undefined
|
|
const setup = (childCtx: Context): void => {
|
|
// Inherited overrides land on the child's own log, so its effective policy
|
|
// is reconstructable from that log alone.
|
|
const childSession = (childCtx.agent as Agent).session
|
|
if (inheritedMode !== undefined) {
|
|
childSession.append('sandbox/mode', { mode: inheritedMode, source: 'delegation' })
|
|
}
|
|
if (inheritedPolicy !== undefined) {
|
|
childSession.append('approval/policy', { policy: inheritedPolicy, source: 'delegation' })
|
|
}
|
|
applyChildComposition(childCtx, {
|
|
persona: request.persona,
|
|
toolFilter: request.toolFilter,
|
|
})
|
|
if (request.outputSchema !== undefined) {
|
|
structured = attachStructuredRuntime(childCtx, request.outputSchema)
|
|
}
|
|
attachDescriptorAppend(childCtx, request.descriptor)
|
|
}
|
|
|
|
const handle = await parent.ctx.agents.create({
|
|
sessionId: childId,
|
|
meta: childSessionMeta(parent, childDepth, activationBoundary),
|
|
...seed !== undefined ? { seed } : {},
|
|
agentOptions: resolveChildAgentOptions(parent, request.agentOptions, childDepth),
|
|
signal: request.signal,
|
|
setup,
|
|
})
|
|
return drivePublishedRun(
|
|
handle,
|
|
request.signal,
|
|
request.prompt,
|
|
childId,
|
|
activationBoundary,
|
|
structured,
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Wrap a published child in the single run lifecycle that owns signal handoff,
|
|
* one turn, result settlement, and quiescent disposal.
|
|
*/
|
|
function drivePublishedRun(
|
|
handle: AgentHandle,
|
|
signal: AbortSignal,
|
|
prompt: ContentBlock[],
|
|
childId: SessionId,
|
|
boundary: number,
|
|
structured: StructuredAttachment | undefined,
|
|
): SubagentRun {
|
|
const child = handle.agent
|
|
const flags = { cancelled: false }
|
|
const onAbort = (): void => {
|
|
flags.cancelled = true
|
|
child.cancel({ kind: 'parent' })
|
|
}
|
|
signal.addEventListener('abort', onAbort, { once: true })
|
|
// Agent creation detaches its creation-only listener before returning. The
|
|
// post-registration check closes that handoff without treating an already
|
|
// published child as a failed start.
|
|
if (signal.aborted) onAbort()
|
|
|
|
const result: Promise<SubagentResult> = (async () => {
|
|
try {
|
|
if (!flags.cancelled) {
|
|
child.followup(createUserMessage({ content: prompt, source: { kind: 'user' } }))
|
|
await child.whenIdle()
|
|
}
|
|
return readResult(
|
|
child,
|
|
boundary,
|
|
flags.cancelled,
|
|
structured ? { captured: structured.captured() } : undefined,
|
|
)
|
|
} finally {
|
|
signal.removeEventListener('abort', onAbort)
|
|
}
|
|
})()
|
|
|
|
return {
|
|
id: childId,
|
|
localAgent: child,
|
|
result,
|
|
async dispose(): Promise<void> {
|
|
signal.removeEventListener('abort', onAbort)
|
|
flags.cancelled = true
|
|
const settlements = await Promise.allSettled([handle.dispose(), result])
|
|
const disposal = settlements[0]
|
|
// The result channel owns run faults; disposal reports only failure to
|
|
// release the published handle after both operations settle.
|
|
if (disposal.status === 'rejected') throw disposal.reason
|
|
},
|
|
}
|
|
}
|
|
|
|
/** Read one settled child's result from events after its activation boundary. */
|
|
function readResult(
|
|
child: Agent,
|
|
boundary: number,
|
|
cancelled: boolean,
|
|
structured?: { captured?: { value: unknown } | undefined },
|
|
): SubagentResult {
|
|
const own = child.session.events.slice(boundary)
|
|
const lastMessage = own.findLast((event): event is SessionEvent<'assistant/message'> => event.type === 'assistant/message')
|
|
const lastEnd = findLastMessageTurnEnd(own)
|
|
const output: ContentBlock[] = lastMessage?.data.message.content ?? []
|
|
const recorded = toStopReason(lastEnd?.data.reason)
|
|
// Disposal can tear the owner down before the loop records its ordinary
|
|
// `aborted` end, yielding `disposed` instead.
|
|
const stopReason: SubagentStopReason = cancelled && recorded !== 'completed' ? 'aborted' : recorded
|
|
if (structured !== undefined) {
|
|
if (structured.captured !== undefined) {
|
|
return { output, structured: structured.captured.value, stopReason }
|
|
}
|
|
if (stopReason === 'completed') return { output, stopReason: cancelled ? 'aborted' : 'error' }
|
|
}
|
|
return { output, stopReason }
|
|
}
|