@sweny-ai/core 0.1.161 → 0.1.163
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/browser.d.ts +3 -1
- package/dist/browser.js +2 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +7 -2
- package/dist/cli/run-history.d.ts +6 -0
- package/dist/cli/run-history.js +10 -1
- package/dist/cli/run-output.d.ts +11 -1
- package/dist/cli/run-output.js +19 -2
- package/dist/decider.d.ts +144 -0
- package/dist/decider.js +260 -0
- package/dist/executor.d.ts +7 -0
- package/dist/executor.js +115 -11
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/mermaid.js +4 -2
- package/dist/schema.d.ts +243 -38
- package/dist/schema.js +136 -3
- package/dist/studio.d.ts +2 -2
- package/dist/types.d.ts +24 -2
- package/dist/when.d.ts +114 -0
- package/dist/when.js +581 -0
- package/dist/workflows/browser.js +1 -1
- package/dist/workflows/triage.yml +15 -4
- package/package.json +1 -1
package/dist/executor.js
CHANGED
|
@@ -16,6 +16,7 @@ import { consoleLogger } from "./types.js";
|
|
|
16
16
|
import { resolveSources } from "./source-resolver.js";
|
|
17
17
|
import { evaluateAll, aggregateEval } from "./eval/index.js";
|
|
18
18
|
import { evaluateRequires } from "./requires.js";
|
|
19
|
+
import { evaluateExpression, isWhenExpression, parseExpression, whenLabel } from "./when.js";
|
|
19
20
|
import { buildRetryPreamble } from "./retry.js";
|
|
20
21
|
import { resolveExecutionModel } from "./model.js";
|
|
21
22
|
import { buildToolAliases } from "./skills/index.js";
|
|
@@ -25,6 +26,7 @@ import { fenceUntrusted } from "./untrusted.js";
|
|
|
25
26
|
import { asClaude } from "./harness/compat.js";
|
|
26
27
|
import { budgetGate, isToolClass, policyGate, resolveHarnessPolicy } from "./harness/policy.js";
|
|
27
28
|
import { BudgetGuard, describeOverrun, minLimits, toLimits } from "./budget.js";
|
|
29
|
+
import { createShadowDecider, finishShadowDecision, startShadowDecision } from "./decider.js";
|
|
28
30
|
import { buildNodePolicy, resolveNodePermissions } from "./node-policy.js";
|
|
29
31
|
import { applySafeOutputs, createEmitOutputTool, createWriteStageState, resolveActor, safeOutputsInstruction, unresolvedOutputs, SAFE_OUTPUT_SCREEN_INSTRUCTION, } from "./safe-outputs.js";
|
|
30
32
|
/**
|
|
@@ -87,6 +89,10 @@ export async function execute(workflow, input, options) {
|
|
|
87
89
|
// `budget` and the caller's (`--max-tokens`, `--max-cost`).
|
|
88
90
|
const budgetGuard = new BudgetGuard(minLimits(toLimits(workflow.budget), toLimits(options.budget)));
|
|
89
91
|
const harnessPolicy = options.harnessPolicy ?? resolveHarnessPolicy(runEnv);
|
|
92
|
+
// Decision model (#357): shadow only. Null (the default) means no HTTP at all.
|
|
93
|
+
const shadow = createShadowDecider(workflow.decider, options.decider, runEnv, (m) => logger.warn(m));
|
|
94
|
+
if (shadow)
|
|
95
|
+
trace.decisions = shadow.records;
|
|
90
96
|
// Build an eval-time alias table from the loaded skills. Each skill owns
|
|
91
97
|
// its own mapping between skill-tool names and equivalent MCP names. Core
|
|
92
98
|
// stays vendor-neutral; this call just unions what the skills declare.
|
|
@@ -253,7 +259,7 @@ export async function execute(workflow, input, options) {
|
|
|
253
259
|
}
|
|
254
260
|
// Apply normal routing rules (dry run gate + resolveNext).
|
|
255
261
|
// TODO: dedupe with requires path — see advanceFromNode helper below
|
|
256
|
-
const next = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, { signal, timeoutMs });
|
|
262
|
+
const next = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, { signal, timeoutMs, shadow });
|
|
257
263
|
currentId = next;
|
|
258
264
|
continue;
|
|
259
265
|
}
|
|
@@ -590,6 +596,7 @@ export async function execute(workflow, input, options) {
|
|
|
590
596
|
currentId = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, {
|
|
591
597
|
signal,
|
|
592
598
|
timeoutMs,
|
|
599
|
+
shadow,
|
|
593
600
|
});
|
|
594
601
|
}
|
|
595
602
|
safeObserve(observer, {
|
|
@@ -1118,15 +1125,17 @@ function resolveConfig(skills, overrides, env = process.env) {
|
|
|
1118
1125
|
* Returns the next node ID, or null when execution should stop.
|
|
1119
1126
|
*/
|
|
1120
1127
|
async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, abort) {
|
|
1121
|
-
// Dry run path gate: stop at the first
|
|
1128
|
+
// Dry run path gate: stop at the first natural-language routing decision.
|
|
1122
1129
|
// Safety does not depend on this (#380): under dry-run every node already
|
|
1123
|
-
// runs read-only (see execute()). The stop keeps
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1130
|
+
// runs read-only (see execute()). The stop keeps dry-run routing free of
|
|
1131
|
+
// model route evaluation, so the path is reproducible from the node outputs
|
|
1132
|
+
// alone. Expression edges (#461) are evaluated by sweny with no model call,
|
|
1133
|
+
// so dry-run follows them: a preview can reach the nodes past a
|
|
1134
|
+
// deterministic branch (still read-only, outputs staged) and stops only
|
|
1135
|
+
// where a model would have to pick the branch.
|
|
1127
1136
|
if (isDryRunInput(input)) {
|
|
1128
1137
|
const outEdges = workflow.edges.filter((e) => e.from === currentId);
|
|
1129
|
-
if (outEdges.some((e) => e.when)) {
|
|
1138
|
+
if (outEdges.some((e) => e.when && !isWhenExpression(e.when))) {
|
|
1130
1139
|
safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "dry run" }, logger);
|
|
1131
1140
|
return null;
|
|
1132
1141
|
}
|
|
@@ -1134,7 +1143,7 @@ async function advanceFromNode(workflow, currentId, results, input, claude, obse
|
|
|
1134
1143
|
const prevId = currentId;
|
|
1135
1144
|
const nextId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger, abort);
|
|
1136
1145
|
if (nextId) {
|
|
1137
|
-
const reason = workflow.edges.find((e) => e.from === prevId && e.to === nextId)?.when ?? "only path";
|
|
1146
|
+
const reason = whenLabel(workflow.edges.find((e) => e.from === prevId && e.to === nextId)?.when) ?? "only path";
|
|
1138
1147
|
trace.edges.push({ from: prevId, to: nextId, reason });
|
|
1139
1148
|
}
|
|
1140
1149
|
return nextId;
|
|
@@ -1144,7 +1153,8 @@ async function advanceFromNode(workflow, currentId, results, input, claude, obse
|
|
|
1144
1153
|
*
|
|
1145
1154
|
* - 0 out-edges → terminal (return null)
|
|
1146
1155
|
* - 1 unconditional edge → follow it
|
|
1147
|
-
* -
|
|
1156
|
+
* - Every conditional edge is an `{ expr }` expression → sweny evaluates them, no model call
|
|
1157
|
+
* - Otherwise multiple or conditional → Claude evaluates
|
|
1148
1158
|
*
|
|
1149
1159
|
* Edges with max_iterations are filtered out once exhausted.
|
|
1150
1160
|
*/
|
|
@@ -1192,6 +1202,12 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
1192
1202
|
safeObserve(observer, { type: "route", from: current, to: defaultEdge.to, reason: "only path" }, logger);
|
|
1193
1203
|
return defaultEdge.to;
|
|
1194
1204
|
}
|
|
1205
|
+
// Deterministic routing (#461): every conditional out-edge is an expression,
|
|
1206
|
+
// so sweny decides without a model call. validateWorkflow rejects a node
|
|
1207
|
+
// that mixes expression and natural-language edges.
|
|
1208
|
+
if (conditionalEdges.every((e) => isWhenExpression(e.when))) {
|
|
1209
|
+
return resolveByExpressions(workflow, current, results, conditionalEdges, defaultEdge, observer, edgeCounts, logger);
|
|
1210
|
+
}
|
|
1195
1211
|
// Claude evaluates which condition matches. Include input so conditions
|
|
1196
1212
|
// can reference workflow-level flags like dryRun, and expose evals so
|
|
1197
1213
|
// routing edges can read `priorNode.evals.X.pass`.
|
|
@@ -1233,18 +1249,36 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
1233
1249
|
}
|
|
1234
1250
|
const choices = conditionalEdges.map((e) => ({
|
|
1235
1251
|
id: e.to,
|
|
1236
|
-
description: e.when,
|
|
1252
|
+
description: whenLabel(e.when),
|
|
1237
1253
|
}));
|
|
1238
1254
|
if (defaultEdge) {
|
|
1239
1255
|
choices.push({ id: defaultEdge.to, description: "None of the above / default path" });
|
|
1240
1256
|
}
|
|
1257
|
+
const question = "Based on the results so far, which condition is true?";
|
|
1258
|
+
// Shadow mode (#357): the decider gets the same question in parallel. Its
|
|
1259
|
+
// promise never rejects and its answer is only logged, so the route below
|
|
1260
|
+
// is the agent's in every case.
|
|
1261
|
+
const pending = abort?.shadow
|
|
1262
|
+
? startShadowDecision(abort.shadow.provider, { question, state: context, choices, signal: abort.signal })
|
|
1263
|
+
: undefined;
|
|
1241
1264
|
const chosen = await claude.evaluate({
|
|
1242
|
-
question
|
|
1265
|
+
question,
|
|
1243
1266
|
context,
|
|
1244
1267
|
choices,
|
|
1245
1268
|
signal: abort?.signal,
|
|
1246
1269
|
timeoutMs: abort?.timeoutMs,
|
|
1247
1270
|
});
|
|
1271
|
+
if (pending && abort?.shadow) {
|
|
1272
|
+
try {
|
|
1273
|
+
const rec = await finishShadowDecision(pending, current, chosen);
|
|
1274
|
+
abort.shadow.records.push(rec);
|
|
1275
|
+
const verdict = rec.outcome === "compared" ? (rec.agree ? "agreed" : "disagreed") : `fell through (${rec.reason})`;
|
|
1276
|
+
logger?.info(` decider (shadow): node '${current}' ${verdict}`, { ...rec });
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
// shadow logging must never affect a route
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1248
1282
|
// Fail closed. `evaluate` returns null when the routing decision could not
|
|
1249
1283
|
// be made (SDK error, timeout, non-success subtype, or an unparseable
|
|
1250
1284
|
// answer). We must NOT fall through to a conditional edge: on a node with a
|
|
@@ -1306,6 +1340,76 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
1306
1340
|
}, logger);
|
|
1307
1341
|
return resolved;
|
|
1308
1342
|
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Pick the out-edge whose `{ expr }` condition is true, with no model call
|
|
1345
|
+
* (#461). Exactly one true edge is taken. None true takes the unconditional
|
|
1346
|
+
* default edge when there is one. Otherwise (no edge true and no default, or
|
|
1347
|
+
* two or more true) it fails closed with a RouteEvaluationError, the same
|
|
1348
|
+
* contract as a failed model route evaluation: sweny never guesses an edge.
|
|
1349
|
+
*
|
|
1350
|
+
* An expression reads only successful nodes' outputs. A missing field, or a
|
|
1351
|
+
* node that did not run or did not succeed, makes that expression false and
|
|
1352
|
+
* is logged as a warning, never a silent true.
|
|
1353
|
+
*/
|
|
1354
|
+
function resolveByExpressions(workflow, current, results, conditionalEdges, defaultEdge, observer, edgeCounts, logger) {
|
|
1355
|
+
const scope = {};
|
|
1356
|
+
for (const [id, r] of results.entries()) {
|
|
1357
|
+
if (r.status === "success")
|
|
1358
|
+
scope[id] = buildPriorNodeContext(r);
|
|
1359
|
+
}
|
|
1360
|
+
const matched = [];
|
|
1361
|
+
for (const edge of conditionalEdges) {
|
|
1362
|
+
const expr = whenLabel(edge.when);
|
|
1363
|
+
let outcome;
|
|
1364
|
+
try {
|
|
1365
|
+
outcome = evaluateExpression(parseExpression(expr), scope);
|
|
1366
|
+
}
|
|
1367
|
+
catch (err) {
|
|
1368
|
+
// validateWorkflow parses every expression before a run, so this only
|
|
1369
|
+
// fires for a workflow that skipped validation. Fail closed.
|
|
1370
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1371
|
+
safeObserve(observer, { type: "route", from: current, to: "(end)", reason: `invalid when expression` }, logger);
|
|
1372
|
+
throw new RouteEvaluationError(current, `invalid when expression on edge '${current}' -> '${edge.to}': ${msg}`);
|
|
1373
|
+
}
|
|
1374
|
+
if (outcome.problem) {
|
|
1375
|
+
logger?.warn(` route expr: '${current}' -> '${edge.to}' is false: ${outcome.problem} (${expr})`, {
|
|
1376
|
+
node: current,
|
|
1377
|
+
to: edge.to,
|
|
1378
|
+
});
|
|
1379
|
+
safeObserve(observer, {
|
|
1380
|
+
type: "node:warning",
|
|
1381
|
+
node: current,
|
|
1382
|
+
reason: `when expression on edge to '${edge.to}' evaluated false: ${outcome.problem}`,
|
|
1383
|
+
fields: [],
|
|
1384
|
+
}, logger);
|
|
1385
|
+
}
|
|
1386
|
+
if (outcome.value)
|
|
1387
|
+
matched.push({ to: edge.to, expr });
|
|
1388
|
+
}
|
|
1389
|
+
const take = (to, reason) => {
|
|
1390
|
+
if (edgeCounts) {
|
|
1391
|
+
const key = `${current}→${to}`;
|
|
1392
|
+
edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);
|
|
1393
|
+
}
|
|
1394
|
+
safeObserve(observer, { type: "route", from: current, to, reason }, logger);
|
|
1395
|
+
return to;
|
|
1396
|
+
};
|
|
1397
|
+
if (matched.length === 1)
|
|
1398
|
+
return take(matched[0].to, matched[0].expr);
|
|
1399
|
+
if (matched.length === 0 && defaultEdge) {
|
|
1400
|
+
logger?.info(` route expr: no expression matched for '${current}'; taking default edge '${defaultEdge.to}'`, {
|
|
1401
|
+
node: current,
|
|
1402
|
+
});
|
|
1403
|
+
return take(defaultEdge.to, "no expression matched; default edge");
|
|
1404
|
+
}
|
|
1405
|
+
const why = matched.length === 0
|
|
1406
|
+
? `no when expression on node '${current}' is true and there is no default (unconditional) edge`
|
|
1407
|
+
: `${matched.length} when expressions on node '${current}' are true (to ${matched.map((m) => m.to).join(", ")}); ` +
|
|
1408
|
+
`exactly one may match`;
|
|
1409
|
+
logger?.error(` route expr: ${why}; refusing to guess. Terminating the run.`, { node: current });
|
|
1410
|
+
safeObserve(observer, { type: "route", from: current, to: "(end)", reason: why }, logger);
|
|
1411
|
+
throw new RouteEvaluationError(current, `${why}; refusing to guess`);
|
|
1412
|
+
}
|
|
1309
1413
|
/**
|
|
1310
1414
|
* Validate a workflow definition before execution.
|
|
1311
1415
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -21,12 +21,14 @@
|
|
|
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, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, SafeOutputPin, AuthorAssociation, } from "./types.js";
|
|
24
|
+
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, 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, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, SafeOutputPin, AuthorAssociation, } from "./types.js";
|
|
25
25
|
export { WORKFLOW_INPUT_TYPES } from "./types.js";
|
|
26
26
|
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, NODE_ACCESS, TOOL_CLASSES, SAFE_OUTPUT_TYPES, SAFE_OUTPUT_APPLIERS, AUTHOR_ASSOCIATIONS, } from "./types.js";
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
|
28
28
|
export { execute, RouteEvaluationError } from "./executor.js";
|
|
29
29
|
export type { ExecuteOptions } from "./executor.js";
|
|
30
|
+
export { systemOneProvider, DecideError, gateVerdict, DECIDER_MIN_CONFIDENCE, DECIDER_MIN_MARGIN } from "./decider.js";
|
|
31
|
+
export type { DecisionProvider, DeciderConfig, DeciderMode, DeciderRecord, SystemOneOptions } from "./decider.js";
|
|
30
32
|
export { BudgetGuard, describeOverrun, minLimits, toLimits } from "./budget.js";
|
|
31
33
|
export type { Budget, BudgetOverrun, BudgetUnit, SpendLimits } from "./budget.js";
|
|
32
34
|
export { resolveNodePermissions, buildNodePolicy } from "./node-policy.js";
|
|
@@ -43,6 +45,8 @@ export type { SkillValidationResult } from "./skills/index.js";
|
|
|
43
45
|
export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
|
|
44
46
|
export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, workflowInputsZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
|
|
45
47
|
export type { WorkflowError } from "./schema.js";
|
|
48
|
+
export { isWhenExpression, whenLabel, parseExpression, evaluateExpression, checkExpression, ExpressionSyntaxError, } from "./when.js";
|
|
49
|
+
export type { ExprNode, ExpressionScope, ExpressionResult } from "./when.js";
|
|
46
50
|
export { loadAndValidateWorkflow, validateParsed } from "./loader.js";
|
|
47
51
|
export type { LoaderResult, LoaderError, LoaderOptions } from "./loader.js";
|
|
48
52
|
export { CURRENT_SPEC_VERSION, MIGRATIONS, migrateWorkflow } from "./migrations.js";
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,8 @@ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRA
|
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
|
28
28
|
// Executor
|
|
29
29
|
export { execute, RouteEvaluationError } from "./executor.js";
|
|
30
|
+
// Decision models, shadow mode (#357)
|
|
31
|
+
export { systemOneProvider, DecideError, gateVerdict, DECIDER_MIN_CONFIDENCE, DECIDER_MIN_MARGIN } from "./decider.js";
|
|
30
32
|
// Least-privilege nodes and safe outputs (#365)
|
|
31
33
|
export { BudgetGuard, describeOverrun, minLimits, toLimits } from "./budget.js";
|
|
32
34
|
export { resolveNodePermissions, buildNodePolicy } from "./node-policy.js";
|
|
@@ -43,6 +45,8 @@ export { github, linear, slack, sentry, datadog, notification, supabase, builtin
|
|
|
43
45
|
export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
|
|
44
46
|
// Schema & validation
|
|
45
47
|
export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, workflowInputsZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
|
|
48
|
+
// Deterministic `when` expressions (#461). Pure, browser-safe.
|
|
49
|
+
export { isWhenExpression, whenLabel, parseExpression, evaluateExpression, checkExpression, ExpressionSyntaxError, } from "./when.js";
|
|
46
50
|
// Loader — canonical read → parse → structural-validate pipeline (the path
|
|
47
51
|
// the CLI uses for `workflow run`, `workflow validate`, and `publish`).
|
|
48
52
|
// Node-only: loader.ts imports `node:fs`, so this is NOT mirrored in browser.ts.
|
package/dist/mermaid.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* (current/success/failed/skipped) and optional execution trace
|
|
10
10
|
* to highlight which edges were taken and show loop iterations.
|
|
11
11
|
*/
|
|
12
|
+
import { whenLabel } from "./when.js";
|
|
12
13
|
/**
|
|
13
14
|
* Convert a Workflow to a Mermaid flowchart diagram string.
|
|
14
15
|
*
|
|
@@ -102,8 +103,9 @@ export function toMermaid(workflow, options = {}) {
|
|
|
102
103
|
const takenCount = edgeTakenCounts.get(key) ?? 0;
|
|
103
104
|
// Build label parts
|
|
104
105
|
const labelParts = [];
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
const when = whenLabel(edge.when);
|
|
107
|
+
if (when)
|
|
108
|
+
labelParts.push(when);
|
|
107
109
|
if (edge.max_iterations)
|
|
108
110
|
labelParts.push(`max ${edge.max_iterations}x`);
|
|
109
111
|
// Show how many times this edge was actually followed
|