@mamund/grail 0.0.0-stage → 0.1.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -2
  3. package/affordanceModel.js +23 -0
  4. package/affordanceRegistry.js +14 -0
  5. package/bindings/binding.js +229 -0
  6. package/bindings/httpBinding.js +173 -0
  7. package/bindings/nodeBinding.js +25 -0
  8. package/bindings/stdioBinding.js +58 -0
  9. package/cli/grail-cli.js +565 -0
  10. package/client.js +77 -0
  11. package/config-loader/loadEnvironment.js +71 -0
  12. package/docs/ENVIRONMENT.md +381 -0
  13. package/docs/grail-trust-model.md +665 -0
  14. package/examples/cli-tour/README.md +346 -0
  15. package/examples/cli-tour/capabilities/farewell.js +3 -0
  16. package/examples/cli-tour/capabilities/hello.js +3 -0
  17. package/examples/cli-tour/config/goal.json +3 -0
  18. package/examples/cli-tour/config/inputs.json +3 -0
  19. package/examples/cli-tour/config/observations.json +23 -0
  20. package/examples/cli-tour/config/registry.json +48 -0
  21. package/examples/cli-tour/config/worldstate.json +4 -0
  22. package/examples/cli-tour/g-loop.sh +7 -0
  23. package/examples/cli-tour/inputs/jane.json +3 -0
  24. package/examples/cli-tour/inputs/mike.json +3 -0
  25. package/examples/cli-tour/inputs/ruth.json +3 -0
  26. package/grail.js +59 -0
  27. package/images/grail-release-banner.png +0 -0
  28. package/index.js +2 -0
  29. package/observationStore.js +110 -0
  30. package/package.json +57 -4
  31. package/schemas/goal.schema.json +14 -0
  32. package/schemas/inputs.schema.json +6 -0
  33. package/schemas/observations.schema.json +75 -0
  34. package/schemas/registry.schema.json +285 -0
  35. package/schemas/worldstate.schema.json +7 -0
  36. package/server.js +201 -0
  37. package/utils/loadJSON.js +33 -0
  38. package/utils/validateWithSchema.js +42 -0
  39. package/worldState.js +36 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 amundsen-com, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,329 @@
1
- # Temporary Holding Version
1
+ <img src="./images/grail-release-banner.png" />
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ GRAIL is a runtime for pursuing a declared goal within a bounded environment of available capabilities.
4
+
5
+ A GRAIL world describes the conditions that matter, the capabilities that can change those conditions, and the effects produced when those capabilities succeed. The runtime works from the goal and the current world state to determine what condition needs to change and which available affordance can change it.
6
+
7
+ The environment defines the possibilities. GRAIL pursues the goal without requiring a predefined workflow or execution path.
8
+
9
+ > Define the environment, not the path.
10
+
11
+ ## Status
12
+
13
+ GRAIL is currently being prepared for beta release. The core runtime, configuration validation, Node/HTTP/stdio bindings, programmatic API, and initial CLI are working. Interfaces and package details may still change before the beta is declared stable.
14
+
15
+ ## Quick start
16
+
17
+ Install and run the most recent beta edition as follows:
18
+
19
+ ```bash
20
+ npm install -g @mamund/grail@beta
21
+ ```
22
+
23
+ During development, install the repository and expose the local CLI:
24
+
25
+ ```bash
26
+ npm install
27
+ npm link
28
+ ```
29
+
30
+ Create a runnable GRAIL world:
31
+
32
+ ```bash
33
+ grail init my-world
34
+ cd my-world
35
+ grail validate
36
+ grail run
37
+ ```
38
+
39
+ `grail init` creates a complete example using a small Node capability. A successful run pursues the generated `greetingCreated` goal and executes the capability needed to establish it.
40
+
41
+ ## A GRAIL world
42
+
43
+ The generated world has this structure:
44
+
45
+ ```text
46
+ my-world/
47
+ ├── capabilities/
48
+ │ └── hello.mjs
49
+ └── config/
50
+ ├── goal.json
51
+ ├── inputs.json
52
+ ├── registry.json
53
+ └── worldstate.json
54
+ ```
55
+
56
+ The four configuration documents have distinct responsibilities:
57
+
58
+ - `goal.json` declares the condition GRAIL is trying to establish.
59
+ - `worldstate.json` records the conditions that currently hold in the world.
60
+ - `inputs.json` supplies initial data available to capabilities.
61
+ - `registry.json` describes available affordances, their preconditions and effects, their inputs, and their executable bindings.
62
+
63
+ Executable domain behavior lives in capabilities. GRAIL remains opaque to the application domain.
64
+
65
+ ## How pursuit works
66
+
67
+ Suppose the goal is:
68
+
69
+ ```json
70
+ {
71
+ "goal": "greetingCreated"
72
+ }
73
+ ```
74
+
75
+ and the initial world contains:
76
+
77
+ ```json
78
+ {
79
+ "greetingCreated": false
80
+ }
81
+ ```
82
+
83
+ The registry can expose an affordance whose effect is `greetingCreated`. GRAIL can select that affordance, resolve its inputs, invoke its bound capability, and apply the declared effect when execution succeeds.
84
+
85
+ Conceptually:
86
+
87
+ ```text
88
+ goal
89
+ ↓
90
+ inspect current world state
91
+ ↓
92
+ select an unresolved condition
93
+ ↓
94
+ select an affordance that can establish it
95
+ ↓
96
+ resolve inputs and execute its capability
97
+ ↓
98
+ apply effects on success
99
+ ↓
100
+ continue until the goal is reached or cannot be resolved
101
+ ```
102
+
103
+ The exact traversal is not encoded as a workflow. It emerges from the current conditions and the affordances available in the world.
104
+
105
+ ## CLI
106
+
107
+ ### `grail init`
108
+
109
+ Create a complete runnable world:
110
+
111
+ ```bash
112
+ grail init my-world
113
+ ```
114
+
115
+ GRAIL refuses to overwrite an existing target directory.
116
+
117
+ ### `grail validate`
118
+
119
+ Load and validate a GRAIL environment without executing it:
120
+
121
+ ```bash
122
+ grail validate
123
+ ```
124
+
125
+ The default configuration directory is `./config`. A different directory can be supplied with:
126
+
127
+ ```bash
128
+ grail validate --config ./path/to/config
129
+ ```
130
+
131
+ ### `grail show`
132
+
133
+ Inspect a validated GRAIL world without executing capabilities:
134
+
135
+ ```bash
136
+ grail show
137
+ grail show registry
138
+ grail show worldstate
139
+ grail show inputs
140
+ grail show goal
141
+ grail show registry --config ./my-world/config
142
+ ```
143
+
144
+ Without a target, `show` prints all four configuration documents as a single JSON object. With a target, it prints that document as formatted JSON. The entire environment must validate first, even when only one document is requested. `show` displays configured initial values, not the worldstate or observations from a previous run, and does not write files.
145
+
146
+ ### `grail run`
147
+
148
+ Pursue the goal declared by an environment:
149
+
150
+ ```bash
151
+ grail run
152
+ ```
153
+
154
+ or:
155
+
156
+ ```bash
157
+ grail run --config ./path/to/config
158
+ ```
159
+
160
+ The CLI writes execution observations to `observations.json` in the selected configuration directory. Each `grail run` starts an independent pursuit and overwrites this file rather than appending to prior runs. Copy or rename the file before another run if you need to retain its execution record. The configured `worldstate.json` is not modified.
161
+
162
+ The goal and inputs declared by the environment can be overridden for a single invocation. A goal override takes precedence over `goal.json`:
163
+
164
+ ```bash
165
+ grail run --config ./config --goal anotherGoal
166
+ ```
167
+
168
+ Inputs can be supplied as an inline JSON object:
169
+
170
+ ```bash
171
+ grail run --config ./config --inputs '{"name":"Mike"}'
172
+ ```
173
+
174
+ or loaded from a JSON file:
175
+
176
+ ```bash
177
+ grail run --config ./config --inputs-file ./cases/mike.json
178
+ ```
179
+
180
+ To inspect results rather than the default execution trace, use `--output`:
181
+
182
+ ```bash
183
+ grail run --output summary
184
+ grail run --output json
185
+ ```
186
+
187
+ `summary` prints the goal, success or failure, capability invocation count, and captured outputs. `json` prints the complete pursuit result (`goal`, `reached`, `worldstate`, and `observations`) as valid JSON suitable for tools such as `jq`. Routine runtime trace messages are suppressed in these modes; errors still appear on stderr. Omitting `--output` preserves the existing trace. The option accepts both `--output json` and `--output=json`. Invalid modes exit with code `2`.
188
+
189
+ Input overrides replace the contents of `inputs.json` for that invocation; they are not merged with it. `--inputs` and `--inputs-file` are mutually exclusive. Neither goal nor input overrides modify the environment files on disk.
190
+
191
+ Relative binding paths are resolved from the GRAIL world root—the parent directory of the selected configuration directory—not from the shell's current working directory. This allows a world to be run from another directory without changing the paths declared by its bindings. Paths supplied with `--inputs-file`, however, are resolved relative to the caller's current working directory.
192
+
193
+ General CLI information is available with:
194
+
195
+ ```bash
196
+ grail --help
197
+ grail --version
198
+ grail init --help
199
+ grail validate --help
200
+ grail run --help
201
+ ```
202
+
203
+ ### Exit codes
204
+
205
+ GRAIL uses process exit codes so CLI commands can be used reliably from shell scripts and other automation:
206
+
207
+ | Code | Meaning |
208
+ | ---: | --- |
209
+ | `0` | The command completed successfully. For `grail run`, the goal was reached. |
210
+ | `1` | GRAIL ran, but the pursuit or capability execution failed. |
211
+ | `2` | The command, configuration, or environment was invalid. |
212
+
213
+ For example, a caller can vary the inputs while GRAIL performs one independent pursuit for each invocation:
214
+
215
+ ```bash
216
+ for input in ./cases/*.json
217
+ do
218
+ grail run --config ./config --inputs-file "$input" || break
219
+ done
220
+ ```
221
+
222
+ Iteration remains the responsibility of the caller; GRAIL pursues the selected goal once per invocation.
223
+
224
+ ### Errors
225
+
226
+ CLI errors identify the kind of failure and provide relevant context. Typical messages include:
227
+
228
+ ```text
229
+ GRAIL: Configuration error: ...
230
+ GRAIL: Execution failed while pursuing goal "...": ...
231
+ GRAIL: Pursuit failed: goal "..." cannot be resolved with the available capabilities.
232
+ ```
233
+
234
+ Configuration and invocation errors exit with code `2`. Execution and pursuit failures exit with code `1`. Expected user errors are reported as concise CLI messages rather than uncaught stack traces.
235
+
236
+ ## Command-line Tour
237
+
238
+ For a hands-on introduction to GRAIL, see [A Tour of GRAIL at the Command Line](./examples/cli-tour/README.md).
239
+
240
+ The tour uses a complete example world to explore:
241
+
242
+ - running and validating a GRAIL world
243
+ - overriding inputs from the command line or a file
244
+ - running a world from different filesystem locations
245
+ - pursuing different goals in the same world
246
+ - recognizing an unresolvable goal
247
+ - using exit codes and shell loops to compose GRAIL with other tools
248
+
249
+ The tour is designed to be run directly from the command line and includes all required capabilities, configuration, and sample inputs.
250
+
251
+ ## Programmatic API
252
+
253
+ GRAIL can also be loaded as a module by another application or front end.
254
+
255
+ Load a filesystem-based environment and pursue its goal:
256
+
257
+ ```javascript
258
+ import { Grail, loadEnvironment } from '@mamund/grail';
259
+
260
+ const environment = loadEnvironment('./config');
261
+
262
+ const grail = new Grail({
263
+ registry: environment.registry,
264
+ worldstate: environment.worldstate,
265
+ inputs: environment.inputs
266
+ });
267
+
268
+ const result = await grail.pursue(environment.goal);
269
+
270
+ console.log(result.reached);
271
+ console.log(result.worldstate);
272
+ console.log(result.observations);
273
+ ```
274
+
275
+ The `Grail` runtime itself does not require filesystem-backed observations. Observations are retained in memory by default. A caller can provide an `observationPath` when file persistence is wanted.
276
+
277
+ `pursue()` returns the pursued goal, whether it was reached, the resulting world state, and the observations collected during execution.
278
+
279
+ ## Bindings
280
+
281
+ An affordance may be connected to an executable capability through a binding. The current runtime supports three protocols:
282
+
283
+ | Protocol | Purpose |
284
+ | --- | --- |
285
+ | `node` | Invoke a local JavaScript module and exported function. |
286
+ | `http` | Invoke a capability through HTTP. |
287
+ | `stdio` | Launch a local process, send JSON inputs on stdin, and consume JSON output from stdout. |
288
+
289
+ Bindings are adapters between GRAIL affordances and capability implementations. Goal pursuit does not depend on the implementation technology behind a capability.
290
+
291
+ ## Execution results and observations
292
+
293
+ Bound capabilities reduce execution to GRAIL's runtime result model. Successful execution permits declared effects to be applied; failed execution does not.
294
+
295
+ Observations retain execution evidence such as invocation details, responses, extracted outputs, and the resulting execution status. This allows GRAIL to keep its runtime contract small while preserving useful diagnostic information.
296
+
297
+ ## Validation and tests
298
+
299
+ Run the current automated test suite with:
300
+
301
+ ```bash
302
+ npm test
303
+ ```
304
+
305
+ The suite covers end-to-end goal pursuit through the public API, stdio execution and output handling, world-relative path resolution, goal and input invocation overrides, and CLI behavior including configuration errors, execution failures, pursuit failures, messages, and exit codes.
306
+
307
+ Tests run as independent suites with start/pass/fail reporting and a 30-second timeout per suite. The CLI regression tests are organized under `test/cli/` into commands, show, output, overrides, and errors. Run an individual suite directly, for example:
308
+
309
+ ```bash
310
+ node test/cli/output.js
311
+ ```
312
+
313
+ Each CLI subprocess also has a 15-second timeout so a hanging command fails with a useful error rather than blocking the entire suite.
314
+
315
+
316
+ ## Design notes
317
+
318
+ Two documents in `docs/` provide additional architectural context:
319
+
320
+ - [What Is Not in the Agent Must Be in the Environment](./docs/ENVIRONMENT.md) discusses GRAIL's environment-first approach to bounded autonomy.
321
+ - [GRAIL Trust Model](./docs/grail-trust-model.md) describes the runtime's trust boundaries and the responsibilities retained by capabilities and the surrounding environment.
322
+
323
+ **Security and trust**: GRAIL executes capabilities defined by a trusted world. Only run reviewed configurations and bindings. Capabilities are responsible for authentication, authorization, and domain security. Observations may contain sensitive data, and the beta runtime does not enforce execution timeouts. See the GRAIL Trust Model for execution boundaries and operational limitations.
324
+
325
+ ## Beta scope
326
+
327
+ The beta is centered on a small runtime and CLI for composing and executing GRAIL worlds. Current work is focused on hardening the public module boundary, CLI behavior, path semantics, error handling, package metadata, documentation, and regression coverage.
328
+
329
+ More advanced selection strategies, additional tooling, richer authoring experiences, and other execution features can build on this foundation without changing the basic model of goal pursuit through conditions, affordances, effects, and bound capabilities.
@@ -0,0 +1,23 @@
1
+ // affordanceModel.js
2
+
3
+ export class Affordance {
4
+ constructor({
5
+ id,
6
+ action,
7
+ type,
8
+ enabled,
9
+ preconditions = [],
10
+ inputs = [],
11
+ effects = [],
12
+ binding
13
+ }) {
14
+ this.id = id;
15
+ this.action = action;
16
+ this.type = type;
17
+ this.enabled = enabled;
18
+ this.preconditions = preconditions;
19
+ this.inputs = inputs;
20
+ this.effects = effects;
21
+ this.binding = binding;
22
+ }
23
+ }
@@ -0,0 +1,14 @@
1
+ // affordanceRegistry.js (loads from file at runtime)
2
+
3
+ import { Affordance } from './affordanceModel.js';
4
+
5
+ export function loadAffordanceRegistry(registryData) {
6
+ const registry = {};
7
+
8
+ for (let key in registryData) {
9
+ registry[key] = new Affordance(registryData[key]);
10
+ }
11
+
12
+ return registry;
13
+ }
14
+
@@ -0,0 +1,229 @@
1
+ import { executeHttpBinding } from "./httpBinding.js";
2
+ import { executeNodeBinding } from "./nodeBinding.js";
3
+ import { executeStdioBinding } from "./stdioBinding.js";
4
+
5
+ export async function executeBinding(binding, inputs, baseDir = process.cwd()) {
6
+ switch (binding.protocol) {
7
+ case "http":
8
+ return executeHttp(binding, inputs);
9
+
10
+ case "node":
11
+ return executeNode(binding, inputs, baseDir);
12
+
13
+ case "stdio":
14
+ return executeStdio(binding, inputs, baseDir);
15
+
16
+ default:
17
+ throw new Error(`Unsupported binding protocol: ${binding.protocol}`);
18
+ }
19
+ }
20
+
21
+ async function executeHttp(binding, inputs) {
22
+ const interaction = await executeHttpBinding(binding, inputs);
23
+ const outputs = extractHttpOutputs(binding.outputs, interaction.response);
24
+
25
+ return {
26
+ ok: interaction.response.ok,
27
+ description: `${binding.method} ${binding.url}`,
28
+ summary: interaction.response.error?.type === "network"
29
+ ? `network error (${interaction.response.error.message})`
30
+ : `HTTP ${interaction.response.status}`,
31
+ invocation: {
32
+ request: interaction.request
33
+ },
34
+ response: {
35
+ status: interaction.response.status,
36
+ headers: interaction.response.headers,
37
+ body: interaction.response.body
38
+ },
39
+ outputs
40
+ };
41
+ }
42
+
43
+ async function executeNode(binding, inputs, baseDir) {
44
+ try {
45
+ const interaction = await executeNodeBinding(binding, inputs, baseDir);
46
+ const outputs = extractNodeOutputs(binding.outputs, interaction.result);
47
+
48
+ return {
49
+ ok: true,
50
+ description: `${binding.module} :: ${binding.function}`,
51
+ summary: `${binding.module} :: ${binding.function}`,
52
+ invocation: {
53
+ module: interaction.module,
54
+ function: interaction.function,
55
+ inputs: interaction.inputs
56
+ },
57
+ response: {
58
+ result: interaction.result
59
+ },
60
+ outputs
61
+ };
62
+ } catch (error) {
63
+ return {
64
+ ok: false,
65
+ description: `${binding.module} :: ${binding.function}`,
66
+ summary: error.message,
67
+ invocation: {
68
+ module: binding.module,
69
+ function: binding.function,
70
+ inputs
71
+ },
72
+ response: {
73
+ error: error.message
74
+ },
75
+ outputs: {}
76
+ };
77
+ }
78
+ }
79
+
80
+ async function executeStdio(binding, inputs, baseDir) {
81
+ try {
82
+ const interaction = await executeStdioBinding(binding, inputs, baseDir);
83
+
84
+ const ok =
85
+ interaction.exitCode === 0 &&
86
+ !interaction.error;
87
+
88
+ const outputs = ok
89
+ ? extractStdioOutputs(binding.outputs, interaction.stdout)
90
+ : {};
91
+
92
+ return {
93
+ ok,
94
+ description: `${binding.command} ${(binding.args || []).join(" ")}`,
95
+ summary: ok
96
+ ? `${binding.command} ${(binding.args || []).join(" ")}`
97
+ : interaction.error,
98
+ invocation: {
99
+ command: interaction.command,
100
+ args: interaction.args,
101
+ inputs: interaction.inputs
102
+ },
103
+ response: {
104
+ exitCode: interaction.exitCode,
105
+ stdout: interaction.stdout,
106
+ stderr: interaction.stderr,
107
+ ...(interaction.error && { error: interaction.error })
108
+ },
109
+ outputs
110
+ };
111
+ } catch (error) {
112
+ return {
113
+ ok: false,
114
+ description: `${binding.command} ${(binding.args || []).join(" ")}`,
115
+ summary: error.message,
116
+ invocation: {
117
+ command: binding.command,
118
+ args: binding.args || [],
119
+ inputs
120
+ },
121
+ response: {
122
+ error: error.message
123
+ },
124
+ outputs: {}
125
+ };
126
+ }
127
+ }
128
+
129
+ function extractHttpOutputs(outputDefinitions, response) {
130
+ const outputs = {};
131
+
132
+ if (!outputDefinitions) {
133
+ return outputs;
134
+ }
135
+
136
+ for (const [outputName, definition] of Object.entries(outputDefinitions)) {
137
+ switch (definition.from) {
138
+ case "body": {
139
+ const extracted = readPath(response.body, definition.path);
140
+ if (extracted.found) {
141
+ outputs[outputName] = extracted.value;
142
+ }
143
+ break;
144
+ }
145
+
146
+ case "header": {
147
+ const headerName = definition.name.toLowerCase();
148
+
149
+ for (const [name, value] of Object.entries(response.headers)) {
150
+ if (name.toLowerCase() === headerName) {
151
+ outputs[outputName] = value;
152
+ break;
153
+ }
154
+ }
155
+ break;
156
+ }
157
+
158
+ case "status":
159
+ outputs[outputName] = response.status;
160
+ break;
161
+ }
162
+ }
163
+
164
+ return outputs;
165
+ }
166
+
167
+ function extractNodeOutputs(outputDefinitions, result) {
168
+ const outputs = {};
169
+
170
+ if (!outputDefinitions) {
171
+ return outputs;
172
+ }
173
+
174
+ for (const [outputName, definition] of Object.entries(outputDefinitions)) {
175
+ if (definition.from !== "result") {
176
+ continue;
177
+ }
178
+
179
+ const extracted = readPath(result, definition.path);
180
+
181
+ if (extracted.found) {
182
+ outputs[outputName] = extracted.value;
183
+ }
184
+ }
185
+
186
+ return outputs;
187
+ }
188
+
189
+ function readPath(value, path) {
190
+ const segments = path.split(".");
191
+ let current = value;
192
+
193
+ for (const segment of segments) {
194
+ if (
195
+ current === null ||
196
+ current === undefined ||
197
+ (typeof current !== "object" && !Array.isArray(current)) ||
198
+ !(segment in current)
199
+ ) {
200
+ return { found: false };
201
+ }
202
+
203
+ current = current[segment];
204
+ }
205
+
206
+ return { found: true, value: current };
207
+ }
208
+
209
+ function extractStdioOutputs(outputDefinitions, stdout) {
210
+ const outputs = {};
211
+
212
+ if (!outputDefinitions) {
213
+ return outputs;
214
+ }
215
+
216
+ for (const [outputName, definition] of Object.entries(outputDefinitions)) {
217
+ if (definition.from !== "stdout") {
218
+ continue;
219
+ }
220
+
221
+ const extracted = readPath(stdout, definition.path);
222
+
223
+ if (extracted.found) {
224
+ outputs[outputName] = extracted.value;
225
+ }
226
+ }
227
+
228
+ return outputs;
229
+ }