@sweny-ai/core 0.1.161 → 0.1.162
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/run-output.js +1 -1
- package/dist/executor.js +89 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/mermaid.js +4 -2
- package/dist/schema.d.ts +53 -12
- package/dist/schema.js +85 -3
- package/dist/studio.d.ts +2 -2
- package/dist/types.d.ts +16 -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/browser.d.ts
CHANGED
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
* - `buildWorkflow` / `refineWorkflow` — depend on the Claude interface.
|
|
14
14
|
* - Testing helpers — `MockClaude`, `createFileSkill` use `node:fs`.
|
|
15
15
|
*/
|
|
16
|
-
export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
|
|
16
|
+
export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
|
|
17
17
|
export { consoleLogger } from "./types.js";
|
|
18
18
|
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, } from "./types.js";
|
|
19
19
|
export { github, linear, slack, sentry, datadog, betterstack, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
20
20
|
export type { SkillValidationResult } from "./skills/index.js";
|
|
21
21
|
export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
|
|
22
22
|
export type { WorkflowError } from "./schema.js";
|
|
23
|
+
export { isWhenExpression, whenLabel, parseExpression, evaluateExpression, checkExpression, ExpressionSyntaxError, } from "./when.js";
|
|
24
|
+
export type { ExprNode, ExpressionScope, ExpressionResult } from "./when.js";
|
|
23
25
|
export { workflowToFlow, flowToWorkflow, applyExecutionEvent, exportAsTypescript, getSkillCatalog } from "./studio.js";
|
|
24
26
|
export type { FlowNode, SkillNodeData, FlowEdge } from "./studio.js";
|
package/dist/browser.js
CHANGED
|
@@ -23,5 +23,7 @@ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRA
|
|
|
23
23
|
export { github, linear, slack, sentry, datadog, betterstack, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
24
24
|
// Schema & validation
|
|
25
25
|
export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
|
|
26
|
+
// Deterministic `when` expressions (#461). Pure, browser-safe.
|
|
27
|
+
export { isWhenExpression, whenLabel, parseExpression, evaluateExpression, checkExpression, ExpressionSyntaxError, } from "./when.js";
|
|
26
28
|
// Studio adapter
|
|
27
29
|
export { workflowToFlow, flowToWorkflow, applyExecutionEvent, exportAsTypescript, getSkillCatalog } from "./studio.js";
|
package/dist/cli/run-output.js
CHANGED
|
@@ -262,7 +262,7 @@ export const WORKFLOW_RUN_OPTIONS = [
|
|
|
262
262
|
["-y, --yes", "Skip the batch confirmation prompt (for CI)"],
|
|
263
263
|
[
|
|
264
264
|
"--dry-run",
|
|
265
|
-
"Run with read-only tools only: write tools, external MCP servers, and shell/file-edit tools are withheld, so nothing is created, posted, or sent. Stops at the first conditional edge. To inspect nodes without running, use --list-nodes.",
|
|
265
|
+
"Run with read-only tools only: write tools, external MCP servers, and shell/file-edit tools are withheld, so nothing is created, posted, or sent. Stops at the first natural-language conditional edge. To inspect nodes without running, use --list-nodes.",
|
|
266
266
|
],
|
|
267
267
|
[
|
|
268
268
|
"--stage",
|
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";
|
|
@@ -1118,15 +1119,17 @@ function resolveConfig(skills, overrides, env = process.env) {
|
|
|
1118
1119
|
* Returns the next node ID, or null when execution should stop.
|
|
1119
1120
|
*/
|
|
1120
1121
|
async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, abort) {
|
|
1121
|
-
// Dry run path gate: stop at the first
|
|
1122
|
+
// Dry run path gate: stop at the first natural-language routing decision.
|
|
1122
1123
|
// 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
|
-
//
|
|
1124
|
+
// runs read-only (see execute()). The stop keeps dry-run routing free of
|
|
1125
|
+
// model route evaluation, so the path is reproducible from the node outputs
|
|
1126
|
+
// alone. Expression edges (#461) are evaluated by sweny with no model call,
|
|
1127
|
+
// so dry-run follows them: a preview can reach the nodes past a
|
|
1128
|
+
// deterministic branch (still read-only, outputs staged) and stops only
|
|
1129
|
+
// where a model would have to pick the branch.
|
|
1127
1130
|
if (isDryRunInput(input)) {
|
|
1128
1131
|
const outEdges = workflow.edges.filter((e) => e.from === currentId);
|
|
1129
|
-
if (outEdges.some((e) => e.when)) {
|
|
1132
|
+
if (outEdges.some((e) => e.when && !isWhenExpression(e.when))) {
|
|
1130
1133
|
safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "dry run" }, logger);
|
|
1131
1134
|
return null;
|
|
1132
1135
|
}
|
|
@@ -1134,7 +1137,7 @@ async function advanceFromNode(workflow, currentId, results, input, claude, obse
|
|
|
1134
1137
|
const prevId = currentId;
|
|
1135
1138
|
const nextId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger, abort);
|
|
1136
1139
|
if (nextId) {
|
|
1137
|
-
const reason = workflow.edges.find((e) => e.from === prevId && e.to === nextId)?.when ?? "only path";
|
|
1140
|
+
const reason = whenLabel(workflow.edges.find((e) => e.from === prevId && e.to === nextId)?.when) ?? "only path";
|
|
1138
1141
|
trace.edges.push({ from: prevId, to: nextId, reason });
|
|
1139
1142
|
}
|
|
1140
1143
|
return nextId;
|
|
@@ -1144,7 +1147,8 @@ async function advanceFromNode(workflow, currentId, results, input, claude, obse
|
|
|
1144
1147
|
*
|
|
1145
1148
|
* - 0 out-edges → terminal (return null)
|
|
1146
1149
|
* - 1 unconditional edge → follow it
|
|
1147
|
-
* -
|
|
1150
|
+
* - Every conditional edge is an `{ expr }` expression → sweny evaluates them, no model call
|
|
1151
|
+
* - Otherwise multiple or conditional → Claude evaluates
|
|
1148
1152
|
*
|
|
1149
1153
|
* Edges with max_iterations are filtered out once exhausted.
|
|
1150
1154
|
*/
|
|
@@ -1192,6 +1196,12 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
1192
1196
|
safeObserve(observer, { type: "route", from: current, to: defaultEdge.to, reason: "only path" }, logger);
|
|
1193
1197
|
return defaultEdge.to;
|
|
1194
1198
|
}
|
|
1199
|
+
// Deterministic routing (#461): every conditional out-edge is an expression,
|
|
1200
|
+
// so sweny decides without a model call. validateWorkflow rejects a node
|
|
1201
|
+
// that mixes expression and natural-language edges.
|
|
1202
|
+
if (conditionalEdges.every((e) => isWhenExpression(e.when))) {
|
|
1203
|
+
return resolveByExpressions(workflow, current, results, conditionalEdges, defaultEdge, observer, edgeCounts, logger);
|
|
1204
|
+
}
|
|
1195
1205
|
// Claude evaluates which condition matches. Include input so conditions
|
|
1196
1206
|
// can reference workflow-level flags like dryRun, and expose evals so
|
|
1197
1207
|
// routing edges can read `priorNode.evals.X.pass`.
|
|
@@ -1233,7 +1243,7 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
1233
1243
|
}
|
|
1234
1244
|
const choices = conditionalEdges.map((e) => ({
|
|
1235
1245
|
id: e.to,
|
|
1236
|
-
description: e.when,
|
|
1246
|
+
description: whenLabel(e.when),
|
|
1237
1247
|
}));
|
|
1238
1248
|
if (defaultEdge) {
|
|
1239
1249
|
choices.push({ id: defaultEdge.to, description: "None of the above / default path" });
|
|
@@ -1306,6 +1316,76 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
1306
1316
|
}, logger);
|
|
1307
1317
|
return resolved;
|
|
1308
1318
|
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Pick the out-edge whose `{ expr }` condition is true, with no model call
|
|
1321
|
+
* (#461). Exactly one true edge is taken. None true takes the unconditional
|
|
1322
|
+
* default edge when there is one. Otherwise (no edge true and no default, or
|
|
1323
|
+
* two or more true) it fails closed with a RouteEvaluationError, the same
|
|
1324
|
+
* contract as a failed model route evaluation: sweny never guesses an edge.
|
|
1325
|
+
*
|
|
1326
|
+
* An expression reads only successful nodes' outputs. A missing field, or a
|
|
1327
|
+
* node that did not run or did not succeed, makes that expression false and
|
|
1328
|
+
* is logged as a warning, never a silent true.
|
|
1329
|
+
*/
|
|
1330
|
+
function resolveByExpressions(workflow, current, results, conditionalEdges, defaultEdge, observer, edgeCounts, logger) {
|
|
1331
|
+
const scope = {};
|
|
1332
|
+
for (const [id, r] of results.entries()) {
|
|
1333
|
+
if (r.status === "success")
|
|
1334
|
+
scope[id] = buildPriorNodeContext(r);
|
|
1335
|
+
}
|
|
1336
|
+
const matched = [];
|
|
1337
|
+
for (const edge of conditionalEdges) {
|
|
1338
|
+
const expr = whenLabel(edge.when);
|
|
1339
|
+
let outcome;
|
|
1340
|
+
try {
|
|
1341
|
+
outcome = evaluateExpression(parseExpression(expr), scope);
|
|
1342
|
+
}
|
|
1343
|
+
catch (err) {
|
|
1344
|
+
// validateWorkflow parses every expression before a run, so this only
|
|
1345
|
+
// fires for a workflow that skipped validation. Fail closed.
|
|
1346
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1347
|
+
safeObserve(observer, { type: "route", from: current, to: "(end)", reason: `invalid when expression` }, logger);
|
|
1348
|
+
throw new RouteEvaluationError(current, `invalid when expression on edge '${current}' -> '${edge.to}': ${msg}`);
|
|
1349
|
+
}
|
|
1350
|
+
if (outcome.problem) {
|
|
1351
|
+
logger?.warn(` route expr: '${current}' -> '${edge.to}' is false: ${outcome.problem} (${expr})`, {
|
|
1352
|
+
node: current,
|
|
1353
|
+
to: edge.to,
|
|
1354
|
+
});
|
|
1355
|
+
safeObserve(observer, {
|
|
1356
|
+
type: "node:warning",
|
|
1357
|
+
node: current,
|
|
1358
|
+
reason: `when expression on edge to '${edge.to}' evaluated false: ${outcome.problem}`,
|
|
1359
|
+
fields: [],
|
|
1360
|
+
}, logger);
|
|
1361
|
+
}
|
|
1362
|
+
if (outcome.value)
|
|
1363
|
+
matched.push({ to: edge.to, expr });
|
|
1364
|
+
}
|
|
1365
|
+
const take = (to, reason) => {
|
|
1366
|
+
if (edgeCounts) {
|
|
1367
|
+
const key = `${current}→${to}`;
|
|
1368
|
+
edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);
|
|
1369
|
+
}
|
|
1370
|
+
safeObserve(observer, { type: "route", from: current, to, reason }, logger);
|
|
1371
|
+
return to;
|
|
1372
|
+
};
|
|
1373
|
+
if (matched.length === 1)
|
|
1374
|
+
return take(matched[0].to, matched[0].expr);
|
|
1375
|
+
if (matched.length === 0 && defaultEdge) {
|
|
1376
|
+
logger?.info(` route expr: no expression matched for '${current}'; taking default edge '${defaultEdge.to}'`, {
|
|
1377
|
+
node: current,
|
|
1378
|
+
});
|
|
1379
|
+
return take(defaultEdge.to, "no expression matched; default edge");
|
|
1380
|
+
}
|
|
1381
|
+
const why = matched.length === 0
|
|
1382
|
+
? `no when expression on node '${current}' is true and there is no default (unconditional) edge`
|
|
1383
|
+
: `${matched.length} when expressions on node '${current}' are true (to ${matched.map((m) => m.to).join(", ")}); ` +
|
|
1384
|
+
`exactly one may match`;
|
|
1385
|
+
logger?.error(` route expr: ${why}; refusing to guess. Terminating the run.`, { node: current });
|
|
1386
|
+
safeObserve(observer, { type: "route", from: current, to: "(end)", reason: why }, logger);
|
|
1387
|
+
throw new RouteEvaluationError(current, `${why}; refusing to guess`);
|
|
1388
|
+
}
|
|
1309
1389
|
/**
|
|
1310
1390
|
* Validate a workflow definition before execution.
|
|
1311
1391
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
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";
|
|
@@ -43,6 +43,8 @@ export type { SkillValidationResult } from "./skills/index.js";
|
|
|
43
43
|
export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
|
|
44
44
|
export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, workflowInputsZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
|
|
45
45
|
export type { WorkflowError } from "./schema.js";
|
|
46
|
+
export { isWhenExpression, whenLabel, parseExpression, evaluateExpression, checkExpression, ExpressionSyntaxError, } from "./when.js";
|
|
47
|
+
export type { ExprNode, ExpressionScope, ExpressionResult } from "./when.js";
|
|
46
48
|
export { loadAndValidateWorkflow, validateParsed } from "./loader.js";
|
|
47
49
|
export type { LoaderResult, LoaderError, LoaderOptions } from "./loader.js";
|
|
48
50
|
export { CURRENT_SPEC_VERSION, MIGRATIONS, migrateWorkflow } from "./migrations.js";
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,8 @@ export { github, linear, slack, sentry, datadog, notification, supabase, builtin
|
|
|
43
43
|
export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
|
|
44
44
|
// Schema & validation
|
|
45
45
|
export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, workflowInputsZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
|
|
46
|
+
// Deterministic `when` expressions (#461). Pure, browser-safe.
|
|
47
|
+
export { isWhenExpression, whenLabel, parseExpression, evaluateExpression, checkExpression, ExpressionSyntaxError, } from "./when.js";
|
|
46
48
|
// Loader — canonical read → parse → structural-validate pipeline (the path
|
|
47
49
|
// the CLI uses for `workflow run`, `workflow validate`, and `publish`).
|
|
48
50
|
// 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
|
package/dist/schema.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import type { Workflow, WorkflowType } from "./types.js";
|
|
11
11
|
import { sourceZ } from "./sources.js";
|
|
12
12
|
import { workflowInputsZ } from "./inputs.js";
|
|
13
|
+
export { isWhenExpression, whenLabel } from "./when.js";
|
|
13
14
|
export { sourceZ };
|
|
14
15
|
export { workflowInputsZ };
|
|
15
16
|
export declare const jsonSchemaZ: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
@@ -1683,17 +1684,27 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
1683
1684
|
export declare const edgeZ: z.ZodObject<{
|
|
1684
1685
|
from: z.ZodString;
|
|
1685
1686
|
to: z.ZodString;
|
|
1686
|
-
when: z.ZodOptional<z.ZodString
|
|
1687
|
+
when: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodObject<{
|
|
1688
|
+
expr: z.ZodString;
|
|
1689
|
+
}, "strict", z.ZodTypeAny, {
|
|
1690
|
+
expr: string;
|
|
1691
|
+
}, {
|
|
1692
|
+
expr: string;
|
|
1693
|
+
}>]>>;
|
|
1687
1694
|
max_iterations: z.ZodOptional<z.ZodNumber>;
|
|
1688
1695
|
}, "strict", z.ZodTypeAny, {
|
|
1689
1696
|
from: string;
|
|
1690
1697
|
to: string;
|
|
1691
|
-
when?: string |
|
|
1698
|
+
when?: string | {
|
|
1699
|
+
expr: string;
|
|
1700
|
+
} | undefined;
|
|
1692
1701
|
max_iterations?: number | undefined;
|
|
1693
1702
|
}, {
|
|
1694
1703
|
from: string;
|
|
1695
1704
|
to: string;
|
|
1696
|
-
when?: string |
|
|
1705
|
+
when?: string | {
|
|
1706
|
+
expr: string;
|
|
1707
|
+
} | undefined;
|
|
1697
1708
|
max_iterations?: number | undefined;
|
|
1698
1709
|
}>;
|
|
1699
1710
|
/**
|
|
@@ -2466,17 +2477,27 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2466
2477
|
edges: z.ZodArray<z.ZodObject<{
|
|
2467
2478
|
from: z.ZodString;
|
|
2468
2479
|
to: z.ZodString;
|
|
2469
|
-
when: z.ZodOptional<z.ZodString
|
|
2480
|
+
when: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodObject<{
|
|
2481
|
+
expr: z.ZodString;
|
|
2482
|
+
}, "strict", z.ZodTypeAny, {
|
|
2483
|
+
expr: string;
|
|
2484
|
+
}, {
|
|
2485
|
+
expr: string;
|
|
2486
|
+
}>]>>;
|
|
2470
2487
|
max_iterations: z.ZodOptional<z.ZodNumber>;
|
|
2471
2488
|
}, "strict", z.ZodTypeAny, {
|
|
2472
2489
|
from: string;
|
|
2473
2490
|
to: string;
|
|
2474
|
-
when?: string |
|
|
2491
|
+
when?: string | {
|
|
2492
|
+
expr: string;
|
|
2493
|
+
} | undefined;
|
|
2475
2494
|
max_iterations?: number | undefined;
|
|
2476
2495
|
}, {
|
|
2477
2496
|
from: string;
|
|
2478
2497
|
to: string;
|
|
2479
|
-
when?: string |
|
|
2498
|
+
when?: string | {
|
|
2499
|
+
expr: string;
|
|
2500
|
+
} | undefined;
|
|
2480
2501
|
max_iterations?: number | undefined;
|
|
2481
2502
|
}>, "many">;
|
|
2482
2503
|
entry: z.ZodString;
|
|
@@ -2846,7 +2867,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2846
2867
|
edges: {
|
|
2847
2868
|
from: string;
|
|
2848
2869
|
to: string;
|
|
2849
|
-
when?: string |
|
|
2870
|
+
when?: string | {
|
|
2871
|
+
expr: string;
|
|
2872
|
+
} | undefined;
|
|
2850
2873
|
max_iterations?: number | undefined;
|
|
2851
2874
|
}[];
|
|
2852
2875
|
entry: string;
|
|
@@ -3021,7 +3044,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
3021
3044
|
edges: {
|
|
3022
3045
|
from: string;
|
|
3023
3046
|
to: string;
|
|
3024
|
-
when?: string |
|
|
3047
|
+
when?: string | {
|
|
3048
|
+
expr: string;
|
|
3049
|
+
} | undefined;
|
|
3025
3050
|
max_iterations?: number | undefined;
|
|
3026
3051
|
}[];
|
|
3027
3052
|
entry: string;
|
|
@@ -3238,7 +3263,9 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
3238
3263
|
edges: {
|
|
3239
3264
|
from: string;
|
|
3240
3265
|
to: string;
|
|
3241
|
-
when?: string |
|
|
3266
|
+
when?: string | {
|
|
3267
|
+
expr: string;
|
|
3268
|
+
} | undefined;
|
|
3242
3269
|
max_iterations?: number | undefined;
|
|
3243
3270
|
}[];
|
|
3244
3271
|
entry: string;
|
|
@@ -3290,7 +3317,7 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
3290
3317
|
} | undefined;
|
|
3291
3318
|
};
|
|
3292
3319
|
export interface WorkflowError {
|
|
3293
|
-
code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "AMBIGUOUS_EDGES" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL" | "EDGE_ITERATIONS_EXCEEDED" | "RETRY_MAX_EXCEEDED" | "PERMISSION_CEILING" | "BUDGET_CEILING" | "OUTPUT_NOT_ALLOWED" | "DUPLICATE_OUTPUT" | "UNSUPPORTED_OUTPUT";
|
|
3320
|
+
code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "AMBIGUOUS_EDGES" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL" | "EDGE_ITERATIONS_EXCEEDED" | "RETRY_MAX_EXCEEDED" | "PERMISSION_CEILING" | "BUDGET_CEILING" | "OUTPUT_NOT_ALLOWED" | "DUPLICATE_OUTPUT" | "UNSUPPORTED_OUTPUT" | "INVALID_WHEN_EXPRESSION" | "MIXED_EDGE_CONDITIONS";
|
|
3294
3321
|
message: string;
|
|
3295
3322
|
nodeId?: string;
|
|
3296
3323
|
}
|
|
@@ -4099,8 +4126,22 @@ export declare const workflowJsonSchema: {
|
|
|
4099
4126
|
readonly minLength: 1;
|
|
4100
4127
|
};
|
|
4101
4128
|
readonly when: {
|
|
4102
|
-
readonly
|
|
4103
|
-
readonly
|
|
4129
|
+
readonly description: "Condition for taking this edge. A string is natural language, evaluated at runtime by the workflow's harness. An object { expr } is a deterministic expression over prior nodes' declared output fields, evaluated by sweny with no model call.";
|
|
4130
|
+
readonly oneOf: readonly [{
|
|
4131
|
+
readonly type: "string";
|
|
4132
|
+
}, {
|
|
4133
|
+
readonly type: "object";
|
|
4134
|
+
readonly required: readonly ["expr"];
|
|
4135
|
+
readonly additionalProperties: false;
|
|
4136
|
+
readonly properties: {
|
|
4137
|
+
readonly expr: {
|
|
4138
|
+
readonly type: "string";
|
|
4139
|
+
readonly minLength: 1;
|
|
4140
|
+
readonly maxLength: 2000;
|
|
4141
|
+
readonly description: "Boolean expression: == != < <= > >= in, && || !, exists, string/number/boolean/null literals, and node.field paths to declared output fields.";
|
|
4142
|
+
};
|
|
4143
|
+
};
|
|
4144
|
+
}];
|
|
4104
4145
|
};
|
|
4105
4146
|
readonly max_iterations: {
|
|
4106
4147
|
readonly type: "integer";
|
package/dist/schema.js
CHANGED
|
@@ -10,6 +10,9 @@ import { z } from "zod";
|
|
|
10
10
|
import { AUTHOR_ASSOCIATIONS, CONTEXT_MODES, EVALUATOR_KINDS, EVAL_POLICIES, MCP_TRANSPORTS, NODE_ACCESS, NODE_ON_FAIL, REQUIRES_ON_FAIL, SAFE_OUTPUT_APPLIERS, SAFE_OUTPUT_EXPIRES_PATTERN, SAFE_OUTPUT_MAX_CEILING, SAFE_OUTPUT_STATES, SAFE_OUTPUT_TYPES, SKILL_CATEGORIES, SKILL_ID_MAX_LENGTH, SKILL_ID_PATTERN, TOOL_ACCESS, TOOL_CLASSES, WORKFLOW_TYPES, } from "./types.js";
|
|
11
11
|
import { sourceZ } from "./sources.js";
|
|
12
12
|
import { workflowInputsZ, WORKFLOW_INPUT_TYPES } from "./inputs.js";
|
|
13
|
+
import { checkExpression, isWhenExpression, WHEN_EXPR_MAX_LENGTH } from "./when.js";
|
|
14
|
+
// Studio and other schema-only consumers label edges without importing the root entry.
|
|
15
|
+
export { isWhenExpression, whenLabel } from "./when.js";
|
|
13
16
|
export { sourceZ };
|
|
14
17
|
export { workflowInputsZ };
|
|
15
18
|
// ─── Zod Schemas ─────────────────────────────────────────────────
|
|
@@ -330,7 +333,9 @@ export const edgeZ = z
|
|
|
330
333
|
.object({
|
|
331
334
|
from: z.string().min(1),
|
|
332
335
|
to: z.string().min(1),
|
|
333
|
-
|
|
336
|
+
// Natural language (the harness picks the edge) or `{ expr }`, a
|
|
337
|
+
// deterministic expression sweny evaluates itself (#461). One or the other.
|
|
338
|
+
when: z.union([z.string(), z.object({ expr: z.string().min(1).max(WHEN_EXPR_MAX_LENGTH) }).strict()]).optional(),
|
|
334
339
|
max_iterations: z.number().int().min(1).max(EDGE_MAX_ITERATIONS_CEILING).optional(),
|
|
335
340
|
})
|
|
336
341
|
.strict();
|
|
@@ -507,6 +512,68 @@ export function validateWorkflow(workflow, knownSkills) {
|
|
|
507
512
|
});
|
|
508
513
|
}
|
|
509
514
|
}
|
|
515
|
+
// Deterministic `when` expressions (#461). Every path must name a declared
|
|
516
|
+
// output field of the edge's source node or one of its ancestors, so a typo
|
|
517
|
+
// is a load error rather than a branch that never fires. A node routes on
|
|
518
|
+
// expressions or on natural language, never both: a mixed node would hand
|
|
519
|
+
// the expression edges to the model and lose the determinism the author
|
|
520
|
+
// asked for.
|
|
521
|
+
const ancestorsCache = new Map();
|
|
522
|
+
const inAll = new Map();
|
|
523
|
+
for (const [from, tos] of outAll)
|
|
524
|
+
for (const to of tos)
|
|
525
|
+
push(inAll, to, from);
|
|
526
|
+
const ancestorsOf = (id) => {
|
|
527
|
+
let seen = ancestorsCache.get(id);
|
|
528
|
+
if (seen)
|
|
529
|
+
return seen;
|
|
530
|
+
seen = new Set([id]);
|
|
531
|
+
const queue = [id];
|
|
532
|
+
for (let i = 0; i < queue.length; i++) {
|
|
533
|
+
for (const from of inAll.get(queue[i]) ?? []) {
|
|
534
|
+
if (!seen.has(from)) {
|
|
535
|
+
seen.add(from);
|
|
536
|
+
queue.push(from);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
ancestorsCache.set(id, seen);
|
|
541
|
+
return seen;
|
|
542
|
+
};
|
|
543
|
+
const outputs = Object.fromEntries(Object.entries(workflow.nodes).map(([id, n]) => [id, n.output]));
|
|
544
|
+
const conditionKinds = new Map();
|
|
545
|
+
for (const edge of workflow.edges) {
|
|
546
|
+
if (!nodeIds.has(edge.from) || !edge.when)
|
|
547
|
+
continue;
|
|
548
|
+
const kinds = conditionKinds.get(edge.from) ?? { expr: 0, nl: 0 };
|
|
549
|
+
conditionKinds.set(edge.from, kinds);
|
|
550
|
+
if (!isWhenExpression(edge.when)) {
|
|
551
|
+
kinds.nl++;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
kinds.expr++;
|
|
555
|
+
const problems = checkExpression(edge.when.expr, {
|
|
556
|
+
from: edge.from,
|
|
557
|
+
ancestors: ancestorsOf(edge.from),
|
|
558
|
+
outputs,
|
|
559
|
+
});
|
|
560
|
+
for (const problem of problems) {
|
|
561
|
+
errors.push({
|
|
562
|
+
code: "INVALID_WHEN_EXPRESSION",
|
|
563
|
+
message: `Edge "${edge.from}" -> "${edge.to}": ${problem}`,
|
|
564
|
+
nodeId: edge.from,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
for (const [from, kinds] of conditionKinds) {
|
|
569
|
+
if (kinds.expr > 0 && kinds.nl > 0) {
|
|
570
|
+
errors.push({
|
|
571
|
+
code: "MIXED_EDGE_CONDITIONS",
|
|
572
|
+
message: `Node "${from}" mixes expression and natural-language 'when' conditions on its out-edges; use one kind per node (an unconditional default edge is fine with either)`,
|
|
573
|
+
nodeId: from,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
510
577
|
// Reachability: BFS from entry. Skipped only when the entry is missing
|
|
511
578
|
// (every node would be reported, which is noise). All other diagnostics
|
|
512
579
|
// accumulate so one pass reports every structural problem.
|
|
@@ -1299,8 +1366,23 @@ export const workflowJsonSchema = {
|
|
|
1299
1366
|
from: { type: "string", minLength: 1 },
|
|
1300
1367
|
to: { type: "string", minLength: 1 },
|
|
1301
1368
|
when: {
|
|
1302
|
-
|
|
1303
|
-
|
|
1369
|
+
description: "Condition for taking this edge. A string is natural language, evaluated at runtime by the workflow's harness. An object { expr } is a deterministic expression over prior nodes' declared output fields, evaluated by sweny with no model call.",
|
|
1370
|
+
oneOf: [
|
|
1371
|
+
{ type: "string" },
|
|
1372
|
+
{
|
|
1373
|
+
type: "object",
|
|
1374
|
+
required: ["expr"],
|
|
1375
|
+
additionalProperties: false,
|
|
1376
|
+
properties: {
|
|
1377
|
+
expr: {
|
|
1378
|
+
type: "string",
|
|
1379
|
+
minLength: 1,
|
|
1380
|
+
maxLength: WHEN_EXPR_MAX_LENGTH,
|
|
1381
|
+
description: "Boolean expression: == != < <= > >= in, && || !, exists, string/number/boolean/null literals, and node.field paths to declared output fields.",
|
|
1382
|
+
},
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
],
|
|
1304
1386
|
},
|
|
1305
1387
|
max_iterations: {
|
|
1306
1388
|
type: "integer",
|
package/dist/studio.d.ts
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* directly to node highlights. node:enter → glow blue,
|
|
39
39
|
* tool:call → show tool name, node:exit → green/red.
|
|
40
40
|
*/
|
|
41
|
-
import type { Workflow, Node, ExecutionEvent, NodeResult, Skill } from "./types.js";
|
|
41
|
+
import type { Workflow, Node, Edge, ExecutionEvent, NodeResult, Skill } from "./types.js";
|
|
42
42
|
export interface FlowNode {
|
|
43
43
|
id: string;
|
|
44
44
|
type: "skillNode";
|
|
@@ -74,7 +74,7 @@ export interface FlowEdge {
|
|
|
74
74
|
/** Array index of this edge in workflow.edges — used for stable lookups. */
|
|
75
75
|
edgeIndex: number;
|
|
76
76
|
data: {
|
|
77
|
-
when?:
|
|
77
|
+
when?: Edge["when"];
|
|
78
78
|
max_iterations?: number;
|
|
79
79
|
isConditional: boolean;
|
|
80
80
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -489,12 +489,26 @@ export interface NodeToolFilter {
|
|
|
489
489
|
/** Skill tool names removed from the node (applied after `allow`). */
|
|
490
490
|
deny?: string[];
|
|
491
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* A deterministic edge condition (#461): a boolean expression over prior
|
|
494
|
+
* nodes' declared output fields, evaluated by sweny with no model call.
|
|
495
|
+
* Grammar and semantics: `when.ts`.
|
|
496
|
+
*/
|
|
497
|
+
export interface WhenExpression {
|
|
498
|
+
expr: string;
|
|
499
|
+
}
|
|
500
|
+
/** An edge condition: natural language (model-evaluated) or a `{ expr }` expression. */
|
|
501
|
+
export type EdgeWhen = string | WhenExpression;
|
|
492
502
|
/** An edge connecting two nodes */
|
|
493
503
|
export interface Edge {
|
|
494
504
|
from: string;
|
|
495
505
|
to: string;
|
|
496
|
-
/**
|
|
497
|
-
|
|
506
|
+
/**
|
|
507
|
+
* Condition for taking this edge. A string is natural language, evaluated at
|
|
508
|
+
* runtime by the workflow's harness. `{ expr }` is a deterministic expression
|
|
509
|
+
* evaluated by sweny with no model call.
|
|
510
|
+
*/
|
|
511
|
+
when?: EdgeWhen;
|
|
498
512
|
/** Max times this edge can be followed (enables retry loops). Default: unlimited. */
|
|
499
513
|
max_iterations?: number;
|
|
500
514
|
}
|
package/dist/when.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic `when` expressions (#461).
|
|
3
|
+
*
|
|
4
|
+
* An edge's `when` is either natural language (a model picks the edge at
|
|
5
|
+
* runtime) or `{ expr: "..." }`, a small boolean expression over prior nodes'
|
|
6
|
+
* declared output fields that sweny evaluates itself, with no model call.
|
|
7
|
+
*
|
|
8
|
+
* Grammar (precedence low to high):
|
|
9
|
+
*
|
|
10
|
+
* expr := or
|
|
11
|
+
* or := and ( "||" and )*
|
|
12
|
+
* and := unary ( "&&" unary )*
|
|
13
|
+
* unary := "!" unary | comparison
|
|
14
|
+
* comparison := operand ( ( "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" ) operand )?
|
|
15
|
+
* operand := literal | list | path | "exists" path | "(" expr ")"
|
|
16
|
+
* list := "[" ( literal ( "," literal )* )? "]"
|
|
17
|
+
* literal := number | 'string' | "string" | true | false | null
|
|
18
|
+
* path := ident ( "." ident )+ (node id, then field, then nested fields)
|
|
19
|
+
*
|
|
20
|
+
* Semantics are strict and total. There is no coercion and no truthiness:
|
|
21
|
+
* `&&`, `||`, `!` and the expression itself need booleans; `<` and friends
|
|
22
|
+
* need two numbers or two strings; `in` needs a list on the right. A missing
|
|
23
|
+
* field (undefined, or a node that did not run or did not succeed) or a type
|
|
24
|
+
* mismatch makes the WHOLE expression false and is reported, so a negation
|
|
25
|
+
* can never turn a missing field into a silent true. `exists path` is the
|
|
26
|
+
* one explicit presence test: true when the value is present and not null,
|
|
27
|
+
* never a warning. `&&` and `||` short-circuit, so `exists a.x && a.x > 0`
|
|
28
|
+
* is safe.
|
|
29
|
+
*
|
|
30
|
+
* Pure module: no I/O, no eval, no Function, browser-safe. Strings in the
|
|
31
|
+
* routed data are compared as data only; nothing in them is ever parsed.
|
|
32
|
+
*/
|
|
33
|
+
import type { EdgeWhen, JSONSchema, WhenExpression } from "./types.js";
|
|
34
|
+
/** Longest expression source accepted. Routing conditions are short. */
|
|
35
|
+
export declare const WHEN_EXPR_MAX_LENGTH = 2000;
|
|
36
|
+
/** True when an edge `when` is the structured `{ expr }` form. */
|
|
37
|
+
export declare function isWhenExpression(when: unknown): when is WhenExpression;
|
|
38
|
+
/** Human-readable label for a `when`: the NL text or the expression source. */
|
|
39
|
+
export declare function whenLabel(when: EdgeWhen | undefined): string | undefined;
|
|
40
|
+
export type Literal = string | number | boolean | null;
|
|
41
|
+
export type ExprNode = {
|
|
42
|
+
kind: "literal";
|
|
43
|
+
value: Literal;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "list";
|
|
46
|
+
items: Literal[];
|
|
47
|
+
} | {
|
|
48
|
+
kind: "path";
|
|
49
|
+
segments: string[];
|
|
50
|
+
} | {
|
|
51
|
+
kind: "exists";
|
|
52
|
+
path: {
|
|
53
|
+
kind: "path";
|
|
54
|
+
segments: string[];
|
|
55
|
+
};
|
|
56
|
+
} | {
|
|
57
|
+
kind: "not";
|
|
58
|
+
operand: ExprNode;
|
|
59
|
+
} | {
|
|
60
|
+
kind: "and" | "or";
|
|
61
|
+
left: ExprNode;
|
|
62
|
+
right: ExprNode;
|
|
63
|
+
} | {
|
|
64
|
+
kind: "cmp";
|
|
65
|
+
op: CmpOp;
|
|
66
|
+
left: ExprNode;
|
|
67
|
+
right: ExprNode;
|
|
68
|
+
};
|
|
69
|
+
export type CmpOp = "==" | "!=" | "<" | "<=" | ">" | ">=" | "in";
|
|
70
|
+
export declare class ExpressionSyntaxError extends Error {
|
|
71
|
+
readonly position: number;
|
|
72
|
+
constructor(message: string, position: number);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parse an expression source into an AST. Throws `ExpressionSyntaxError` on
|
|
76
|
+
* any syntax problem, including an empty source, an over-long source, or
|
|
77
|
+
* nesting deeper than the cap.
|
|
78
|
+
*/
|
|
79
|
+
export declare function parseExpression(src: string): ExprNode;
|
|
80
|
+
/**
|
|
81
|
+
* Per-node data an expression may read, keyed by node id. A node that did not
|
|
82
|
+
* run or did not succeed is absent, so every path into it reads as missing.
|
|
83
|
+
*/
|
|
84
|
+
export type ExpressionScope = Record<string, Record<string, unknown> | undefined>;
|
|
85
|
+
export interface ExpressionResult {
|
|
86
|
+
/** The edge is taken only when this is true. */
|
|
87
|
+
value: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Why the expression was forced false: a missing field or a type mismatch.
|
|
90
|
+
* Absent when it evaluated normally (to true or false).
|
|
91
|
+
*/
|
|
92
|
+
problem?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Evaluate a parsed expression against node outputs. Never throws: a missing
|
|
96
|
+
* field or a type mismatch yields `{ value: false, problem }`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function evaluateExpression(ast: ExprNode, scope: ExpressionScope): ExpressionResult;
|
|
99
|
+
/**
|
|
100
|
+
* Check an expression against the workflow it routes. Returns one message per
|
|
101
|
+
* problem, empty when the expression is valid:
|
|
102
|
+
* - the source must parse;
|
|
103
|
+
* - every path's node must be the edge's source or one of its ancestors;
|
|
104
|
+
* - that node must declare `output.properties`, and the field (and any
|
|
105
|
+
* nested field) must be declared there;
|
|
106
|
+
* - a literal compared with a declared field must fit its declared type,
|
|
107
|
+
* and a string literal compared with an enum field must be in the enum;
|
|
108
|
+
* - a field used where a boolean is needed must be declared boolean.
|
|
109
|
+
*/
|
|
110
|
+
export declare function checkExpression(src: string, ctx: {
|
|
111
|
+
from: string;
|
|
112
|
+
ancestors: Set<string>;
|
|
113
|
+
outputs: Record<string, JSONSchema | undefined>;
|
|
114
|
+
}): string[];
|