Files
claude-code-haha/src/tools/WorkflowTool/createWorkflowCommand.ts
T
程序员阿江(Relakkes) 3ce7c67822 feat(workflows): dynamic workflow orchestration end to end
A workflow is a JS script the model writes in the moment and hands to the
Workflow tool, which runs it in a locked-down `node:vm` and orchestrates
subagents through `agent()`/`parallel()`/`pipeline()`/`phase()`. Saving one
as a `/name` command is the secondary path; the inline script is the point.

Runtime: cross-realm value marshalling so a script cannot reach the host
`Function`, determinism guards on `Date.now()`/`Math.random()` (they would
make a resume replay diverge), a FIFO concurrency gate, and a journal that
lets an interrupted run resume from its longest unchanged prefix.

Desktop: the run shows up as a `workflow` section in the existing activity
panel — phases as headings, their agents beneath. A workflow agent is an
ordinary subagent run by the same runner, so its row opens the existing
subagent page rather than a parallel viewer of its own; that needed a
`by-agent` lookup, because these agents have no parent `Agent` tool call to
key off. Finished runs are rebuilt from the per-agent sidecars when a
session is reopened, since the live progress stream does not outlive the
process.
2026-08-10 00:17:03 +08:00

68 lines
2.6 KiB
TypeScript

import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import type { Command } from '../../types/command.js'
import { loadWorkflows } from '../../utils/workflows/discovery.js'
import { areWorkflowsEnabled } from '../../utils/workflows/enabled.js'
import type { WorkflowDefinition } from '../../utils/workflows/types.js'
import { WORKFLOW_TOOL_NAME } from './constants.js'
/**
* Turn every discovered workflow into a `/<name>` command.
*
* The command is a prompt, not a direct tool call: whatever the user typed
* after the name has to become the `args` value, and only the model can decide
* whether "issues 1024, 1025" is a list of numbers or a sentence. The prompt
* pins everything else so the model's only job is that conversion.
*/
export async function getWorkflowCommands(cwd?: string): Promise<Command[]> {
if (!areWorkflowsEnabled()) return []
const workflows = await loadWorkflows(cwd)
return workflows.map(workflow => createWorkflowCommand(workflow))
}
export function createWorkflowCommand(workflow: WorkflowDefinition): Command {
return {
type: 'prompt',
name: workflow.name,
description: workflow.description,
progressMessage: `running the ${workflow.name} workflow`,
contentLength: workflow.description.length + 200,
argNames: ['args'],
source: workflowCommandSource(workflow),
isEnabled: () => areWorkflowsEnabled(),
userFacingName: () => workflow.name,
async getPromptForCommand(args: string): Promise<ContentBlockParam[]> {
const argsLine =
args.trim() === ''
? 'Pass no `args`.'
: `Convert the following invocation text into the \`args\` value and pass it: ${JSON.stringify(args.trim())}. ` +
'If it is a list of items, pass a real JSON array, not a string.'
return [
{
type: 'text',
text:
`Run the saved workflow "${workflow.name}" by calling ${WORKFLOW_TOOL_NAME} exactly once with ` +
`{ name: "${workflow.name}" }. ${argsLine}\n` +
'Do not write a new script and do not do the work yourself — the saved workflow already ' +
'contains the orchestration. After the tool returns, end your turn; the result arrives as a ' +
'task notification.',
},
]
},
} as Command
}
function workflowCommandSource(
workflow: WorkflowDefinition,
): 'builtin' | 'userSettings' | 'projectSettings' | 'plugin' {
switch (workflow.source) {
case 'built-in':
return 'builtin'
case 'plugin':
return 'plugin'
case 'projectSettings':
return 'projectSettings'
default:
return 'userSettings'
}
}