/** * @deepseek-ai/dsh-headless — one-shot direct Agent driver. The bundle patch * rides over dsh-base without Host, HTTP, or browser plugins; this runner * creates one Agent through the core registry, drives the task to quiescence, * flushes its Session, prints the final assistant text, and exits. * * @module @deepseek-ai/dsh-headless */ import { randomUUID } from 'node:crypto' import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { installModelSelection } from '@deepseek-ai/dsh-agent' import type { ModelSelectionRef } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-agent-default-model' import { createUserMessage } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' // Empty type import carries the loader Context merge for the settlement await. import type {} from '@deepseek-ai/cordis-plugin-loader' /** Stable Cordis plugin name. */ export const name = 'headless-runner' /** Core services required before the one-shot turn can start. */ export const inject = ['agentDefaultModel', 'agents', 'sessions'] /** Plugin config: the task resolved from this app's injected provider service. */ export interface Config { /** The prompt text for the single run. */ task: string } export const Config: z = z.object({ task: z.string().required(), }) /** Outcome of one owned run interval. */ interface RunOutcome { text: string reason: SessionEvent<'turn/end'>['data']['reason'] | undefined } /** * Process-facing effects of one run, injectable for tests. The launcher owns * bounded tree shutdown and wires `exit()` to it. */ export interface HeadlessIo { stdout: { write(chunk: string): unknown } stderr: { write(chunk: string): unknown } /** Request process exit with `code` after the tree disposes. */ exit(code: number): void } declare module '@deepseek-ai/cordis' { interface Context { /** Process-facing effects provided before the headless tree mounts. */ headlessIo?: HeadlessIo } } /** Aggregate the last assistant text and turn outcome in one owned interval. */ function summarize(events: readonly SessionEvent[], firstSeq: number): RunOutcome { let started = false let text = '' let reason: SessionEvent<'turn/end'>['data']['reason'] | undefined for (const event of events) { if (event.seq < firstSeq) continue if (event.type === 'turn/start') { started = true continue } if (!started) continue if (event.type === 'assistant/message') { const joined = event.data.message.content .filter(block => block.type === 'text') .map(block => block.text) .join('') if (joined !== '') text = joined } if (event.type === 'turn/end') reason = event.data.reason } return { text, reason } } /** Report an unexpected direct-driver failure and request a failing exit. */ function fail(io: HeadlessIo, error: unknown): void { io.stderr.write(`dsh: ${error instanceof Error ? error.message : String(error)}\n`) io.exit(1) } /** * Run one task through a freshly created Agent and request process exit. * @param ctx - plugin context carrying the Agent, default model, Session, and launcher IO services. * @param task - one-shot task text. * @param io - process-facing effects. */ async function run(ctx: Context, task: string, io: HeadlessIo): Promise { // Loader siblings mount concurrently. Await the complete application before // creating an Agent so its scoped tools and adapters are not half-composed. await ctx.get('loader')?.await() const agents = ctx.get('agents') const defaultModel = ctx.get('agentDefaultModel') const sessions = ctx.get('sessions') // Early process shutdown can dispose the tree while settlement is pending. if (agents === undefined || defaultModel === undefined || sessions === undefined) return const selection = defaultModel.currentSelection() const { agent } = await agents.create({ sessionId: SessionId(`session-${randomUUID()}`), meta: { cwd: process.cwd() }, agentOptions: { provider: selection.provider, model: selection.model }, setup: (agentCtx) => { const selected: ModelSelectionRef = { current: selection, assembled: undefined } installModelSelection(agentCtx, selected) }, }) await agent.whenIdle() const firstSeq = agent.session.seq agent.followup(createUserMessage({ content: [{ type: 'text', text: task }], source: { kind: 'user' }, })) await agent.whenIdle() await sessions.flush(agent.session) const outcome = summarize(agent.session.events, firstSeq) io.stdout.write(outcome.text + '\n') if (outcome.reason?.kind === 'error') { io.stderr.write(`dsh: ${outcome.reason.error.code}: ${outcome.reason.error.message}\n`) } io.exit(outcome.reason?.kind === 'completed' ? 0 : 1) } /** * Mount the one-shot direct driver. * @param ctx - plugin context carrying core services and the launcher-owned IO seam. * @param config - validated task config. */ export function apply(ctx: Context, config: Config): void { const io = ctx.headlessIo if (io === undefined) { throw new Error('headless-runner: the launcher must provide ctx.headlessIo before the tree mounts') } void run(ctx, config.task, io).catch((error: unknown) => { fail(io, error) }) }