2026-07-05 13:29:35 +08:00
/**
2026-07-12 03:36:43 +08:00
* The model-facing `workflow` tool: run a JavaScript orchestration script that fans out
* subagents, and return the script's final value. Pure schema + lifecycle shaping — script
* parsing, execution, caps, and cancellation live behind `ctx.workflows`
* (`@deepseek-ai/dsh-workflow`), so a hardened engine swaps in without touching what the model
2026-07-13 23:27:00 +08:00
* sees. Execution awaits `run.result` and always disposes the run; non-completed reasons become tool
* errors, and background collection remains deferred. Presentation is an args-only generic card
* titled from `meta.name`. Explicit-ask usage guidance is registered as the tool's own prompt
* section rather than deployment persona prose.
2026-07-05 13:29:35 +08:00
* @module @deepseek-ai/dsh-tool-workflow
*/
import type { Context } from 'cordis'
import z from 'schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { ToolCallView , ToolResultView } from '@deepseek-ai/dsh-tools'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
2026-07-21 03:08:35 +08:00
import type { JsonValue } from '@deepseek-ai/dsh-session'
2026-07-05 13:29:35 +08:00
import type { WorkflowResult , WorkflowRun } from '@deepseek-ai/dsh-workflow'
2026-07-06 03:14:07 +08:00
// Declaration merge only: makes ctx.systemPrompt visible for the section registration.
import type { } from '@deepseek-ai/dsh-system-prompt'
2026-07-05 13:29:35 +08:00
export const name = 'tool-workflow'
2026-07-06 03:14:07 +08:00
export const inject = [ 'tools' , 'workflows' , 'systemPrompt' ]
2026-07-05 13:29:35 +08:00
/** Config: the model-facing tool name plus result rendering caps. */
export interface Config {
/** The model-facing tool name to register (default `workflow`). */
toolName? : string
/** Rendered-result ceiling, in characters: a longer JSON value is truncated with a notice (default 50000). */
maxResultChars? : number
}
export const Config : z < Config > = z . object ( {
toolName : z.string ( ) . default ( 'workflow' ) ,
maxResultChars : z.natural ( ) . min ( 1 ) . default ( 50 _000 ) ,
} )
2026-07-09 18:16:51 +08:00
type ResolvedConfig = Required < Config >
2026-07-05 13:29:35 +08:00
/**
* The script-authoring contract, embedded in the tool description. This IS the
2026-07-08 00:51:19 +08:00
* model-facing spec: the meta block, the hooks and their exact semantics, and
* the supported schema subset.
2026-07-05 13:29:35 +08:00
*/
const DESCRIPTION = ` Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.
2026-07-14 21:57:52 +08:00
The workflow's identity rides the \` meta \` parameter as JSON: required \` name \` (short kebab-case) and \` description \` strings, optional \` whenToUse \` string and \` phases \` array ( \` {title, detail?, provider?, model?} \` ). The \` script \` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO \` export const meta \` statement — meta is a parameter, not code), running with top-level await; end with \` return <value> \` — the value must be JSON-serializable and is this tool's result.
2026-07-05 13:29:35 +08:00
Script-body hooks:
2026-07-21 17:44:46 +08:00
- \` agent(prompt, opts?): Promise<any> \` — run one subagent to completion. Without \` opts.schema \` it resolves to the child's final text; with \` opts.schema \` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves \` null \` when the child fails (filter with \` .filter(Boolean) \` ). Other opts: \` label \` (display), \` phase \` (progress group), and independent \` provider \` / \` model \` LLM target overrides (either may be provided alone). Anything else ( \` effort \` / \` isolation \` / \` agentType \` ) is rejected loudly.
2026-07-05 13:29:35 +08:00
- \` pipeline(items, ...stages): Promise<any[]> \` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives \` (prev, item, index) \` . An ordinary stage throw drops that ITEM to \` null \` and skips its remaining stages.
- \` parallel(thunks): Promise<any[]> \` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to \` null \` .
- \` phase(title) \` — start a progress phase; \` log(message) \` — narrate progress; \` args \` — the tool call's \` args \` input, verbatim.
Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item \` null \` .
2026-07-08 00:51:19 +08:00
Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. `
2026-07-05 13:29:35 +08:00
2026-07-09 20:09:10 +08:00
type WorkflowCallArgs = {
script : string
2026-07-14 21:57:52 +08:00
meta : {
name : string
description : string
whenToUse? : string
phases ? : { title : string ; detail? : string ; provider? : string ; model? : string } [ ]
}
2026-07-09 20:09:10 +08:00
args? : Record < string , unknown >
2026-07-05 13:29:35 +08:00
}
2026-07-09 20:09:10 +08:00
/** The pending-state card: a generic card titled by the workflow's meta name. */
2026-07-05 13:29:35 +08:00
function presentWorkflowCall ( args : WorkflowCallArgs ) : ToolCallView {
return {
card : 'generic' ,
2026-07-09 20:09:10 +08:00
title : ` workflow: ${ args . meta . name } ` ,
2026-07-05 13:29:35 +08:00
rawInput : args.script ,
}
}
/** The completed-state card: keep the pending title; render the result content as-is. */
function presentWorkflowResult ( args : WorkflowCallArgs , result : { content : ContentBlock [ ] ; isError : boolean } ) : ToolResultView {
void args
void result
return { card : 'generic' }
}
/** A non-`completed` stop reason means the script did not finish cleanly. */
function stopReasonError ( result : WorkflowResult ) : string | undefined {
switch ( result . stopReason ) {
case 'completed' :
return undefined
case 'cancelled' :
return ` workflow run was cancelled ${ result . error !== undefined ? ` ( ${ result . error } ) ` : '' } `
case 'error' :
return ` workflow run failed: ${ result . error ? ? 'unknown error' } `
/* v8 ignore start -- defensive: WorkflowStopReason is a closed union, exhaustive by construction; a future variant fails here loudly */
default :
return ` workflow run ended abnormally ( ${ String ( result . stopReason satisfies never ) } ) `
/* v8 ignore stop */
}
}
/** Render the run's outcome text: the meta name, agent count, and the JSON value (capped). */
2026-07-21 03:08:35 +08:00
function renderResult ( name : string , agentsStarted : number , value : JsonValue , maxChars : number ) : string {
2026-07-05 13:29:35 +08:00
// The engine returns JSON data (null for a valueless script), so stringify never yields undefined.
2026-07-21 03:08:35 +08:00
const rendered = JSON . stringify ( value , null , 2 )
2026-07-05 13:29:35 +08:00
const clipped = rendered . length > maxChars
? ` ${ rendered . slice ( 0 , maxChars ) } \ n… [truncated: ${ rendered . length - maxChars } more characters] `
: rendered
2026-07-21 03:08:35 +08:00
return ` workflow " ${ name } " completed ( ${ agentsStarted } agent ${ agentsStarted === 1 ? '' : 's' } ). \ nReturn value: \ n ${ clipped } `
2026-07-05 13:29:35 +08:00
}
export function apply ( ctx : Context , config : Config ) : void {
2026-07-09 18:16:51 +08:00
// schemastery (the exported Config schema) has already filled the defaulted
// fields; the assertion records that resolution, not a hidden fallback.
const { toolName , maxResultChars } = config as ResolvedConfig
2026-07-06 03:14:07 +08:00
// Usage policy ships with the tool (the master convention: tool guidance
// lives in tool plugins as prompt sections, not in the deployment persona).
ctx . systemPrompt . section ( {
name : ` tool: ${ toolName } ` ,
order : 115 ,
text : ` Use the ${ toolName } tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls. ` ,
} )
2026-07-05 13:29:35 +08:00
ctx . tools . register ( defineTool ( {
2026-07-06 03:14:07 +08:00
name : toolName ,
2026-07-05 13:29:35 +08:00
description : DESCRIPTION ,
parameters : {
script : {
type : 'string' ,
required : true ,
2026-07-09 20:09:10 +08:00
description : 'The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return <json-value>`).' ,
} ,
meta : {
type : 'object' ,
2026-07-21 01:11:55 +08:00
additionalProperties : true ,
2026-07-09 20:09:10 +08:00
required : true ,
description : 'The workflow identity block (plain JSON — never code).' ,
properties : {
name : { type : 'string' , required : true , description : 'Short kebab-case workflow name.' } ,
description : { type : 'string' , required : true , description : 'One-line description of what the workflow does.' } ,
whenToUse : { type : 'string' , description : 'Optional guidance on when this workflow applies.' } ,
phases : {
type : 'array' ,
description : 'Optional phase declarations matched by phase() calls.' ,
items : {
type : 'object' ,
2026-07-21 01:11:55 +08:00
additionalProperties : true ,
2026-07-09 20:09:10 +08:00
properties : {
title : { type : 'string' , required : true , description : 'The phase title phase() calls match by exact string.' } ,
detail : { type : 'string' , description : 'Optional one-line description of the phase.' } ,
2026-07-14 21:57:52 +08:00
provider : { type : 'string' , description : 'Optional provider override this phase is expected to use.' } ,
2026-07-09 20:09:10 +08:00
model : { type : 'string' , description : 'Optional model override this phase is expected to use.' } ,
} ,
} ,
} ,
} ,
2026-07-05 13:29:35 +08:00
} ,
args : {
type : 'object' ,
2026-07-21 01:11:55 +08:00
additionalProperties : true ,
2026-07-05 13:29:35 +08:00
description : 'Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {"files": [...]}).' ,
} ,
} ,
2026-07-21 03:08:35 +08:00
output : {
schema : {
type : 'object' ,
additionalProperties : false ,
properties : {
runId : { type : 'string' , required : true } ,
agentsStarted : { type : 'integer' , required : true } ,
result : { type : 'json' , required : true } ,
} ,
} ,
render : ( args , value ) = > [ {
type : 'text' ,
text : renderResult ( args . meta . name , value . agentsStarted , value . result , maxResultChars ) ,
} ] ,
} ,
async execute ( args , exec ) {
2026-07-05 13:29:35 +08:00
const parent = exec . agent
if ( ! parent ) {
// The loop sets `exec.agent` for every model-driven call; its absence
// means a non-agent caller invoked the tool directly, which has no
// parent to attribute the children to. Fail loud rather than guess.
throw new Error ( 'workflow tool requires a calling agent (exec.agent was undefined)' )
}
2026-07-09 20:09:10 +08:00
// Meta/body validation failures (META_INVALID/SCRIPT_PARSE) throw
// synchronously here and become isError results via the registry — the
// model sees the violation list and can correct the call.
2026-07-05 13:29:35 +08:00
const run : WorkflowRun = ctx . workflows . start ( {
script : args.script ,
2026-07-09 20:09:10 +08:00
meta : args.meta ,
2026-07-05 13:29:35 +08:00
. . . args . args !== undefined ? { args : args.args } : { } ,
parent ,
2026-07-19 23:38:54 +08:00
signal : exec.signal ,
2026-07-05 13:29:35 +08:00
} )
2026-07-12 03:36:43 +08:00
// Bridge the tool's abort signal to the run: if the parent step is aborted while the
2026-07-13 23:27:00 +08:00
// script is in flight, cancel the whole run. The signal also enters the engine directly, but
// this local bridge preserves the tool contract even if an implementation ignores it.
2026-07-05 13:29:35 +08:00
const onAbort = ( ) : void = > { run . cancel ( 'parent step aborted' ) }
2026-07-19 23:38:54 +08:00
exec . signal . addEventListener ( 'abort' , onAbort , { once : true } )
2026-07-05 13:29:35 +08:00
try {
const result = await run . result
const error = stopReasonError ( result )
if ( error !== undefined ) {
// Map a non-clean finish to an isError result (the registry turns a
// throw into an isError). Report the reason, not partial output.
throw new Error ( error )
}
2026-07-21 03:08:35 +08:00
return {
runId : run.id ,
agentsStarted : result.agentsStarted ,
result : result.value as JsonValue ,
}
2026-07-05 13:29:35 +08:00
} finally {
2026-07-19 23:38:54 +08:00
exec . signal . removeEventListener ( 'abort' , onAbort )
2026-07-05 13:29:35 +08:00
// Always reach run quiescence — never leak a live script or children.
await run . dispose ( )
}
} ,
presentCall : args = > presentWorkflowCall ( args ) ,
presentResult : ( args , result ) = > presentWorkflowResult ( args , result ) ,
} ) )
}