@sweny-ai/core 0.1.102 → 0.1.104

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/dist/executor.js CHANGED
@@ -17,7 +17,15 @@ import { resolveSources } from "./source-resolver.js";
17
17
  import { evaluateAll, aggregateEval } from "./eval/index.js";
18
18
  import { evaluateRequires } from "./requires.js";
19
19
  import { buildRetryPreamble } from "./retry.js";
20
+ import { resolveExecutionModel } from "./model.js";
20
21
  import { buildToolAliases } from "./skills/index.js";
22
+ /**
23
+ * Default workflow-level step budget. Counts total node executions across the
24
+ * whole run (loop iterations included). Generous enough for legitimate deep
25
+ * workflows with controlled cycles, tight enough to terminate a runaway loop
26
+ * before it burns meaningful model spend.
27
+ */
28
+ export const DEFAULT_MAX_STEPS = 200;
21
29
  /**
22
30
  * Execute a workflow from entry to completion.
23
31
  *
@@ -31,11 +39,13 @@ export async function execute(workflow, input, options) {
31
39
  // Inline skills (instruction/mcp only) become Skill objects with empty tools/config.
32
40
  // The caller's skill map takes precedence — inline skills only fill gaps.
33
41
  const skills = mergeInlineSkills(options.skills, workflow.skills);
34
- const config = resolveConfig(skills, options.config);
42
+ const config = resolveConfig(skills, options.config, options.env ?? process.env);
35
43
  const logger = options.logger ?? consoleLogger;
36
44
  const results = new Map();
37
45
  const edgeCounts = new Map(); // "from→to" → times followed
38
46
  const nodeRunCounts = new Map(); // node → times executed
47
+ const maxSteps = options.max_steps ?? DEFAULT_MAX_STEPS;
48
+ let stepCount = 0; // total node executions across the run (loop iterations included)
39
49
  const trace = { steps: [], edges: [], sources: {} };
40
50
  validate(workflow, skills);
41
51
  // Build an eval-time alias table from the loaded skills. Each skill owns
@@ -86,6 +96,17 @@ export async function execute(workflow, input, options) {
86
96
  const node = workflow.nodes[currentId];
87
97
  if (!node)
88
98
  throw new Error(`Unknown node: "${currentId}"`);
99
+ // Workflow-level hard cap. Per-edge max_iterations bounds individual
100
+ // back-edges, but a self-loop (or LLM route) with no max_iterations is
101
+ // otherwise unbounded. This budget terminates the run loudly instead of
102
+ // hanging CI / the action and burning model spend.
103
+ stepCount++;
104
+ if (stepCount > maxSteps) {
105
+ throw new Error(`step budget exceeded: workflow '${workflow.id}' ran ${stepCount} steps (max_steps: ${maxSteps}). ` +
106
+ `This usually means an unbounded cycle, e.g. a back-edge with no 'max_iterations' or a route that keeps ` +
107
+ `looping. Add 'max_iterations' to the offending edge or raise 'max_steps' if the workflow legitimately ` +
108
+ `needs more steps.`);
109
+ }
89
110
  const iteration = (nodeRunCounts.get(currentId) ?? 0) + 1;
90
111
  nodeRunCounts.set(currentId, iteration);
91
112
  const resolvedInstruction = resolvedSources[`nodes.${currentId}.instruction`].content;
@@ -159,6 +180,9 @@ export async function execute(workflow, input, options) {
159
180
  let result;
160
181
  let currentInstruction = instruction;
161
182
  const retry = node.retry;
183
+ // Per-node execution model: node.model ?? workflow.model. When undefined,
184
+ // claude.run falls back to its own client default (then Claude Code's).
185
+ const nodeModel = resolveExecutionModel(node, workflow);
162
186
  while (true) {
163
187
  result = await claude.run({
164
188
  instruction: currentInstruction,
@@ -167,6 +191,7 @@ export async function execute(workflow, input, options) {
167
191
  outputSchema: node.output,
168
192
  maxTurns: node.max_turns,
169
193
  disallowedTools: node.disallowed_tools,
194
+ model: nodeModel,
170
195
  onProgress: (message) => {
171
196
  safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
172
197
  },
@@ -187,25 +212,35 @@ export async function execute(workflow, input, options) {
187
212
  // validation. Type mismatches, formats, additionalProperties, etc.
188
213
  // stay the workflow author's problem and don't have the same
189
214
  // routing-correctness blast radius.
215
+ // A missing required field is a contract violation handled on the same
216
+ // footing as an eval failure: an otherwise-recoverable one-off omission
217
+ // should self-heal on a second attempt when the node has `retry`
218
+ // configured. We synthesize an eval-shaped failure so the retry preamble
219
+ // and trace bookkeeping below treat it identically. When no retry is
220
+ // configured (or the budget is exhausted), it still hard-fails the node.
190
221
  const missingRequired = findMissingRequiredFields(result.data, node.output);
222
+ let outcome;
191
223
  if (missingRequired.length > 0) {
192
224
  const msg = `output schema violation: required field(s) missing from emitted data: ${missingRequired.join(", ")}`;
193
- result.status = "failed";
194
- result.data = { ...(result.data ?? {}), error: msg };
195
- logger.warn(` ${msg}`, { node: currentId });
196
- break;
225
+ const failure = { name: "output_required", kind: "value", pass: false, reasoning: msg };
226
+ outcome = { pass: false, error: msg, failures: [failure] };
227
+ // Still record the synthesized failure as the node's eval result so
228
+ // observers and downstream context see the contract violation.
229
+ result.evals = [failure];
230
+ }
231
+ else {
232
+ const evalResults = await evaluateAll(node.eval, result, {
233
+ aliases: toolAliases,
234
+ claude,
235
+ node,
236
+ workflow,
237
+ });
238
+ result.evals = evalResults;
239
+ outcome = aggregateEval(evalResults, node.eval_policy ?? "all_pass");
197
240
  }
198
- const evalResults = await evaluateAll(node.eval, result, {
199
- aliases: toolAliases,
200
- claude,
201
- node,
202
- workflow,
203
- });
204
- result.evals = evalResults;
205
- const outcome = aggregateEval(evalResults, node.eval_policy ?? "all_pass");
206
241
  if (outcome.pass)
207
242
  break;
208
- // Apply eval failure to the result.
243
+ // Apply the failure to the result.
209
244
  result.status = "failed";
210
245
  result.data = { ...result.data, error: outcome.error };
211
246
  if (!retry || attempt >= retry.max) {
@@ -225,6 +260,7 @@ export async function execute(workflow, input, options) {
225
260
  claude,
226
261
  logger,
227
262
  context,
263
+ model: nodeModel,
228
264
  });
229
265
  currentInstruction = `${preamble}\n\n---\n\n${instruction}`;
230
266
  safeObserve(observer, { type: "node:retry", node: currentId, attempt: attempt + 1, reason: outcome.error ?? "eval failed", preamble }, logger);
@@ -562,13 +598,18 @@ function resolveSkillInstructions(skillIds, skills) {
562
598
  /**
563
599
  * Resolve config values: check explicit overrides first, then env vars.
564
600
  * Throws if a required field is missing.
601
+ *
602
+ * `env` is the environment map to read `field.env` from. Callers thread
603
+ * `options.env ?? process.env` so an explicit env map is honored consistently
604
+ * with Source resolution (which reads the same map). Defaults to
605
+ * `process.env` when omitted.
565
606
  */
566
- function resolveConfig(skills, overrides) {
607
+ function resolveConfig(skills, overrides, env = process.env) {
567
608
  const config = {};
568
609
  const missing = [];
569
610
  for (const skill of skills.values()) {
570
611
  for (const [key, field] of Object.entries(skill.config)) {
571
- const value = overrides?.[key] ?? (field.env ? process.env[field.env] : undefined);
612
+ const value = overrides?.[key] ?? (field.env ? env[field.env] : undefined);
572
613
  if (value) {
573
614
  config[key] = value;
574
615
  }
@@ -691,14 +732,36 @@ async function resolveNext(workflow, current, results, input, claude, observer,
691
732
  context,
692
733
  choices,
693
734
  });
694
- // Validate that Claude returned a valid target
735
+ // Validate that Claude returned a valid target.
695
736
  const validTargets = new Set(outEdges.map((e) => e.to));
696
- const resolved = validTargets.has(chosen) ? chosen : (defaultEdge?.to ?? outEdges[0].to); // fall back to default or first edge
737
+ if (!validTargets.has(chosen)) {
738
+ // The evaluator returned a target that is not a live out-edge. If there's
739
+ // an explicit default (unconditional) edge, take it as the documented
740
+ // fallback. Otherwise this is a hard error: silently jumping to
741
+ // `outEdges[0]` (often the loop-back edge) launders garbage model output
742
+ // into a plausible-looking route and, combined with an unbounded cycle,
743
+ // produces an infinite loop. Stop loudly instead.
744
+ if (!defaultEdge) {
745
+ logger?.warn(` route eval: evaluator returned invalid target '${chosen}' for node '${current}' and there is no ` +
746
+ `default (unconditional) edge; terminating this branch. Valid targets: ${[...validTargets].join(", ")}`, { node: current, chosen, validTargets: [...validTargets] });
747
+ safeObserve(observer, {
748
+ type: "route",
749
+ from: current,
750
+ to: "(end)",
751
+ reason: `invalid route target '${chosen}' with no default edge`,
752
+ }, logger);
753
+ return null;
754
+ }
755
+ }
756
+ const resolved = validTargets.has(chosen) ? chosen : defaultEdge.to;
697
757
  // Track edge usage for max_iterations
698
758
  if (edgeCounts) {
699
759
  const key = `${current}→${resolved}`;
700
760
  edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);
701
761
  }
762
+ // Report the matched choice's description. The `?? "default"` fallback now
763
+ // only fires when a default edge genuinely exists (the no-default invalid
764
+ // case returned null above), so it can no longer mislabel a garbage route.
702
765
  safeObserve(observer, {
703
766
  type: "route",
704
767
  from: current,
package/dist/index.d.ts CHANGED
@@ -21,18 +21,22 @@
21
21
  * })
22
22
  * ```
23
23
  */
24
- export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, } from "./types.js";
24
+ export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
25
25
  export { WORKFLOW_INPUT_TYPES } from "./types.js";
26
+ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
26
27
  export { consoleLogger } from "./types.js";
27
28
  export { execute } from "./executor.js";
28
29
  export type { ExecuteOptions } from "./executor.js";
29
- export { ClaudeClient } from "./claude.js";
30
- export type { ClaudeClientOptions } from "./claude.js";
30
+ export { ClaudeClient, resolveAuthEnv } from "./claude.js";
31
+ export type { ClaudeClientOptions, SwenyAuthMode, ResolveAuthEnvOpts } from "./claude.js";
32
+ export { resolveExecutionModel } from "./model.js";
31
33
  export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
32
34
  export type { SkillValidationResult } from "./skills/index.js";
33
35
  export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
34
- export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, workflowInputsZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "./schema.js";
36
+ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, workflowInputsZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
35
37
  export type { WorkflowError } from "./schema.js";
38
+ export { loadAndValidateWorkflow, validateParsed } from "./loader.js";
39
+ export type { LoaderResult, LoaderError, LoaderOptions } from "./loader.js";
36
40
  export { validateRuntimeInput, summarizeInputShape } from "./inputs.js";
37
41
  export { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "./mcp.js";
38
42
  export type { ProviderContextOptions, SkillMcpOptions } from "./mcp.js";
package/dist/index.js CHANGED
@@ -22,17 +22,25 @@
22
22
  * ```
23
23
  */
24
24
  export { WORKFLOW_INPUT_TYPES } from "./types.js";
25
+ // Runtime enum constants + skill-id helpers
26
+ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
25
27
  export { consoleLogger } from "./types.js";
26
28
  // Executor
27
29
  export { execute } from "./executor.js";
28
30
  // Claude client
29
- export { ClaudeClient } from "./claude.js";
31
+ export { ClaudeClient, resolveAuthEnv } from "./claude.js";
32
+ // Execution model resolution
33
+ export { resolveExecutionModel } from "./model.js";
30
34
  // Skills
31
35
  export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
32
36
  // Node-only skill discovery (filesystem-based)
33
37
  export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
34
38
  // Schema & validation
35
- export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, workflowInputsZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "./schema.js";
39
+ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, workflowInputsZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
40
+ // Loader — canonical read → parse → structural-validate pipeline (the path
41
+ // the CLI uses for `workflow run`, `workflow validate`, and `publish`).
42
+ // Node-only: loader.ts imports `node:fs`, so this is NOT mirrored in browser.ts.
43
+ export { loadAndValidateWorkflow, validateParsed } from "./loader.js";
36
44
  // Workflow input validation
37
45
  export { validateRuntimeInput, summarizeInputShape } from "./inputs.js";
38
46
  // MCP auto-injection
package/dist/inputs.js CHANGED
@@ -161,7 +161,16 @@ export function validateRuntimeInput(declared, raw) {
161
161
  // and the cascade `rules`/`context` mechanism inject extra keys into
162
162
  // the input bag. The declaration narrows the contract for documented
163
163
  // fields without locking down the whole object.
164
+ //
165
+ // Prototype-pollution guard: `raw` is `unknown` and callers (Studio,
166
+ // programmatic scripts) may hand us hand-built objects, or `JSON.parse`
167
+ // output containing an enumerable own `__proto__` key. Writing such a
168
+ // key with `out[k] = v` would reassign the result's prototype. Skip the
169
+ // three dangerous keys so untrusted input can never touch the prototype
170
+ // chain. `out` stays a normal object literal for downstream consumers.
164
171
  for (const [k, v] of Object.entries(provided)) {
172
+ if (isUnsafeKey(k))
173
+ continue;
165
174
  if (!Object.prototype.hasOwnProperty.call(declared, k)) {
166
175
  out[k] = v;
167
176
  }
@@ -195,6 +204,15 @@ export function summarizeInputShape(declared, resolved) {
195
204
  return shape;
196
205
  }
197
206
  // ─── Internals ──────────────────────────────────────────────────
207
+ /**
208
+ * Keys that must never be copied from untrusted input onto a plain object,
209
+ * because assigning them mutates the prototype chain rather than adding an
210
+ * own property. `JSON.parse('{"__proto__":{...}}')` produces an enumerable
211
+ * own `__proto__` key that `Object.entries` surfaces.
212
+ */
213
+ function isUnsafeKey(key) {
214
+ return key === "__proto__" || key === "constructor" || key === "prototype";
215
+ }
198
216
  function checkValueType(value, type) {
199
217
  switch (type) {
200
218
  case "string":
package/dist/loader.js CHANGED
@@ -13,7 +13,7 @@ import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
14
  import { parse as parseYaml } from "yaml";
15
15
  import { ZodError } from "zod";
16
- import { workflowZ, validateWorkflow } from "./schema.js";
16
+ import { workflowZ, validateWorkflow, preflightLegacyVerify } from "./schema.js";
17
17
  /**
18
18
  * Load a workflow YAML or JSON file, parse with the canonical Zod schema,
19
19
  * and run structural validation. All three phases contribute to `errors`.
@@ -47,6 +47,20 @@ export function validateParsed(raw, options = {}) {
47
47
  const kind = raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw;
48
48
  return { ok: false, errors: [{ message: `Expected a workflow object, got ${kind}`, code: "SCHEMA" }] };
49
49
  }
50
+ // Detect legacy `verify:` blocks (renamed to `eval:` in v0.2.0) before the
51
+ // Zod parse. workflowZ is intentionally non-strict at the top level, so a
52
+ // top-level `verify:` would be silently STRIPPED instead of surfacing the
53
+ // migration message. parseWorkflow runs this preflight already; validateParsed
54
+ // (the entrypoint for loadAndValidateWorkflow / Studio import) must too. The
55
+ // loader contract is "return errors, do not throw", so map the throw to a
56
+ // SCHEMA LoaderError rather than letting it propagate.
57
+ try {
58
+ preflightLegacyVerify(raw);
59
+ }
60
+ catch (err) {
61
+ const msg = err instanceof Error ? err.message : String(err);
62
+ return { ok: false, errors: [{ message: msg, code: "SCHEMA" }] };
63
+ }
50
64
  let parsed;
51
65
  try {
52
66
  parsed = workflowZ.parse(raw);
@@ -0,0 +1,11 @@
1
+ import type { Node, Workflow } from "./types.js";
2
+ /**
3
+ * Resolve the execution model for a node: `node.model ?? workflow.model`.
4
+ *
5
+ * Returns `undefined` when neither is set. The client default (if any) is
6
+ * applied one layer down, inside `ClaudeClient.run` (`model ?? this.model`),
7
+ * so the full three-layer cascade is node → workflow → client default →
8
+ * Claude Code default. Callers SHOULD pass `undefined` through to `run`
9
+ * rather than substituting a hardcoded model.
10
+ */
11
+ export declare function resolveExecutionModel(node: Node | undefined, workflow: Workflow | undefined): string | undefined;
package/dist/model.js ADDED
@@ -0,0 +1,19 @@
1
+ // ─── Execution model resolution ─────────────────────────────────────
2
+ //
3
+ // Mirrors the judge-model cascade in eval/index.ts, but for the model that
4
+ // executes a node (not the model that evaluates it). Unlike the judge
5
+ // resolver, there is NO package-level default: when no layer specifies a
6
+ // model, this returns undefined so the executor emits no SDK `model` option
7
+ // and Claude Code's own default applies.
8
+ /**
9
+ * Resolve the execution model for a node: `node.model ?? workflow.model`.
10
+ *
11
+ * Returns `undefined` when neither is set. The client default (if any) is
12
+ * applied one layer down, inside `ClaudeClient.run` (`model ?? this.model`),
13
+ * so the full three-layer cascade is node → workflow → client default →
14
+ * Claude Code default. Callers SHOULD pass `undefined` through to `run`
15
+ * rather than substituting a hardcoded model.
16
+ */
17
+ export function resolveExecutionModel(node, workflow) {
18
+ return node?.model ?? workflow?.model;
19
+ }
package/dist/retry.d.ts CHANGED
@@ -12,6 +12,11 @@ export interface BuildRetryPreambleOptions {
12
12
  claude: Claude;
13
13
  logger: Logger;
14
14
  context: Record<string, unknown>;
15
+ /**
16
+ * Per-node execution model. The reflection reasons about this node, so it
17
+ * runs on the node's model. When undefined, the client default applies.
18
+ */
19
+ model?: string;
15
20
  }
16
21
  /**
17
22
  * Build the retry preamble for a single retry attempt.
package/dist/retry.js CHANGED
@@ -21,7 +21,7 @@ const DEFAULT_PREAMBLE_HEADER = "## Previous attempt failed evaluation";
21
21
  * default static preamble and logs a warning.
22
22
  */
23
23
  export async function buildRetryPreamble(opts) {
24
- const { retry, evalFailures, toolCalls, nodeInstruction, claude, logger, context } = opts;
24
+ const { retry, evalFailures, toolCalls, nodeInstruction, claude, logger, context, model } = opts;
25
25
  const failureList = formatEvalFailures(evalFailures);
26
26
  const inst = retry.instruction;
27
27
  if (typeof inst === "string") {
@@ -31,7 +31,7 @@ export async function buildRetryPreamble(opts) {
31
31
  const reflectPrompt = "reflect" in inst && typeof inst.reflect === "string" ? inst.reflect : DEFAULT_REFLECTION_PROMPT;
32
32
  const askInstruction = buildReflectionPrompt(reflectPrompt, nodeInstruction, failureList, toolCalls);
33
33
  try {
34
- const diagnosis = await claude.ask({ instruction: askInstruction, context });
34
+ const diagnosis = await claude.ask({ instruction: askInstruction, context, model });
35
35
  const trimmed = diagnosis.trim();
36
36
  if (trimmed.length > 0) {
37
37
  return `## Reflection on previous attempt\n\n${trimmed}\n\n${DEFAULT_PREAMBLE_HEADER}\n\n${failureList}`;