refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
233
packages/subagent/subagent-in-process-driver/src/index.ts
Normal file
233
packages/subagent/subagent-in-process-driver/src/index.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* 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-in-process-driver
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { foldConsumedWork } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
|
||||
import { SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import { createUserMessage, type ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
appendDelegatedPolicyOverrides,
|
||||
applyChildComposition,
|
||||
assertSubagentMaxDepth,
|
||||
captureDelegatedPolicyOverrides,
|
||||
childSessionMeta,
|
||||
finalAssistantOutput,
|
||||
resolveChildAgentOptions,
|
||||
resolveChildDepth,
|
||||
} from '@deepseek-ai/dsh-subagent'
|
||||
import type {
|
||||
ResolvedSubagentStartRequest,
|
||||
SubagentDescriptorData,
|
||||
SubagentResult,
|
||||
SubagentRun,
|
||||
SubagentStopReason,
|
||||
} from '@deepseek-ai/dsh-subagent'
|
||||
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'
|
||||
// A pre-step rejection discarded the claimed prompt: the task was
|
||||
// declined, and the caller must not read the run as done.
|
||||
case 'blocked':
|
||||
return 'refusal'
|
||||
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 inherited = captureDelegatedPolicyOverrides(parent)
|
||||
|
||||
let structured: StructuredAttachment | undefined
|
||||
const setup = (childCtx: Context): void => {
|
||||
appendDelegatedPolicyOverrides((childCtx.agent as Agent).session, inherited)
|
||||
applyChildComposition(childCtx, parent, {
|
||||
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)
|
||||
// `droppedUnrun` is deliberately unread: a one-shot prompt is claimed by its
|
||||
// awaited first turn almost immediately, and the owner's own teardown is the
|
||||
// `cancelled` flag below. A cancellation with no accounting turn resolves
|
||||
// `error` through `toStopReason(undefined)`, which never overstates success.
|
||||
const lastEnd = foldConsumedWork(own).end
|
||||
// The seam's canonical selection rule; a partial answer survives cancel and truncation.
|
||||
const output: ContentBlock[] = finalAssistantOutput(own) ?? []
|
||||
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 }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-in-process-driver`.
|
||||
* @module @deepseek-ai/dsh-subagent-in-process-driver/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-in-process-driver'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'subagent-in-process-driver-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
|
||||
* beyond contracts enforced at its owning seam.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
142
packages/subagent/subagent-in-process-driver/src/structured.ts
Normal file
142
packages/subagent/subagent-in-process-driver/src/structured.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Child-scoped structured-output tool, prompt instruction, terminal guard, and authoritative
|
||||
* result capture for in-process subagents. Each child registers its real schema on its own
|
||||
* scope, so concurrent runs do not interact and disposal leaves no global residue. The prompt
|
||||
* contribution is ordinary reconstructed request state.
|
||||
*
|
||||
* Capture commits only after the authoritative `tools/result` succeeds; Code Mode capture also
|
||||
* waits for the enclosing `run_code` result. The terminal result marker and monotonic tool
|
||||
* guard prevent later calls from reopening a completed structured run.
|
||||
* @module @deepseek-ai/dsh-subagent-in-process-driver/structured
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import type { ToolExecution, ToolRunContext } from '@deepseek-ai/dsh-tools'
|
||||
import { ToolArgsError, validateJsonSchemaValue, type ObjectJsonSchema } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
/** The model-facing tool name a structured child must call to finish. */
|
||||
export const STRUCTURED_OUTPUT_TOOL = 'structured_output'
|
||||
|
||||
/**
|
||||
* The instruction registered as the child's trailing (order-190, the end of
|
||||
* the tool-guidance band) scoped prompt section: the demand travels with the
|
||||
* tool, as ordinary prompt state of exactly one agent.
|
||||
*/
|
||||
export const STRUCTURED_OUTPUT_INSTRUCTION
|
||||
= 'When you have your final answer, you MUST report it by calling the '
|
||||
+ `\`${STRUCTURED_OUTPUT_TOOL}\` tool with arguments matching its parameter schema exactly. `
|
||||
+ 'Do not finish with a plain text answer: only the tool call counts as your result.'
|
||||
|
||||
/** One structured run's live handle: read the captured value once the child settles. */
|
||||
export interface StructuredAttachment {
|
||||
/**
|
||||
* The captured value, once the child called the tool with valid arguments
|
||||
* and the authoritative final tool result accepted that call.
|
||||
* @returns the committed value, or undefined while none was accepted.
|
||||
*/
|
||||
captured(): { value: unknown } | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the scoped capture tool, instruction, and enforcement to a child during
|
||||
* its creation window. Child disposal removes every registration.
|
||||
* @param childCtx - the child agent's scope context (`setup`'s argument).
|
||||
* @param schema - the trusted, already-asserted schema subset to enforce (see
|
||||
* `assertObjectJsonSchema` in dsh-tools).
|
||||
* @returns the attachment handle (read `captured()` after the child settles).
|
||||
*/
|
||||
export function attachStructuredRuntime(childCtx: Context, schema: ObjectJsonSchema): StructuredAttachment {
|
||||
/**
|
||||
* Validated values staged by the capture tool body, awaiting THEIR OWN
|
||||
* authoritative `tools/result` notification. The execution object's identity
|
||||
* uniquely identifies a trip through the pipeline: adapter call ids may
|
||||
* repeat across steps, but another execution can never reach this WeakMap
|
||||
* entry. This is distinct from the opaque `ToolExecutionToken` used to
|
||||
* correlate nested transports. The final notification always deletes its own
|
||||
* stage, whether the result succeeded or failed.
|
||||
*/
|
||||
const staged = new WeakMap<ToolExecution, { value: unknown }>()
|
||||
/** Successful nested capture waiting for its enclosing transport to commit. */
|
||||
let pending: { parent: ToolExecution['token']; value: unknown } | undefined
|
||||
let captured: { value: unknown } | undefined
|
||||
|
||||
const schemaEntry: ToolSchema = {
|
||||
name: STRUCTURED_OUTPUT_TOOL,
|
||||
description:
|
||||
'Report your final structured result. Call this exactly once, when your answer is complete; '
|
||||
+ 'the arguments must match this tool\'s parameter schema exactly.',
|
||||
// ToolSchema.parameters is the wire-level JSON Schema object; the
|
||||
// asserted subset type is structurally exactly that.
|
||||
parameters: schema as unknown as Record<string, unknown>,
|
||||
}
|
||||
|
||||
childCtx.tools.register({
|
||||
...schemaEntry,
|
||||
output: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: { recorded: { type: 'boolean', const: true } },
|
||||
required: ['recorded'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
render: () => [{ type: 'text', text: 'Structured output recorded.' }],
|
||||
},
|
||||
execute(args: unknown, exec: ToolRunContext): Promise<{ recorded: true }> {
|
||||
const violations = validateJsonSchemaValue(schema, args)
|
||||
// ToolArgsError → isError result with INVALID_ARGS: the model retries
|
||||
// within the same turn, exactly like a schema-validated defineTool call.
|
||||
if (violations.length > 0) throw new ToolArgsError(violations)
|
||||
// Two-phase commit, keyed by THIS execution: later transformable
|
||||
// waterfalls may still turn the success into an error. ToolRuntime has
|
||||
// already frozen model-bound arguments at the actual input boundary.
|
||||
staged.set(exec, { value: args })
|
||||
exec.concludeTurn()
|
||||
return Promise.resolve({ recorded: true })
|
||||
},
|
||||
})
|
||||
|
||||
childCtx.systemPrompt.section({
|
||||
name: `tool:${STRUCTURED_OUTPUT_TOOL}`,
|
||||
order: 190,
|
||||
text: STRUCTURED_OUTPUT_INSTRUCTION,
|
||||
})
|
||||
|
||||
// Terminal WITHIN the step. Guards run after the whole pre-execute
|
||||
// waterfall and compose monotonically (deny or abstain, never allow), so a
|
||||
// later prepended listener cannot resurrect dispatch. Calls that precede
|
||||
// capture in the same response remain untouched.
|
||||
childCtx.tools.guard(exec => captured === undefined && pending === undefined
|
||||
? undefined
|
||||
: `structured output already recorded: the run is complete, so \`${exec.name}\` is not executed`)
|
||||
|
||||
// The capture COMMIT observes the immutable, authoritative result after the
|
||||
// complete pipeline and outer error normalization. This notification cannot
|
||||
// transform the outcome, so there is no wrapper outside the commit verdict.
|
||||
childCtx.on('tools/result', function (this: unknown, exec, result) {
|
||||
if (exec.name === STRUCTURED_OUTPUT_TOOL) {
|
||||
const entry = staged.get(exec)
|
||||
if (entry === undefined) return
|
||||
staged.delete(exec)
|
||||
if (result.isError) return
|
||||
if (exec.parent === undefined) {
|
||||
/* v8 ignore else -- sequential agent-loop dispatch lets the guard block every later supported call */
|
||||
if (captured === undefined) captured = { value: entry.value }
|
||||
} else {
|
||||
/* v8 ignore else -- Code Mode serializes sub-dispatches, so the guard blocks every later supported call */
|
||||
if (captured === undefined && pending === undefined) {
|
||||
pending = { parent: exec.parent, value: entry.value }
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (pending?.parent !== exec.token) return
|
||||
const entry = pending
|
||||
pending = undefined
|
||||
if (result.isError) return
|
||||
/* v8 ignore else -- Code Mode serializes outer executions, so the guard blocks every later supported call */
|
||||
if (captured === undefined) captured = { value: entry.value }
|
||||
})
|
||||
|
||||
return { captured: () => captured }
|
||||
}
|
||||
Reference in New Issue
Block a user