@six5536/smllm-wasm 0.5.0 → 0.7.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/README.md +158 -3
- package/dist/index.d.ts +7 -2
- package/dist/index.js +17 -2
- package/dist/types.d.ts +105 -2
- package/dist/wrap.d.ts +41 -5
- package/dist/wrap.js +57 -3
- package/package.json +1 -1
- package/wasm/bundler/smllm_wasm.d.ts +28 -2
- package/wasm/bundler/smllm_wasm_bg.js +201 -12
- package/wasm/bundler/smllm_wasm_bg.wasm +0 -0
- package/wasm/bundler/smllm_wasm_bg.wasm.d.ts +5 -0
- package/wasm/web/smllm_wasm.d.ts +33 -2
- package/wasm/web/smllm_wasm.js +201 -12
- package/wasm/web/smllm_wasm_bg.wasm +0 -0
- package/wasm/web/smllm_wasm_bg.wasm.d.ts +5 -0
package/README.md
CHANGED
|
@@ -6,11 +6,13 @@ or Node. The large language model (LLM) can run anywhere. The API is typed: it t
|
|
|
6
6
|
objects, and the package includes TypeScript declarations.
|
|
7
7
|
|
|
8
8
|
This README helps you run the engine in your own host. It shows a first example, then describes
|
|
9
|
-
the host methods, storage, the
|
|
10
|
-
CLI.
|
|
9
|
+
the host methods, storage, the events that only the host fires, machines that the host drives for
|
|
10
|
+
a model without tools, the Node helpers, and when to use the WebAssembly engine instead of the CLI.
|
|
11
11
|
|
|
12
12
|
- [Host](#host)
|
|
13
13
|
- [Storage](#storage)
|
|
14
|
+
- [Host-only events](#host-only-events)
|
|
15
|
+
- [Host-driven machines](#host-driven-machines)
|
|
14
16
|
- [Run in Node](#run-in-node)
|
|
15
17
|
- [WebAssembly or CLI](#webassembly-or-cli)
|
|
16
18
|
- [Licence](#licence)
|
|
@@ -21,7 +23,7 @@ engine:
|
|
|
21
23
|
```js
|
|
22
24
|
import init, { Engine, memoryStorage } from "@six5536/smllm-wasm"; // or from "@six5536/smllm-wasm/bundler"
|
|
23
25
|
|
|
24
|
-
await init(); // the bundler build needs no init
|
|
26
|
+
await init(); // in a browser or Node.js, while the bundler build needs no init
|
|
25
27
|
const compiled = await (await fetch("machines.json")).json(); // `smllm compile`
|
|
26
28
|
const engine = new Engine(
|
|
27
29
|
compiled,
|
|
@@ -43,6 +45,10 @@ const stop = engine.stop(session, stopHookActive);
|
|
|
43
45
|
// "runaway": let it stop, show stop.text to the user; "allow": let it stop.
|
|
44
46
|
```
|
|
45
47
|
|
|
48
|
+
A param takes a string. A param that the machine declares with `type: json` takes any JSON value,
|
|
49
|
+
in `callTool`, `fire`, and `fireAsHost`, and the host receives its compact JSON text. A JSON string stays a string, even when its text spells an object. The events list shows
|
|
50
|
+
the param as a JSON value, with its JSON Schema.
|
|
51
|
+
|
|
46
52
|
The `examples/wasm` folder of the [smllm repository](https://github.com/six5536/smllm) holds a
|
|
47
53
|
whole agent loop, in TypeScript.
|
|
48
54
|
|
|
@@ -59,10 +65,16 @@ host leaves the method out:
|
|
|
59
65
|
- `read(file)` reads a prompt file. `smllm compile` inlines prompt files, so a host rarely needs
|
|
60
66
|
this method.
|
|
61
67
|
- `isMatch(pattern, value)` matches a pattern. By default, it uses `new RegExp`.
|
|
68
|
+
- `checkJson(schema, value)` checks a `json` param's value against its schema, and returns one
|
|
69
|
+
message for each error, such as `at /steps: [] has fewer than 1 item`, or none when the value
|
|
70
|
+
passes. The engine refuses an event whose value has errors. The package has no validator of its
|
|
71
|
+
own, so give it one of yours, such as Ajv's. Without `checkJson`, the engine takes the value
|
|
72
|
+
unchecked, and `unsupported()` names each `json` param with a schema.
|
|
62
73
|
- `now()` returns the time. By default, it uses `Date.now`.
|
|
63
74
|
- `random()` returns a random number. By default, it uses `crypto.getRandomValues`.
|
|
64
75
|
- `history(machine, id, entry)` and `put(kind, key, record)` receive writes. By default, the
|
|
65
76
|
engine drops them.
|
|
77
|
+
- `wantsAudit()` and `audit(entry, reply)` keep an audit, as the following section describes.
|
|
66
78
|
|
|
67
79
|
A host method that throws is a host failure: a failed guard or action, or a rejected pattern. The
|
|
68
80
|
reply text shows the failure, and the engine stays usable.
|
|
@@ -80,6 +92,149 @@ write.
|
|
|
80
92
|
IndexedDB, or one that sends the records to a server. `exportState()` and `importState(state)`
|
|
81
93
|
move the whole state at once, for backups and moves.
|
|
82
94
|
|
|
95
|
+
## Audit
|
|
96
|
+
|
|
97
|
+
An audit records every call that drove a session, accepted or refused: `bind`, `callTool`,
|
|
98
|
+
`events`, `fire`, `fireAsHost`, and `stop`. `view`, `status`, and `promptSubmitted` record
|
|
99
|
+
nothing. To keep an audit, give the storage or the host `wantsAudit()`, which returns true, and
|
|
100
|
+
`audit(entry, reply)`, which receives each `AuditEntry` and the text of the call's reply:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const storage = {
|
|
104
|
+
...memoryStorage(),
|
|
105
|
+
wantsAudit: () => true,
|
|
106
|
+
audit(entry: AuditEntry, reply: string) {
|
|
107
|
+
console.log(entry.source, entry.call, entry.event, entry.outcome, entry.reason);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
An entry holds the time, the session, the source (`agent`, or `host` for `fireAsHost`), the call,
|
|
113
|
+
the instance with its states before and after, the params, the names of the events that the reply
|
|
114
|
+
offered, the outcome with the reason for a refusal, and for a call that created the session its
|
|
115
|
+
binding. Keep what you need of the reply's text. A hash names its algorithm, as `sha256:` and the
|
|
116
|
+
hex of the SHA-256 of the text's UTF-8 bytes, which is the default that the `smllm` CLI uses.
|
|
117
|
+
|
|
118
|
+
## Host-only events
|
|
119
|
+
|
|
120
|
+
A host-only event is an event that only the host fires, such as the result of checks that the host
|
|
121
|
+
runs outside the machine. A machine marks it with `source: host` in `meta.events`:
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
meta:
|
|
125
|
+
smllm: 1
|
|
126
|
+
events:
|
|
127
|
+
checksPassed: { source: host }
|
|
128
|
+
checksFailed: { source: host }
|
|
129
|
+
states:
|
|
130
|
+
WORK:
|
|
131
|
+
on: { submit: CHECKING }
|
|
132
|
+
CHECKING:
|
|
133
|
+
on: { checksPassed: DONE, checksFailed: WORK }
|
|
134
|
+
DONE: { type: final }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The agent never sees a host-only event: the events lists of the tool's replies, of `view`, of
|
|
138
|
+
`events`, and of `stop` leave it out. `fire` and `callTool` refuse it with an error that names it
|
|
139
|
+
as an event that only the host fires. The host fires it with `fireAsHost`, which takes the same
|
|
140
|
+
arguments as `fire` and fires any event that the state offers:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
engine.callTool({ session, event: "submit" }); // the agent: WORK → CHECKING
|
|
144
|
+
const passed = await runChecks(); // your own checks, which can be async
|
|
145
|
+
engine.fireAsHost(session, passed ? "checksPassed" : "checksFailed");
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Fire an event that the agent chose with `fire` or `callTool`, never with `fireAsHost`, because
|
|
149
|
+
`fireAsHost` would then take a host-only event from the agent. Keep `fireAsHost` for the host's
|
|
150
|
+
own events.
|
|
151
|
+
|
|
152
|
+
`source: host` is a security boundary only when the host drives a model without tools. An agent
|
|
153
|
+
that runs under a harness, such as Claude Code, usually has a shell, and there the marker
|
|
154
|
+
prevents mistakes rather than an attack. A harness has no host to fire the events, so the smllm
|
|
155
|
+
CLI refuses to start an instance of such a machine, and `smllm validate` notes that the machine
|
|
156
|
+
needs a host to drive it.
|
|
157
|
+
|
|
158
|
+
## Host-driven machines
|
|
159
|
+
|
|
160
|
+
A host can drive a machine for a model that has no tools and only answers in text, such as a
|
|
161
|
+
model that the host calls through the Messages API with no tools. The host shows the model the
|
|
162
|
+
state, reads its answer as one event, runs its own checks, and fires the results. Four methods
|
|
163
|
+
serve it, and none of them changes anything:
|
|
164
|
+
|
|
165
|
+
- `hostView(key)` returns `{ location, model, host, invoke }`: where the session is, the events
|
|
166
|
+
that the model can fire, the events that only the host fires, and the host's work that the
|
|
167
|
+
state invokes. Each event has its `name`, `description`, and `params`, and a JSON param has its
|
|
168
|
+
bundled `schema` as an object. Each `invoke` has its `src`, the name of the work, and its
|
|
169
|
+
`input`, an object of fixed values, if the machine gives one: run the work, then fire its result
|
|
170
|
+
with `fireAsHost`.
|
|
171
|
+
- `stateText(key, { schemas })` returns the state's text for the model: its instructions, the
|
|
172
|
+
model's events with their params, and a request to answer with one event as JSON. Each JSON
|
|
173
|
+
param shows its schema, unless `schemas` is `false`.
|
|
174
|
+
- `answerSchema(key)` returns a JSON Schema of a valid answer, as an object, for a model that can
|
|
175
|
+
constrain its output, such as the Claude API's structured outputs.
|
|
176
|
+
- `checkAnswer(key, text)` reads the model's answer, a bare JSON object or one fenced block with
|
|
177
|
+
nothing outside it. It returns `{ ok: true, event, params }`, whose params are plain
|
|
178
|
+
JavaScript values ready for `fire`, or `{ ok: false, error }`, whose error the host sends back
|
|
179
|
+
to the model as it stands. Every malformed answer gets the same error.
|
|
180
|
+
|
|
181
|
+
This loop runs a machine from start to end. The machine is an extractor builder for bank
|
|
182
|
+
statements, which is in the `examples/extractor` folder of the smllm repository. The model
|
|
183
|
+
answers with an extractor, a JSON value with a schema, or asks for more pages. The host checks
|
|
184
|
+
the extractor and fires `checks_passed` or `checks_failed`, and the machine gives up after five
|
|
185
|
+
attempts:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
const engine = new Engine(compiled, {}, memoryStorage());
|
|
189
|
+
// A keyless `enter` creates the session and starts the machine in one call.
|
|
190
|
+
const { session } = engine.fire(undefined, "enter", { stateMachine: "extractor-build" });
|
|
191
|
+
for (;;) {
|
|
192
|
+
const view = engine.hostView(session);
|
|
193
|
+
if (view.location.machine === null) break; // a final state: the session is idle again
|
|
194
|
+
if (view.model.length > 0) {
|
|
195
|
+
let prompt = `${engine.stateText(session)}\n${statement}`;
|
|
196
|
+
for (;;) {
|
|
197
|
+
const answer = engine.checkAnswer(session, await askModel(prompt)); // your adapter
|
|
198
|
+
if (answer.ok) {
|
|
199
|
+
engine.fire(session, answer.event, answer.params); // the model chose the event
|
|
200
|
+
if (answer.event === "extractor") extractor = answer.params.json;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
prompt = answer.error; // the refusal, as it stands; count it as an attempt
|
|
204
|
+
}
|
|
205
|
+
} else if (view.invoke.length > 0) {
|
|
206
|
+
// The state names its work, so the host finds its code by name, not by the state's.
|
|
207
|
+
for (const { src, input } of view.invoke) await work[src](session, input);
|
|
208
|
+
} else {
|
|
209
|
+
throw new Error(`nothing to do in ${view.location.state}`); // a person's step: wait for them
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`work` maps each `src` to the host's code, which fires the work's result:
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
const work = {
|
|
218
|
+
async runChecks(session) {
|
|
219
|
+
const errors = await runChecks(extractor); // your checks, which can be async
|
|
220
|
+
if (errors.length === 0) engine.fireAsHost(session, "checks_passed");
|
|
221
|
+
else engine.fireAsHost(session, "checks_failed", { errors: errors.join("\n") });
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The model is never offered a built-in event, such as `yield`, `pause`, or `unmatched`, or a
|
|
227
|
+
host-only event, because those belong to whoever drives the session, which here is the host. The
|
|
228
|
+
host fires them as it needs them. An outcome that the model can choose, such as "I can't do this"
|
|
229
|
+
or "ask the user", is an event that the machine declares.
|
|
230
|
+
|
|
231
|
+
One machine can mix the model's steps, the host's deterministic steps, and a person's steps. A
|
|
232
|
+
state that offers the model events is a step of the model. A state whose events are all the
|
|
233
|
+
host's is a step of the host, such as `CHECKING`, or a person's step, such as an approval for
|
|
234
|
+
which the host fires the person's decision. A state that offers both lists is a step of the model
|
|
235
|
+
that the host can interrupt: the extractor builder's `READ` offers the host `timed_out`, which the
|
|
236
|
+
host fires when the model takes too long.
|
|
237
|
+
|
|
83
238
|
## Run in Node
|
|
84
239
|
|
|
85
240
|
`@six5536/smllm-wasm/node` provides a host and a storage for Node:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import load, { Engine as Raw, initSync } from "../wasm/web/smllm_wasm.js";
|
|
2
2
|
import { Engine as Base } from "./wrap.js";
|
|
3
|
-
|
|
3
|
+
type Input = Parameters<typeof load>[0];
|
|
4
|
+
/**
|
|
5
|
+
* Load the engine's WebAssembly once. Without an argument, a browser fetches
|
|
6
|
+
* the package's file, and Node.js, which can't fetch a `file:` URL, reads it.
|
|
7
|
+
*/
|
|
8
|
+
export default function init(input?: Input): ReturnType<typeof load>;
|
|
4
9
|
export { initSync };
|
|
5
10
|
export type * from "./types.js";
|
|
6
11
|
export { memoryStorage } from "./storage.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
// smllm-wasm for browsers and Node (the web build): call `init()` (or
|
|
2
2
|
// `initSync`) once, then use `Engine`. The string API is `smllm-wasm/raw`.
|
|
3
|
-
import
|
|
3
|
+
import load, { Engine as Raw, initSync } from "../wasm/web/smllm_wasm.js";
|
|
4
4
|
import { Engine as Base } from "./wrap.js";
|
|
5
|
-
|
|
5
|
+
// sokf-implements: WASM-6_AC-2
|
|
6
|
+
/**
|
|
7
|
+
* Load the engine's WebAssembly once. Without an argument, a browser fetches
|
|
8
|
+
* the package's file, and Node.js, which can't fetch a `file:` URL, reads it.
|
|
9
|
+
*/
|
|
10
|
+
export default async function init(input) {
|
|
11
|
+
const node = globalThis.process?.versions?.node;
|
|
12
|
+
if (input === undefined && node) {
|
|
13
|
+
// A variable keeps a bundler for the browser from resolving `node:fs`.
|
|
14
|
+
const fs = "node:fs/promises";
|
|
15
|
+
const { readFile } = (await import(/* @vite-ignore */ fs));
|
|
16
|
+
const file = new URL("../wasm/web/smllm_wasm_bg.wasm", import.meta.url);
|
|
17
|
+
return load({ module_or_path: await readFile(file) });
|
|
18
|
+
}
|
|
19
|
+
return load(input);
|
|
20
|
+
}
|
|
6
21
|
export { initSync };
|
|
7
22
|
export { memoryStorage } from "./storage.js";
|
|
8
23
|
/** smllm's engine over compiled machines (`smllm compile` output). */
|
package/dist/types.d.ts
CHANGED
|
@@ -53,15 +53,58 @@ export interface HistoryEntry {
|
|
|
53
53
|
params: Record<string, string>;
|
|
54
54
|
trace: string[];
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* One audit entry: a call that drove a session, accepted or refused, handed
|
|
58
|
+
* to `audit` with the text of its reply when the host or the storage wants
|
|
59
|
+
* an audit.
|
|
60
|
+
*/
|
|
61
|
+
export interface AuditEntry {
|
|
62
|
+
/** Unix ms. */
|
|
63
|
+
at: number;
|
|
64
|
+
session: string;
|
|
65
|
+
/** `host` for `fireAsHost`, else `agent`. */
|
|
66
|
+
source: "agent" | "host";
|
|
67
|
+
/** `view` is a tool call with no event. */
|
|
68
|
+
call: "bind" | "view" | "menu" | "fire" | "stop";
|
|
69
|
+
event: string | null;
|
|
70
|
+
machine: string | null;
|
|
71
|
+
instance: string | null;
|
|
72
|
+
/** The instance's state before the call. */
|
|
73
|
+
from: string | null;
|
|
74
|
+
/** The state where the session is after the call. */
|
|
75
|
+
to: string | null;
|
|
76
|
+
/** Each param as its text, a JSON value as its compact JSON. */
|
|
77
|
+
params: Record<string, string>;
|
|
78
|
+
/** The names of the events that the reply offered. */
|
|
79
|
+
offered: string[];
|
|
80
|
+
/** `accepted` or `refused`; for `stop`, the decision. */
|
|
81
|
+
outcome: "accepted" | "refused" | "allow" | "block" | "runaway";
|
|
82
|
+
/** Why the engine refused the call. */
|
|
83
|
+
reason: string | null;
|
|
84
|
+
trace: string[];
|
|
85
|
+
/** The binding of the session that the call created. */
|
|
86
|
+
binding: {
|
|
87
|
+
harness: string;
|
|
88
|
+
hostSession: string | null;
|
|
89
|
+
cwd: string;
|
|
90
|
+
configs: string[];
|
|
91
|
+
} | null;
|
|
92
|
+
}
|
|
56
93
|
/**
|
|
57
94
|
* What the engine needs from its host. Every method is optional:
|
|
58
95
|
* - `supports`, `check`, `run`: host guard/action kinds (`command`); none by default.
|
|
59
96
|
* A command's `params.cwd` is relative to the compiled config file (`nodeHost`'s `configDir`).
|
|
60
97
|
* - `read`: prompt files (`smllm compile` inlines them, so rarely needed).
|
|
61
98
|
* - `isMatch`: `new RegExp(pattern).test(value)` by default.
|
|
99
|
+
* - `checkJson`: the errors of a `json` param's value against its schema, one message each, such as
|
|
100
|
+
* `at /steps: [] has fewer than 1 item`, and none when the value passes. Without it, the engine takes
|
|
101
|
+
* the value unchecked, and `unsupported()` names each such param.
|
|
62
102
|
* - `now`, `random`: `Date.now()` and `crypto.getRandomValues` by default.
|
|
63
103
|
* - `history`: each history entry as it happens; dropped by default.
|
|
64
104
|
* - `put`: each session, binding and instance as it is saved (write-through storage); dropped by default.
|
|
105
|
+
* - `wantsAudit`, `audit`: whether to keep an audit, false by default, and each audit entry with the
|
|
106
|
+
* text of its reply. The host keeps what it likes of the text, such as `sha256:` and the hex of the
|
|
107
|
+
* SHA-256 of its UTF-8 bytes, the default that `smllm` uses.
|
|
65
108
|
* A method that throws is a host failure the engine reports, never a broken engine.
|
|
66
109
|
*/
|
|
67
110
|
export interface Host {
|
|
@@ -71,22 +114,30 @@ export interface Host {
|
|
|
71
114
|
run?(kind: string, params: Record<string, unknown>, env: Record<string, string>, cwd: string): string | void;
|
|
72
115
|
read?(file: string): string | undefined;
|
|
73
116
|
isMatch?(pattern: string, value: string): boolean;
|
|
117
|
+
checkJson?(schema: Record<string, unknown> | boolean, value: unknown): string[];
|
|
74
118
|
now?(): number;
|
|
75
119
|
random?(): number;
|
|
76
120
|
history?(machine: string, id: string, entry: HistoryEntry): void;
|
|
77
121
|
put?(kind: RecordKind, key: string, record: unknown): void;
|
|
122
|
+
wantsAudit?(): boolean;
|
|
123
|
+
audit?(entry: AuditEntry, reply: string): void;
|
|
78
124
|
}
|
|
79
125
|
/** What `put` hands on: a session (key: its key), a binding (`harness/hostSession`, record: the session key) or an instance (`machine/id`). */
|
|
80
126
|
export type RecordKind = "session" | "binding" | "instance";
|
|
81
127
|
/**
|
|
82
128
|
* Where an engine's state lives (WASM-2): `load` once, into `importState`;
|
|
83
129
|
* then each saved record through `put` and each history entry through
|
|
84
|
-
* `history`, as they happen, so persisting costs what changed
|
|
130
|
+
* `history`, as they happen, so persisting costs what changed; and, when
|
|
131
|
+
* `wantsAudit` returns true, each audit entry through `audit`.
|
|
85
132
|
*/
|
|
86
133
|
export interface Storage {
|
|
87
134
|
load(): State | undefined;
|
|
88
135
|
put(kind: RecordKind, key: string, record: unknown): void;
|
|
89
136
|
history(machine: string, id: string, entry: HistoryEntry): void;
|
|
137
|
+
/** Whether the storage keeps an audit; false when absent. */
|
|
138
|
+
wantsAudit?(): boolean;
|
|
139
|
+
/** Each audit entry, with the text of the call's reply. */
|
|
140
|
+
audit?(entry: AuditEntry, reply: string): void;
|
|
90
141
|
}
|
|
91
142
|
/** The agent-facing `smllm` tool: the same definition the MCP server offers. */
|
|
92
143
|
export interface Tool {
|
|
@@ -99,8 +150,60 @@ export interface Tool {
|
|
|
99
150
|
export interface ToolArgs {
|
|
100
151
|
session?: string;
|
|
101
152
|
event?: string;
|
|
102
|
-
|
|
153
|
+
/** A string for each param, or any JSON value for a param that the events list shows as a JSON value. */
|
|
154
|
+
params?: Record<string, unknown>;
|
|
103
155
|
}
|
|
156
|
+
/** How `stateText` renders the state. */
|
|
157
|
+
export interface StateTextOptions {
|
|
158
|
+
/** Show each JSON param's schema; `true` by default. */
|
|
159
|
+
schemas?: boolean;
|
|
160
|
+
}
|
|
161
|
+
/** A param of an offered event. */
|
|
162
|
+
export interface OfferedParam {
|
|
163
|
+
name: string;
|
|
164
|
+
required: boolean;
|
|
165
|
+
/** `json`: the param takes any JSON value. */
|
|
166
|
+
type: "string" | "json";
|
|
167
|
+
/** The allowed values; empty when any string is allowed. */
|
|
168
|
+
enum: string[];
|
|
169
|
+
pattern: string | null;
|
|
170
|
+
description: string | null;
|
|
171
|
+
/** A JSON param's JSON Schema, bundled, or `null`. */
|
|
172
|
+
schema: Record<string, unknown> | boolean | null;
|
|
173
|
+
}
|
|
174
|
+
/** An event that the state offers. */
|
|
175
|
+
export interface Offered {
|
|
176
|
+
name: string;
|
|
177
|
+
description: string | null;
|
|
178
|
+
params: OfferedParam[];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The state as a host that drives the machine sees it: the events that the
|
|
182
|
+
* model can answer with, and the events that only the host fires (with
|
|
183
|
+
* `fireAsHost`). Neither list holds the built-in events, which the host
|
|
184
|
+
* fires as it needs them.
|
|
185
|
+
*/
|
|
186
|
+
export interface HostView {
|
|
187
|
+
location: Location;
|
|
188
|
+
model: Offered[];
|
|
189
|
+
host: Offered[];
|
|
190
|
+
/** The host's work that the state invokes: run each, and fire its result with `fireAsHost`. */
|
|
191
|
+
invoke: Invoked[];
|
|
192
|
+
}
|
|
193
|
+
/** One piece of the host's work that a state names with `invoke`: its name, and its fixed input. */
|
|
194
|
+
export interface Invoked {
|
|
195
|
+
src: string;
|
|
196
|
+
input?: Record<string, unknown>;
|
|
197
|
+
}
|
|
198
|
+
/** A model's answer, checked: the event and its params, ready for `fire`, or the refusal to send back to the model. */
|
|
199
|
+
export type CheckedAnswer = {
|
|
200
|
+
ok: true;
|
|
201
|
+
event: string;
|
|
202
|
+
params: Record<string, unknown>;
|
|
203
|
+
} | {
|
|
204
|
+
ok: false;
|
|
205
|
+
error: string;
|
|
206
|
+
};
|
|
104
207
|
/** Sessions, bindings and instances by key, as `exportState` returns them and `importState` takes them. */
|
|
105
208
|
export interface State {
|
|
106
209
|
sessions: Record<string, unknown>;
|
package/dist/wrap.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Host, RecordKind, Reply, SessionStatus, State, Storage, StopDecision, Tool, ToolArgs } from "./types.js";
|
|
1
|
+
import type { CheckedAnswer, Host, HostView, RecordKind, Reply, SessionStatus, State, StateTextOptions, Storage, StopDecision, Tool, ToolArgs } from "./types.js";
|
|
2
2
|
/** The host the wasm calls: strings in, strings out, every method there. */
|
|
3
3
|
export interface RawHost {
|
|
4
4
|
supports(kind: string): boolean;
|
|
@@ -6,10 +6,14 @@ export interface RawHost {
|
|
|
6
6
|
run(kind: string, params: string, env: string, cwd: string): string;
|
|
7
7
|
read(file: string): string | undefined;
|
|
8
8
|
isMatch(pattern: string, value: string): boolean;
|
|
9
|
+
checksJson(): boolean;
|
|
10
|
+
checkJson(schema: string, value: string): string;
|
|
9
11
|
now(): number;
|
|
10
12
|
random(): number;
|
|
11
13
|
history(machine: string, id: string, entry: string): void;
|
|
12
14
|
put(kind: RecordKind, key: string, record: string): void;
|
|
15
|
+
wantsAudit(): boolean;
|
|
16
|
+
audit(entry: string, reply: string): void;
|
|
13
17
|
}
|
|
14
18
|
/** A build's wasm-bindgen `Engine`: JSON text in and out. */
|
|
15
19
|
export interface RawEngine {
|
|
@@ -21,8 +25,13 @@ export interface RawEngine {
|
|
|
21
25
|
events(key: string): string;
|
|
22
26
|
status(key: string): string;
|
|
23
27
|
fire(key: string | null | undefined, event: string, params: string): string;
|
|
28
|
+
fireAsHost(key: string | null | undefined, event: string, params: string): string;
|
|
24
29
|
stop(key: string, stopHookActive: boolean): string;
|
|
25
30
|
promptSubmitted(key: string): void;
|
|
31
|
+
stateText(key: string, schemas: boolean): string;
|
|
32
|
+
answerSchema(key: string): string;
|
|
33
|
+
checkAnswer(key: string, text: string): string;
|
|
34
|
+
hostView(key: string): string;
|
|
26
35
|
exportState(): string;
|
|
27
36
|
importState(state: string): void;
|
|
28
37
|
free(): void;
|
|
@@ -36,7 +45,8 @@ export declare class Engine {
|
|
|
36
45
|
protected static Raw: RawClass;
|
|
37
46
|
/**
|
|
38
47
|
* `storage`, when given, is loaded now and then receives every saved
|
|
39
|
-
* record and history entry
|
|
48
|
+
* record and history entry, and each audit entry when it wants an audit
|
|
49
|
+
* (its `put`, `history` and `audit` go before `host`'s).
|
|
40
50
|
*/
|
|
41
51
|
constructor(compiled: string | object, host?: Host, storage?: Storage);
|
|
42
52
|
/** The agent-facing tool, to hand to an LLM's tool list (WASM-1). */
|
|
@@ -47,15 +57,41 @@ export declare class Engine {
|
|
|
47
57
|
unsupported(): string[];
|
|
48
58
|
/** Bind a harness session (reused when `hostSession` is already bound). */
|
|
49
59
|
bind(harness: string, hostSession?: string, cwd?: string): Reply;
|
|
50
|
-
/** Where the session is; changes nothing. */
|
|
60
|
+
/** Where the session is; changes nothing and records no audit entry. A look of the agent's is `callTool` with no event. */
|
|
51
61
|
view(key: string): Reply;
|
|
52
62
|
/** The events list of the session's state. */
|
|
53
63
|
events(key: string): Reply;
|
|
54
64
|
status(key: string): SessionStatus;
|
|
55
|
-
/**
|
|
56
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Fire an event for the agent; with no key, only `enter` (it starts a
|
|
67
|
+
* session). An event that the machine marks with `source: host` is
|
|
68
|
+
* refused: fire it with `fireAsHost`. Fire an event that the agent chose
|
|
69
|
+
* here, never with `fireAsHost`. A param takes a string, or any JSON
|
|
70
|
+
* value when the events list shows it as a JSON value.
|
|
71
|
+
*/
|
|
72
|
+
fire(key: string | undefined, event: string, params?: Record<string, unknown>): Reply;
|
|
73
|
+
/**
|
|
74
|
+
* Fire an event as the host: as `fire`, and also an event that the machine
|
|
75
|
+
* marks with `source: host`, such as the result of the host's own checks.
|
|
76
|
+
*/
|
|
77
|
+
fireAsHost(key: string | undefined, event: string, params?: Record<string, unknown>): Reply;
|
|
57
78
|
stop(key: string, stopHookActive?: boolean): StopDecision;
|
|
58
79
|
promptSubmitted(key: string): void;
|
|
80
|
+
/**
|
|
81
|
+
* The state's text for a model without tools: its instructions, the
|
|
82
|
+
* events that the model can answer with, and how to answer. Each JSON
|
|
83
|
+
* param shows its schema unless `schemas` is `false`.
|
|
84
|
+
*/
|
|
85
|
+
stateText(key: string, options?: StateTextOptions): string;
|
|
86
|
+
/** The JSON Schema of a valid answer: a `oneOf` with one branch for each event of the model. */
|
|
87
|
+
answerSchema(key: string): Record<string, unknown>;
|
|
88
|
+
/**
|
|
89
|
+
* Check the model's text answer: the event and its params, ready for
|
|
90
|
+
* `fire`, or a refusal to send back to the model. It fires nothing.
|
|
91
|
+
*/
|
|
92
|
+
checkAnswer(key: string, text: string): CheckedAnswer;
|
|
93
|
+
/** The state's offered events in two lists: the model's, and the host's own. */
|
|
94
|
+
hostView(key: string): HostView;
|
|
59
95
|
exportState(): State;
|
|
60
96
|
importState(state: State | string): void;
|
|
61
97
|
/** Free the wasm memory now, rather than when collected. */
|
package/dist/wrap.js
CHANGED
|
@@ -10,6 +10,10 @@ function adapt(host) {
|
|
|
10
10
|
run: (kind, params, env, cwd) => host.run ? (host.run(kind, parse(params), parse(env), cwd) ?? "") : `${kind} is not supported`,
|
|
11
11
|
read: (file) => host.read?.(file) ?? undefined,
|
|
12
12
|
isMatch: (pattern, value) => host.isMatch ? host.isMatch(pattern, value) : new RegExp(pattern).test(value),
|
|
13
|
+
checksJson: () => typeof host.checkJson === "function",
|
|
14
|
+
// `checksJson` guards the call; an answer that isn't a list of strings,
|
|
15
|
+
// such as `undefined`, reaches the engine as `null`, which refuses it.
|
|
16
|
+
checkJson: (schema, value) => JSON.stringify(host.checkJson(parse(schema), parse(value)) ?? null),
|
|
13
17
|
now: () => (host.now ? host.now() : Date.now()),
|
|
14
18
|
random: () => (host.random ? host.random() : crypto.getRandomValues(new Uint32Array(1))[0]),
|
|
15
19
|
history: (machine, id, entry) => {
|
|
@@ -18,6 +22,10 @@ function adapt(host) {
|
|
|
18
22
|
put: (kind, key, record) => {
|
|
19
23
|
host.put?.(kind, key, parse(record));
|
|
20
24
|
},
|
|
25
|
+
wantsAudit: () => Boolean(host.wantsAudit?.()),
|
|
26
|
+
audit: (entry, reply) => {
|
|
27
|
+
host.audit?.(parse(entry), reply);
|
|
28
|
+
},
|
|
21
29
|
};
|
|
22
30
|
}
|
|
23
31
|
/** smllm's engine over compiled machines (`smllm compile` output). */
|
|
@@ -27,7 +35,8 @@ export class Engine {
|
|
|
27
35
|
#raw;
|
|
28
36
|
/**
|
|
29
37
|
* `storage`, when given, is loaded now and then receives every saved
|
|
30
|
-
* record and history entry
|
|
38
|
+
* record and history entry, and each audit entry when it wants an audit
|
|
39
|
+
* (its `put`, `history` and `audit` go before `host`'s).
|
|
31
40
|
*/
|
|
32
41
|
constructor(compiled, host = {}, storage) {
|
|
33
42
|
const json = typeof compiled === "string" ? compiled : JSON.stringify(compiled);
|
|
@@ -43,6 +52,15 @@ export class Engine {
|
|
|
43
52
|
storage.history(machine, id, entry);
|
|
44
53
|
host.history?.(machine, id, entry);
|
|
45
54
|
},
|
|
55
|
+
wantsAudit: () => Boolean(storage.wantsAudit?.() || host.wantsAudit?.()),
|
|
56
|
+
audit: (entry, reply) => {
|
|
57
|
+
if (storage.wantsAudit?.()) {
|
|
58
|
+
storage.audit?.(entry, reply);
|
|
59
|
+
}
|
|
60
|
+
if (host.wantsAudit?.()) {
|
|
61
|
+
host.audit?.(entry, reply);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
46
64
|
}
|
|
47
65
|
: host;
|
|
48
66
|
this.#raw = new Raw(json, adapt(all));
|
|
@@ -68,7 +86,7 @@ export class Engine {
|
|
|
68
86
|
bind(harness, hostSession, cwd = "") {
|
|
69
87
|
return parse(this.#raw.bind(harness, hostSession, cwd));
|
|
70
88
|
}
|
|
71
|
-
/** Where the session is; changes nothing. */
|
|
89
|
+
/** Where the session is; changes nothing and records no audit entry. A look of the agent's is `callTool` with no event. */
|
|
72
90
|
view(key) {
|
|
73
91
|
return parse(this.#raw.view(key));
|
|
74
92
|
}
|
|
@@ -79,16 +97,52 @@ export class Engine {
|
|
|
79
97
|
status(key) {
|
|
80
98
|
return parse(this.#raw.status(key));
|
|
81
99
|
}
|
|
82
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Fire an event for the agent; with no key, only `enter` (it starts a
|
|
102
|
+
* session). An event that the machine marks with `source: host` is
|
|
103
|
+
* refused: fire it with `fireAsHost`. Fire an event that the agent chose
|
|
104
|
+
* here, never with `fireAsHost`. A param takes a string, or any JSON
|
|
105
|
+
* value when the events list shows it as a JSON value.
|
|
106
|
+
*/
|
|
83
107
|
fire(key, event, params = {}) {
|
|
84
108
|
return parse(this.#raw.fire(key, event, JSON.stringify(params)));
|
|
85
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Fire an event as the host: as `fire`, and also an event that the machine
|
|
112
|
+
* marks with `source: host`, such as the result of the host's own checks.
|
|
113
|
+
*/
|
|
114
|
+
fireAsHost(key, event, params = {}) {
|
|
115
|
+
return parse(this.#raw.fireAsHost(key, event, JSON.stringify(params)));
|
|
116
|
+
}
|
|
86
117
|
stop(key, stopHookActive = false) {
|
|
87
118
|
return parse(this.#raw.stop(key, stopHookActive));
|
|
88
119
|
}
|
|
89
120
|
promptSubmitted(key) {
|
|
90
121
|
this.#raw.promptSubmitted(key);
|
|
91
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The state's text for a model without tools: its instructions, the
|
|
125
|
+
* events that the model can answer with, and how to answer. Each JSON
|
|
126
|
+
* param shows its schema unless `schemas` is `false`.
|
|
127
|
+
*/
|
|
128
|
+
stateText(key, options = {}) {
|
|
129
|
+
return this.#raw.stateText(key, options.schemas ?? true);
|
|
130
|
+
}
|
|
131
|
+
/** The JSON Schema of a valid answer: a `oneOf` with one branch for each event of the model. */
|
|
132
|
+
answerSchema(key) {
|
|
133
|
+
return parse(this.#raw.answerSchema(key));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check the model's text answer: the event and its params, ready for
|
|
137
|
+
* `fire`, or a refusal to send back to the model. It fires nothing.
|
|
138
|
+
*/
|
|
139
|
+
checkAnswer(key, text) {
|
|
140
|
+
return parse(this.#raw.checkAnswer(key, text));
|
|
141
|
+
}
|
|
142
|
+
/** The state's offered events in two lists: the model's, and the host's own. */
|
|
143
|
+
hostView(key) {
|
|
144
|
+
return parse(this.#raw.hostView(key));
|
|
145
|
+
}
|
|
92
146
|
exportState() {
|
|
93
147
|
return parse(this.#raw.exportState());
|
|
94
148
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@six5536/smllm-wasm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "smllm's engine as WebAssembly: run smllm state machines that drive an LLM agent, in a browser or Node.",
|
|
5
5
|
"homepage": "https://github.com/six5536/smllm#readme",
|
|
6
6
|
"repository": {
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
export class Engine {
|
|
8
8
|
free(): void;
|
|
9
9
|
[Symbol.dispose](): void;
|
|
10
|
+
/**
|
|
11
|
+
* The JSON Schema of a valid answer, as JSON (ENG-8).
|
|
12
|
+
*/
|
|
13
|
+
answerSchema(key: string): string;
|
|
10
14
|
/**
|
|
11
15
|
* Bind a session; returns the reply JSON (its `session` is the key).
|
|
12
16
|
*/
|
|
@@ -18,6 +22,12 @@ export class Engine {
|
|
|
18
22
|
* sees its mistake as the tool's answer.
|
|
19
23
|
*/
|
|
20
24
|
callTool(args: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* A model's answer checked (ENG-9), as JSON: `{"ok": true, "event",
|
|
27
|
+
* "params"}`, whose params are JSON values ready for `fire`, or
|
|
28
|
+
* `{"ok": false, "error"}`.
|
|
29
|
+
*/
|
|
30
|
+
checkAnswer(key: string, text: string): string;
|
|
21
31
|
/**
|
|
22
32
|
* The events list.
|
|
23
33
|
*/
|
|
@@ -27,9 +37,19 @@ export class Engine {
|
|
|
27
37
|
*/
|
|
28
38
|
exportState(): string;
|
|
29
39
|
/**
|
|
30
|
-
* Fire an event
|
|
40
|
+
* Fire an event as the host, host-only events included (ENG-6).
|
|
41
|
+
*/
|
|
42
|
+
fireAsHost(key: string | null | undefined, event: string, params: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Fire an event for the agent; `params` is a JSON object, of strings
|
|
45
|
+
* except for a JSON param's value.
|
|
31
46
|
*/
|
|
32
47
|
fire(key: string | null | undefined, event: string, params: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* The state's offered events in two lists, the model's and the
|
|
50
|
+
* host's, with where the session is, as JSON (ENG-10).
|
|
51
|
+
*/
|
|
52
|
+
hostView(key: string): string;
|
|
33
53
|
/**
|
|
34
54
|
* Replace the store from [`Engine::export_state`] JSON.
|
|
35
55
|
*/
|
|
@@ -42,6 +62,11 @@ export class Engine {
|
|
|
42
62
|
* A user prompt arrived.
|
|
43
63
|
*/
|
|
44
64
|
promptSubmitted(key: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* The state's text for a model without tools; `schemas` shows each
|
|
67
|
+
* JSON param's schema (ENG-7).
|
|
68
|
+
*/
|
|
69
|
+
stateText(key: string, schemas: boolean): string;
|
|
45
70
|
/**
|
|
46
71
|
* Where the session is, as the `smllm statusline --json` object (ENG-3).
|
|
47
72
|
*/
|
|
@@ -60,7 +85,8 @@ export class Engine {
|
|
|
60
85
|
*/
|
|
61
86
|
tool(): string;
|
|
62
87
|
/**
|
|
63
|
-
* Guard and action kinds this host cannot run,
|
|
88
|
+
* Guard and action kinds this host cannot run, and the `json` params
|
|
89
|
+
* whose schema it can't check, as a JSON array.
|
|
64
90
|
*/
|
|
65
91
|
unsupported(): string;
|
|
66
92
|
/**
|