|
| 1 | +import { |
| 2 | + createFileJournalStore, |
| 3 | + type AgentRunParams, |
| 4 | + type AgentRunResult, |
| 5 | + type HostHandle, |
| 6 | + type ProgressEvent, |
| 7 | + type WorkflowHostContext, |
| 8 | + type WorkflowPorts, |
| 9 | +} from '@claude-code-best/workflow-engine' |
| 10 | +import { getCwd } from '../utils/cwd.js' |
| 11 | +import { logForDebugging } from '../utils/debug.js' |
| 12 | +import { getProjectRoot } from '../bootstrap/state.js' |
| 13 | +import { logEvent } from '../services/analytics/index.js' |
| 14 | +import { assembleToolPool } from '../tools.js' |
| 15 | +import { finalizeAgentTool } from '@claude-code-best/builtin-tools/tools/AgentTool/agentToolUtils.js' |
| 16 | +import { runAgent } from '@claude-code-best/builtin-tools/tools/AgentTool/runAgent.js' |
| 17 | +import { |
| 18 | + isBuiltInAgent, |
| 19 | + type AgentDefinition, |
| 20 | + type BuiltInAgentDefinition, |
| 21 | +} from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js' |
| 22 | +import { createUserMessage, extractTextContent } from '../utils/messages.js' |
| 23 | +import { createAgentId } from '../utils/uuid.js' |
| 24 | +import type { Message } from '../types/message.js' |
| 25 | +import type { AppState } from '../state/AppState.js' |
| 26 | +import type { SetAppState } from '../Task.js' |
| 27 | +import { |
| 28 | + completeWorkflowTask, |
| 29 | + failWorkflowTask, |
| 30 | + killWorkflowTask, |
| 31 | + registerLocalWorkflowTask, |
| 32 | +} from '../tasks/LocalWorkflowTask/LocalWorkflowTask.js' |
| 33 | +import { |
| 34 | + makeHostHandle, |
| 35 | + readHostBundle, |
| 36 | + type WorkflowHostBundle, |
| 37 | +} from './hostHandle.js' |
| 38 | +import { applyProgressEvent } from './progressStore.js' |
| 39 | +import type { ToolUseContext } from '../Tool.js' |
| 40 | + |
| 41 | +/** workflow 子 agent 的缺省定义(通用研究/执行 agent)。 */ |
| 42 | +const WORKFLOW_AGENT: BuiltInAgentDefinition = { |
| 43 | + agentType: 'workflow-worker', |
| 44 | + whenToUse: 'workflow 脚本内 agent() 钩子派发的子任务', |
| 45 | + tools: ['*'], |
| 46 | + source: 'built-in', |
| 47 | + baseDir: 'built-in', |
| 48 | + getSystemPrompt: () => |
| 49 | + 'You are a workflow sub-agent. Complete the task concisely; your final text is the return value relayed to the workflow.', |
| 50 | +} |
| 51 | + |
| 52 | +type RunBinding = { |
| 53 | + runId: string |
| 54 | + taskId: string |
| 55 | + setAppState: SetAppState |
| 56 | + abortController: AbortController |
| 57 | + workflowName: string |
| 58 | +} |
| 59 | + |
| 60 | +/** 每次工具调用从 toolUseContext 构造 WorkflowHostContext。 */ |
| 61 | +function makeHostFactory(): WorkflowPorts['hostFactory'] { |
| 62 | + return ({ context, canUseTool, parentMessage }): WorkflowHostContext => { |
| 63 | + const ctx = context as ToolUseContext |
| 64 | + return { |
| 65 | + handle: makeHostHandle({ |
| 66 | + toolUseContext: ctx, |
| 67 | + canUseTool: canUseTool as WorkflowHostBundle['canUseTool'], |
| 68 | + parentMessage: parentMessage as WorkflowHostBundle['parentMessage'], |
| 69 | + agentId: ctx.agentId, |
| 70 | + }), |
| 71 | + cwd: getCwd(), |
| 72 | + // v1:无 turn 级预算注入点;engine 支持 budget 但此处传 null |
| 73 | + budgetTotal: null, |
| 74 | + toolUseId: ctx.toolUseId, |
| 75 | + } |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +function resolveAgentDefinition( |
| 80 | + agentType: string | undefined, |
| 81 | + toolUseContext: ToolUseContext, |
| 82 | +): AgentDefinition { |
| 83 | + if (!agentType) return WORKFLOW_AGENT |
| 84 | + const found = toolUseContext.options.agentDefinitions.activeAgents.find( |
| 85 | + a => a.agentType === agentType, |
| 86 | + ) |
| 87 | + return found ?? WORKFLOW_AGENT |
| 88 | +} |
| 89 | + |
| 90 | +async function runWorkflowSubAgent( |
| 91 | + params: AgentRunParams, |
| 92 | + host: HostHandle, |
| 93 | +): Promise<AgentRunResult> { |
| 94 | + const bundle = readHostBundle(host) |
| 95 | + const { toolUseContext, canUseTool } = bundle |
| 96 | + const appState = toolUseContext.getAppState() |
| 97 | + const agentDef = resolveAgentDefinition(params.agentType, toolUseContext) |
| 98 | + const agentId = createAgentId() |
| 99 | + |
| 100 | + const workerPermissionContext = { |
| 101 | + ...appState.toolPermissionContext, |
| 102 | + mode: agentDef.permissionMode ?? 'acceptEdits', |
| 103 | + } |
| 104 | + const workerTools = assembleToolPool( |
| 105 | + workerPermissionContext, |
| 106 | + appState.mcp.tools, |
| 107 | + ) |
| 108 | + |
| 109 | + // schema → 通过 prompt 追加 JSON Schema 指令(非交互模式 StructuredOutput 已启用) |
| 110 | + const promptText = params.schema |
| 111 | + ? `${params.prompt}\n\nYou MUST return your final answer by calling the StructuredOutput tool with a value matching this JSON Schema:\n${JSON.stringify(params.schema)}` |
| 112 | + : params.prompt |
| 113 | + |
| 114 | + const promptMessages = [createUserMessage({ content: promptText })] |
| 115 | + const messages: Message[] = [] |
| 116 | + const startTime = Date.now() |
| 117 | + |
| 118 | + try { |
| 119 | + for await (const msg of runAgent({ |
| 120 | + agentDefinition: agentDef, |
| 121 | + promptMessages, |
| 122 | + toolUseContext, |
| 123 | + canUseTool, |
| 124 | + isAsync: true, |
| 125 | + querySource: toolUseContext.options.querySource ?? 'workflow', |
| 126 | + availableTools: workerTools, |
| 127 | + override: { agentId }, |
| 128 | + ...(params.model ? { model: params.model as never } : {}), |
| 129 | + })) { |
| 130 | + messages.push(msg as Message) |
| 131 | + } |
| 132 | + } catch (e) { |
| 133 | + logForDebugging(`workflow sub-agent error: ${(e as Error).message}`) |
| 134 | + return { kind: 'dead' } |
| 135 | + } |
| 136 | + |
| 137 | + const finalized = finalizeAgentTool(messages, agentId, { |
| 138 | + prompt: params.prompt, |
| 139 | + resolvedAgentModel: toolUseContext.options.mainLoopModel, |
| 140 | + isBuiltInAgent: isBuiltInAgent(agentDef), |
| 141 | + startTime, |
| 142 | + agentType: agentDef.agentType, |
| 143 | + isAsync: true, |
| 144 | + }) |
| 145 | + const outputTokens = |
| 146 | + finalized.usage?.output_tokens ?? finalized.totalTokens ?? 0 |
| 147 | + |
| 148 | + if (params.schema) { |
| 149 | + const structured = extractStructuredOutput(finalized.content) |
| 150 | + if (structured === null) return { kind: 'dead' } |
| 151 | + return { kind: 'ok', output: structured as object, usage: { outputTokens } } |
| 152 | + } |
| 153 | + const text = extractTextContent(finalized.content, '\n') |
| 154 | + return { kind: 'ok', output: text, usage: { outputTokens } } |
| 155 | +} |
| 156 | + |
| 157 | +/** 从 agent 最终消息中提取 StructuredOutput 产出的 JSON 对象;解析失败返回 null。 */ |
| 158 | +function extractStructuredOutput( |
| 159 | + content: Array<{ type: string; text?: string }>, |
| 160 | +): unknown | null { |
| 161 | + for (const block of content) { |
| 162 | + if (block.type === 'text' && block.text) { |
| 163 | + const trimmed = block.text.trim() |
| 164 | + const start = trimmed.indexOf('{') |
| 165 | + const end = trimmed.lastIndexOf('}') |
| 166 | + if (start >= 0 && end > start) { |
| 167 | + try { |
| 168 | + return JSON.parse(trimmed.slice(start, end + 1)) |
| 169 | + } catch { |
| 170 | + // 继续 |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + return null |
| 176 | +} |
| 177 | + |
| 178 | +/** 构造完整端口集。adapter 维护 runId → RunBinding 映射供 progress/kill 路由。 */ |
| 179 | +export function createWorkflowAdapter(): WorkflowPorts { |
| 180 | + const bindings = new Map<string, RunBinding>() |
| 181 | + const runsDir = `${getProjectRoot()}/.claude/workflow-runs` |
| 182 | + |
| 183 | + return { |
| 184 | + hostFactory: makeHostFactory(), |
| 185 | + |
| 186 | + agentRunner: { |
| 187 | + runAgentToResult: runWorkflowSubAgent, |
| 188 | + }, |
| 189 | + |
| 190 | + progressEmitter: { |
| 191 | + emit(event: ProgressEvent) { |
| 192 | + applyProgressEvent(event) |
| 193 | + }, |
| 194 | + }, |
| 195 | + |
| 196 | + taskRegistrar: { |
| 197 | + register(opts, host) { |
| 198 | + const bundle = readHostBundle(host) |
| 199 | + const setAppState = |
| 200 | + bundle.toolUseContext.setAppStateForTasks ?? |
| 201 | + bundle.toolUseContext.setAppState |
| 202 | + const abortController = new AbortController() |
| 203 | + const taskId = registerLocalWorkflowTask(setAppState, { |
| 204 | + description: opts.summary ?? opts.workflowName, |
| 205 | + workflowName: opts.workflowName, |
| 206 | + workflowFile: opts.workflowFile ?? '', |
| 207 | + summary: opts.summary, |
| 208 | + ...(opts.toolUseId ? { toolUseId: opts.toolUseId } : {}), |
| 209 | + abortController, |
| 210 | + }) |
| 211 | + const runId = opts.runId ?? taskId |
| 212 | + bindings.set(runId, { |
| 213 | + runId, |
| 214 | + taskId, |
| 215 | + setAppState, |
| 216 | + abortController, |
| 217 | + workflowName: opts.workflowName, |
| 218 | + }) |
| 219 | + logEvent('tengu_workflow_started', {}) |
| 220 | + return { runId, signal: abortController.signal } |
| 221 | + }, |
| 222 | + |
| 223 | + complete(runId, summary) { |
| 224 | + const b = bindings.get(runId) |
| 225 | + if (!b) return |
| 226 | + completeWorkflowTask(b.taskId, b.setAppState) |
| 227 | + logForDebugging(`workflow ${runId} completed: ${summary ?? ''}`) |
| 228 | + }, |
| 229 | + |
| 230 | + fail(runId, error) { |
| 231 | + const b = bindings.get(runId) |
| 232 | + if (!b) return |
| 233 | + failWorkflowTask(b.taskId, b.setAppState) |
| 234 | + logForDebugging(`workflow ${runId} failed: ${error}`) |
| 235 | + }, |
| 236 | + |
| 237 | + kill(runId) { |
| 238 | + const b = bindings.get(runId) |
| 239 | + if (!b) return |
| 240 | + killWorkflowTask(b.taskId, b.setAppState) |
| 241 | + }, |
| 242 | + |
| 243 | + pendingAction(runId) { |
| 244 | + // v1:skip/retry UI 未接线;从 task 状态读 pendingAgentAction(若有)。 |
| 245 | + const b = bindings.get(runId) |
| 246 | + if (!b) return null |
| 247 | + return null |
| 248 | + }, |
| 249 | + }, |
| 250 | + |
| 251 | + journalStore: createFileJournalStore(runsDir), |
| 252 | + |
| 253 | + permissionGate: { |
| 254 | + // 引擎实际用 ctx.signal(register 返回的 AbortController)判定 abort。 |
| 255 | + isAborted: () => false, |
| 256 | + }, |
| 257 | + |
| 258 | + logger: { |
| 259 | + debug: msg => logForDebugging(msg), |
| 260 | + event: name => logForDebugging(`workflow event: ${name}`), |
| 261 | + }, |
| 262 | + } |
| 263 | +} |
| 264 | + |
| 265 | +// 抑制未使用类型导入(AppState 用于 RunBinding.setAppState 类型推导) |
| 266 | +export type _AppStateUsed = AppState |
0 commit comments