agency-orchestrator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +268 -0
- package/README.zh-CN.md +225 -0
- package/dist/agents/loader.d.ts +11 -0
- package/dist/agents/loader.js +93 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +276 -0
- package/dist/connectors/claude.d.ts +6 -0
- package/dist/connectors/claude.js +38 -0
- package/dist/connectors/interface.d.ts +5 -0
- package/dist/connectors/interface.js +1 -0
- package/dist/connectors/ollama.d.ts +9 -0
- package/dist/connectors/ollama.js +36 -0
- package/dist/connectors/openai-compatible.d.ts +14 -0
- package/dist/connectors/openai-compatible.js +43 -0
- package/dist/core/dag.d.ts +17 -0
- package/dist/core/dag.js +90 -0
- package/dist/core/executor.d.ts +20 -0
- package/dist/core/executor.js +174 -0
- package/dist/core/parser.d.ts +6 -0
- package/dist/core/parser.js +118 -0
- package/dist/core/template.d.ts +12 -0
- package/dist/core/template.js +30 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +139 -0
- package/dist/output/reporter.d.ts +19 -0
- package/dist/output/reporter.js +96 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +2 -0
- package/package.json +66 -0
- package/workflows/content-pipeline.yaml +83 -0
- package/workflows/product-review.yaml +77 -0
- package/workflows/story-creation.yaml +102 -0
- package/workflows/test-deepseek.yaml +68 -0
- package/workflows/test-real.yaml +35 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { renderTemplate } from './template.js';
|
|
2
|
+
import { loadAgent } from '../agents/loader.js';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
export async function executeDAG(dag, options) {
|
|
5
|
+
const { connector, agentsDir, llmConfig, concurrency, inputs, onStepComplete, onStepStart, } = options;
|
|
6
|
+
// 变量上下文:inputs + 每步的 output
|
|
7
|
+
const context = new Map(inputs);
|
|
8
|
+
const startTime = Date.now();
|
|
9
|
+
const stepResults = [];
|
|
10
|
+
const timeout = llmConfig.timeout || 120_000;
|
|
11
|
+
const maxRetry = llmConfig.retry ?? 3;
|
|
12
|
+
for (const level of dag.levels) {
|
|
13
|
+
// 同层节点可并行,但受 concurrency 限制
|
|
14
|
+
const { onBatchStart, onBatchComplete } = options;
|
|
15
|
+
const allTasks = level.map(id => dag.nodes.get(id));
|
|
16
|
+
// 过滤掉已被标记为 skipped 的节点
|
|
17
|
+
const tasks = allTasks.filter(node => {
|
|
18
|
+
if (node.status === 'skipped') {
|
|
19
|
+
node.endTime = Date.now();
|
|
20
|
+
node.startTime = node.endTime;
|
|
21
|
+
stepResults.push({
|
|
22
|
+
id: node.step.id,
|
|
23
|
+
role: node.step.role,
|
|
24
|
+
status: 'skipped',
|
|
25
|
+
duration: 0,
|
|
26
|
+
tokens: { input: 0, output: 0 },
|
|
27
|
+
});
|
|
28
|
+
onStepComplete?.(node);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
// 按 concurrency 分批执行
|
|
34
|
+
for (let i = 0; i < tasks.length; i += concurrency) {
|
|
35
|
+
const batch = tasks.slice(i, i + concurrency);
|
|
36
|
+
onBatchStart?.(batch);
|
|
37
|
+
const results = await Promise.allSettled(batch.map(node => executeStep(node, {
|
|
38
|
+
connector,
|
|
39
|
+
agentsDir,
|
|
40
|
+
llmConfig,
|
|
41
|
+
context,
|
|
42
|
+
timeout,
|
|
43
|
+
maxRetry,
|
|
44
|
+
onStepStart,
|
|
45
|
+
})));
|
|
46
|
+
// 处理结果
|
|
47
|
+
for (let j = 0; j < batch.length; j++) {
|
|
48
|
+
const node = batch[j];
|
|
49
|
+
const result = results[j];
|
|
50
|
+
if (result.status === 'fulfilled') {
|
|
51
|
+
node.status = 'completed';
|
|
52
|
+
node.result = result.value;
|
|
53
|
+
if (node.step.output) {
|
|
54
|
+
context.set(node.step.output, result.value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
node.status = 'failed';
|
|
59
|
+
node.error = result.reason instanceof Error
|
|
60
|
+
? result.reason.message
|
|
61
|
+
: String(result.reason);
|
|
62
|
+
// 标记所有下游为 skipped
|
|
63
|
+
markDownstreamSkipped(dag, node.step.id);
|
|
64
|
+
}
|
|
65
|
+
node.endTime = Date.now();
|
|
66
|
+
stepResults.push({
|
|
67
|
+
id: node.step.id,
|
|
68
|
+
role: node.step.role,
|
|
69
|
+
status: node.status,
|
|
70
|
+
output: node.result,
|
|
71
|
+
error: node.error,
|
|
72
|
+
duration: (node.endTime || 0) - (node.startTime || 0),
|
|
73
|
+
tokens: node.tokenUsage || { input: 0, output: 0 },
|
|
74
|
+
});
|
|
75
|
+
onStepComplete?.(node);
|
|
76
|
+
}
|
|
77
|
+
onBatchComplete?.(batch);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const totalDuration = Date.now() - startTime;
|
|
81
|
+
const totalTokens = stepResults.reduce((acc, s) => ({
|
|
82
|
+
input: acc.input + s.tokens.input,
|
|
83
|
+
output: acc.output + s.tokens.output,
|
|
84
|
+
}), { input: 0, output: 0 });
|
|
85
|
+
return {
|
|
86
|
+
name: '', // 由调用方填充
|
|
87
|
+
success: stepResults.every(s => s.status !== 'failed'),
|
|
88
|
+
steps: stepResults,
|
|
89
|
+
totalDuration,
|
|
90
|
+
totalTokens,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function executeStep(node, opts) {
|
|
94
|
+
node.status = 'running';
|
|
95
|
+
node.startTime = Date.now();
|
|
96
|
+
opts.onStepStart?.(node);
|
|
97
|
+
// 人工审批节点
|
|
98
|
+
if (node.step.type === 'approval') {
|
|
99
|
+
return await handleApproval(node, opts.context);
|
|
100
|
+
}
|
|
101
|
+
// 加载角色定义
|
|
102
|
+
const agent = loadAgent(opts.agentsDir, node.step.role);
|
|
103
|
+
const systemPrompt = agent.systemPrompt;
|
|
104
|
+
// 渲染任务模板
|
|
105
|
+
const userMessage = renderTemplate(node.step.task, opts.context);
|
|
106
|
+
// 带重试的 LLM 调用
|
|
107
|
+
let lastError = null;
|
|
108
|
+
for (let attempt = 0; attempt <= opts.maxRetry; attempt++) {
|
|
109
|
+
try {
|
|
110
|
+
const result = await withTimeout(opts.connector.chat(systemPrompt, userMessage, opts.llmConfig), opts.timeout);
|
|
111
|
+
node.tokenUsage = { input: result.usage.input_tokens, output: result.usage.output_tokens };
|
|
112
|
+
return result.content;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
116
|
+
if (attempt < opts.maxRetry && isRetryable(lastError)) {
|
|
117
|
+
// 指数退避: 1s, 2s, 4s
|
|
118
|
+
await sleep(1000 * Math.pow(2, attempt));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
throw lastError || new Error(`step "${node.step.id}" 执行失败`);
|
|
124
|
+
}
|
|
125
|
+
async function handleApproval(node, context) {
|
|
126
|
+
const prompt = node.step.prompt
|
|
127
|
+
? renderTemplate(node.step.prompt, context)
|
|
128
|
+
: '请确认是否继续 (yes/no):';
|
|
129
|
+
// 如果有 input 引用,先显示内容
|
|
130
|
+
if (node.step.task) {
|
|
131
|
+
const content = renderTemplate(node.step.task, context);
|
|
132
|
+
console.log('\n' + content);
|
|
133
|
+
}
|
|
134
|
+
const rl = createInterface({
|
|
135
|
+
input: process.stdin,
|
|
136
|
+
output: process.stdout,
|
|
137
|
+
});
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
rl.question(`\n⏸️ ${prompt} `, (answer) => {
|
|
140
|
+
rl.close();
|
|
141
|
+
resolve(answer.trim());
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function markDownstreamSkipped(dag, failedId) {
|
|
146
|
+
const node = dag.nodes.get(failedId);
|
|
147
|
+
if (!node)
|
|
148
|
+
return;
|
|
149
|
+
for (const depId of node.dependents) {
|
|
150
|
+
const depNode = dag.nodes.get(depId);
|
|
151
|
+
if (depNode && depNode.status === 'pending') {
|
|
152
|
+
depNode.status = 'skipped';
|
|
153
|
+
markDownstreamSkipped(dag, depId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function withTimeout(promise, ms) {
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const timer = setTimeout(() => reject(new Error(`超时 (${ms}ms)`)), ms);
|
|
160
|
+
promise
|
|
161
|
+
.then(val => { clearTimeout(timer); resolve(val); })
|
|
162
|
+
.catch(err => { clearTimeout(timer); reject(err); });
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function isRetryable(error) {
|
|
166
|
+
const msg = error.message.toLowerCase();
|
|
167
|
+
return msg.includes('429') || msg.includes('rate') ||
|
|
168
|
+
msg.includes('500') || msg.includes('502') ||
|
|
169
|
+
msg.includes('503') || msg.includes('timeout') ||
|
|
170
|
+
msg.includes('econnreset');
|
|
171
|
+
}
|
|
172
|
+
function sleep(ms) {
|
|
173
|
+
return new Promise(r => setTimeout(r, ms));
|
|
174
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML → WorkflowDefinition 解析器
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
export function parseWorkflow(filePath) {
|
|
7
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
8
|
+
const doc = yaml.load(raw);
|
|
9
|
+
// 基本校验
|
|
10
|
+
if (!doc || typeof doc !== 'object') {
|
|
11
|
+
throw new Error(`工作流文件格式错误: ${filePath}`);
|
|
12
|
+
}
|
|
13
|
+
if (!doc.name || typeof doc.name !== 'string') {
|
|
14
|
+
throw new Error('工作流缺少 name 字段');
|
|
15
|
+
}
|
|
16
|
+
if (!doc.steps || !Array.isArray(doc.steps) || doc.steps.length === 0) {
|
|
17
|
+
throw new Error('工作流缺少 steps 或 steps 为空');
|
|
18
|
+
}
|
|
19
|
+
if (!doc.llm || typeof doc.llm !== 'object') {
|
|
20
|
+
throw new Error('工作流缺少 llm 配置');
|
|
21
|
+
}
|
|
22
|
+
const llm = doc.llm;
|
|
23
|
+
if (!llm.provider || !llm.model) {
|
|
24
|
+
throw new Error('llm 配置缺少 provider 或 model');
|
|
25
|
+
}
|
|
26
|
+
// 校验每个 step
|
|
27
|
+
const stepIds = new Set();
|
|
28
|
+
const steps = doc.steps;
|
|
29
|
+
for (const step of steps) {
|
|
30
|
+
if (!step.id)
|
|
31
|
+
throw new Error('step 缺少 id');
|
|
32
|
+
if (stepIds.has(step.id))
|
|
33
|
+
throw new Error(`step id 重复: ${step.id}`);
|
|
34
|
+
stepIds.add(step.id);
|
|
35
|
+
if (step.type !== 'approval' && !step.role) {
|
|
36
|
+
throw new Error(`step "${step.id}" 缺少 role`);
|
|
37
|
+
}
|
|
38
|
+
if (!step.task && step.type !== 'approval') {
|
|
39
|
+
throw new Error(`step "${step.id}" 缺少 task`);
|
|
40
|
+
}
|
|
41
|
+
// depends_on 的引用校验在 validateWorkflow() 中处理
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
name: doc.name,
|
|
45
|
+
description: doc.description,
|
|
46
|
+
agents_dir: doc.agents_dir || './agents',
|
|
47
|
+
llm: doc.llm,
|
|
48
|
+
concurrency: doc.concurrency || 2,
|
|
49
|
+
inputs: doc.inputs,
|
|
50
|
+
steps,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 验证工作流定义(不执行),返回错误列表
|
|
55
|
+
*/
|
|
56
|
+
export function validateWorkflow(workflow) {
|
|
57
|
+
const errors = [];
|
|
58
|
+
const stepIds = new Set(workflow.steps.map(s => s.id));
|
|
59
|
+
for (const step of workflow.steps) {
|
|
60
|
+
// 检查 depends_on 引用
|
|
61
|
+
if (step.depends_on) {
|
|
62
|
+
for (const dep of step.depends_on) {
|
|
63
|
+
if (!stepIds.has(dep)) {
|
|
64
|
+
errors.push(`step "${step.id}" 依赖不存在的 step: "${dep}"`);
|
|
65
|
+
}
|
|
66
|
+
if (dep === step.id) {
|
|
67
|
+
errors.push(`step "${step.id}" 不能依赖自己`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 检查 {{变量}} 引用
|
|
72
|
+
const varRefs = step.task?.match(/\{\{(\w+)\}\}/g) || [];
|
|
73
|
+
for (const ref of varRefs) {
|
|
74
|
+
const varName = ref.slice(2, -2);
|
|
75
|
+
// 变量要么来自 inputs,要么来自某个 step 的 output
|
|
76
|
+
const inputDef = workflow.inputs?.find(i => i.name === varName);
|
|
77
|
+
const isOutput = workflow.steps.some(s => s.output === varName);
|
|
78
|
+
if (!inputDef && !isOutput) {
|
|
79
|
+
errors.push(`step "${step.id}" 引用了未定义的变量: {{${varName}}}`);
|
|
80
|
+
}
|
|
81
|
+
// 可选输入无默认值用在模板中 → 警告
|
|
82
|
+
if (inputDef && !inputDef.required && inputDef.default === undefined) {
|
|
83
|
+
errors.push(`step "${step.id}" 使用了可选输入 {{${varName}}},但未设置默认值(未提供时为空字符串)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 检查循环依赖
|
|
88
|
+
const cycleError = detectCycle(workflow.steps);
|
|
89
|
+
if (cycleError)
|
|
90
|
+
errors.push(cycleError);
|
|
91
|
+
return errors;
|
|
92
|
+
}
|
|
93
|
+
function detectCycle(steps) {
|
|
94
|
+
const visited = new Set();
|
|
95
|
+
const inStack = new Set();
|
|
96
|
+
const adj = new Map();
|
|
97
|
+
for (const step of steps) {
|
|
98
|
+
adj.set(step.id, step.depends_on || []);
|
|
99
|
+
}
|
|
100
|
+
function dfs(id) {
|
|
101
|
+
visited.add(id);
|
|
102
|
+
inStack.add(id);
|
|
103
|
+
for (const dep of adj.get(id) || []) {
|
|
104
|
+
if (inStack.has(dep))
|
|
105
|
+
return true;
|
|
106
|
+
if (!visited.has(dep) && dfs(dep))
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
inStack.delete(id);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
for (const step of steps) {
|
|
113
|
+
if (!visited.has(step.id) && dfs(step.id)) {
|
|
114
|
+
return '工作流存在循环依赖';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{变量}} 模板引擎
|
|
3
|
+
* 简单的字符串替换,不需要复杂的模板语法
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 替换字符串中的 {{变量名}} 为上下文中的值
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderTemplate(template: string, context: Map<string, string>): string;
|
|
9
|
+
/**
|
|
10
|
+
* 提取模板中引用的所有变量名
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractVariables(template: string): string[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{变量}} 模板引擎
|
|
3
|
+
* 简单的字符串替换,不需要复杂的模板语法
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 替换字符串中的 {{变量名}} 为上下文中的值
|
|
7
|
+
*/
|
|
8
|
+
export function renderTemplate(template, context) {
|
|
9
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_match, varName) => {
|
|
10
|
+
const value = context.get(varName);
|
|
11
|
+
if (value === undefined) {
|
|
12
|
+
throw new Error(`模板变量未定义: {{${varName}}}`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 提取模板中引用的所有变量名
|
|
19
|
+
*/
|
|
20
|
+
export function extractVariables(template) {
|
|
21
|
+
const vars = [];
|
|
22
|
+
const regex = /\{\{(\w+)\}\}/g;
|
|
23
|
+
let match;
|
|
24
|
+
while ((match = regex.exec(template)) !== null) {
|
|
25
|
+
if (!vars.includes(match[1])) {
|
|
26
|
+
vars.push(match[1]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return vars;
|
|
30
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agency-orchestrator — 公开 API
|
|
3
|
+
*
|
|
4
|
+
* 使用方式:
|
|
5
|
+
* import { run, validate, plan } from 'agency-orchestrator';
|
|
6
|
+
*/
|
|
7
|
+
export { parseWorkflow, validateWorkflow } from './core/parser.js';
|
|
8
|
+
export { buildDAG, formatDAG } from './core/dag.js';
|
|
9
|
+
export { executeDAG } from './core/executor.js';
|
|
10
|
+
export { renderTemplate, extractVariables } from './core/template.js';
|
|
11
|
+
export { loadAgent, listAgents } from './agents/loader.js';
|
|
12
|
+
export { ClaudeConnector } from './connectors/claude.js';
|
|
13
|
+
export { OllamaConnector } from './connectors/ollama.js';
|
|
14
|
+
export { OpenAICompatibleConnector } from './connectors/openai-compatible.js';
|
|
15
|
+
export { saveResults } from './output/reporter.js';
|
|
16
|
+
export type { WorkflowDefinition, StepDefinition, LLMConfig, LLMConnector, LLMResult, AgentDefinition, WorkflowResult, StepResult, DAGNode, } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* 一行运行工作流(高级 API)
|
|
19
|
+
*/
|
|
20
|
+
export declare function run(workflowPath: string, inputs: Record<string, string>, options?: {
|
|
21
|
+
outputDir?: string;
|
|
22
|
+
quiet?: boolean;
|
|
23
|
+
}): Promise<import('./types.js').WorkflowResult>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agency-orchestrator — 公开 API
|
|
3
|
+
*
|
|
4
|
+
* 使用方式:
|
|
5
|
+
* import { run, validate, plan } from 'agency-orchestrator';
|
|
6
|
+
*/
|
|
7
|
+
export { parseWorkflow, validateWorkflow } from './core/parser.js';
|
|
8
|
+
export { buildDAG, formatDAG } from './core/dag.js';
|
|
9
|
+
export { executeDAG } from './core/executor.js';
|
|
10
|
+
export { renderTemplate, extractVariables } from './core/template.js';
|
|
11
|
+
export { loadAgent, listAgents } from './agents/loader.js';
|
|
12
|
+
export { ClaudeConnector } from './connectors/claude.js';
|
|
13
|
+
export { OllamaConnector } from './connectors/ollama.js';
|
|
14
|
+
export { OpenAICompatibleConnector } from './connectors/openai-compatible.js';
|
|
15
|
+
export { saveResults } from './output/reporter.js';
|
|
16
|
+
import { parseWorkflow, validateWorkflow } from './core/parser.js';
|
|
17
|
+
import { buildDAG } from './core/dag.js';
|
|
18
|
+
import { executeDAG } from './core/executor.js';
|
|
19
|
+
import { ClaudeConnector } from './connectors/claude.js';
|
|
20
|
+
import { OllamaConnector } from './connectors/ollama.js';
|
|
21
|
+
import { OpenAICompatibleConnector } from './connectors/openai-compatible.js';
|
|
22
|
+
import { saveResults, printStepResult, printStepRunning, clearRunningLine, printSummary } from './output/reporter.js';
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { resolve, dirname } from 'node:path';
|
|
25
|
+
/**
|
|
26
|
+
* 一行运行工作流(高级 API)
|
|
27
|
+
*/
|
|
28
|
+
export async function run(workflowPath, inputs, options) {
|
|
29
|
+
const workflow = parseWorkflow(workflowPath);
|
|
30
|
+
// 自动解析 agents_dir
|
|
31
|
+
workflow.agents_dir = resolveAgentsDir(workflow.agents_dir, workflowPath);
|
|
32
|
+
// 校验
|
|
33
|
+
const errors = validateWorkflow(workflow);
|
|
34
|
+
if (errors.length > 0) {
|
|
35
|
+
throw new Error(`工作流校验失败:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
36
|
+
}
|
|
37
|
+
// 构建 DAG
|
|
38
|
+
const dag = buildDAG(workflow);
|
|
39
|
+
// 创建 connector
|
|
40
|
+
let connector;
|
|
41
|
+
switch (workflow.llm.provider) {
|
|
42
|
+
case 'claude':
|
|
43
|
+
connector = new ClaudeConnector(workflow.llm.api_key);
|
|
44
|
+
break;
|
|
45
|
+
case 'ollama':
|
|
46
|
+
connector = new OllamaConnector(workflow.llm.base_url);
|
|
47
|
+
break;
|
|
48
|
+
case 'deepseek':
|
|
49
|
+
connector = new OpenAICompatibleConnector({
|
|
50
|
+
apiKey: workflow.llm.api_key || process.env.DEEPSEEK_API_KEY,
|
|
51
|
+
baseUrl: workflow.llm.base_url || 'https://api.deepseek.com/v1',
|
|
52
|
+
});
|
|
53
|
+
break;
|
|
54
|
+
case 'openai':
|
|
55
|
+
connector = new OpenAICompatibleConnector({
|
|
56
|
+
apiKey: workflow.llm.api_key || process.env.OPENAI_API_KEY,
|
|
57
|
+
baseUrl: workflow.llm.base_url || 'https://api.openai.com/v1',
|
|
58
|
+
});
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`暂不支持 provider: ${workflow.llm.provider}(支持 claude / deepseek / openai / ollama)`);
|
|
62
|
+
}
|
|
63
|
+
// 构建输入
|
|
64
|
+
const inputMap = new Map(Object.entries(inputs));
|
|
65
|
+
// 检查必填输入 + 注入默认值
|
|
66
|
+
for (const def of workflow.inputs || []) {
|
|
67
|
+
if (def.required && !inputMap.has(def.name)) {
|
|
68
|
+
throw new Error(`缺少必填输入: ${def.name}`);
|
|
69
|
+
}
|
|
70
|
+
// 可选输入未提供时使用默认值
|
|
71
|
+
if (!inputMap.has(def.name) && def.default !== undefined) {
|
|
72
|
+
inputMap.set(def.name, def.default);
|
|
73
|
+
}
|
|
74
|
+
// 可选输入无默认值且未提供 → 设为空字符串(避免模板引擎崩溃)
|
|
75
|
+
if (!inputMap.has(def.name)) {
|
|
76
|
+
inputMap.set(def.name, '');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 执行
|
|
80
|
+
let stepCounter = 0;
|
|
81
|
+
const totalSteps = workflow.steps.length;
|
|
82
|
+
const quiet = options?.quiet ?? false;
|
|
83
|
+
if (!quiet) {
|
|
84
|
+
console.log(`\n 工作流: ${workflow.name}`);
|
|
85
|
+
console.log(` 步骤数: ${totalSteps} | 并发: ${workflow.concurrency} | 模型: ${workflow.llm.model}`);
|
|
86
|
+
console.log('─'.repeat(50));
|
|
87
|
+
}
|
|
88
|
+
const result = await executeDAG(dag, {
|
|
89
|
+
connector,
|
|
90
|
+
agentsDir: workflow.agents_dir,
|
|
91
|
+
llmConfig: workflow.llm,
|
|
92
|
+
concurrency: workflow.concurrency || 2,
|
|
93
|
+
inputs: inputMap,
|
|
94
|
+
onBatchStart: quiet ? undefined : (nodes) => {
|
|
95
|
+
printStepRunning(nodes);
|
|
96
|
+
},
|
|
97
|
+
onBatchComplete: quiet ? undefined : (nodes) => {
|
|
98
|
+
clearRunningLine();
|
|
99
|
+
for (const node of nodes) {
|
|
100
|
+
stepCounter++;
|
|
101
|
+
printStepResult(node, stepCounter, totalSteps);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
result.name = workflow.name;
|
|
106
|
+
// 保存结果
|
|
107
|
+
const outputDir = options?.outputDir || '.ao-output';
|
|
108
|
+
const outputPath = saveResults(result, outputDir);
|
|
109
|
+
if (!quiet) {
|
|
110
|
+
printSummary(result, outputPath);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 自动查找 agents 目录
|
|
116
|
+
* 优先级:YAML 中指定的路径 → 相对于 workflow 文件 → 常见位置
|
|
117
|
+
*/
|
|
118
|
+
function resolveAgentsDir(agentsDir, workflowPath) {
|
|
119
|
+
// 1. 如果 YAML 中指定的路径存在,直接用
|
|
120
|
+
const absolute = resolve(agentsDir);
|
|
121
|
+
if (existsSync(absolute))
|
|
122
|
+
return absolute;
|
|
123
|
+
// 2. 相对于 workflow 文件所在目录
|
|
124
|
+
const relToWorkflow = resolve(dirname(workflowPath), agentsDir);
|
|
125
|
+
if (existsSync(relToWorkflow))
|
|
126
|
+
return relToWorkflow;
|
|
127
|
+
// 3. 常见位置
|
|
128
|
+
const candidates = [
|
|
129
|
+
resolve('agency-agents-zh'),
|
|
130
|
+
resolve('../agency-agents-zh'),
|
|
131
|
+
resolve('node_modules/agency-agents-zh'),
|
|
132
|
+
];
|
|
133
|
+
for (const dir of candidates) {
|
|
134
|
+
if (existsSync(dir))
|
|
135
|
+
return dir;
|
|
136
|
+
}
|
|
137
|
+
// 找不到就返回原值,让后续报错
|
|
138
|
+
return agentsDir;
|
|
139
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WorkflowResult } from '../types.js';
|
|
2
|
+
import type { DAGNode } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* 保存工作流执行结果到文件
|
|
5
|
+
*/
|
|
6
|
+
export declare function saveResults(result: WorkflowResult, outputDir: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* 打印一个步骤的完整结果(标题 + 内容),不拆成 start/complete
|
|
9
|
+
*/
|
|
10
|
+
export declare function printStepResult(node: DAGNode, stepIndex: number, totalSteps: number): void;
|
|
11
|
+
/**
|
|
12
|
+
* 打印正在运行的步骤提示(简短一行)
|
|
13
|
+
*/
|
|
14
|
+
export declare function printStepRunning(nodes: DAGNode[]): void;
|
|
15
|
+
/**
|
|
16
|
+
* 清除"执行中"提示行
|
|
17
|
+
*/
|
|
18
|
+
export declare function clearRunningLine(): void;
|
|
19
|
+
export declare function printSummary(result: WorkflowResult, outputPath: string): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 执行结果输出和保存
|
|
3
|
+
*/
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* 保存工作流执行结果到文件
|
|
8
|
+
*/
|
|
9
|
+
export function saveResults(result, outputDir) {
|
|
10
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
11
|
+
const dirName = `${result.name}-${timestamp}`;
|
|
12
|
+
const dir = join(outputDir, dirName);
|
|
13
|
+
const stepsDir = join(dir, 'steps');
|
|
14
|
+
mkdirSync(stepsDir, { recursive: true });
|
|
15
|
+
// 保存每步的输出
|
|
16
|
+
for (let i = 0; i < result.steps.length; i++) {
|
|
17
|
+
const step = result.steps[i];
|
|
18
|
+
const filename = `${i + 1}-${step.id}.md`;
|
|
19
|
+
const content = step.output || step.error || '(无输出)';
|
|
20
|
+
writeFileSync(join(stepsDir, filename), content, 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
// 保存最终输出(最后一个成功步骤的结果)
|
|
23
|
+
const lastCompleted = [...result.steps].reverse().find(s => s.status === 'completed');
|
|
24
|
+
if (lastCompleted?.output) {
|
|
25
|
+
writeFileSync(join(dir, 'summary.md'), lastCompleted.output, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
// 保存元数据
|
|
28
|
+
const metadata = {
|
|
29
|
+
name: result.name,
|
|
30
|
+
success: result.success,
|
|
31
|
+
totalDuration: `${(result.totalDuration / 1000).toFixed(1)}s`,
|
|
32
|
+
totalTokens: result.totalTokens,
|
|
33
|
+
steps: result.steps.map(s => ({
|
|
34
|
+
id: s.id,
|
|
35
|
+
role: s.role,
|
|
36
|
+
status: s.status,
|
|
37
|
+
duration: `${(s.duration / 1000).toFixed(1)}s`,
|
|
38
|
+
tokens: s.tokens,
|
|
39
|
+
})),
|
|
40
|
+
};
|
|
41
|
+
writeFileSync(join(dir, 'metadata.json'), JSON.stringify(metadata, null, 2), 'utf-8');
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 打印一个步骤的完整结果(标题 + 内容),不拆成 start/complete
|
|
46
|
+
*/
|
|
47
|
+
export function printStepResult(node, stepIndex, totalSteps) {
|
|
48
|
+
const role = node.step.role || node.step.type || '?';
|
|
49
|
+
const duration = ((node.endTime || 0) - (node.startTime || 0)) / 1000;
|
|
50
|
+
const tokens = node.tokenUsage
|
|
51
|
+
? `${node.tokenUsage.input + node.tokenUsage.output} tokens`
|
|
52
|
+
: '';
|
|
53
|
+
console.log(`\n ── [${stepIndex}/${totalSteps}] ${node.step.id} (${role}) ──`);
|
|
54
|
+
if (node.status === 'completed') {
|
|
55
|
+
console.log(` 完成 | ${duration.toFixed(1)}s | ${tokens}`);
|
|
56
|
+
if (node.result) {
|
|
57
|
+
console.log('');
|
|
58
|
+
for (const line of node.result.split('\n')) {
|
|
59
|
+
console.log(` ${line}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (node.status === 'failed') {
|
|
64
|
+
console.log(` 失败: ${node.error}`);
|
|
65
|
+
}
|
|
66
|
+
else if (node.status === 'skipped') {
|
|
67
|
+
console.log(` 跳过 (上游失败)`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 打印正在运行的步骤提示(简短一行)
|
|
72
|
+
*/
|
|
73
|
+
export function printStepRunning(nodes) {
|
|
74
|
+
if (nodes.length === 1) {
|
|
75
|
+
process.stdout.write(`\n ... ${nodes[0].step.id} 执行中`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const ids = nodes.map(n => n.step.id).join(' + ');
|
|
79
|
+
process.stdout.write(`\n ... ${ids} 并行执行中`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 清除"执行中"提示行
|
|
84
|
+
*/
|
|
85
|
+
export function clearRunningLine() {
|
|
86
|
+
process.stdout.write('\r\x1b[K');
|
|
87
|
+
}
|
|
88
|
+
export function printSummary(result, outputPath) {
|
|
89
|
+
const totalTokens = result.totalTokens.input + result.totalTokens.output;
|
|
90
|
+
const duration = (result.totalDuration / 1000).toFixed(1);
|
|
91
|
+
const completedSteps = result.steps.filter(s => s.status === 'completed').length;
|
|
92
|
+
console.log('\n\n' + '='.repeat(50));
|
|
93
|
+
console.log(` ${result.success ? '完成' : '部分失败'}: ${completedSteps}/${result.steps.length} 步 | ${duration}s | ${totalTokens} tokens`);
|
|
94
|
+
console.log(` 详细输出: ${outputPath}`);
|
|
95
|
+
console.log('='.repeat(50));
|
|
96
|
+
}
|