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.
Files changed (40) hide show
  1. package/README.md +50 -10
  2. package/dist/artifacts.js +73 -3
  3. package/dist/doctor/checks/executors.js +2 -2
  4. package/dist/flow-state.js +138 -1
  5. package/dist/index.js +175 -61
  6. package/dist/interactive/controller.js +56 -23
  7. package/dist/interactive/ink/index.js +22 -1
  8. package/dist/interactive/tree.js +2 -2
  9. package/dist/pipeline/auto-flow.js +9 -6
  10. package/dist/pipeline/context.js +6 -5
  11. package/dist/pipeline/declarative-flows.js +39 -20
  12. package/dist/pipeline/flow-catalog.js +36 -14
  13. package/dist/pipeline/flow-specs/auto-common.json +1 -0
  14. package/dist/pipeline/flow-specs/auto-golang.json +27 -1
  15. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +13 -1
  16. package/dist/pipeline/flow-specs/plan.json +4 -2
  17. package/dist/pipeline/launch-profile-config.js +30 -18
  18. package/dist/pipeline/node-contract.js +1 -0
  19. package/dist/pipeline/node-registry.js +74 -5
  20. package/dist/pipeline/nodes/flow-run-node.js +188 -173
  21. package/dist/pipeline/nodes/llm-prompt-node.js +15 -33
  22. package/dist/pipeline/plugin-loader.js +389 -0
  23. package/dist/pipeline/plugin-types.js +1 -0
  24. package/dist/pipeline/registry.js +71 -4
  25. package/dist/pipeline/spec-compiler.js +1 -0
  26. package/dist/pipeline/spec-loader.js +14 -0
  27. package/dist/pipeline/spec-validator.js +6 -0
  28. package/dist/pipeline/value-resolver.js +2 -1
  29. package/dist/plugin-sdk.js +1 -0
  30. package/dist/runtime/artifact-registry.js +3 -0
  31. package/dist/runtime/execution-routing.js +25 -19
  32. package/dist/runtime/interactive-execution-routing.js +66 -57
  33. package/docs/example/.flows/examples/claude-example.json +50 -0
  34. package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
  35. package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
  36. package/docs/examples/.flows/claude-example.json +50 -0
  37. package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
  38. package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
  39. package/docs/plugin-sdk.md +731 -0
  40. package/package.json +6 -2
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { TaskRunnerError } from "../errors.js";
3
- import { ALLOWED_MODELS_BY_EXECUTOR, DEFAULT_LAUNCH_PROFILE, defaultModelForExecutor, isAllowedModelForExecutor, resolveLaunchProfile, } from "../pipeline/launch-profile-config.js";
3
+ import { allowedModelsForExecutor, DEFAULT_LAUNCH_PROFILE, defaultModelForExecutor, isAllowedModelForExecutor, resolveLaunchProfile, } from "../pipeline/launch-profile-config.js";
4
4
  import { BUILT_IN_EXECUTION_PRESET_IDS, EXECUTION_ROUTING_GROUPS, } from "../pipeline/execution-routing-config.js";
5
5
  export const BUILT_IN_EXECUTION_PRESETS = {
6
6
  balanced: {
@@ -62,12 +62,14 @@ function stableRoutingPayload(routing) {
62
62
  export function executionRoutingFingerprint(routing) {
63
63
  return createHash("sha256").update(stableRoutingPayload({ ...routing, fingerprint: "" })).digest("hex");
64
64
  }
65
- export function validateExecutionRoute(executor, model) {
66
- const allowedModels = ALLOWED_MODELS_BY_EXECUTOR[executor];
67
- if (!allowedModels) {
65
+ export function validateExecutionRoute(executor, model, executors) {
66
+ if (!executors && !isAllowedModelForExecutor(executor, model)) {
68
67
  throw new TaskRunnerError(`Unsupported llm executor '${executor}'.`);
69
68
  }
70
- if (!allowedModels.includes(model)) {
69
+ if (executors && !executors.getRouting(executor)) {
70
+ throw new TaskRunnerError(`Unsupported llm executor '${executor}'.`);
71
+ }
72
+ if (!isAllowedModelForExecutor(executor, model, executors)) {
71
73
  throw new TaskRunnerError(`Model '${model}' is not allowed for executor '${executor}'.`);
72
74
  }
73
75
  }
@@ -77,25 +79,25 @@ export function toExecutionRouteSelection(route) {
77
79
  model: route.model,
78
80
  };
79
81
  }
80
- export function resolveExecutionRoute(selection, fallback) {
82
+ export function resolveExecutionRoute(selection, fallback, executors) {
81
83
  const launchProfile = resolveLaunchProfile({
82
84
  executor: selection.executor,
83
85
  model: selection.model,
84
- }, fallback);
85
- validateExecutionRoute(launchProfile.executor, launchProfile.model);
86
+ }, fallback, executors);
87
+ validateExecutionRoute(launchProfile.executor, launchProfile.model, executors);
86
88
  return launchProfile;
87
89
  }
88
90
  export function resolveExecutionRouting(options) {
89
91
  const fallbackDefaultRoute = options.fallbackDefaultRoute ?? DEFAULT_LAUNCH_PROFILE;
90
92
  const preset = options.presetId ? BUILT_IN_EXECUTION_PRESETS[options.presetId] : null;
91
93
  const defaultRouteSelection = options.defaultRoute ?? preset?.defaultRoute ?? toExecutionRouteSelection(fallbackDefaultRoute);
92
- const defaultRoute = resolveExecutionRoute(defaultRouteSelection, fallbackDefaultRoute);
94
+ const defaultRoute = resolveExecutionRoute(defaultRouteSelection, fallbackDefaultRoute, options.executors);
93
95
  const groups = Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => {
94
96
  const override = options.currentRunOverrides?.[group]
95
97
  ?? options.presetOverrides?.[group]
96
98
  ?? preset?.groupOverrides?.[group]
97
99
  ?? { executor: "default", model: "default" };
98
- return [group, resolveExecutionRoute(override, defaultRoute)];
100
+ return [group, resolveExecutionRoute(override, defaultRoute, options.executors)];
99
101
  }));
100
102
  const fingerprint = executionRoutingFingerprint({
101
103
  defaultRoute,
@@ -143,18 +145,18 @@ export function executorsForRoutingGroups(routing, groups) {
143
145
  }
144
146
  return requiredExecutors;
145
147
  }
146
- export function normalizeEditableExecutionRouting(routes) {
148
+ export function normalizeEditableExecutionRouting(routes, executors) {
147
149
  const normalizedRoutes = {};
148
150
  const validationErrors = [];
149
151
  for (const group of EXECUTION_ROUTING_GROUPS) {
150
152
  const route = routes[group];
151
- if (isAllowedModelForExecutor(route.executor, route.model)) {
153
+ if (isAllowedModelForExecutor(route.executor, route.model, executors)) {
152
154
  normalizedRoutes[group] = { ...route };
153
155
  continue;
154
156
  }
155
157
  normalizedRoutes[group] = {
156
158
  executor: route.executor,
157
- model: defaultModelForExecutor(route.executor),
159
+ model: defaultModelForExecutor(route.executor, executors),
158
160
  };
159
161
  validationErrors.push(`${routingGroupLabel(group)} model '${route.model}' is not allowed for executor '${route.executor}'. Select a ${route.executor} model.`);
160
162
  }
@@ -184,10 +186,14 @@ export function defaultExecutionRouting() {
184
186
  },
185
187
  });
186
188
  }
187
- export function resolveStoredExecutionRoutingSnapshot(routing) {
188
- validateExecutionRoute(routing.defaultRoute.executor, routing.defaultRoute.model);
189
+ export function resolveStoredExecutionRoutingSnapshot(routing, executors) {
190
+ if (executors) {
191
+ validateExecutionRoute(routing.defaultRoute.executor, routing.defaultRoute.model, executors);
192
+ }
189
193
  for (const group of EXECUTION_ROUTING_GROUPS) {
190
- validateExecutionRoute(routing.groups[group].executor, routing.groups[group].model);
194
+ if (executors) {
195
+ validateExecutionRoute(routing.groups[group].executor, routing.groups[group].model, executors);
196
+ }
191
197
  }
192
198
  const fingerprint = executionRoutingFingerprint({
193
199
  defaultRoute: routing.defaultRoute,
@@ -218,9 +224,9 @@ export function singleLaunchProfileExecutionRouting(launchProfile) {
218
224
  currentRunOverrides: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => [group, toExecutionRouteSelection(launchProfile)])),
219
225
  });
220
226
  }
221
- export function modelOptionsForExecutor(executor) {
222
- const defaultModel = defaultModelForExecutor(executor);
223
- return ALLOWED_MODELS_BY_EXECUTOR[executor].map((model) => ({
227
+ export function modelOptionsForExecutor(executor, executors) {
228
+ const defaultModel = defaultModelForExecutor(executor, executors);
229
+ return allowedModelsForExecutor(executor, executors).map((model) => ({
224
230
  value: model,
225
231
  label: model === defaultModel ? `${model} [default]` : model,
226
232
  }));
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { flowRoutingGroups, flowRoutingKey } from "../pipeline/flow-catalog.js";
3
3
  import { loadNamedDeclarativeFlow } from "../pipeline/declarative-flows.js";
4
+ import { createPipelineRegistryContext } from "../pipeline/plugin-loader.js";
4
5
  import { EXECUTION_ROUTING_GROUPS, } from "../pipeline/execution-routing-config.js";
5
6
  import { DEFAULT_LAUNCH_PROFILE, defaultModelForExecutor, isAllowedModelForExecutor, } from "../pipeline/launch-profile-config.js";
6
7
  import { FlowInterruptedError, TaskRunnerError } from "../errors.js";
@@ -58,15 +59,9 @@ function executionPresetSelectionForm(flowEntry, flowDefaultAvailable, lastUsedA
58
59
  ],
59
60
  };
60
61
  }
61
- function executorFieldOptions() {
62
- return [
63
- { value: "codex", label: "codex" },
64
- { value: "opencode", label: "opencode" },
65
- ];
66
- }
67
- function selectedExecutorFromValues(values, fieldId, fallbackExecutor) {
68
- const candidate = values[fieldId];
69
- return candidate === "codex" || candidate === "opencode" ? candidate : fallbackExecutor;
62
+ function selectedExecutorFromValues(values, fieldId, fallbackExecutor, registryContext) {
63
+ const candidate = typeof values[fieldId] === "string" ? values[fieldId] : "";
64
+ return registryContext.executors.getRouting(candidate)?.kind === "llm" ? candidate : fallbackExecutor;
70
65
  }
71
66
  function routingEditorDraftFromRouting(routing) {
72
67
  return {
@@ -83,7 +78,11 @@ function routingEditorDraftFromRouting(routing) {
83
78
  ])),
84
79
  };
85
80
  }
86
- function advancedRoutingEditorForm(activeGroups, draft, validationMessage) {
81
+ function advancedRoutingEditorForm(activeGroups, draft, registryContext, validationMessage) {
82
+ const executorOptions = registryContext.executors.llmExecutors().map((entry) => ({
83
+ value: entry.id,
84
+ label: entry.id,
85
+ }));
87
86
  const fields = [
88
87
  {
89
88
  id: "default_route_executor",
@@ -91,7 +90,7 @@ function advancedRoutingEditorForm(activeGroups, draft, validationMessage) {
91
90
  label: "Default route executor",
92
91
  required: true,
93
92
  default: draft.defaultRoute.executor,
94
- options: executorFieldOptions(),
93
+ options: executorOptions,
95
94
  },
96
95
  {
97
96
  id: "default_route_model",
@@ -100,8 +99,8 @@ function advancedRoutingEditorForm(activeGroups, draft, validationMessage) {
100
99
  help: "Available models follow the selected default executor.",
101
100
  required: true,
102
101
  default: draft.defaultRoute.model,
103
- options: modelOptionsForExecutor(draft.defaultRoute.executor),
104
- optionsFromValues: (values) => modelOptionsForExecutor(selectedExecutorFromValues(values, "default_route_executor", draft.defaultRoute.executor)),
102
+ options: modelOptionsForExecutor(draft.defaultRoute.executor, registryContext.executors),
103
+ optionsFromValues: (values) => modelOptionsForExecutor(selectedExecutorFromValues(values, "default_route_executor", draft.defaultRoute.executor, registryContext), registryContext.executors),
105
104
  },
106
105
  ...activeGroups.flatMap((group) => {
107
106
  const route = draft.groups[group];
@@ -112,7 +111,7 @@ function advancedRoutingEditorForm(activeGroups, draft, validationMessage) {
112
111
  label: `${routingGroupLabel(group)} executor`,
113
112
  required: true,
114
113
  default: route.executor,
115
- options: executorFieldOptions(),
114
+ options: executorOptions,
116
115
  },
117
116
  {
118
117
  id: `${group}_model`,
@@ -121,8 +120,8 @@ function advancedRoutingEditorForm(activeGroups, draft, validationMessage) {
121
120
  help: `Available models follow the selected ${routingGroupLabel(group)} executor.`,
122
121
  required: true,
123
122
  default: route.model,
124
- options: modelOptionsForExecutor(route.executor),
125
- optionsFromValues: (values) => modelOptionsForExecutor(selectedExecutorFromValues(values, `${group}_executor`, route.executor)),
123
+ options: modelOptionsForExecutor(route.executor, registryContext.executors),
124
+ optionsFromValues: (values) => modelOptionsForExecutor(selectedExecutorFromValues(values, `${group}_executor`, route.executor, registryContext), registryContext.executors),
126
125
  },
127
126
  ];
128
127
  }),
@@ -202,8 +201,8 @@ function presetNameForm() {
202
201
  ],
203
202
  };
204
203
  }
205
- function normalizeEditableRoute(label, route) {
206
- if (isAllowedModelForExecutor(route.executor, route.model)) {
204
+ function normalizeEditableRoute(label, route, executors) {
205
+ if (isAllowedModelForExecutor(route.executor, route.model, executors)) {
207
206
  return {
208
207
  route: { ...route },
209
208
  validationErrors: [],
@@ -212,20 +211,20 @@ function normalizeEditableRoute(label, route) {
212
211
  return {
213
212
  route: {
214
213
  executor: route.executor,
215
- model: defaultModelForExecutor(route.executor),
214
+ model: defaultModelForExecutor(route.executor, executors),
216
215
  },
217
216
  validationErrors: [
218
217
  `${label} model '${route.model}' is not allowed for executor '${route.executor}'. Select a ${route.executor} model.`,
219
218
  ],
220
219
  };
221
220
  }
222
- function normalizeEditableRoutingDraft(draft) {
223
- const defaultRoute = normalizeEditableRoute("Default route", draft.defaultRoute);
221
+ function normalizeEditableRoutingDraft(draft, executors) {
222
+ const defaultRoute = normalizeEditableRoute("Default route", draft.defaultRoute, executors);
224
223
  const groups = {};
225
224
  const currentRunOverrides = {};
226
225
  const validationErrors = [...defaultRoute.validationErrors];
227
226
  for (const group of EXECUTION_ROUTING_GROUPS) {
228
- const normalizedRoute = normalizeEditableRoute(routingGroupLabel(group), draft.groups[group]);
227
+ const normalizedRoute = normalizeEditableRoute(routingGroupLabel(group), draft.groups[group], executors);
229
228
  groups[group] = normalizedRoute.route;
230
229
  if (normalizedRoute.route.executor !== defaultRoute.route.executor
231
230
  || normalizedRoute.route.model !== defaultRoute.route.model) {
@@ -274,40 +273,47 @@ function formatAsciiTable(headers, rows, maxWidths) {
274
273
  const separator = `|-${widths.map((width) => "-".repeat(width)).join("-|-")}-|`;
275
274
  return [formatRow(headers), separator, ...rows.map((row) => formatRow(row))];
276
275
  }
277
- function collectEffectiveRoutedStepRows(flow, cwd, routing, prefixSegments = [], ancestry = []) {
276
+ function collectEffectiveRoutedStepRows(flow, cwd, routing, registryContext, prefixSegments = [], ancestry = []) {
278
277
  if (ancestry.includes(flow.absolutePath)) {
279
- return [];
278
+ return Promise.resolve([]);
280
279
  }
281
280
  const nextAncestry = [...ancestry, flow.absolutePath];
282
281
  const rows = [];
283
- for (const phase of flow.phases) {
284
- for (const step of phase.steps) {
285
- const stepRef = `${phase.id}.${step.id}`;
286
- if (step.routingGroup) {
287
- const route = routing.groups[step.routingGroup];
288
- rows.push({
289
- step: [...prefixSegments, stepRef].join(" > "),
290
- group: routingGroupLabel(step.routingGroup),
291
- executor: route.executor,
292
- model: route.model,
282
+ const run = async () => {
283
+ for (const phase of flow.phases) {
284
+ for (const step of phase.steps) {
285
+ const stepRef = `${phase.id}.${step.id}`;
286
+ if (step.routingGroup) {
287
+ const route = routing.groups[step.routingGroup];
288
+ rows.push({
289
+ step: [...prefixSegments, stepRef].join(" > "),
290
+ group: routingGroupLabel(step.routingGroup),
291
+ executor: route.executor,
292
+ model: route.model,
293
+ });
294
+ }
295
+ if (step.node !== "flow-run") {
296
+ continue;
297
+ }
298
+ const nestedFlowName = step.params?.fileName;
299
+ if (!nestedFlowName || !("const" in nestedFlowName) || typeof nestedFlowName.const !== "string") {
300
+ continue;
301
+ }
302
+ const nestedFlow = await loadNamedDeclarativeFlow(nestedFlowName.const, cwd, {
303
+ ...(registryContext ? { registryContext } : {}),
293
304
  });
305
+ const nestedFlowLabel = path.basename(nestedFlow.fileName, path.extname(nestedFlow.fileName));
306
+ rows.push(...await collectEffectiveRoutedStepRows(nestedFlow, cwd, routing, registryContext, [...prefixSegments, stepRef, nestedFlowLabel], nextAncestry));
294
307
  }
295
- if (step.node !== "flow-run") {
296
- continue;
297
- }
298
- const nestedFlowName = step.params?.fileName;
299
- if (!nestedFlowName || !("const" in nestedFlowName) || typeof nestedFlowName.const !== "string") {
300
- continue;
301
- }
302
- const nestedFlow = loadNamedDeclarativeFlow(nestedFlowName.const, cwd);
303
- const nestedFlowLabel = path.basename(nestedFlow.fileName, path.extname(nestedFlow.fileName));
304
- rows.push(...collectEffectiveRoutedStepRows(nestedFlow, cwd, routing, [...prefixSegments, stepRef, nestedFlowLabel], nextAncestry));
305
308
  }
306
- }
307
- return rows;
309
+ return rows;
310
+ };
311
+ return run();
308
312
  }
309
- export function describeEffectiveRoutingPreview(flowEntry, routing, cwd) {
310
- const previewGroups = flowRoutingGroups(flowEntry, cwd);
313
+ export async function describeEffectiveRoutingPreview(flowEntry, routing, cwd, registryContext) {
314
+ const previewGroups = await flowRoutingGroups(flowEntry, cwd, {
315
+ ...(registryContext ? { registryContext } : {}),
316
+ });
311
317
  const summaryRows = [
312
318
  ["Default", routing.defaultRoute.executor, routing.defaultRoute.model],
313
319
  ...previewGroups.map((group) => [
@@ -320,7 +326,7 @@ export function describeEffectiveRoutingPreview(flowEntry, routing, cwd) {
320
326
  "Effective routes:",
321
327
  ...formatAsciiTable(["Scope", "Executor", "Model"], summaryRows, [18, 12, 36]),
322
328
  ];
323
- const routedSteps = collapseRepeatedEffectiveRoutedStepRows(collectEffectiveRoutedStepRows(flowEntry.flow, cwd, routing));
329
+ const routedSteps = collapseRepeatedEffectiveRoutedStepRows(await collectEffectiveRoutedStepRows(flowEntry.flow, cwd, routing, registryContext));
324
330
  if (routedSteps.length === 0) {
325
331
  return lines.join("\n");
326
332
  }
@@ -332,7 +338,8 @@ export function describeEffectiveRoutingPreview(flowEntry, routing, cwd) {
332
338
  ].join("\n");
333
339
  }
334
340
  export async function requestInteractiveExecutionRouting(flowEntry, requestUserInput) {
335
- const previewGroups = flowRoutingGroups(flowEntry, process.cwd());
341
+ const registryContext = await createPipelineRegistryContext(process.cwd());
342
+ const previewGroups = await flowRoutingGroups(flowEntry, process.cwd(), { registryContext });
336
343
  const flowKey = flowRoutingKey(flowEntry);
337
344
  const namedPresets = getNamedExecutionPresets();
338
345
  const flowDefault = getFlowDefaultExecutionRouting(flowKey);
@@ -346,14 +353,14 @@ export async function requestInteractiveExecutionRouting(flowEntry, requestUserI
346
353
  throw new TaskRunnerError("Flow default routing is unavailable.");
347
354
  }
348
355
  selectedPreset = { kind: "flow-default", label: "Flow default" };
349
- routing = resolveStoredExecutionRoutingSnapshot(flowDefault.routing);
356
+ routing = resolveStoredExecutionRoutingSnapshot(flowDefault.routing, registryContext.executors);
350
357
  }
351
358
  else if (selectedPresetValue === "last-used") {
352
359
  if (!lastUsed) {
353
360
  throw new TaskRunnerError("Last-used routing is unavailable.");
354
361
  }
355
362
  selectedPreset = { kind: "last-used", label: "Last used" };
356
- routing = resolveStoredExecutionRoutingSnapshot(lastUsed.routing);
363
+ routing = resolveStoredExecutionRoutingSnapshot(lastUsed.routing, registryContext.executors);
357
364
  }
358
365
  else if (selectedPresetValue.startsWith("named:")) {
359
366
  const presetName = selectedPresetValue.slice("named:".length);
@@ -362,7 +369,7 @@ export async function requestInteractiveExecutionRouting(flowEntry, requestUserI
362
369
  throw new TaskRunnerError(`Named preset '${presetName}' is unavailable.`);
363
370
  }
364
371
  selectedPreset = { kind: "named", presetId: presetName, label: `Named preset: ${presetName}` };
365
- routing = resolveStoredExecutionRoutingSnapshot(namedPreset.routing);
372
+ routing = resolveStoredExecutionRoutingSnapshot(namedPreset.routing, registryContext.executors);
366
373
  }
367
374
  else if (selectedPresetValue.startsWith("built-in:")) {
368
375
  const presetId = selectedPresetValue.slice("built-in:".length);
@@ -371,11 +378,12 @@ export async function requestInteractiveExecutionRouting(flowEntry, requestUserI
371
378
  throw new TaskRunnerError(`Unknown execution preset '${presetId}'.`);
372
379
  }
373
380
  selectedPreset = { kind: "built-in", presetId, label: preset.label };
374
- routing = resolveExecutionRouting({ presetId });
381
+ routing = resolveExecutionRouting({ presetId, executors: registryContext.executors });
375
382
  }
376
383
  else {
377
384
  selectedPreset = { kind: "custom", label: "Custom" };
378
385
  routing = resolveExecutionRouting({
386
+ executors: registryContext.executors,
379
387
  defaultRoute: {
380
388
  executor: DEFAULT_LAUNCH_PROFILE.executor,
381
389
  model: DEFAULT_LAUNCH_PROFILE.model,
@@ -385,7 +393,7 @@ export async function requestInteractiveExecutionRouting(flowEntry, requestUserI
385
393
  let editorDraft = routingEditorDraftFromRouting(routing);
386
394
  let editorValidationMessage;
387
395
  for (;;) {
388
- const previewText = `Preset: ${selectedPreset.label}\n${describeEffectiveRoutingPreview(flowEntry, routing, process.cwd())}`;
396
+ const previewText = `Preset: ${selectedPreset.label}\n${await describeEffectiveRoutingPreview(flowEntry, routing, process.cwd(), registryContext)}`;
389
397
  const actionResult = await requestUserInput(routingActionForm(previewText));
390
398
  const action = String(actionResult.values.action ?? "start");
391
399
  if (action === "cancel") {
@@ -395,7 +403,7 @@ export async function requestInteractiveExecutionRouting(flowEntry, requestUserI
395
403
  saveLastUsedExecutionRouting(flowKey, routing, selectedPreset);
396
404
  return { routing, selectedPreset };
397
405
  }
398
- const routingFormResult = await requestUserInput(advancedRoutingEditorForm(previewGroups, editorDraft, editorValidationMessage));
406
+ const routingFormResult = await requestUserInput(advancedRoutingEditorForm(previewGroups, editorDraft, registryContext, editorValidationMessage));
399
407
  const requestedDefaultRoute = {
400
408
  executor: String(routingFormResult.values.default_route_executor ?? editorDraft.defaultRoute.executor),
401
409
  model: String(routingFormResult.values.default_route_model ?? editorDraft.defaultRoute.model),
@@ -418,7 +426,7 @@ export async function requestInteractiveExecutionRouting(flowEntry, requestUserI
418
426
  ];
419
427
  })),
420
428
  };
421
- const normalizedDraft = normalizeEditableRoutingDraft(requestedDraft);
429
+ const normalizedDraft = normalizeEditableRoutingDraft(requestedDraft, registryContext.executors);
422
430
  editorDraft = normalizedDraft.draft;
423
431
  if (normalizedDraft.validationErrors.length > 0) {
424
432
  editorValidationMessage = normalizedDraft.validationErrors.join("\n");
@@ -426,6 +434,7 @@ export async function requestInteractiveExecutionRouting(flowEntry, requestUserI
426
434
  }
427
435
  try {
428
436
  routing = resolveExecutionRouting({
437
+ executors: registryContext.executors,
429
438
  defaultRoute: {
430
439
  executor: normalizedDraft.draft.defaultRoute.executor,
431
440
  model: normalizedDraft.draft.defaultRoute.model,
@@ -0,0 +1,50 @@
1
+ {
2
+ "kind": "claude-example-flow",
3
+ "version": 1,
4
+ "description": "Project-local reference flow for the Claude example plugin.",
5
+ "phases": [
6
+ {
7
+ "id": "example",
8
+ "steps": [
9
+ {
10
+ "id": "run_claude_prompt",
11
+ "node": "llm-prompt",
12
+ "prompt": {
13
+ "inlineTemplate": "Use the current workspace. Create the JSON file `.agentweaver/.artifacts/examples/claude-example-proof.json` with exactly this shape: `{ \"status\": \"ok\", \"executor\": \"claude\", \"message\": \"<one short sentence proving the Claude example plugin executed through llm-prompt>\", \"model\": \"<the exact model name you used, or an empty string if unavailable>\" }`. Write valid JSON only to that file. After the file is written, reply with exactly `wrote .agentweaver/.artifacts/examples/claude-example-proof.json`."
14
+ },
15
+ "params": {
16
+ "labelText": {
17
+ "const": "Run Claude via llm-prompt"
18
+ },
19
+ "executor": {
20
+ "const": "claude"
21
+ },
22
+ "requiredArtifacts": {
23
+ "list": [
24
+ {
25
+ "const": ".agentweaver/.artifacts/examples/claude-example-proof.json"
26
+ }
27
+ ]
28
+ },
29
+ "missingArtifactsMessage": {
30
+ "const": "The Claude example flow requires Claude to write the fixed proof artifact."
31
+ }
32
+ },
33
+ "expect": [
34
+ {
35
+ "kind": "require-artifacts",
36
+ "paths": {
37
+ "list": [
38
+ {
39
+ "const": ".agentweaver/.artifacts/examples/claude-example-proof.json"
40
+ }
41
+ ]
42
+ },
43
+ "message": "The Claude example flow must write the fixed proof artifact."
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }
@@ -0,0 +1,149 @@
1
+ import { AGENTWEAVER_PLUGIN_SDK_VERSION } from "agentweaver/plugin-sdk";
2
+
3
+ export const CLAUDE_EXAMPLE_PLUGIN_SDK_VERSION = AGENTWEAVER_PLUGIN_SDK_VERSION;
4
+ export const CLAUDE_EXECUTOR_ID = "claude";
5
+ export const CLAUDE_AUTH_STATUS_ERROR = "Claude CLI authentication is required. Run 'claude auth status' and sign in before using the example flow.";
6
+ export const CLAUDE_NORMALIZATION_ERROR =
7
+ "Claude JSON normalization failed: no supported assistant text was found in result, message.content[*].text, or content[*].text.";
8
+
9
+ function nonEmptyString(value) {
10
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
11
+ }
12
+
13
+ function resolveSetting(override, env, envKey, defaultValue) {
14
+ const direct = nonEmptyString(typeof override === "number" ? String(override) : override);
15
+ if (direct) {
16
+ return direct;
17
+ }
18
+ const fromEnv = nonEmptyString(env?.[envKey]);
19
+ if (fromEnv) {
20
+ return fromEnv;
21
+ }
22
+ return nonEmptyString(defaultValue);
23
+ }
24
+
25
+ function resolveStringListSetting(override, env, envKey, defaultValue) {
26
+ const direct = nonEmptyString(override);
27
+ const envValue = nonEmptyString(env?.[envKey]);
28
+ const effective = direct ?? envValue ?? nonEmptyString(defaultValue);
29
+ if (!effective) {
30
+ return [];
31
+ }
32
+ return effective
33
+ .split(/[,\s]+/)
34
+ .map((item) => item.trim())
35
+ .filter((item) => item.length > 0);
36
+ }
37
+
38
+ function extractTextFragments(items) {
39
+ if (!Array.isArray(items)) {
40
+ return null;
41
+ }
42
+ const fragments = items
43
+ .map((item) => (item && typeof item === "object" ? item.text : undefined))
44
+ .filter((text) => typeof text === "string" && text.trim().length > 0);
45
+ return fragments.length > 0 ? fragments.join("\n") : null;
46
+ }
47
+
48
+ export function normalizeClaudePayload(payload, effectiveModel = "") {
49
+ const resultText = nonEmptyString(payload?.result);
50
+ const messageContentText = extractTextFragments(payload?.message?.content);
51
+ const contentText = extractTextFragments(payload?.content);
52
+ const output = resultText ?? messageContentText ?? contentText;
53
+ if (!output) {
54
+ throw new Error(CLAUDE_NORMALIZATION_ERROR);
55
+ }
56
+ return {
57
+ output,
58
+ model: nonEmptyString(payload?.model) ?? effectiveModel,
59
+ rawResponse: payload,
60
+ };
61
+ }
62
+
63
+ export async function verifyClaudeAuth(runtime, env, config) {
64
+ const command = runtime.resolveCmd(config.defaultCommand, config.commandEnvVar);
65
+ try {
66
+ await runtime.runCommand([command, "auth", "status"], {
67
+ env,
68
+ label: "claude:auth-status",
69
+ printFailureOutput: false,
70
+ });
71
+ } catch {
72
+ throw new Error(CLAUDE_AUTH_STATUS_ERROR);
73
+ }
74
+ return command;
75
+ }
76
+
77
+ export const claudeExecutorDefinition = {
78
+ kind: CLAUDE_EXECUTOR_ID,
79
+ version: 1,
80
+ defaultConfig: {
81
+ commandEnvVar: "CLAUDE_BIN",
82
+ defaultCommand: "claude",
83
+ modelEnvVar: "CLAUDE_MODEL",
84
+ defaultModel: "",
85
+ maxTurnsEnvVar: "CLAUDE_MAX_TURNS",
86
+ defaultMaxTurns: "",
87
+ permissionModeEnvVar: "CLAUDE_PERMISSION_MODE",
88
+ defaultPermissionMode: "bypassPermissions",
89
+ allowedToolsEnvVar: "CLAUDE_ALLOWED_TOOLS",
90
+ defaultAllowedTools: "",
91
+ disallowedToolsEnvVar: "CLAUDE_DISALLOWED_TOOLS",
92
+ defaultDisallowedTools: "",
93
+ addCwdAsAllowedDir: true,
94
+ },
95
+ async execute(context, input, config) {
96
+ const env = input?.env ?? context.env;
97
+ const command = input?.command ?? context.runtime.resolveCmd(config.defaultCommand, config.commandEnvVar);
98
+ const effectiveModel = resolveSetting(input?.model, env, config.modelEnvVar, config.defaultModel);
99
+ const effectiveMaxTurns = resolveSetting(input?.maxTurns, env, config.maxTurnsEnvVar, config.defaultMaxTurns);
100
+ const permissionMode = resolveSetting(undefined, env, config.permissionModeEnvVar, config.defaultPermissionMode);
101
+ const allowedTools = resolveStringListSetting(undefined, env, config.allowedToolsEnvVar, config.defaultAllowedTools);
102
+ const disallowedTools = resolveStringListSetting(undefined, env, config.disallowedToolsEnvVar, config.defaultDisallowedTools);
103
+ const argv = [
104
+ command,
105
+ "-p",
106
+ String(input?.prompt ?? ""),
107
+ "--output-format",
108
+ "json",
109
+ ...(config.addCwdAsAllowedDir ? ["--add-dir", context.cwd] : []),
110
+ ...(permissionMode ? ["--permission-mode", permissionMode] : []),
111
+ ...(allowedTools.length > 0 ? ["--allowedTools", ...allowedTools] : []),
112
+ ...(disallowedTools.length > 0 ? ["--disallowedTools", ...disallowedTools] : []),
113
+ ...(effectiveModel ? ["--model", effectiveModel] : []),
114
+ ...(effectiveMaxTurns ? ["--max-turns", effectiveMaxTurns] : []),
115
+ ];
116
+ const stdout = await context.runtime.runCommand(argv, {
117
+ env,
118
+ dryRun: context.dryRun,
119
+ verbose: context.verbose,
120
+ label: `claude:${effectiveModel || "default"}`,
121
+ printFailureOutput: true,
122
+ });
123
+ let payload;
124
+ try {
125
+ payload = JSON.parse(stdout);
126
+ } catch (error) {
127
+ throw new Error(`Claude CLI returned invalid JSON: ${error.message}`);
128
+ }
129
+ const normalized = normalizeClaudePayload(payload, effectiveModel ?? "");
130
+ return {
131
+ output: normalized.output,
132
+ command,
133
+ model: normalized.model ?? "",
134
+ rawResponse: normalized.rawResponse,
135
+ };
136
+ },
137
+ };
138
+
139
+ export const executors = [
140
+ {
141
+ id: CLAUDE_EXECUTOR_ID,
142
+ definition: claudeExecutorDefinition,
143
+ routing: {
144
+ kind: "llm",
145
+ defaultModel: "sonnet",
146
+ models: ["sonnet", "opus", "haiku"],
147
+ },
148
+ },
149
+ ];
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "claude-example-plugin",
3
+ "sdk_version": 1,
4
+ "entrypoint": "index.js",
5
+ "name": "Claude Example Plugin",
6
+ "version": "0.1.0",
7
+ "description": "Project-local Claude CLI reference plugin for AgentWeaver."
8
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "kind": "claude-example-flow",
3
+ "version": 1,
4
+ "description": "Project-local reference flow for the Claude example plugin.",
5
+ "phases": [
6
+ {
7
+ "id": "example",
8
+ "steps": [
9
+ {
10
+ "id": "run_claude_prompt",
11
+ "node": "llm-prompt",
12
+ "prompt": {
13
+ "inlineTemplate": "Use the current workspace. Create the JSON file `.agentweaver/.artifacts/examples/claude-example-proof.json` with exactly this shape: `{ \"status\": \"ok\", \"executor\": \"claude\", \"message\": \"<one short sentence proving the Claude example plugin executed through llm-prompt>\", \"model\": \"<the exact model name you used, or an empty string if unavailable>\" }`. Write valid JSON only to that file. After the file is written, reply with exactly `wrote .agentweaver/.artifacts/examples/claude-example-proof.json`."
14
+ },
15
+ "params": {
16
+ "labelText": {
17
+ "const": "Run Claude via llm-prompt"
18
+ },
19
+ "executor": {
20
+ "const": "claude"
21
+ },
22
+ "requiredArtifacts": {
23
+ "list": [
24
+ {
25
+ "const": ".agentweaver/.artifacts/examples/claude-example-proof.json"
26
+ }
27
+ ]
28
+ },
29
+ "missingArtifactsMessage": {
30
+ "const": "The Claude example flow requires Claude to write the fixed proof artifact."
31
+ }
32
+ },
33
+ "expect": [
34
+ {
35
+ "kind": "require-artifacts",
36
+ "paths": {
37
+ "list": [
38
+ {
39
+ "const": ".agentweaver/.artifacts/examples/claude-example-proof.json"
40
+ }
41
+ ]
42
+ },
43
+ "message": "The Claude example flow must write the fixed proof artifact."
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }