@vaudience/aigentflow-flow-validator 0.21.0 → 0.23.0

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/PARITY.md CHANGED
@@ -4,7 +4,57 @@ This document maps every rule in this JavaScript validator back to the AIgentFlo
4
4
  Go reference implementation, records the intentional divergences, and defines the
5
5
  discipline for keeping the two in sync.
6
6
 
7
- **Tracks AIgentFlow flow schema: `v2.827.0`** (`SPEC_VERSION` in [`src/spec/aigentflow-spec.json`](./src/spec/aigentflow-spec.json)), which adds the `here` executor scheme, the `strict_templates` flow key and the rule `template_default_piped` (package 0.21.0, below). Earlier: the warning `compliance_residency_unenforced` (package 0.20.0). `orchestrator.region` / `orchestrator.enforce_region` (package 0.19.0). Earlier: the AigentChat executor was removed (package 0.18.0). Earlier additions: the `examples:` block (package 0.17.0), `server_owned_query_key` (package 0.15.1) from the AIgentFlow release after v2.790.0, `credential_endpoint_unpaired` (package 0.15.2) from AIgentFlow v2.793.0 and the release after it, and `display_name_too_long` (package 0.15.3) from AIgentFlow v2.802.0.
7
+ **Tracks AIgentFlow flow schema: `v2.831.0`** (`SPEC_VERSION` in [`src/spec/aigentflow-spec.json`](./src/spec/aigentflow-spec.json)), which adds the rule `unknown_function_operation` and a clearer `unknown_executor_scheme` message (package 0.23.0, below). Earlier: the rule `flow_data_key_reserved` (package 0.22.0). Earlier: the `here` executor scheme, the `strict_templates` flow key and the rule `template_default_piped` (package 0.21.0). Earlier: the warning `compliance_residency_unenforced` (package 0.20.0). `orchestrator.region` / `orchestrator.enforce_region` (package 0.19.0). Earlier: the AigentChat executor was removed (package 0.18.0). Earlier additions: the `examples:` block (package 0.17.0), `server_owned_query_key` (package 0.15.1) from the AIgentFlow release after v2.790.0, `credential_endpoint_unpaired` (package 0.15.2) from AIgentFlow v2.793.0 and the release after it, and `display_name_too_long` (package 0.15.3) from AIgentFlow v2.802.0.
8
+
9
+ > package 0.23.0 — **`unknown_function_operation`**, in step with the reference (AIgentFlow
10
+ > v2.831.0; `specVersion` 2.831.0). One new **error**: a step or loop sub-step whose executor
11
+ > is `function://<a>/<b>` where `<a>/<b>` is not one of the 32 registered built-ins. Before
12
+ > this, `function://utils/noop` validated and then failed every run with "function
13
+ > 'utils/noop' not found".
14
+ >
15
+ > | Rule (reference) | Code | Severity | Module |
16
+ > | -------------------------------------------- | ---------------------------- | -------- | ---------------------------------------------------- |
17
+ > | `validateFunctionOperationKnown` (parser.go) | `unknown_function_operation` | error | `validators/executors.ts`, spec `functionOperations` |
18
+ >
19
+ > - **Operation** = authority + `/` + path of the executor URL (so `binary-processor/validate-and-store`).
20
+ > The set is closed and sorted in spec `functionOperations.operations`. Templated executors
21
+ > (containing `{{`), unparsable URLs (`invalid_executor_url` owns them) and other schemes are
22
+ > skipped; the scheme match is exact and case-sensitive.
23
+ > - **Finding.** Field `steps.<id>.executor` (loop sub-step: `steps.<id>.loop.steps[i].executor`,
24
+ > step id `<id>.<sub>`); message `step "<id>" uses function://<op>, which is not a built-in
25
+ function. Available: <sorted list, ", ">`; the suggestion carries the list.
26
+ > - **Divergence 23** (below): the reference tolerates this code on its load door.
27
+ > - **`unknown_executor_scheme` text (still a warning).** For `decide` the message appends
28
+ > `Decision models run through ai://{provider}/decide (for example ai://openai/decide), not
29
+ through a scheme of their own.`; for any other unknown scheme with a known scheme within
30
+ > edit distance 2 (case-insensitive; the closest, ties to the alphabetically first) it
31
+ > appends `Did you mean '<scheme>'?`. No verdict changes.
32
+
33
+ > package 0.22.0 — **`flow_data_key_reserved`**, in step with the reference (AIgentFlow
34
+ > v2.828.0; `specVersion` 2.828.0). One new **error**: a key of the top-level `data:` map
35
+ > may not name engine state. `data:` is seeded into the mission's `.data` namespace, which
36
+ > is also where the engine keeps its own bookkeeping, so a flow declaring such a key would
37
+ > pre-seed or forge it (a taint list, the current flow, the mission's organization).
38
+ >
39
+ > | Rule (reference) | Code | Severity | Module |
40
+ > | -------------------------------------------------------------- | ------------------------ | -------- | --------------------------------------------------------- |
41
+ > | `FlowDataKeyReservation`, `validateFlowDataKeysAreNotReserved` | `flow_data_key_reserved` | error | `validators/flowDataKeys.ts`, spec `flowDataReservedKeys` |
42
+ >
43
+ > - **Reserved** (exact, case-sensitive; spec `flowDataReservedKeys`): `memory`,
44
+ > `orchestrator`, `campaign`, `voiceagent`, `org_id`, `current_flow`, `modified_flow`,
45
+ > `modification_history`, `modification_pending`, `modification_requests`,
46
+ > `bottleneck_state`; or a key starting with `_`, `.`, `memory.`, `orchestrator.`,
47
+ > `campaign.` or `voiceagent.`. Each carries the reference's reason. A key that merely
48
+ > contains a reserved word (`memory_notes`, `orgid`, `my_memory`) or differs in case is legal.
49
+ > - **Finding.** Field `data.<key>`, one per key, sorted by key; the message is the
50
+ > reference's `data: key "<key>" is reserved: <reason>. It would write into the engine's
51
+ own state, so it is not seeded.` and the suggestion its text (spec keys `messageFormat`,
52
+ > `suggestion`). There is no loop-body twin, so no demotion.
53
+ > - **Divergence from the reference's run door.** The reference tolerates this code when a
54
+ > stored flow is _run_ (the key is skipped, the flow still runs) and refuses it at save.
55
+ > This validator answers "would this save", so it is an error here, as for every other
56
+ > save-door rule. Key order is JavaScript's UTF-16 code-unit order, Go's is byte order;
57
+ > they differ only for keys with characters outside the Basic Multilingual Plane.
8
58
 
9
59
  > package 0.21.0 — **`here://`, `strict_templates` and `template_default_piped`**, in step
10
60
  > with the reference (AIgentFlow v2.827.0; `specVersion` 2.827.0). Three changes.
@@ -1084,6 +1134,8 @@ Comparison contract: **error `code` + `valid` verdict**, not message wording. Th
1084
1134
  | `tool_discovery` vocabulary, mock scenario `delay`, empty `output:` entry | `validateToolDiscoveryVocabulary`, `validateMockScenarioDelays`, `validateOutputParameters` (parser.go) | `validators/saveDoor.ts` | `validate.test.ts`, `conformance.test.ts` |
1085
1135
  | `executor_config` `${NAME}` references outside the key's scope (`executor_config_env_scope`) | `ValidateExecutorConfigEnvScopes` (parser.go) | `validators/executorConfigEnv.ts` | `v0150-save-door.test.ts`, `conformance.test.ts` |
1086
1136
  | A step or loop sub-step `query:` declaring a server-owned key (`server_owned_query_key`) | `validateNoServerOwnedStepQueryKeys` (validation.go), `IsServerOwnedParamKey` | `validators/serverOwnedQueryKeys.ts` | `v0151-save-door.test.ts`, `conformance.test.ts` |
1137
+ | A `function://` operation outside the built-in set (`unknown_function_operation`) | `validateFunctionOperationKnown` (parser.go) | `validators/executors.ts` | `v0230-function-operation.test.ts`, `conformance.test.ts` |
1138
+ | A top-level `data:` key naming engine state (`flow_data_key_reserved`) | `FlowDataKeyReservation`, `validateFlowDataKeysAreNotReserved` (validation.flowdata.go) | `validators/flowDataKeys.ts` | `v0220-flow-data-keys.test.ts`, `conformance.test.ts` |
1087
1139
  | An endpoint a server-supplied credential will never be sent to (`credential_endpoint_unpaired`, warning) | `validateCredentialEndpointPairing`, `validateFamilyCredentialEndpointPairing` (validation.go) | `validators/credentialEndpoint.ts`, `validators/goUrl.ts` | `v0152-credential-endpoint.test.ts`, `conformance.test.ts` |
1088
1140
  | The `examples:` block: 58 codes, one walker (ids, inputs, references, expectations, checkpoints, variants) | the examples walker (`ValidateExamples`) | `validators/examples.ts`, `validators/goUrl.ts` | `examples.test.ts`, `conformance.test.ts` |
1089
1141
  | Orchestrator `.exons` frontmatter: spec, provider, `requirements.resources`, `tools.allow` withholds | `validateOrchestrator` (parser.go), `validateOrchestratorToolAllowWithholds` | `validators/exons.ts` | `v0150-save-door.test.ts`, `conformance.test.ts` |
@@ -1353,6 +1405,11 @@ number their shared divergences alike._
1353
1405
  - The recursion bound of 32 on a nested value is the reference's own and is not in the
1354
1406
  spec.
1355
1407
 
1408
+ 23. **`unknown_function_operation` is tolerated on the reference's load door (0.23.0).** The
1409
+ reference refuses an unknown `function://` operation when a flow is _saved_ but loads
1410
+ (and then fails at run time) a stored flow that already carries one. This validator
1411
+ answers "would this save", so it is an error here, as for every other save-door rule.
1412
+
1356
1413
  ---
1357
1414
 
1358
1415
  ## The examples block
package/README.md CHANGED
@@ -116,7 +116,7 @@ Low-level: parse YAML into `{ flow?, parseErrors, parseWarnings }` without valid
116
116
 
117
117
  ### `SPEC_VERSION: string`
118
118
 
119
- The AIgentFlow flow-schema version this build tracks (e.g. `"2.827.0"`).
119
+ The AIgentFlow flow-schema version this build tracks (e.g. `"2.831.0"`).
120
120
 
121
121
  ### Options
122
122
 
@@ -174,6 +174,7 @@ interface ValidationIssue {
174
174
  - **Orchestrator `.exons` frontmatter** — the document must open with a closed `---` frontmatter spec (`orchestrator_exons_parse_failed`) that sets `execution.provider` (`orchestrator_exons_no_provider`) and declares no `requirements.resources` (`exons_resources_unhonoured`); a tool that `orchestrator.tools` and the definition's `tools.allow` disagree about is never offered (`orchestrator_tool_withheld`, warning). The document body is not parsed: that needs the `.exons` engine (see PARITY.md).
175
175
  - **`executor_config` environment references** — a `${NAME}` value may name only the variables of the provider or protocol it is written under (`executor_config_env_scope`).
176
176
  - **Server-owned query keys** — a step's or loop sub-step's `query:` may not declare `aiv_api_key`, `aiv_base_url` or `aiv_delegation`, which only the server sets (`server_owned_query_key`).
177
+ - **Reserved `data:` keys** — a key of the top-level `data:` map may not name engine state: `memory`, `orchestrator`, `campaign`, `voiceagent`, `org_id`, `current_flow`, the `modif*`/`bottleneck_state` bookkeeping names, a key starting with `_` or `.`, or one under `memory.`, `orchestrator.`, `campaign.`, `voiceagent.` (`flow_data_key_reserved`).
177
178
  - **Credential endpoints** — an endpoint a server-supplied credential will never be sent to (warning, `credential_endpoint_unpaired`): an `ai://` `<provider>_base_url` or a literal `executor_config.<provider>.base_url` with no key of the flow's own, a family secret that is a server `${AIGENTFLOW_…}` variable beside a literal non-default endpoint.
178
179
  - **Credential bindings** — `stored/{provider}/{name}` format, `inject_as`, `credential`/`credentials` mutual exclusion.
179
180
  - **Expression functions** — exactly one of `package`/`function`.
package/dist/cli.js CHANGED
@@ -438,7 +438,7 @@ function validateBasicStructure(flow, issues) {
438
438
  message: "Executor is required for each step",
439
439
  code: "missing_required_field",
440
440
  stepId: stepID,
441
- suggestion: "Specify an executor URL (e.g., 'function://demo/processor')"
441
+ suggestion: "Specify an executor URL (e.g., 'function://utils/log')"
442
442
  });
443
443
  } else if (executorText === null) {
444
444
  issues.error({
@@ -455,7 +455,7 @@ function validateBasicStructure(flow, issues) {
455
455
  // src/spec/aigentflow-spec.json
456
456
  var aigentflow_spec_default = {
457
457
  $comment: "Vendored enum surface mirrored from the AIgentFlow Go reference implementation. Regenerate / review on every flow-YAML schema change. See PARITY.md. Values here are public protocol/type names only.",
458
- specVersion: "2.827.0",
458
+ specVersion: "2.831.0",
459
459
  inputSchemaVersion: 1,
460
460
  executorSchemes: [
461
461
  "a2a",
@@ -965,6 +965,75 @@ var aigentflow_spec_default = {
965
965
  stepFieldFormat: "steps.%s.query.%s",
966
966
  loopStepFieldFormat: "steps.%s.loop.steps.%s.query.%s"
967
967
  },
968
+ functionOperations: {
969
+ $comment: `unknown_function_operation (ERROR): a step or loop sub-step whose executor is function://<authority>/<path> where authority+"/"+path is not in operations. Templated executors (containing templateActionOpen), unparsable URLs (invalid_executor_url owns them) and other schemes are skipped; the scheme match is exact and case-sensitive, as in the reference. The message is messageFormat with the stepId, the operation, then operations joined by ', '. Reference: validateFunctionOperationKnown (parser.go), functionSchemaOperations.`,
970
+ code: "unknown_function_operation",
971
+ scheme: "function",
972
+ messageFormat: "step %s uses function://%s, which is not a built-in function. Available: %s",
973
+ suggestionFormat: "Use one of the built-in function operations: %s. For custom logic use a script://, http:// or ai:// executor instead.",
974
+ operations: [
975
+ "binary-processor/validate-and-store",
976
+ "data/create",
977
+ "data/from-json",
978
+ "data/to-json",
979
+ "data/validate",
980
+ "demo/adult-processor",
981
+ "demo/calculator",
982
+ "demo/data-processor",
983
+ "demo/finalizer",
984
+ "demo/senior-processor",
985
+ "demo/youth-processor",
986
+ "document/finalize",
987
+ "document/prepare",
988
+ "json/validate",
989
+ "math/basic",
990
+ "math/calculate",
991
+ "math/sum",
992
+ "results-compiler/aggregate",
993
+ "system/info",
994
+ "text/analysis",
995
+ "text/count-characters",
996
+ "text/count-lines",
997
+ "text/count-occurrences",
998
+ "text/count-words",
999
+ "text/find-pattern",
1000
+ "text/generate",
1001
+ "text/replace-text",
1002
+ "text/search",
1003
+ "utils/datetime",
1004
+ "utils/format-date",
1005
+ "utils/log",
1006
+ "utils/parse-date"
1007
+ ]
1008
+ },
1009
+ flowDataReservedKeys: {
1010
+ $comment: "flow_data_key_reserved (ERROR, no loop demotion): a key of the top-level data: map that names engine state. data: is seeded into the mission's .data namespace, where the engine keeps its own bookkeeping, so a flow declaring such a key would forge it. Matching is exact and case-sensitive. A key is reserved when it is one of exact (each with its reason), or starts with one of prefixes (checked in order, first wins; each with its reason). The finding's field is fieldFormat with the key; the message is messageFormat with the key (JSON-quoted, as Go's %q) then the reason; the suggestion is verbatim. Findings are sorted by key. Reference: FlowDataKeyReservation, validateFlowDataKeysAreNotReserved (aigentflow#247 review).",
1011
+ code: "flow_data_key_reserved",
1012
+ fieldFormat: "data.%s",
1013
+ messageFormat: "data: key %s is reserved: %s. It would write into the engine's own state, so it is not seeded.",
1014
+ suggestion: "Rename the key (no leading `_` or `.`, and not one of the engine namespaces memory., orchestrator., campaign., voiceagent.).",
1015
+ exact: {
1016
+ org_id: "the mission's organization, set by the engine for Thalamus/Engram calls",
1017
+ memory: "the memory scope namespace",
1018
+ orchestrator: "the orchestrator's state namespace",
1019
+ campaign: "the campaign's state namespace",
1020
+ voiceagent: "the voice agent's state namespace",
1021
+ current_flow: "engine bookkeeping (the current flow definition)",
1022
+ modified_flow: "engine bookkeeping (dynamic flow modification)",
1023
+ modification_history: "engine bookkeeping (dynamic flow modification)",
1024
+ modification_pending: "engine bookkeeping (dynamic flow modification)",
1025
+ modification_requests: "engine bookkeeping (dynamic flow modification)",
1026
+ bottleneck_state: "engine bookkeeping (bottleneck analysis)"
1027
+ },
1028
+ prefixes: [
1029
+ { prefix: "_", reason: "keys starting with `_` are engine bookkeeping (current flow, loop and for_each context, taint lists such as `__untrusted_*`)" },
1030
+ { prefix: ".", reason: "keys starting with `.` are engine step-response slots" },
1031
+ { prefix: "memory.", reason: "the `memory.` namespace belongs to the engine" },
1032
+ { prefix: "orchestrator.", reason: "the `orchestrator.` namespace belongs to the orchestrator" },
1033
+ { prefix: "campaign.", reason: "the `campaign.` namespace belongs to the campaign engine" },
1034
+ { prefix: "voiceagent.", reason: "the `voiceagent.` namespace belongs to the voice agent" }
1035
+ ]
1036
+ },
968
1037
  credentialEndpointPairing: {
969
1038
  $comment: "credential_endpoint_unpaired (WARNING, never an error): the one statically certain shape of AIgentFlow's run-time credential/endpoint pairing refusal. ai: an ai://<provider>/... step (top-level or loop sub-step) whose query holds a non-empty string <provider>+providerBaseUrlParamSuffix while neither its query (genericKeyParam or <provider>+providerKeyParamSuffix, non-empty strings) nor executor_config.<provider>.api_key (a literal, not an env reference) supplies a key; and executor_config.<provider>.base_url for a provider in executorConfigProviders when it is a literal, the block has no literal api_key, and it is not the case that at least one step uses the provider and every such step brings its own query key. keylessProviders are exempt from both. families: a step whose executor resolves (the key protocol+familyKeySeparator+driver, else protocol) to a row. implicitServerCredential rows are never judged. A row warns when its secret (the first non-empty string secretParam in the query, else per configKey in order: executor_config api_key when configApiKeyParam is a secretParam, else the first non-empty string secretParam under extra) is exactly an env reference to a serverEnv name on a row with expandsServerEnvReferences, and its endpoint (the first non-empty string endpointParam in the query, else per configKey: base_url when configBaseUrlParam is an endpointParam, else the first non-empty string endpointParam under extra) is a literal: no templateActionOpen, not an env reference, and not of the same origin as a defaultEndpoint (lower-cased scheme://host:port, defaultPorts filled). serverEnv is the reference's per-family server-variable set, evaluated. Field formats are the reference's. Reference: validateCredentialEndpointPairing (DC-FORGE-231) and validateFamilyCredentialEndpointPairing (DC-FORGE-233).",
970
1039
  stepFieldFormat: "steps.%s.query.%s",
@@ -1979,6 +2048,14 @@ var SERVER_OWNED_QUERY_KEYS = {
1979
2048
  stepFieldFormat: aigentflow_spec_default.serverOwnedQueryKeys.stepFieldFormat,
1980
2049
  loopStepFieldFormat: aigentflow_spec_default.serverOwnedQueryKeys.loopStepFieldFormat
1981
2050
  };
2051
+ var FLOW_DATA_RESERVED_KEYS = {
2052
+ code: aigentflow_spec_default.flowDataReservedKeys.code,
2053
+ fieldFormat: aigentflow_spec_default.flowDataReservedKeys.fieldFormat,
2054
+ messageFormat: aigentflow_spec_default.flowDataReservedKeys.messageFormat,
2055
+ suggestion: aigentflow_spec_default.flowDataReservedKeys.suggestion,
2056
+ exact: aigentflow_spec_default.flowDataReservedKeys.exact,
2057
+ prefixes: aigentflow_spec_default.flowDataReservedKeys.prefixes
2058
+ };
1982
2059
  var CREDENTIAL_ENDPOINT_PAIRING = {
1983
2060
  stepFieldFormat: aigentflow_spec_default.credentialEndpointPairing.stepFieldFormat,
1984
2061
  loopStepFieldFormat: aigentflow_spec_default.credentialEndpointPairing.loopStepFieldFormat,
@@ -2044,9 +2121,51 @@ var TEMPLATE_DEFAULT_PIPED = {
2044
2121
  messageFormat: aigentflow_spec_default.templateDefaultPiped.messageFormat,
2045
2122
  suggestion: aigentflow_spec_default.templateDefaultPiped.suggestion
2046
2123
  };
2124
+ var FUNCTION_OPERATIONS = {
2125
+ code: aigentflow_spec_default.functionOperations.code,
2126
+ scheme: aigentflow_spec_default.functionOperations.scheme,
2127
+ messageFormat: aigentflow_spec_default.functionOperations.messageFormat,
2128
+ suggestionFormat: aigentflow_spec_default.functionOperations.suggestionFormat,
2129
+ operations: new Set(aigentflow_spec_default.functionOperations.operations),
2130
+ sorted: aigentflow_spec_default.functionOperations.operations
2131
+ };
2047
2132
 
2048
2133
  // src/validators/executors.ts
2049
2134
  var SCHEME_SEPARATOR = "://";
2135
+ var UNKNOWN_SCHEME_BASE = (scheme) => `Unknown executor scheme '${scheme}'. It may be valid in a newer AIgentFlow release, or it could be a typo.`;
2136
+ var DECIDE_SCHEME = "decide";
2137
+ var DECIDE_HINT = "Decision models run through ai://{provider}/decide (for example ai://openai/decide), not through a scheme of their own.";
2138
+ var MAX_SCHEME_EDIT_DISTANCE = 2;
2139
+ function editDistance(a, b) {
2140
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
2141
+ for (let i = 1; i <= a.length; i++) {
2142
+ const cur = [i];
2143
+ for (let j = 1; j <= b.length; j++) {
2144
+ cur[j] = Math.min(
2145
+ prev[j] + 1,
2146
+ cur[j - 1] + 1,
2147
+ prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
2148
+ );
2149
+ }
2150
+ prev = cur;
2151
+ }
2152
+ return prev[b.length];
2153
+ }
2154
+ function unknownSchemeMessage(scheme) {
2155
+ const lower = scheme.toLowerCase();
2156
+ const base = UNKNOWN_SCHEME_BASE(scheme);
2157
+ if (lower === DECIDE_SCHEME) return `${base} ${DECIDE_HINT}`;
2158
+ let best;
2159
+ let bestDist = MAX_SCHEME_EDIT_DISTANCE + 1;
2160
+ for (const known of [...EXECUTOR_SCHEMES].sort()) {
2161
+ const d = editDistance(lower, known);
2162
+ if (d < bestDist) {
2163
+ best = known;
2164
+ bestDist = d;
2165
+ }
2166
+ }
2167
+ return best === void 0 ? base : `${base} Did you mean '${best}'?`;
2168
+ }
2050
2169
  function checkExecutor(executor, field, stepID, issues) {
2051
2170
  const sepIdx = executor.indexOf(SCHEME_SEPARATOR);
2052
2171
  const scheme = sepIdx > 0 ? executor.slice(0, sepIdx) : "";
@@ -2065,11 +2184,28 @@ function checkExecutor(executor, field, stepID, issues) {
2065
2184
  if (!EXECUTOR_SCHEMES.has(scheme.toLowerCase())) {
2066
2185
  issues.warn({
2067
2186
  field,
2068
- message: `Unknown executor scheme '${scheme}'. It may be valid in a newer AIgentFlow release, or it could be a typo.`,
2187
+ message: unknownSchemeMessage(scheme),
2069
2188
  code: "unknown_executor_scheme",
2070
2189
  stepId: stepID
2071
2190
  });
2191
+ return;
2072
2192
  }
2193
+ checkFunctionOperation(executor, field, stepID, templated, issues);
2194
+ }
2195
+ function checkFunctionOperation(executor, field, stepID, templated, issues) {
2196
+ if (templated) return;
2197
+ const m = EXECUTOR_URL_PATTERN.exec(executor);
2198
+ if (m === null || m[1] !== FUNCTION_OPERATIONS.scheme) return;
2199
+ const op = `${m[2]}/${m[3]}`;
2200
+ if (FUNCTION_OPERATIONS.operations.has(op)) return;
2201
+ const list = FUNCTION_OPERATIONS.sorted.join(", ");
2202
+ issues.error({
2203
+ field,
2204
+ message: FUNCTION_OPERATIONS.messageFormat.replace("%s", () => JSON.stringify(stepID)).replace("%s", () => op).replace("%s", () => list),
2205
+ code: FUNCTION_OPERATIONS.code,
2206
+ stepId: stepID,
2207
+ suggestion: FUNCTION_OPERATIONS.suggestionFormat.replace("%s", () => list)
2208
+ });
2073
2209
  }
2074
2210
  function validateExecutors(flow, issues) {
2075
2211
  const steps = flow.steps;
@@ -3901,16 +4037,16 @@ function skipSpaces(src, idx) {
3901
4037
  }
3902
4038
  return i;
3903
4039
  }
3904
- function scanString(src, start, quote) {
4040
+ function scanString(src, start, quote2) {
3905
4041
  let i = start + 1;
3906
- const raw = quote === "`";
4042
+ const raw = quote2 === "`";
3907
4043
  while (i < src.length) {
3908
4044
  const c = src[i];
3909
4045
  if (!raw && c === "\\") {
3910
4046
  i += 2;
3911
4047
  continue;
3912
4048
  }
3913
- if (c === quote) return i;
4049
+ if (c === quote2) return i;
3914
4050
  i += 1;
3915
4051
  }
3916
4052
  return -1;
@@ -4513,6 +4649,35 @@ function validateRetiredKeys(flow, issues) {
4513
4649
  }
4514
4650
  }
4515
4651
 
4652
+ // src/validators/flowDataKeys.ts
4653
+ var KEY_DATA = "data";
4654
+ function quote(key) {
4655
+ return JSON.stringify(key);
4656
+ }
4657
+ function flowDataKeyReservation(key) {
4658
+ if (Object.prototype.hasOwnProperty.call(FLOW_DATA_RESERVED_KEYS.exact, key)) {
4659
+ return FLOW_DATA_RESERVED_KEYS.exact[key];
4660
+ }
4661
+ for (const p of FLOW_DATA_RESERVED_KEYS.prefixes) {
4662
+ if (key.startsWith(p.prefix)) return p.reason;
4663
+ }
4664
+ return void 0;
4665
+ }
4666
+ function validateFlowDataKeys(flow, issues) {
4667
+ const data = flow[KEY_DATA];
4668
+ if (!isRecord(data)) return;
4669
+ for (const key of Object.keys(data).sort()) {
4670
+ const reason = flowDataKeyReservation(key);
4671
+ if (reason === void 0) continue;
4672
+ issues.error({
4673
+ field: FLOW_DATA_RESERVED_KEYS.fieldFormat.replace("%s", () => key),
4674
+ message: FLOW_DATA_RESERVED_KEYS.messageFormat.replace("%s", () => quote(key)).replace("%s", () => reason),
4675
+ code: FLOW_DATA_RESERVED_KEYS.code,
4676
+ suggestion: FLOW_DATA_RESERVED_KEYS.suggestion
4677
+ });
4678
+ }
4679
+ }
4680
+
4516
4681
  // src/validators/saveDoor.ts
4517
4682
  var TOOL_DISCOVERY_MODES = /* @__PURE__ */ new Set(["eager", "lazy", "off"]);
4518
4683
  var TEMPLATE_ACTION_OPEN2 = "{{";
@@ -6977,6 +7142,7 @@ function validateParsed(flow, opts, sources) {
6977
7142
  validateSaveDoorExtras(f, issues);
6978
7143
  validateExecutorConfigEnvScopes(f, issues);
6979
7144
  validateServerOwnedQueryKeys(f, issues);
7145
+ validateFlowDataKeys(f, issues);
6980
7146
  validateCredentialEndpointPairing(f, issues);
6981
7147
  validateExamples(f, issues);
6982
7148
  validateOrchestratorExons(f, issues);
package/dist/index.cjs CHANGED
@@ -435,7 +435,7 @@ function validateBasicStructure(flow, issues) {
435
435
  message: "Executor is required for each step",
436
436
  code: "missing_required_field",
437
437
  stepId: stepID,
438
- suggestion: "Specify an executor URL (e.g., 'function://demo/processor')"
438
+ suggestion: "Specify an executor URL (e.g., 'function://utils/log')"
439
439
  });
440
440
  } else if (executorText === null) {
441
441
  issues.error({
@@ -451,7 +451,7 @@ function validateBasicStructure(flow, issues) {
451
451
 
452
452
  // src/spec/aigentflow-spec.json
453
453
  var aigentflow_spec_default = {
454
- specVersion: "2.827.0",
454
+ specVersion: "2.831.0",
455
455
  inputSchemaVersion: 1,
456
456
  executorSchemes: [
457
457
  "a2a",
@@ -948,6 +948,73 @@ var aigentflow_spec_default = {
948
948
  stepFieldFormat: "steps.%s.query.%s",
949
949
  loopStepFieldFormat: "steps.%s.loop.steps.%s.query.%s"
950
950
  },
951
+ functionOperations: {
952
+ code: "unknown_function_operation",
953
+ scheme: "function",
954
+ messageFormat: "step %s uses function://%s, which is not a built-in function. Available: %s",
955
+ suggestionFormat: "Use one of the built-in function operations: %s. For custom logic use a script://, http:// or ai:// executor instead.",
956
+ operations: [
957
+ "binary-processor/validate-and-store",
958
+ "data/create",
959
+ "data/from-json",
960
+ "data/to-json",
961
+ "data/validate",
962
+ "demo/adult-processor",
963
+ "demo/calculator",
964
+ "demo/data-processor",
965
+ "demo/finalizer",
966
+ "demo/senior-processor",
967
+ "demo/youth-processor",
968
+ "document/finalize",
969
+ "document/prepare",
970
+ "json/validate",
971
+ "math/basic",
972
+ "math/calculate",
973
+ "math/sum",
974
+ "results-compiler/aggregate",
975
+ "system/info",
976
+ "text/analysis",
977
+ "text/count-characters",
978
+ "text/count-lines",
979
+ "text/count-occurrences",
980
+ "text/count-words",
981
+ "text/find-pattern",
982
+ "text/generate",
983
+ "text/replace-text",
984
+ "text/search",
985
+ "utils/datetime",
986
+ "utils/format-date",
987
+ "utils/log",
988
+ "utils/parse-date"
989
+ ]
990
+ },
991
+ flowDataReservedKeys: {
992
+ code: "flow_data_key_reserved",
993
+ fieldFormat: "data.%s",
994
+ messageFormat: "data: key %s is reserved: %s. It would write into the engine's own state, so it is not seeded.",
995
+ suggestion: "Rename the key (no leading `_` or `.`, and not one of the engine namespaces memory., orchestrator., campaign., voiceagent.).",
996
+ exact: {
997
+ org_id: "the mission's organization, set by the engine for Thalamus/Engram calls",
998
+ memory: "the memory scope namespace",
999
+ orchestrator: "the orchestrator's state namespace",
1000
+ campaign: "the campaign's state namespace",
1001
+ voiceagent: "the voice agent's state namespace",
1002
+ current_flow: "engine bookkeeping (the current flow definition)",
1003
+ modified_flow: "engine bookkeeping (dynamic flow modification)",
1004
+ modification_history: "engine bookkeeping (dynamic flow modification)",
1005
+ modification_pending: "engine bookkeeping (dynamic flow modification)",
1006
+ modification_requests: "engine bookkeeping (dynamic flow modification)",
1007
+ bottleneck_state: "engine bookkeeping (bottleneck analysis)"
1008
+ },
1009
+ prefixes: [
1010
+ { prefix: "_", reason: "keys starting with `_` are engine bookkeeping (current flow, loop and for_each context, taint lists such as `__untrusted_*`)" },
1011
+ { prefix: ".", reason: "keys starting with `.` are engine step-response slots" },
1012
+ { prefix: "memory.", reason: "the `memory.` namespace belongs to the engine" },
1013
+ { prefix: "orchestrator.", reason: "the `orchestrator.` namespace belongs to the orchestrator" },
1014
+ { prefix: "campaign.", reason: "the `campaign.` namespace belongs to the campaign engine" },
1015
+ { prefix: "voiceagent.", reason: "the `voiceagent.` namespace belongs to the voice agent" }
1016
+ ]
1017
+ },
951
1018
  credentialEndpointPairing: {
952
1019
  stepFieldFormat: "steps.%s.query.%s",
953
1020
  loopStepFieldFormat: "steps.%s.loop.steps.%s.query.%s",
@@ -1885,6 +1952,14 @@ var SERVER_OWNED_QUERY_KEYS = {
1885
1952
  stepFieldFormat: aigentflow_spec_default.serverOwnedQueryKeys.stepFieldFormat,
1886
1953
  loopStepFieldFormat: aigentflow_spec_default.serverOwnedQueryKeys.loopStepFieldFormat
1887
1954
  };
1955
+ var FLOW_DATA_RESERVED_KEYS = {
1956
+ code: aigentflow_spec_default.flowDataReservedKeys.code,
1957
+ fieldFormat: aigentflow_spec_default.flowDataReservedKeys.fieldFormat,
1958
+ messageFormat: aigentflow_spec_default.flowDataReservedKeys.messageFormat,
1959
+ suggestion: aigentflow_spec_default.flowDataReservedKeys.suggestion,
1960
+ exact: aigentflow_spec_default.flowDataReservedKeys.exact,
1961
+ prefixes: aigentflow_spec_default.flowDataReservedKeys.prefixes
1962
+ };
1888
1963
  var CREDENTIAL_ENDPOINT_PAIRING = {
1889
1964
  stepFieldFormat: aigentflow_spec_default.credentialEndpointPairing.stepFieldFormat,
1890
1965
  loopStepFieldFormat: aigentflow_spec_default.credentialEndpointPairing.loopStepFieldFormat,
@@ -1947,9 +2022,51 @@ var TEMPLATE_DEFAULT_PIPED = {
1947
2022
  messageFormat: aigentflow_spec_default.templateDefaultPiped.messageFormat,
1948
2023
  suggestion: aigentflow_spec_default.templateDefaultPiped.suggestion
1949
2024
  };
2025
+ var FUNCTION_OPERATIONS = {
2026
+ code: aigentflow_spec_default.functionOperations.code,
2027
+ scheme: aigentflow_spec_default.functionOperations.scheme,
2028
+ messageFormat: aigentflow_spec_default.functionOperations.messageFormat,
2029
+ suggestionFormat: aigentflow_spec_default.functionOperations.suggestionFormat,
2030
+ operations: new Set(aigentflow_spec_default.functionOperations.operations),
2031
+ sorted: aigentflow_spec_default.functionOperations.operations
2032
+ };
1950
2033
 
1951
2034
  // src/validators/executors.ts
1952
2035
  var SCHEME_SEPARATOR = "://";
2036
+ var UNKNOWN_SCHEME_BASE = (scheme) => `Unknown executor scheme '${scheme}'. It may be valid in a newer AIgentFlow release, or it could be a typo.`;
2037
+ var DECIDE_SCHEME = "decide";
2038
+ var DECIDE_HINT = "Decision models run through ai://{provider}/decide (for example ai://openai/decide), not through a scheme of their own.";
2039
+ var MAX_SCHEME_EDIT_DISTANCE = 2;
2040
+ function editDistance(a, b) {
2041
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
2042
+ for (let i = 1; i <= a.length; i++) {
2043
+ const cur = [i];
2044
+ for (let j = 1; j <= b.length; j++) {
2045
+ cur[j] = Math.min(
2046
+ prev[j] + 1,
2047
+ cur[j - 1] + 1,
2048
+ prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
2049
+ );
2050
+ }
2051
+ prev = cur;
2052
+ }
2053
+ return prev[b.length];
2054
+ }
2055
+ function unknownSchemeMessage(scheme) {
2056
+ const lower = scheme.toLowerCase();
2057
+ const base = UNKNOWN_SCHEME_BASE(scheme);
2058
+ if (lower === DECIDE_SCHEME) return `${base} ${DECIDE_HINT}`;
2059
+ let best;
2060
+ let bestDist = MAX_SCHEME_EDIT_DISTANCE + 1;
2061
+ for (const known of [...EXECUTOR_SCHEMES].sort()) {
2062
+ const d = editDistance(lower, known);
2063
+ if (d < bestDist) {
2064
+ best = known;
2065
+ bestDist = d;
2066
+ }
2067
+ }
2068
+ return best === void 0 ? base : `${base} Did you mean '${best}'?`;
2069
+ }
1953
2070
  function checkExecutor(executor, field, stepID, issues) {
1954
2071
  const sepIdx = executor.indexOf(SCHEME_SEPARATOR);
1955
2072
  const scheme = sepIdx > 0 ? executor.slice(0, sepIdx) : "";
@@ -1968,11 +2085,28 @@ function checkExecutor(executor, field, stepID, issues) {
1968
2085
  if (!EXECUTOR_SCHEMES.has(scheme.toLowerCase())) {
1969
2086
  issues.warn({
1970
2087
  field,
1971
- message: `Unknown executor scheme '${scheme}'. It may be valid in a newer AIgentFlow release, or it could be a typo.`,
2088
+ message: unknownSchemeMessage(scheme),
1972
2089
  code: "unknown_executor_scheme",
1973
2090
  stepId: stepID
1974
2091
  });
2092
+ return;
1975
2093
  }
2094
+ checkFunctionOperation(executor, field, stepID, templated, issues);
2095
+ }
2096
+ function checkFunctionOperation(executor, field, stepID, templated, issues) {
2097
+ if (templated) return;
2098
+ const m = EXECUTOR_URL_PATTERN.exec(executor);
2099
+ if (m === null || m[1] !== FUNCTION_OPERATIONS.scheme) return;
2100
+ const op = `${m[2]}/${m[3]}`;
2101
+ if (FUNCTION_OPERATIONS.operations.has(op)) return;
2102
+ const list = FUNCTION_OPERATIONS.sorted.join(", ");
2103
+ issues.error({
2104
+ field,
2105
+ message: FUNCTION_OPERATIONS.messageFormat.replace("%s", () => JSON.stringify(stepID)).replace("%s", () => op).replace("%s", () => list),
2106
+ code: FUNCTION_OPERATIONS.code,
2107
+ stepId: stepID,
2108
+ suggestion: FUNCTION_OPERATIONS.suggestionFormat.replace("%s", () => list)
2109
+ });
1976
2110
  }
1977
2111
  function validateExecutors(flow, issues) {
1978
2112
  const steps = flow.steps;
@@ -3804,16 +3938,16 @@ function skipSpaces(src, idx) {
3804
3938
  }
3805
3939
  return i;
3806
3940
  }
3807
- function scanString(src, start, quote) {
3941
+ function scanString(src, start, quote2) {
3808
3942
  let i = start + 1;
3809
- const raw = quote === "`";
3943
+ const raw = quote2 === "`";
3810
3944
  while (i < src.length) {
3811
3945
  const c = src[i];
3812
3946
  if (!raw && c === "\\") {
3813
3947
  i += 2;
3814
3948
  continue;
3815
3949
  }
3816
- if (c === quote) return i;
3950
+ if (c === quote2) return i;
3817
3951
  i += 1;
3818
3952
  }
3819
3953
  return -1;
@@ -4416,6 +4550,35 @@ function validateRetiredKeys(flow, issues) {
4416
4550
  }
4417
4551
  }
4418
4552
 
4553
+ // src/validators/flowDataKeys.ts
4554
+ var KEY_DATA = "data";
4555
+ function quote(key) {
4556
+ return JSON.stringify(key);
4557
+ }
4558
+ function flowDataKeyReservation(key) {
4559
+ if (Object.prototype.hasOwnProperty.call(FLOW_DATA_RESERVED_KEYS.exact, key)) {
4560
+ return FLOW_DATA_RESERVED_KEYS.exact[key];
4561
+ }
4562
+ for (const p of FLOW_DATA_RESERVED_KEYS.prefixes) {
4563
+ if (key.startsWith(p.prefix)) return p.reason;
4564
+ }
4565
+ return void 0;
4566
+ }
4567
+ function validateFlowDataKeys(flow, issues) {
4568
+ const data = flow[KEY_DATA];
4569
+ if (!isRecord(data)) return;
4570
+ for (const key of Object.keys(data).sort()) {
4571
+ const reason = flowDataKeyReservation(key);
4572
+ if (reason === void 0) continue;
4573
+ issues.error({
4574
+ field: FLOW_DATA_RESERVED_KEYS.fieldFormat.replace("%s", () => key),
4575
+ message: FLOW_DATA_RESERVED_KEYS.messageFormat.replace("%s", () => quote(key)).replace("%s", () => reason),
4576
+ code: FLOW_DATA_RESERVED_KEYS.code,
4577
+ suggestion: FLOW_DATA_RESERVED_KEYS.suggestion
4578
+ });
4579
+ }
4580
+ }
4581
+
4419
4582
  // src/validators/saveDoor.ts
4420
4583
  var TOOL_DISCOVERY_MODES = /* @__PURE__ */ new Set(["eager", "lazy", "off"]);
4421
4584
  var TEMPLATE_ACTION_OPEN2 = "{{";
@@ -6878,6 +7041,7 @@ function validateParsed(flow, opts, sources) {
6878
7041
  validateSaveDoorExtras(f, issues);
6879
7042
  validateExecutorConfigEnvScopes(f, issues);
6880
7043
  validateServerOwnedQueryKeys(f, issues);
7044
+ validateFlowDataKeys(f, issues);
6881
7045
  validateCredentialEndpointPairing(f, issues);
6882
7046
  validateExamples(f, issues);
6883
7047
  validateOrchestratorExons(f, issues);