@scfloworg/actions 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.
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The types of the action contract (`packages/core/docs/spec/meta-model.md`,
3
+ * section Action). They hold no model: an action is an id, a description for
4
+ * people and AI, its parameters as JSON Schema and a body.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,212 @@
1
+ # Actions
2
+
3
+ The neutral action package holds the part of the action contract that does not
4
+ need the engine. The rules of actions are in the specification
5
+ ([Action](../../core/docs/spec/meta-model.md#action),
6
+ [Script](../../core/docs/spec/model-command-language.md#script) and
7
+ [Action Package](../../core/docs/spec/typescript-reference-implementation.md#action-package));
8
+ this page gives the use of the package.
9
+
10
+ ## Actions
11
+
12
+ ```ts
13
+ interface ActionDefinition {
14
+ id: string; // namespace/name, both identifiers
15
+ title?: string;
16
+ description?: string;
17
+ kind?: "read" | "action"; // action by default
18
+ destructive?: boolean;
19
+ exposure?: "visible" | "hidden"; // visible by default
20
+ parameters?: JsonSchema | string; // a JSON Schema object, or its JSON text
21
+ writes?: readonly string[]; // kept for the engine
22
+ }
23
+
24
+ interface Action<C = RunTrace> extends ActionDefinition {
25
+ run(params: any, context: C): unknown; // sync or async
26
+ }
27
+ ```
28
+
29
+ A definition is the declaration of a manifest: a module lists it without code.
30
+ An `Action` adds its body, which receives the parameters after their check and
31
+ the context its host builds. Without a host context, the body receives its
32
+ `RunTrace`.
33
+
34
+ ## Parameters
35
+
36
+ `parameters` is a JSON Schema object, draft 2020-12, the version of MCP tools,
37
+ checked by Ajv before the body runs, so a body checks nothing by hand.
38
+
39
+ - Its `type` is `object`. Absent, the action takes an empty object, and no
40
+ parameters (`undefined`) stand for `{}`.
41
+ - An unknown keyword is refused when the action is registered, with
42
+ `constraint_violation`, as a schema that is not valid. `format` is an
43
+ annotation: no format vocabulary is loaded.
44
+ - `default` fills an absent property. The body receives a copy: the parameters
45
+ of the caller do not change.
46
+ - Parameters that break the schema are refused with `invalid_parameters`; its
47
+ message lists every breach.
48
+
49
+ **`ref`.** The keyword `ref` names a schema of the model: the value is the id of
50
+ an instance of it, or the exact `display_name` of one instance, in any
51
+ language, and the body receives the key. The package reads no instance, so the
52
+ host gives a resolver:
53
+
54
+ ```ts
55
+ type RefResolver = (schema: string, value: string) => RefMatches | Promise<RefMatches>;
56
+ interface RefMatches {
57
+ id: string | null; // the key of the instance whose id is the value
58
+ names: readonly string[]; // the keys of the instances whose display_name is the value
59
+ }
60
+ ```
61
+
62
+ The package applies the rule: an id wins over a name that equals it, one name
63
+ gives its key, and no instance or several are refused with
64
+ `invalid_parameters`. A `ref` is found wherever it sits in the schema, in an
65
+ array or a nested object included. Without a resolver, a `ref` value reaches
66
+ the body as it is.
67
+
68
+ `validateParameters(definition, params, resolveRef?)` runs this check without a
69
+ registry.
70
+
71
+ ## The registry
72
+
73
+ ```ts
74
+ const registry = createActionRegistry<C>();
75
+ registry.register(action);
76
+ registry.get(id); registry.list();
77
+ const { result, entry } = await registry.run(id, params, host);
78
+ ```
79
+
80
+ - `register` refuses an id that is not a namespace, a slash and a name with
81
+ `constraint_violation`, and an id already registered with `duplicate_name`.
82
+ It compiles the parameters once.
83
+ - `run` refuses an unknown id with `unknown_action`. It checks the parameters,
84
+ runs the body and gives its result with the run entry of the call. A refused
85
+ call throws its exception as it is.
86
+ - **It opens no transaction.** The host runs the call inside the transaction of
87
+ the action, and rolls it back when the call throws.
88
+
89
+ The host of a run:
90
+
91
+ ```ts
92
+ interface RunHost<C> {
93
+ context?(trace: RunTrace): C; // the context of each body, built on its trace
94
+ resolveRef?: RefResolver;
95
+ onEntry?(entry: RunEntry): void; // each top-level entry, done or refused
96
+ }
97
+
98
+ interface RunTrace {
99
+ action: string;
100
+ command(command, result): void; // records a command the body emitted
101
+ call(id, params?): Promise<unknown>; // runs another action, recorded as a step
102
+ }
103
+ ```
104
+
105
+ The engine builds the context of a body on the trace: each command it runs for
106
+ the body is recorded with `trace.command`, and its `call` goes through
107
+ `trace.call`, so the run log holds what the body did. Which actions a body may
108
+ call, and which schemas it may write, are checked by the engine.
109
+
110
+ ## The run log
111
+
112
+ A run entry follows `run_entry` of the specification:
113
+
114
+ ```ts
115
+ interface RunEntry {
116
+ action: string;
117
+ parameters: string; // canonical JSON, after the check, refs resolved
118
+ steps: RunStep[]; // { command, result } or { call: RunEntry }
119
+ outcome: "done" | "refused";
120
+ error?: string; // the code of the exception of a refused call
121
+ result?: string; // canonical JSON; absent when the body returned none
122
+ }
123
+ ```
124
+
125
+ - Every call has an entry, a refused one included. A call refused by its
126
+ parameters records the parameters it was given.
127
+ - A nested call is a step of the entry that makes it, refused or not.
128
+ - A command is recorded as the body emitted it, so `${now()}` and the keys of
129
+ the engine stay placeholders, and its result in canonical JSON.
130
+ - `canonicalJson(value)` writes the canonical form: the keys of every object
131
+ sorted and no white space. A replay compares steps in this form.
132
+ - `createRunLog()` keeps entries in memory: pass `log.record` as `onEntry`.
133
+
134
+ ## Batches
135
+
136
+ ```ts
137
+ const { result, entries } = await runBatch(registry, {
138
+ commands: [
139
+ { call: "scflow/create_actor", with: { display_name: "Plant", type: "plant" }, as: "plant" },
140
+ { call: "carbon/update_actor_emissions", with: { id: "${plant.id}", co2: 12 } },
141
+ ],
142
+ result: "${plant}",
143
+ }, host);
144
+ ```
145
+
146
+ The package runs a batch whose steps are calls, each with `with` and `as` only.
147
+
148
+ - The batch is checked whole first: any other step, such as a command, `when`,
149
+ `each` or `fail`, and a batch with `parameters`, are refused with
150
+ `invalid_script_step`, before any call. The engine runs such a batch through
151
+ its script runner.
152
+ - Before each call, the placeholders of `with` are resolved against the results
153
+ named so far, by the rules of placeholders on arrays: an index, `*` for every
154
+ entry, or entry `0` by default. A placeholder that is the whole string gives
155
+ the value of any type, one inside text gives its text, and a value that
156
+ resolves to nothing leaves its property out.
157
+ - A placeholder whose first segment is not an `as` name set yet is refused with
158
+ `unknown_reference`. A placeholder that is not a path, such as `${now()}`, is
159
+ passed on to the called action.
160
+ - The result of the batch is its `result` placeholder, or the result of its last
161
+ call. `entries` holds the entry of each call, which `host.onEntry` also
162
+ receives.
163
+ - A refused call stops the batch and its exception is thrown. **The host runs
164
+ the batch in one transaction**, rolled back when it throws, so a batch is
165
+ undone as one.
166
+
167
+ ## Export to AI tools
168
+
169
+ `toAiTools(actions)` maps each `visible` action to an MCP tool definition:
170
+
171
+ | Tool | From the action |
172
+ | --- | --- |
173
+ | `name` | `toolName(id)`: the slash replaced by a hyphen, `scflow/create_actor` gives `scflow-create_actor` |
174
+ | `title` | As it is |
175
+ | `description` | As it is, followed by the rule of a top-level `anyOf` of `required` alone |
176
+ | `inputSchema` | `parameters`, `ref` included, without `anyOf`, `oneOf` or `allOf` at its top level, or an empty object schema |
177
+ | `annotations.readOnlyHint` | `kind: read` |
178
+ | `annotations.destructiveHint` | `destructive` |
179
+
180
+ A hyphen never appears in an identifier, so `actionIdOfTool(name)` gives the id
181
+ back for a call that comes from an AI client. An action whose `exposure` is
182
+ `hidden` is left out; `toAiTool(action)` maps one action whatever its exposure.
183
+
184
+ `toAiTools(actions, { languages })` takes the ordered languages of the dataset: the description of each localized text parameter (a string or a map of language to string, in an `anyOf`) then names them and the source language, the first. The schema forbids nothing more, because the list offers and expects; left out or empty, the tools are as before. `parametersWithLanguages(action, languages)` gives the same for one action.
185
+
186
+ AI providers refuse `anyOf`, `oneOf` and `allOf` at the top level of a tool's
187
+ schema, and one refused tool fails the whole request. The export leaves them out,
188
+ and the action still checks its full `parameters` when it runs. An `anyOf` whose
189
+ branches each hold only a `required` of one property means "at least one of
190
+ these": the description states it, as `Give at least one of: color, icon.`
191
+ These keywords stay at any deeper level, where providers accept them.
192
+
193
+ ## Hand-off to the engine
194
+
195
+ These parts are given to the package by the engine, in later pieces:
196
+
197
+ - the resolver of `ref` (`RunHost.resolveRef`) and the context of bodies built
198
+ on the trace (`RunHost.context`), with the action declarations per module
199
+ and the generated actions;
200
+ - the steps `when`, `each` and `fail` of a batch, and procedures, run by the
201
+ script runner of the engine.
202
+
203
+ The replay of a body against its run entry is `replayEntry` of
204
+ `@scfloworg/conformance`, which runs on this package's registry and needs no
205
+ engine.
206
+
207
+ ## Errors
208
+
209
+ The package throws `ActionError`, whose `code` is one of `invalid_parameters`,
210
+ `unknown_action`, `duplicate_name`, `constraint_violation`,
211
+ `invalid_script_step` and `unknown_reference`, the codes the engine uses for the
212
+ same causes. An exception of a body is thrown as it is.
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@scfloworg/actions",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "registry": "https://registry.npmjs.org/",
8
+ "access": "public"
9
+ },
10
+ "description": "The neutral action package: the action type aligned on MCP tools, parameters checked by a JSON Schema validator, a registry, batches of calls, the export to AI tools and the run log. No SCFlow dependency. See README.md.",
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "docs"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc -b",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run"
27
+ },
28
+ "dependencies": {
29
+ "ajv": "^8.17.1"
30
+ },
31
+ "devDependencies": {
32
+ "@scfloworg/conformance": "^0.1.0",
33
+ "typescript": "^5.6.3",
34
+ "vitest": "^4.1.10"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/SCFlowOrg/scflow-core.git",
39
+ "directory": "packages/actions"
40
+ }
41
+ }