@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/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@mamund/grail",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A runtime for pursuing goals within bounded environments of available capabilities.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "grail": "./cli/grail-cli.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node test/run-all.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "keywords": [
17
+ "agents",
18
+ "autonomous-agents",
19
+ "agentic",
20
+ "runtime",
21
+ "orchestration",
22
+ "affordances"
23
+ ],
24
+ "author": "Mike Amundsen",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/mamund/grail-release.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/mamund/grail-release/issues"
32
+ },
33
+ "homepage": "https://github.com/mamund/grail-release#readme",
34
+ "dependencies": {
35
+ "ajv": "^8.17.1",
36
+ "ajv-formats": "^3.0.1"
37
+ },
38
+ "exports": {
39
+ ".": "./index.js"
40
+ },
41
+ "files": [
42
+ "index.js",
43
+ "grail.js",
44
+ "client.js",
45
+ "server.js",
46
+ "affordanceModel.js",
47
+ "affordanceRegistry.js",
48
+ "observationStore.js",
49
+ "worldState.js",
50
+ "bindings/",
51
+ "cli/",
52
+ "config-loader/",
53
+ "utils/",
54
+ "schemas/",
55
+ "docs/",
56
+ "examples/cli-tour/",
57
+ "images/"
58
+ ]
59
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Goal File",
4
+ "type": "object",
5
+ "required": ["goal"],
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "goal": {
9
+ "type": "string",
10
+ "description": "The name of the final goal affordance to pursue"
11
+ }
12
+ }
13
+ }
14
+
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": true
5
+ }
6
+
@@ -0,0 +1,75 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "GRAIL Observations",
4
+ "type": "array",
5
+ "items": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "invocation",
10
+ "response",
11
+ "outputs",
12
+ "result"
13
+ ],
14
+ "properties": {
15
+ "invocation": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": [
19
+ "id",
20
+ "affordance",
21
+ "timestamp",
22
+ "request"
23
+ ],
24
+ "properties": {
25
+ "id": {
26
+ "type": "string",
27
+ "minLength": 1
28
+ },
29
+ "affordance": {
30
+ "type": "string",
31
+ "minLength": 1
32
+ },
33
+ "timestamp": {
34
+ "type": "string",
35
+ "format": "date-time"
36
+ },
37
+ "request": {
38
+ "type": "object"
39
+ }
40
+ }
41
+ },
42
+ "response": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": [
46
+ "status",
47
+ "headers",
48
+ "body"
49
+ ],
50
+ "properties": {
51
+ "status": {
52
+ "type": "integer",
53
+ "minimum": 100,
54
+ "maximum": 599
55
+ },
56
+ "headers": {
57
+ "type": "object"
58
+ },
59
+ "body": {}
60
+ }
61
+ },
62
+ "outputs": {
63
+ "type": "object"
64
+ },
65
+ "result": {
66
+ "type": "string",
67
+ "enum": [
68
+ "SUCCESS",
69
+ "BLOCKED",
70
+ "FAIL"
71
+ ]
72
+ }
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,285 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Affordance Registry",
4
+ "type": "object",
5
+ "additionalProperties": {
6
+ "type": "object",
7
+ "required": [
8
+ "id",
9
+ "action",
10
+ "type",
11
+ "preconditions",
12
+ "inputs",
13
+ "effects"
14
+ ],
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "enabled": {
18
+ "type": "boolean"
19
+ },
20
+ "id": {
21
+ "type": "string"
22
+ },
23
+ "action": {
24
+ "type": "string"
25
+ },
26
+ "type": {
27
+ "type": "string",
28
+ "enum": [
29
+ "task"
30
+ ]
31
+ },
32
+ "preconditions": {
33
+ "type": "array",
34
+ "items": {
35
+ "type": "string"
36
+ }
37
+ },
38
+ "inputs": {
39
+ "type": "object",
40
+ "additionalProperties": {
41
+ "type": "string",
42
+ "minLength": 1
43
+ }
44
+ },
45
+ "effects": {
46
+ "type": "array",
47
+ "items": {
48
+ "type": "string"
49
+ }
50
+ },
51
+ "execute": {
52
+ "type": "object",
53
+ "required": [
54
+ "type",
55
+ "name"
56
+ ],
57
+ "properties": {
58
+ "type": {
59
+ "type": "string",
60
+ "enum": [
61
+ "function"
62
+ ]
63
+ },
64
+ "name": {
65
+ "type": "string"
66
+ }
67
+ },
68
+ "additionalProperties": true
69
+ },
70
+ "binding": {
71
+ "oneOf": [
72
+ {
73
+ "type": "object",
74
+ "required": [
75
+ "protocol",
76
+ "method",
77
+ "url"
78
+ ],
79
+ "additionalProperties": false,
80
+ "properties": {
81
+ "enabled": {
82
+ "type": "boolean"
83
+ },
84
+ "protocol": {
85
+ "type": "string",
86
+ "enum": [
87
+ "http"
88
+ ]
89
+ },
90
+ "method": {
91
+ "type": "string",
92
+ "enum": [
93
+ "GET",
94
+ "POST",
95
+ "PUT",
96
+ "PATCH",
97
+ "DELETE"
98
+ ]
99
+ },
100
+ "url": {
101
+ "type": "string"
102
+ },
103
+ "contentType": {
104
+ "type": "string",
105
+ "enum": [
106
+ "application/json",
107
+ "application/x-www-form-urlencoded"
108
+ ]
109
+ },
110
+ "parameters": {
111
+ "type": "object",
112
+ "additionalProperties": {
113
+ "type": "object",
114
+ "required": [
115
+ "in"
116
+ ],
117
+ "additionalProperties": false,
118
+ "properties": {
119
+ "in": {
120
+ "type": "string",
121
+ "enum": [
122
+ "path",
123
+ "query",
124
+ "header",
125
+ "body"
126
+ ]
127
+ },
128
+ "name": {
129
+ "type": "string",
130
+ "minLength": 1
131
+ }
132
+ }
133
+ }
134
+ },
135
+ "outputs": {
136
+ "type": "object",
137
+ "additionalProperties": {
138
+ "oneOf": [
139
+ {
140
+ "type": "object",
141
+ "required": [
142
+ "from",
143
+ "path"
144
+ ],
145
+ "additionalProperties": false,
146
+ "properties": {
147
+ "from": {
148
+ "const": "body"
149
+ },
150
+ "path": {
151
+ "type": "string",
152
+ "minLength": 1
153
+ }
154
+ }
155
+ },
156
+ {
157
+ "type": "object",
158
+ "required": [
159
+ "from",
160
+ "name"
161
+ ],
162
+ "additionalProperties": false,
163
+ "properties": {
164
+ "from": {
165
+ "const": "header"
166
+ },
167
+ "name": {
168
+ "type": "string",
169
+ "minLength": 1
170
+ }
171
+ }
172
+ },
173
+ {
174
+ "type": "object",
175
+ "required": [
176
+ "from"
177
+ ],
178
+ "additionalProperties": false,
179
+ "properties": {
180
+ "from": {
181
+ "const": "status"
182
+ }
183
+ }
184
+ }
185
+ ]
186
+ }
187
+ }
188
+ }
189
+ },
190
+ {
191
+ "type": "object",
192
+ "required": [
193
+ "protocol",
194
+ "module",
195
+ "function"
196
+ ],
197
+ "additionalProperties": false,
198
+ "properties": {
199
+ "enabled": {
200
+ "type": "boolean"
201
+ },
202
+ "protocol": {
203
+ "const": "node"
204
+ },
205
+ "module": {
206
+ "type": "string",
207
+ "minLength": 1
208
+ },
209
+ "function": {
210
+ "type": "string",
211
+ "minLength": 1
212
+ },
213
+ "outputs": {
214
+ "type": "object",
215
+ "additionalProperties": {
216
+ "type": "object",
217
+ "required": [
218
+ "from",
219
+ "path"
220
+ ],
221
+ "additionalProperties": false,
222
+ "properties": {
223
+ "from": {
224
+ "const": "result"
225
+ },
226
+ "path": {
227
+ "type": "string",
228
+ "minLength": 1
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+ },
235
+ {
236
+ "type": "object",
237
+ "required": [
238
+ "protocol",
239
+ "command"
240
+ ],
241
+ "additionalProperties": false,
242
+ "properties": {
243
+ "enabled": {
244
+ "type": "boolean"
245
+ },
246
+ "protocol": {
247
+ "const": "stdio"
248
+ },
249
+ "command": {
250
+ "type": "string",
251
+ "minLength": 1
252
+ },
253
+ "args": {
254
+ "type": "array",
255
+ "items": {
256
+ "type": "string"
257
+ }
258
+ },
259
+ "outputs": {
260
+ "type": "object",
261
+ "additionalProperties": {
262
+ "type": "object",
263
+ "required": [
264
+ "from",
265
+ "path"
266
+ ],
267
+ "additionalProperties": false,
268
+ "properties": {
269
+ "from": {
270
+ "const": "stdout"
271
+ },
272
+ "path": {
273
+ "type": "string",
274
+ "minLength": 1
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+ ]
282
+ }
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": {
5
+ "type": "boolean"
6
+ }
7
+ }
package/server.js ADDED
@@ -0,0 +1,201 @@
1
+ import { executeBinding } from "./bindings/binding.js";
2
+
3
+ // server.js (generic broker)
4
+ export class Server {
5
+ constructor(worldState, affordanceRegistry, observationStore, baseDir = process.cwd()) {
6
+ this.worldState = worldState;
7
+ this.affordanceRegistry = affordanceRegistry;
8
+ this.observationStore = observationStore;
9
+ this.baseDir = baseDir;
10
+ }
11
+
12
+ async attempt(affordance, inputs) {
13
+ console.log(`\n[SERVER] Attempting affordance: ${affordance.action}`);
14
+
15
+ const unmetPreconditions = affordance.preconditions.filter(
16
+ pre => !this.worldState.isPreconditionMet(pre)
17
+ );
18
+
19
+ if (unmetPreconditions.length > 0) {
20
+ const pre = this.selectUnmetCondition(unmetPreconditions);
21
+
22
+ console.log(`[SERVER] Blocked: selected unmet precondition: ${pre}`);
23
+
24
+ const candidates = this.findAffordancesForCondition(pre);
25
+ const nextAffordance = this.selectAffordance(candidates);
26
+
27
+ if (nextAffordance) {
28
+ return {
29
+ result: "BLOCKED",
30
+ condition: pre,
31
+ offeredAffordances: [nextAffordance]
32
+ };
33
+ }
34
+
35
+ return {
36
+ result: "BLOCKED",
37
+ condition: pre,
38
+ offeredAffordances: []
39
+ };
40
+ }
41
+
42
+ const resolvedInputs = {};
43
+
44
+ for (const [inputName, source] of Object.entries(affordance.inputs)) {
45
+ const resolution = this.resolveInput(source, inputs);
46
+
47
+ if (!resolution.resolved) {
48
+ console.log(`[SERVER] Blocked: missing input: ${inputName}`);
49
+
50
+ return {
51
+ result: "BLOCKED",
52
+ offeredAffordances: []
53
+ };
54
+ }
55
+
56
+ resolvedInputs[inputName] = resolution.value;
57
+ }
58
+
59
+ if (
60
+ affordance.binding &&
61
+ affordance.binding.enabled !== false
62
+ ) {
63
+ const interaction = await executeBinding(
64
+ affordance.binding,
65
+ resolvedInputs,
66
+ this.baseDir
67
+ );
68
+
69
+ console.log(`[SERVER] Executing binding: ${interaction.description}`);
70
+
71
+ this.observationStore.append({
72
+ invocation: {
73
+ id: this.observationStore.nextInvocationId(),
74
+ affordance: affordance.action,
75
+ timestamp: new Date().toISOString(),
76
+ ...interaction.invocation
77
+ },
78
+ response: interaction.response,
79
+ outputs: interaction.outputs,
80
+ result: interaction.ok ? "SUCCESS" : "FAIL"
81
+ });
82
+
83
+ if (!interaction.ok) {
84
+ console.log(`[SERVER] Binding failed: ${interaction.summary}`);
85
+
86
+ return {
87
+ result: "FAIL",
88
+ offeredAffordances: []
89
+ };
90
+ }
91
+
92
+ console.log(`[SERVER] Binding succeeded: ${interaction.summary}`);
93
+ }
94
+
95
+ console.log(
96
+ `[SERVER] Success: applying effects - ${affordance.effects}`
97
+ );
98
+
99
+ this.worldState.applyEffects(affordance.effects);
100
+
101
+ return {
102
+ result: "SUCCESS",
103
+ offeredAffordances: []
104
+ };
105
+ }
106
+
107
+ resolveGoal(goal) {
108
+ if (this.worldState.isPreconditionMet(goal)) {
109
+ return {
110
+ result: "SUCCESS",
111
+ offeredAffordances: []
112
+ };
113
+ }
114
+
115
+ const candidates = this.findAffordancesForCondition(goal);
116
+ const nextAffordance = this.selectAffordance(candidates);
117
+
118
+ if (nextAffordance) {
119
+ return {
120
+ result: "BLOCKED",
121
+ condition: goal,
122
+ offeredAffordances: [nextAffordance]
123
+ };
124
+ }
125
+
126
+ return {
127
+ result: "BLOCKED",
128
+ condition: goal,
129
+ offeredAffordances: []
130
+ };
131
+ }
132
+
133
+ resolveInput(source, inputs) {
134
+ const inputPrefix = "$inputs.";
135
+
136
+ if (source.startsWith(inputPrefix)) {
137
+ const inputName = source.slice(inputPrefix.length);
138
+
139
+ if (!inputName || !(inputName in inputs)) {
140
+ return { resolved: false };
141
+ }
142
+
143
+ return {
144
+ resolved: true,
145
+ value: inputs[inputName]
146
+ };
147
+ }
148
+
149
+ if (source.startsWith("$outputs.")) {
150
+ return this.observationStore.resolve(source);
151
+ }
152
+
153
+ return { resolved: false };
154
+ }
155
+
156
+ selectUnmetCondition(conditions) {
157
+ if (conditions.length === 0) {
158
+ return null;
159
+ }
160
+
161
+ return conditions[
162
+ Math.floor(Math.random() * conditions.length)
163
+ ];
164
+ }
165
+
166
+ selectAffordance(affordances) {
167
+ const eligible = affordances.filter(affordance => {
168
+ const observations =
169
+ this.observationStore.getByAffordance(affordance.action);
170
+
171
+ return !observations.some(
172
+ observation => observation.result === "FAIL"
173
+ );
174
+ });
175
+
176
+ if (eligible.length === 0) {
177
+ return null;
178
+ }
179
+
180
+ return eligible[
181
+ Math.floor(Math.random() * eligible.length)
182
+ ];
183
+ }
184
+
185
+ findAffordancesForCondition(condition) {
186
+ const candidates = [];
187
+
188
+ for (let key in this.affordanceRegistry) {
189
+ const candidate = this.affordanceRegistry[key];
190
+
191
+ if (
192
+ candidate.enabled !== false &&
193
+ candidate.effects.includes(condition)
194
+ ) {
195
+ candidates.push(candidate);
196
+ }
197
+ }
198
+
199
+ return candidates;
200
+ }
201
+ }
@@ -0,0 +1,33 @@
1
+ // utils/loadJSON.js
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { validateConfig } from './validateWithSchema.js';
5
+
6
+ /**
7
+ * Load a JSON file from disk and parse it without validation.
8
+ * Use for legacy or optional config files.
9
+ *
10
+ * @param {string} filePath - Full path to the JSON file
11
+ * @returns {object} Parsed JSON content
12
+ */
13
+ export function loadJSON(filePath) {
14
+ const raw = fs.readFileSync(filePath, 'utf8');
15
+ return JSON.parse(raw);
16
+ }
17
+
18
+ /**
19
+ * Load and validate a JSON file using a provided schema.
20
+ * Throws an error on schema mismatch or parsing failure.
21
+ *
22
+ * @param {string} filePath - Full path to the JSON file
23
+ * @param {string} schemaName - File name of the schema in /schemas (e.g. "goal.schema.json")
24
+ * @returns {object} Parsed and validated JSON content
25
+ */
26
+ export function loadAndValidateJSON(filePath, schemaName, { silent = false } = {}) {
27
+ const raw = fs.readFileSync(filePath, 'utf8');
28
+ const json = JSON.parse(raw);
29
+ validateConfig(json, schemaName, path.basename(filePath), { silent });
30
+ return json;
31
+ }
32
+
33
+