Carved out of #170 per review feedback — the foundation the workflow tool builds on, now standing alone on master: - dsh-tools: the structured-output JSON Schema subset (StructuredOutputSchema, assertSupportedOutputSchema, validateStructuredValue) — rejects loud outside the enforced subset, listing every violation - dsh-subagent: SubagentStartRequest.outputSchema / SubagentResult.structured become a real capability; the service rejects a schema'd request whose provider lacks it - dsh-subagent-inprocess: the shared structured runtime — one global structured_output capture tool, a prepend final-assembly listener that strips the placeholder for plain agents and swaps in the run's own schema (plus the calling instruction as a trailing section) for structured children, an agent/turn-continuation veto once captured, and the capture/nudge loop in the run driver (structuredNudgeRetries, cancellation honored mid-nudge); lifetime refcounted by backends and live runs - subagent-spawn / subagent-fork flip outputSchema: true One deliberate divergence from the #170 revision: the backends do NOT add 'tools' to their plugin inject. Doing so deferred their apply past the todo plugin, and the delegation tool mirrors provider lifecycle — so the model-visible tool order of every existing prompt changed, invalidating every recorded snapshot fixture. The runtime now gates its capture-tool registration on tools availability itself (sync when live, a scoped inject fiber when the Loader starts the backend first), keeping this PR byte-invisible to existing transcripts: all 35 snapshot scenarios pass against master's fixtures unchanged.
90 lines
3.9 KiB
TypeScript
90 lines
3.9 KiB
TypeScript
/**
|
|
* The in-process SPAWN subagent backend: registers a {@link SubagentProvider}
|
|
* on `ctx.subagents` that runs each child as a FRESH child {@link Agent} on the
|
|
* same cordis context (its own session, own system prompt, zero parent
|
|
* context). The cheapest transport, reusing the agent factory's quiescent
|
|
* teardown.
|
|
*
|
|
* The run mechanics live in `@deepseek-ai/dsh-subagent-inprocess`
|
|
* ({@link startInProcessRun}); this backend just passes NO seed (a fresh
|
|
* child). The fork backend is an independent peer over the same driver.
|
|
*
|
|
* Structured output (`outputSchema`) is supported via the driver's shared
|
|
* structured runtime: the backend acquires it for its plugin lifetime (so the
|
|
* capture tool and request-shaping listeners exist before any run), and each
|
|
* structured run holds its own acquisition until it settles.
|
|
*
|
|
* Plugin export shape: named `name`/`inject`/`Config`/`apply`, NO default.
|
|
*
|
|
* @module @deepseek-ai/dsh-subagent-spawn
|
|
*/
|
|
|
|
import type { Context } from 'cordis'
|
|
import z from 'schemastery'
|
|
import type { SubagentCapabilities, SubagentProvider, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
|
|
import { acquireStructuredRuntime, startInProcessRun } from '@deepseek-ai/dsh-subagent-inprocess'
|
|
|
|
export const name = 'subagent-spawn'
|
|
// `tools` is deliberately NOT injected: the structured runtime gates its own
|
|
// capture-tool registration on `tools` availability internally, so this
|
|
// backend's apply timing — and with it the provider-mirroring delegation
|
|
// tool's position in the model-visible tool list — stays what it was before
|
|
// structured output existed.
|
|
export const inject = ['subagents', 'agents']
|
|
|
|
/** Config: the registry name to register the provider under, plus structured-run tuning. */
|
|
export interface Config {
|
|
/** Provider name on `ctx.subagents` (default `spawn`). */
|
|
providerName: string
|
|
/**
|
|
* How many times a structured run re-prompts a child that finished cleanly
|
|
* without calling `structured_output` before giving up (default 1).
|
|
*/
|
|
structuredNudgeRetries: number
|
|
}
|
|
|
|
export const Config: z<Config> = z.object({
|
|
providerName: z.string().default('spawn'),
|
|
structuredNudgeRetries: z.natural().default(1),
|
|
})
|
|
|
|
/**
|
|
* The spawn provider. Supports `depthLimit` (it constructs the child, so it can
|
|
* enforce a recursion cap) and `outputSchema` (via the shared in-process
|
|
* structured runtime); NOT `toolFilter` in this cut — a request that needs it
|
|
* is rejected by the service before `start` runs.
|
|
*/
|
|
class SpawnProvider implements SubagentProvider {
|
|
readonly capabilities: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: false }
|
|
// Context contract: a spawned child starts fresh — it never sees the parent conversation.
|
|
readonly inheritsParentContext = false
|
|
|
|
constructor(
|
|
readonly name: string,
|
|
private readonly ctx: Context,
|
|
private readonly structuredNudgeRetries: number,
|
|
) {}
|
|
|
|
start(request: SubagentStartRequest) {
|
|
// Fresh child: no seed. The shared driver mints ids, stamps cwd/lineage/
|
|
// depth, drives the one-shot (including the structured capture/nudge loop
|
|
// when the request carries an outputSchema), and maps the result.
|
|
return startInProcessRun(this.ctx, request, {
|
|
providerName: this.name,
|
|
structuredNudgeRetries: this.structuredNudgeRetries,
|
|
})
|
|
}
|
|
}
|
|
|
|
export function apply(ctx: Context, config: Config): void {
|
|
// Hold the structured runtime for the plugin's lifetime, so the capture tool
|
|
// and its request-shaping listeners are registered before the first
|
|
// structured run and torn down when the last backend unloads (live runs hold
|
|
// their own acquisitions, so an unload mid-run cannot strand a child).
|
|
ctx.effect(() => {
|
|
const acquisition = acquireStructuredRuntime(ctx)
|
|
return () => { acquisition.release() }
|
|
}, 'subagent-spawn structured runtime')
|
|
ctx.subagents.registerProvider(new SpawnProvider(config.providerName, ctx, config.structuredNudgeRetries))
|
|
}
|