Skip to content

Commit 03f2764

Browse files
feat(workflow): core adapter + wiring, register in tools.ts (Phase 4)
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 09c436d commit 03f2764

5 files changed

Lines changed: 444 additions & 5 deletions

File tree

src/tools.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,7 @@ const ListPeersTool = feature('UDS_INBOX')
150150
.ListPeersTool
151151
: null
152152
const WorkflowTool = feature('WORKFLOW_SCRIPTS')
153-
? (() => {
154-
require('@claude-code-best/builtin-tools/tools/WorkflowTool/bundled/index.js').initBundledWorkflows()
155-
return require('@claude-code-best/builtin-tools/tools/WorkflowTool/WorkflowTool.js')
156-
.WorkflowTool
157-
})()
153+
? require('./workflow/wiring.js').createWorkflowToolCore()
158154
: null
159155
/* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
160156
import type { ToolPermissionContext } from './Tool.js'

src/workflow/adapter.ts

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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

src/workflow/hostHandle.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {
2+
createHostHandle,
3+
unwrapHostHandle,
4+
type HostHandle,
5+
} from '@claude-code-best/workflow-engine'
6+
import type { CanUseToolFn } from '../hooks/useCanUseTool.js'
7+
import type { AssistantMessage } from '../types/message.js'
8+
import type { AgentId } from '../types/ids.js'
9+
import type { ToolUseContext } from '../Tool.js'
10+
11+
/** HostHandle 内含的不透明 bundle(核心侧解包后使用)。 */
12+
export type WorkflowHostBundle = {
13+
toolUseContext: ToolUseContext
14+
canUseTool: CanUseToolFn
15+
parentMessage: AssistantMessage
16+
agentId?: AgentId
17+
}
18+
19+
export function makeHostHandle(bundle: WorkflowHostBundle): HostHandle {
20+
return createHostHandle(bundle)
21+
}
22+
23+
export function readHostBundle(handle: HostHandle): WorkflowHostBundle {
24+
return unwrapHostHandle(handle) as WorkflowHostBundle
25+
}

0 commit comments

Comments
 (0)