agentweaver 0.1.16 → 0.1.17
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/README.md +50 -10
- package/dist/artifacts.js +73 -3
- package/dist/doctor/checks/executors.js +2 -2
- package/dist/flow-state.js +138 -1
- package/dist/index.js +175 -61
- package/dist/interactive/controller.js +56 -23
- package/dist/interactive/ink/index.js +22 -1
- package/dist/interactive/tree.js +2 -2
- package/dist/pipeline/auto-flow.js +9 -6
- package/dist/pipeline/context.js +6 -5
- package/dist/pipeline/declarative-flows.js +39 -20
- package/dist/pipeline/flow-catalog.js +36 -14
- package/dist/pipeline/flow-specs/auto-common.json +1 -0
- package/dist/pipeline/flow-specs/auto-golang.json +27 -1
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +13 -1
- package/dist/pipeline/flow-specs/plan.json +4 -2
- package/dist/pipeline/launch-profile-config.js +30 -18
- package/dist/pipeline/node-contract.js +1 -0
- package/dist/pipeline/node-registry.js +74 -5
- package/dist/pipeline/nodes/flow-run-node.js +188 -173
- package/dist/pipeline/nodes/llm-prompt-node.js +15 -33
- package/dist/pipeline/plugin-loader.js +389 -0
- package/dist/pipeline/plugin-types.js +1 -0
- package/dist/pipeline/registry.js +71 -4
- package/dist/pipeline/spec-compiler.js +1 -0
- package/dist/pipeline/spec-loader.js +14 -0
- package/dist/pipeline/spec-validator.js +6 -0
- package/dist/pipeline/value-resolver.js +2 -1
- package/dist/plugin-sdk.js +1 -0
- package/dist/runtime/artifact-registry.js +3 -0
- package/dist/runtime/execution-routing.js +25 -19
- package/dist/runtime/interactive-execution-routing.js +66 -57
- package/docs/example/.flows/examples/claude-example.json +50 -0
- package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
- package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
- package/docs/examples/.flows/claude-example.json +50 -0
- package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
- package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
- package/docs/plugin-sdk.md +731 -0
- package/package.json +6 -2
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, symlinkSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { TaskRunnerError } from "../errors.js";
|
|
5
|
+
import { agentweaverHome } from "../runtime/agentweaver-home.js";
|
|
6
|
+
import { agentweaverConfigDir } from "../runtime/env-loader.js";
|
|
7
|
+
import { createNodeRegistry } from "./node-registry.js";
|
|
8
|
+
import { createExecutorRegistry } from "./registry.js";
|
|
9
|
+
import { AGENTWEAVER_PLUGIN_SDK_VERSION, } from "./plugin-types.js";
|
|
10
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(MODULE_DIR, "../..");
|
|
12
|
+
function projectPluginsDir(cwd) {
|
|
13
|
+
return path.join(cwd, ".agentweaver", ".plugins");
|
|
14
|
+
}
|
|
15
|
+
function globalPluginsDir() {
|
|
16
|
+
return path.join(agentweaverConfigDir(), ".plugins");
|
|
17
|
+
}
|
|
18
|
+
function sdkPackageRoot() {
|
|
19
|
+
return agentweaverHome(PACKAGE_ROOT);
|
|
20
|
+
}
|
|
21
|
+
function sdkAliasPath(pluginRoot) {
|
|
22
|
+
return path.join(pluginRoot, "node_modules", "agentweaver");
|
|
23
|
+
}
|
|
24
|
+
function ensurePluginSdkAlias(pluginRoot) {
|
|
25
|
+
const aliasPath = sdkAliasPath(pluginRoot);
|
|
26
|
+
const targetPath = sdkPackageRoot();
|
|
27
|
+
mkdirSync(path.dirname(aliasPath), { recursive: true });
|
|
28
|
+
if (!existsSync(aliasPath)) {
|
|
29
|
+
symlinkSync(targetPath, aliasPath, process.platform === "win32" ? "junction" : "dir");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const existing = lstatSync(aliasPath);
|
|
33
|
+
if (!existing.isSymbolicLink()) {
|
|
34
|
+
throw new TaskRunnerError(`Plugin installation '${pluginRoot}' has ${aliasPath}, but it is not a symlink. Remove it so AgentWeaver can expose agentweaver/plugin-sdk.`);
|
|
35
|
+
}
|
|
36
|
+
const resolvedTarget = path.resolve(path.dirname(aliasPath), readlinkSync(aliasPath));
|
|
37
|
+
if (resolvedTarget !== targetPath) {
|
|
38
|
+
throw new TaskRunnerError(`Plugin installation '${pluginRoot}' points ${aliasPath} to ${resolvedTarget}, expected ${targetPath}. Remove it and retry.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isPlainObject(value) {
|
|
42
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function isPositiveInteger(value) {
|
|
45
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
46
|
+
}
|
|
47
|
+
function isStringArray(value) {
|
|
48
|
+
return Array.isArray(value) && value.every((candidate) => typeof candidate === "string" && candidate.trim().length > 0);
|
|
49
|
+
}
|
|
50
|
+
function normalizeExecutorRouting(value, pluginId, pathLabel) {
|
|
51
|
+
if (!isPlainObject(value)) {
|
|
52
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' executor routing at ${pathLabel} must be an object.`);
|
|
53
|
+
}
|
|
54
|
+
if (value["kind"] !== "llm") {
|
|
55
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' executor routing at ${pathLabel} must use kind 'llm'.`);
|
|
56
|
+
}
|
|
57
|
+
const defaultModel = typeof value["defaultModel"] === "string" ? value["defaultModel"].trim() : "";
|
|
58
|
+
const models = value["models"];
|
|
59
|
+
if (!defaultModel) {
|
|
60
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' executor routing at ${pathLabel} must define a non-empty defaultModel.`);
|
|
61
|
+
}
|
|
62
|
+
if (!isStringArray(models)) {
|
|
63
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' executor routing at ${pathLabel} must define a non-empty string[] models.`);
|
|
64
|
+
}
|
|
65
|
+
if (!models.includes(defaultModel)) {
|
|
66
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' executor routing at ${pathLabel} must include defaultModel '${defaultModel}' in models.`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
kind: "llm",
|
|
70
|
+
defaultModel,
|
|
71
|
+
models: [...models],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function assertJsonSerializable(value, pluginId, pathLabel) {
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const visit = (candidate, candidatePath) => {
|
|
77
|
+
if (candidate === null ||
|
|
78
|
+
typeof candidate === "string" ||
|
|
79
|
+
typeof candidate === "number" ||
|
|
80
|
+
typeof candidate === "boolean") {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(candidate)) {
|
|
84
|
+
candidate.forEach((item, index) => visit(item, `${candidatePath}[${index}]`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (typeof candidate === "object") {
|
|
88
|
+
if (seen.has(candidate)) {
|
|
89
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' has circular defaultConfig at ${pathLabel} (${candidatePath}).`);
|
|
90
|
+
}
|
|
91
|
+
seen.add(candidate);
|
|
92
|
+
for (const [key, item] of Object.entries(candidate)) {
|
|
93
|
+
visit(item, `${candidatePath}.${key}`);
|
|
94
|
+
}
|
|
95
|
+
seen.delete(candidate);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' has non-JSON-serializable defaultConfig at ${pathLabel} (${candidatePath}).`);
|
|
99
|
+
};
|
|
100
|
+
visit(value, pathLabel);
|
|
101
|
+
}
|
|
102
|
+
function parseManifest(manifestPath, directoryName) {
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw new TaskRunnerError(`Failed to parse plugin manifest ${manifestPath}: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
if (!isPlainObject(parsed)) {
|
|
111
|
+
throw new TaskRunnerError(`Plugin manifest ${manifestPath} must contain a JSON object.`);
|
|
112
|
+
}
|
|
113
|
+
const manifest = parsed;
|
|
114
|
+
const id = typeof manifest["id"] === "string" ? manifest["id"].trim() : "";
|
|
115
|
+
const entrypoint = typeof manifest["entrypoint"] === "string" ? manifest["entrypoint"].trim() : "";
|
|
116
|
+
const sdkVersion = manifest["sdk_version"];
|
|
117
|
+
if (!id) {
|
|
118
|
+
throw new TaskRunnerError(`Plugin manifest ${manifestPath} must define a non-empty 'id'.`);
|
|
119
|
+
}
|
|
120
|
+
if (!isPositiveInteger(sdkVersion)) {
|
|
121
|
+
throw new TaskRunnerError(`Plugin '${id}' manifest ${manifestPath} must define a positive integer 'sdk_version'.`);
|
|
122
|
+
}
|
|
123
|
+
if (!entrypoint) {
|
|
124
|
+
throw new TaskRunnerError(`Plugin '${id}' manifest ${manifestPath} must define a non-empty 'entrypoint'.`);
|
|
125
|
+
}
|
|
126
|
+
if (id !== directoryName) {
|
|
127
|
+
throw new TaskRunnerError(`Plugin manifest id '${id}' does not match installation directory '${directoryName}' at ${manifestPath}.`);
|
|
128
|
+
}
|
|
129
|
+
if (sdkVersion !== AGENTWEAVER_PLUGIN_SDK_VERSION) {
|
|
130
|
+
throw new TaskRunnerError(`Plugin '${id}' manifest ${manifestPath} declares sdk_version ${sdkVersion}, but AgentWeaver supports ${AGENTWEAVER_PLUGIN_SDK_VERSION}.`);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
sdk_version: sdkVersion,
|
|
135
|
+
entrypoint,
|
|
136
|
+
...(typeof manifest["name"] === "string" ? { name: manifest["name"] } : {}),
|
|
137
|
+
...(typeof manifest["version"] === "string" ? { version: manifest["version"] } : {}),
|
|
138
|
+
...(typeof manifest["description"] === "string" ? { description: manifest["description"] } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function resolveEntrypoint(pluginRoot, manifestPath, manifest) {
|
|
142
|
+
const entrypointPath = path.resolve(path.dirname(manifestPath), manifest.entrypoint);
|
|
143
|
+
const relative = path.relative(pluginRoot, entrypointPath);
|
|
144
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
145
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' manifest ${manifestPath} resolves entrypoint outside plugin root ${pluginRoot}: ${entrypointPath}.`);
|
|
146
|
+
}
|
|
147
|
+
const extension = path.extname(entrypointPath);
|
|
148
|
+
if (extension !== ".js" && extension !== ".mjs") {
|
|
149
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' manifest ${manifestPath} must point to an ESM .js or .mjs entrypoint, got '${manifest.entrypoint}'.`);
|
|
150
|
+
}
|
|
151
|
+
return entrypointPath;
|
|
152
|
+
}
|
|
153
|
+
async function loadPluginModule(manifest, manifestPath, entrypointPath) {
|
|
154
|
+
try {
|
|
155
|
+
ensurePluginSdkAlias(path.dirname(manifestPath));
|
|
156
|
+
const namespace = await import(pathToFileURL(entrypointPath).href);
|
|
157
|
+
if ("default" in namespace) {
|
|
158
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' manifest ${manifestPath} must use named exports only; default exports are not supported.`);
|
|
159
|
+
}
|
|
160
|
+
return namespace;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error instanceof TaskRunnerError) {
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
throw new TaskRunnerError(`Failed to load plugin '${manifest.id}' entrypoint ${entrypointPath} from ${manifestPath}: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function validatePromptMode(value, pluginId, pathLabel) {
|
|
170
|
+
if (value === "required" || value === "allowed" || value === "forbidden") {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' has invalid prompt mode at ${pathLabel}.`);
|
|
174
|
+
}
|
|
175
|
+
function normalizeExecutorRegistration(candidate, index, manifest, manifestPath, entrypointPath) {
|
|
176
|
+
const pathLabel = `executors[${index}]`;
|
|
177
|
+
if (!isPlainObject(candidate)) {
|
|
178
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' registration ${pathLabel} must be an object.`);
|
|
179
|
+
}
|
|
180
|
+
const id = typeof candidate["id"] === "string" ? candidate["id"].trim() : "";
|
|
181
|
+
const definition = candidate["definition"];
|
|
182
|
+
if (!id) {
|
|
183
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' registration ${pathLabel} must define a non-empty 'id'.`);
|
|
184
|
+
}
|
|
185
|
+
if (!isPlainObject(definition)) {
|
|
186
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' executor '${id}' must define an object 'definition'.`);
|
|
187
|
+
}
|
|
188
|
+
if (definition["kind"] !== id) {
|
|
189
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' executor '${id}' must match definition.kind.`);
|
|
190
|
+
}
|
|
191
|
+
if (!isPositiveInteger(definition["version"])) {
|
|
192
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' executor '${id}' must define a positive integer definition.version.`);
|
|
193
|
+
}
|
|
194
|
+
if (typeof definition["execute"] !== "function") {
|
|
195
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' executor '${id}' must define a function definition.execute.`);
|
|
196
|
+
}
|
|
197
|
+
assertJsonSerializable(definition["defaultConfig"], manifest.id, `${pathLabel}.definition.defaultConfig`);
|
|
198
|
+
return {
|
|
199
|
+
type: "executor",
|
|
200
|
+
id,
|
|
201
|
+
pluginId: manifest.id,
|
|
202
|
+
manifestPath,
|
|
203
|
+
entrypointPath,
|
|
204
|
+
definition: definition,
|
|
205
|
+
...(candidate["routing"] !== undefined
|
|
206
|
+
? { routing: normalizeExecutorRouting(candidate["routing"], manifest.id, `${pathLabel}.routing`) }
|
|
207
|
+
: {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function normalizeNodeMetadata(metadata, id, pluginId, pathLabel) {
|
|
211
|
+
if (metadata["kind"] !== id) {
|
|
212
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' node '${id}' must match metadata.kind.`);
|
|
213
|
+
}
|
|
214
|
+
if (!isPositiveInteger(metadata["version"])) {
|
|
215
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' node '${id}' must define a positive integer metadata.version.`);
|
|
216
|
+
}
|
|
217
|
+
const normalized = {
|
|
218
|
+
kind: id,
|
|
219
|
+
version: metadata["version"],
|
|
220
|
+
prompt: validatePromptMode(metadata["prompt"], pluginId, `${pathLabel}.prompt`),
|
|
221
|
+
};
|
|
222
|
+
if (metadata["requiredParams"] !== undefined) {
|
|
223
|
+
if (!isStringArray(metadata["requiredParams"])) {
|
|
224
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' node '${id}' must define requiredParams as string[].`);
|
|
225
|
+
}
|
|
226
|
+
normalized.requiredParams = metadata["requiredParams"];
|
|
227
|
+
}
|
|
228
|
+
if (metadata["executors"] !== undefined) {
|
|
229
|
+
if (!isStringArray(metadata["executors"])) {
|
|
230
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' node '${id}' must define executors as string[].`);
|
|
231
|
+
}
|
|
232
|
+
normalized.executors = metadata["executors"];
|
|
233
|
+
}
|
|
234
|
+
if (metadata["nestedFlowParam"] !== undefined) {
|
|
235
|
+
if (typeof metadata["nestedFlowParam"] !== "string" || metadata["nestedFlowParam"].trim().length === 0) {
|
|
236
|
+
throw new TaskRunnerError(`Plugin '${pluginId}' node '${id}' must define nestedFlowParam as a non-empty string.`);
|
|
237
|
+
}
|
|
238
|
+
normalized.nestedFlowParam = metadata["nestedFlowParam"];
|
|
239
|
+
}
|
|
240
|
+
return normalized;
|
|
241
|
+
}
|
|
242
|
+
function normalizeNodeRegistration(candidate, index, manifest, manifestPath, entrypointPath) {
|
|
243
|
+
const pathLabel = `nodes[${index}]`;
|
|
244
|
+
if (!isPlainObject(candidate)) {
|
|
245
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' registration ${pathLabel} must be an object.`);
|
|
246
|
+
}
|
|
247
|
+
const id = typeof candidate["id"] === "string" ? candidate["id"].trim() : "";
|
|
248
|
+
const definition = candidate["definition"];
|
|
249
|
+
const metadata = candidate["metadata"];
|
|
250
|
+
if (!id) {
|
|
251
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' registration ${pathLabel} must define a non-empty 'id'.`);
|
|
252
|
+
}
|
|
253
|
+
if (!isPlainObject(definition)) {
|
|
254
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' node '${id}' must define an object 'definition'.`);
|
|
255
|
+
}
|
|
256
|
+
if (!isPlainObject(metadata)) {
|
|
257
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' node '${id}' must define an object 'metadata'.`);
|
|
258
|
+
}
|
|
259
|
+
if (definition["kind"] !== id) {
|
|
260
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' node '${id}' must match definition.kind.`);
|
|
261
|
+
}
|
|
262
|
+
if (!isPositiveInteger(definition["version"])) {
|
|
263
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' node '${id}' must define a positive integer definition.version.`);
|
|
264
|
+
}
|
|
265
|
+
if (typeof definition["run"] !== "function") {
|
|
266
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' node '${id}' must define a function definition.run.`);
|
|
267
|
+
}
|
|
268
|
+
const normalizedMetadata = normalizeNodeMetadata(metadata, id, manifest.id, `${pathLabel}.metadata`);
|
|
269
|
+
if (definition["version"] !== normalizedMetadata.version) {
|
|
270
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' node '${id}' must use the same version in definition and metadata.`);
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
type: "node",
|
|
274
|
+
id,
|
|
275
|
+
pluginId: manifest.id,
|
|
276
|
+
manifestPath,
|
|
277
|
+
entrypointPath,
|
|
278
|
+
definition: definition,
|
|
279
|
+
metadata: normalizedMetadata,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function normalizePluginModule(manifest, manifestPath, entrypointPath, namespace) {
|
|
283
|
+
const executorsValue = namespace["executors"];
|
|
284
|
+
const nodesValue = namespace["nodes"];
|
|
285
|
+
if (executorsValue !== undefined && !Array.isArray(executorsValue)) {
|
|
286
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' manifest ${manifestPath} must export 'executors' as an array.`);
|
|
287
|
+
}
|
|
288
|
+
if (nodesValue !== undefined && !Array.isArray(nodesValue)) {
|
|
289
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' manifest ${manifestPath} must export 'nodes' as an array.`);
|
|
290
|
+
}
|
|
291
|
+
const executorItems = executorsValue ?? [];
|
|
292
|
+
const nodeItems = nodesValue ?? [];
|
|
293
|
+
if (executorsValue === undefined && nodesValue === undefined) {
|
|
294
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' manifest ${manifestPath} must export named 'executors' and/or 'nodes' arrays.`);
|
|
295
|
+
}
|
|
296
|
+
if (executorItems.length === 0 && nodeItems.length === 0) {
|
|
297
|
+
throw new TaskRunnerError(`Plugin '${manifest.id}' manifest ${manifestPath} must export at least one non-empty recognized registration array.`);
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
manifest,
|
|
301
|
+
manifestPath,
|
|
302
|
+
entrypointPath,
|
|
303
|
+
executors: executorItems.map((candidate, index) => normalizeExecutorRegistration(candidate, index, manifest, manifestPath, entrypointPath)),
|
|
304
|
+
nodes: nodeItems.map((candidate, index) => normalizeNodeRegistration(candidate, index, manifest, manifestPath, entrypointPath)),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async function discoverPluginsFromRoot(pluginsRoot) {
|
|
308
|
+
if (!existsSync(pluginsRoot)) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
const entries = readdirSync(pluginsRoot, { withFileTypes: true })
|
|
312
|
+
.filter((entry) => entry.isDirectory())
|
|
313
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
314
|
+
const plugins = [];
|
|
315
|
+
for (const entry of entries) {
|
|
316
|
+
const pluginRoot = path.join(pluginsRoot, entry.name);
|
|
317
|
+
const manifestPath = path.join(pluginRoot, "plugin.json");
|
|
318
|
+
if (!existsSync(manifestPath)) {
|
|
319
|
+
throw new TaskRunnerError(`Plugin installation '${entry.name}' is missing manifest ${manifestPath}.`);
|
|
320
|
+
}
|
|
321
|
+
const manifest = parseManifest(manifestPath, entry.name);
|
|
322
|
+
const entrypointPath = resolveEntrypoint(pluginRoot, manifestPath, manifest);
|
|
323
|
+
const namespace = await loadPluginModule(manifest, manifestPath, entrypointPath);
|
|
324
|
+
plugins.push(normalizePluginModule(manifest, manifestPath, entrypointPath, namespace));
|
|
325
|
+
}
|
|
326
|
+
return plugins;
|
|
327
|
+
}
|
|
328
|
+
export async function discoverProjectPlugins(cwd) {
|
|
329
|
+
return discoverPluginsFromRoot(projectPluginsDir(cwd));
|
|
330
|
+
}
|
|
331
|
+
export async function discoverGlobalPlugins() {
|
|
332
|
+
return discoverPluginsFromRoot(globalPluginsDir());
|
|
333
|
+
}
|
|
334
|
+
function validateUniquePluginIds(plugins) {
|
|
335
|
+
const byId = new Map();
|
|
336
|
+
for (const plugin of plugins) {
|
|
337
|
+
const duplicate = byId.get(plugin.manifest.id);
|
|
338
|
+
if (duplicate) {
|
|
339
|
+
throw new TaskRunnerError(`Duplicate plugin id '${plugin.manifest.id}' conflicts between ${duplicate.manifestPath} and ${plugin.manifestPath}.`);
|
|
340
|
+
}
|
|
341
|
+
byId.set(plugin.manifest.id, plugin);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function cacheKeyForPlugins(cwd, plugins) {
|
|
345
|
+
if (plugins.length === 0) {
|
|
346
|
+
return `built-in:${path.resolve(cwd)}`;
|
|
347
|
+
}
|
|
348
|
+
const suffix = plugins
|
|
349
|
+
.map((plugin) => `${plugin.manifest.id}@${plugin.manifestPath}:${plugin.entrypointPath}`)
|
|
350
|
+
.join("|");
|
|
351
|
+
return `plugins:${path.resolve(cwd)}:${suffix}`;
|
|
352
|
+
}
|
|
353
|
+
function validatePluginNodeDependencies(plugins, executorRegistry) {
|
|
354
|
+
for (const plugin of plugins) {
|
|
355
|
+
for (const node of plugin.nodes) {
|
|
356
|
+
for (const executorId of node.metadata.executors ?? []) {
|
|
357
|
+
if (!executorRegistry.has(executorId)) {
|
|
358
|
+
throw new TaskRunnerError(`Plugin '${plugin.manifest.id}' node '${node.id}' requires unknown executor '${executorId}'.`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
export async function createPipelineRegistryContext(cwd) {
|
|
365
|
+
const plugins = [
|
|
366
|
+
...await discoverGlobalPlugins(),
|
|
367
|
+
...await discoverProjectPlugins(cwd),
|
|
368
|
+
];
|
|
369
|
+
validateUniquePluginIds(plugins);
|
|
370
|
+
const executorRegistry = createExecutorRegistry(plugins.flatMap((plugin) => plugin.executors));
|
|
371
|
+
const nodeRegistry = createNodeRegistry(plugins.flatMap((plugin) => plugin.nodes));
|
|
372
|
+
validatePluginNodeDependencies(plugins, executorRegistry);
|
|
373
|
+
return {
|
|
374
|
+
cwd: path.resolve(cwd),
|
|
375
|
+
cacheKey: cacheKeyForPlugins(cwd, plugins),
|
|
376
|
+
executors: executorRegistry,
|
|
377
|
+
nodes: nodeRegistry,
|
|
378
|
+
plugins,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
export function createBuiltInRegistryContext(cwd) {
|
|
382
|
+
return {
|
|
383
|
+
cwd: path.resolve(cwd),
|
|
384
|
+
cacheKey: `built-in:${path.resolve(cwd)}`,
|
|
385
|
+
executors: createExecutorRegistry(),
|
|
386
|
+
nodes: createNodeRegistry(),
|
|
387
|
+
plugins: [],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const AGENTWEAVER_PLUGIN_SDK_VERSION = 1;
|
|
@@ -7,6 +7,18 @@ import { jiraFetchExecutor } from "../executors/jira-fetch-executor.js";
|
|
|
7
7
|
import { opencodeExecutor } from "../executors/opencode-executor.js";
|
|
8
8
|
import { processExecutor } from "../executors/process-executor.js";
|
|
9
9
|
import { telegramNotifierExecutor } from "../executors/telegram-notifier-executor.js";
|
|
10
|
+
import { TaskRunnerError } from "../errors.js";
|
|
11
|
+
export const BUILT_IN_EXECUTOR_IDS = [
|
|
12
|
+
"process",
|
|
13
|
+
"command-check",
|
|
14
|
+
"fetch-gitlab-diff",
|
|
15
|
+
"fetch-gitlab-review",
|
|
16
|
+
"git-commit",
|
|
17
|
+
"jira-fetch",
|
|
18
|
+
"codex",
|
|
19
|
+
"opencode",
|
|
20
|
+
"telegram-notifier",
|
|
21
|
+
];
|
|
10
22
|
const builtInExecutors = {
|
|
11
23
|
process: processExecutor,
|
|
12
24
|
"command-check": commandCheckExecutor,
|
|
@@ -18,16 +30,71 @@ const builtInExecutors = {
|
|
|
18
30
|
opencode: opencodeExecutor,
|
|
19
31
|
"telegram-notifier": telegramNotifierExecutor,
|
|
20
32
|
};
|
|
21
|
-
|
|
33
|
+
const builtInExecutorRouting = {
|
|
34
|
+
codex: {
|
|
35
|
+
kind: "llm",
|
|
36
|
+
defaultModel: "gpt-5.5",
|
|
37
|
+
models: ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex"],
|
|
38
|
+
},
|
|
39
|
+
opencode: {
|
|
40
|
+
kind: "llm",
|
|
41
|
+
defaultModel: "minimax-coding-plan/MiniMax-M2.7",
|
|
42
|
+
models: [
|
|
43
|
+
"opencode/minimax-m2.5-free",
|
|
44
|
+
"minimax-coding-plan/MiniMax-M2.7",
|
|
45
|
+
"zhipuai-coding-plan/glm-5.1",
|
|
46
|
+
"zhipuai-coding-plan/glm-4.7",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
function coreOwner(id) {
|
|
51
|
+
return {
|
|
52
|
+
kind: "core",
|
|
53
|
+
id: `core:${id}`,
|
|
54
|
+
manifestPath: "built-in executor registry",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function createExecutorRegistry(pluginExecutors = []) {
|
|
58
|
+
const definitions = new Map(Object.entries(builtInExecutors));
|
|
59
|
+
const routing = new Map(Object.entries(builtInExecutorRouting).map(([id, definition]) => [id, definition]));
|
|
60
|
+
const owners = new Map(Object.keys(builtInExecutors).map((id) => [id, coreOwner(id)]));
|
|
61
|
+
for (const registration of pluginExecutors) {
|
|
62
|
+
const existingOwner = owners.get(registration.id);
|
|
63
|
+
if (existingOwner) {
|
|
64
|
+
throw new TaskRunnerError(`Duplicate executor id '${registration.id}' conflicts between ${existingOwner.id} (${existingOwner.manifestPath}) and plugin '${registration.pluginId}' (${registration.manifestPath}).`);
|
|
65
|
+
}
|
|
66
|
+
definitions.set(registration.id, registration.definition);
|
|
67
|
+
if (registration.routing) {
|
|
68
|
+
routing.set(registration.id, registration.routing);
|
|
69
|
+
}
|
|
70
|
+
owners.set(registration.id, {
|
|
71
|
+
kind: "plugin",
|
|
72
|
+
id: registration.pluginId,
|
|
73
|
+
manifestPath: registration.manifestPath,
|
|
74
|
+
entrypointPath: registration.entrypointPath,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
22
77
|
return {
|
|
23
78
|
get(id) {
|
|
24
|
-
|
|
79
|
+
const definition = definitions.get(id);
|
|
80
|
+
if (!definition) {
|
|
81
|
+
throw new TaskRunnerError(`Unknown executor '${id}'.`);
|
|
82
|
+
}
|
|
83
|
+
return definition;
|
|
25
84
|
},
|
|
26
85
|
has(id) {
|
|
27
|
-
return id
|
|
86
|
+
return definitions.has(id);
|
|
28
87
|
},
|
|
29
88
|
ids() {
|
|
30
|
-
return
|
|
89
|
+
return [...definitions.keys()];
|
|
90
|
+
},
|
|
91
|
+
getRouting(id) {
|
|
92
|
+
return routing.get(id) ?? null;
|
|
93
|
+
},
|
|
94
|
+
llmExecutors() {
|
|
95
|
+
return [...routing.entries()]
|
|
96
|
+
.filter((entry) => entry[1].kind === "llm")
|
|
97
|
+
.map(([id, definition]) => ({ id, routing: definition }));
|
|
31
98
|
},
|
|
32
99
|
};
|
|
33
100
|
}
|
|
@@ -21,6 +21,7 @@ function interpolateValueSpec(value, repeatVars) {
|
|
|
21
21
|
artifactList: {
|
|
22
22
|
...value.artifactList,
|
|
23
23
|
taskKey: interpolateValueSpec(value.artifactList.taskKey, repeatVars),
|
|
24
|
+
...(value.artifactList.iteration ? { iteration: interpolateValueSpec(value.artifactList.iteration, repeatVars) } : {}),
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
}
|
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { TaskRunnerError } from "../errors.js";
|
|
5
|
+
import { agentweaverConfigDir } from "../runtime/env-loader.js";
|
|
5
6
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const BUILT_IN_FLOW_SPECS_DIR = path.join(MODULE_DIR, "flow-specs");
|
|
7
8
|
function parseFlowSpec(filePath) {
|
|
@@ -18,6 +19,9 @@ export function resolveBuiltInFlowSpecPath(fileName) {
|
|
|
18
19
|
export function projectFlowSpecsDir(cwd) {
|
|
19
20
|
return path.join(cwd, ".agentweaver", ".flows");
|
|
20
21
|
}
|
|
22
|
+
export function globalFlowSpecsDir() {
|
|
23
|
+
return path.join(agentweaverConfigDir(), ".flows");
|
|
24
|
+
}
|
|
21
25
|
export function listBuiltInFlowSpecFiles() {
|
|
22
26
|
if (!existsSync(BUILT_IN_FLOW_SPECS_DIR)) {
|
|
23
27
|
return [];
|
|
@@ -48,6 +52,13 @@ export function listProjectFlowSpecFiles(cwd) {
|
|
|
48
52
|
}
|
|
49
53
|
return collectJsonFilesRecursively(directory);
|
|
50
54
|
}
|
|
55
|
+
export function listGlobalFlowSpecFiles() {
|
|
56
|
+
const directory = globalFlowSpecsDir();
|
|
57
|
+
if (!existsSync(directory)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return collectJsonFilesRecursively(directory);
|
|
61
|
+
}
|
|
51
62
|
export function loadFlowSpecSync(source) {
|
|
52
63
|
return parseFlowSpec(source.source === "built-in" ? resolveBuiltInFlowSpecPath(source.fileName) : source.filePath);
|
|
53
64
|
}
|
|
@@ -57,3 +68,6 @@ export function loadBuiltInFlowSpecSync(fileName) {
|
|
|
57
68
|
export function loadProjectFlowSpecSync(filePath) {
|
|
58
69
|
return loadFlowSpecSync({ source: "project-local", filePath });
|
|
59
70
|
}
|
|
71
|
+
export function loadGlobalFlowSpecSync(filePath) {
|
|
72
|
+
return loadFlowSpecSync({ source: "global", filePath });
|
|
73
|
+
}
|
|
@@ -19,6 +19,9 @@ function validateArtifactRefSpec(spec, path) {
|
|
|
19
19
|
function validateArtifactListRefSpec(spec, path) {
|
|
20
20
|
assert(isArtifactListRefKind(spec.kind), `Unknown artifact list kind '${spec.kind}' at ${path}.kind`);
|
|
21
21
|
validateValueSpec(spec.taskKey, `${path}.taskKey`);
|
|
22
|
+
if (spec.iteration) {
|
|
23
|
+
validateValueSpec(spec.iteration, `${path}.iteration`);
|
|
24
|
+
}
|
|
22
25
|
}
|
|
23
26
|
function assert(condition, message) {
|
|
24
27
|
if (!condition) {
|
|
@@ -241,6 +244,9 @@ function validateExpandedValueSpec(value, phases, currentPhaseIndex, currentStep
|
|
|
241
244
|
}
|
|
242
245
|
if ("artifactList" in value) {
|
|
243
246
|
validateExpandedValueSpec(value.artifactList.taskKey, phases, currentPhaseIndex, currentStepIndex, `${path}.artifactList.taskKey`, allowCurrentStepRef);
|
|
247
|
+
if (value.artifactList.iteration) {
|
|
248
|
+
validateExpandedValueSpec(value.artifactList.iteration, phases, currentPhaseIndex, currentStepIndex, `${path}.artifactList.iteration`, allowCurrentStepRef);
|
|
249
|
+
}
|
|
244
250
|
return;
|
|
245
251
|
}
|
|
246
252
|
if ("template" in value) {
|
|
@@ -211,11 +211,12 @@ function resolveArtifact(spec, context) {
|
|
|
211
211
|
}
|
|
212
212
|
function resolveArtifactList(spec, context) {
|
|
213
213
|
const taskKey = String(resolveValue(spec.taskKey, context));
|
|
214
|
+
const iteration = spec.iteration === undefined ? undefined : Number(resolveValue(spec.iteration, context));
|
|
214
215
|
switch (spec.kind) {
|
|
215
216
|
case "bug-analyze-artifacts":
|
|
216
217
|
return bugAnalyzeArtifacts(taskKey);
|
|
217
218
|
case "plan-artifacts":
|
|
218
|
-
return planArtifacts(taskKey);
|
|
219
|
+
return planArtifacts(taskKey, iteration);
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
222
|
export function resolveValue(value, context) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AGENTWEAVER_PLUGIN_SDK_VERSION } from "./pipeline/plugin-types.js";
|
|
@@ -81,6 +81,9 @@ function collectManifestFiles(rootDir) {
|
|
|
81
81
|
for (const entry of entries) {
|
|
82
82
|
const fullPath = path.join(current, entry.name);
|
|
83
83
|
if (entry.isDirectory()) {
|
|
84
|
+
if (entry.name === "restart-archives") {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
84
87
|
queue.push(fullPath);
|
|
85
88
|
continue;
|
|
86
89
|
}
|