@six5536/smllm-wasm 0.0.0 → 0.4.1

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 CHANGED
@@ -1,3 +1,138 @@
1
- # @six5536/smllm-wasm
1
+ # smllm-wasm
2
2
 
3
- This version only reserves the name. smllm publishes its real versions from https://github.com/six5536/smllm.
3
+ `@six5536/smllm-wasm` is smllm's engine, `smllm-core`, compiled to WebAssembly. It is for a host
4
+ that runs state machines in its own process, such as a browser, a worker, a serverless function,
5
+ or Node. The large language model (LLM) can run anywhere. The API is typed: it takes objects and returns
6
+ objects, and the package includes TypeScript declarations.
7
+
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 Node helpers, and when to use the WebAssembly engine instead of the
10
+ CLI.
11
+
12
+ - [Host](#host)
13
+ - [Storage](#storage)
14
+ - [Run in Node](#run-in-node)
15
+ - [WebAssembly or CLI](#webassembly-or-cli)
16
+ - [Licence](#licence)
17
+
18
+ This example loads compiled machines, starts a session, and passes the model's tool calls to the
19
+ engine:
20
+
21
+ ```js
22
+ import init, { Engine, memoryStorage } from "@six5536/smllm-wasm"; // or from "@six5536/smllm-wasm/bundler"
23
+
24
+ await init(); // the bundler build needs no init
25
+ const compiled = await (await fetch("machines.json")).json(); // `smllm compile`
26
+ const engine = new Engine(
27
+ compiled,
28
+ {
29
+ // Every host method is optional; supports / check / run add host guard and
30
+ // action kinds (`command`), none by default.
31
+ },
32
+ memoryStorage(), // or your own Storage (see Storage); optional
33
+ );
34
+ const { session, text } = engine.bind("web");
35
+ // Give the model the same `smllm` tool the MCP server offers, and pass its
36
+ // calls through: a malformed call comes back as an answer, never a throw.
37
+ const tool = engine.tool(); // { name, description, inputSchema }
38
+ const reply = engine.callTool({ session, event: "enter", params: { stateMachine: "dev" } });
39
+ // When the model tries to stop (`stopHookActive`: it is already continuing
40
+ // because of an earlier block):
41
+ const stop = engine.stop(session, stopHookActive);
42
+ // stop.decision "block": hold the model, show it stop.text (the events list);
43
+ // "runaway": let it stop, show stop.text to the user; "allow": let it stop.
44
+ ```
45
+
46
+ The `examples/wasm` folder of the [smllm repository](https://github.com/six5536/smllm) holds a
47
+ whole agent loop, in TypeScript.
48
+
49
+ ## Host
50
+
51
+ A host can supply these methods. Each one is optional, and the engine uses its default when the
52
+ host leaves the method out:
53
+
54
+ - `supports(kind)` says which guard and action kinds the host runs. By default, it supports none.
55
+ - `check(kind, params, env, cwd)` checks a guard of a supported kind.
56
+ - `run(kind, params, env, cwd)` runs an action of a supported kind. It returns nothing on
57
+ success, and the detail of the failure otherwise. A command's `params.cwd` is relative to the
58
+ compiled config file.
59
+ - `read(file)` reads a prompt file. `smllm compile` inlines prompt files, so a host rarely needs
60
+ this method.
61
+ - `isMatch(pattern, value)` matches a pattern. By default, it uses `new RegExp`.
62
+ - `now()` returns the time. By default, it uses `Date.now`.
63
+ - `random()` returns a random number. By default, it uses `crypto.getRandomValues`.
64
+ - `history(machine, id, entry)` and `put(kind, key, record)` receive writes. By default, the
65
+ engine drops them.
66
+
67
+ A host method that throws is a host failure: a failed guard or action, or a rejected pattern. The
68
+ reply text shows the failure, and the engine stays usable.
69
+
70
+ ## Storage
71
+
72
+ The engine works in memory and writes through to storage. It passes each session, binding, and
73
+ instance to `put` when it saves them, and each history entry to `history`. Persistence therefore
74
+ costs only what changed, never a snapshot for each call.
75
+
76
+ A `Storage` is `{ load(), put(kind, key, record), history(machine, id, entry) }`. The engine
77
+ takes a `Storage` as the constructor's third argument, loads it once, and then hands it every
78
+ write.
79
+ `memoryStorage()` keeps the state in memory. In a browser, you can write a `Storage` over
80
+ IndexedDB, or one that sends the records to a server. `exportState()` and `importState(state)`
81
+ move the whole state at once, for backups and moves.
82
+
83
+ ## Run in Node
84
+
85
+ `@six5536/smllm-wasm/node` provides a host and a storage for Node:
86
+
87
+ ```js
88
+ import { nodeFileStorage, nodeHost } from "@six5536/smllm-wasm/node";
89
+
90
+ const engine = new Engine(
91
+ compiled,
92
+ nodeHost({ configDir: "/path/to/the/compiled/config/folder" }),
93
+ nodeFileStorage("/path/to/state"),
94
+ );
95
+ ```
96
+
97
+ `nodeHost` runs `command` guards and actions as the smllm CLI does:
98
+
99
+ - It runs a string `run` through the shell, and a list without one.
100
+ - It sets the `SMLLM_*` environment variables.
101
+ - It applies a timeout, 60 seconds by default, and the timeout also kills the processes that the
102
+ command started.
103
+ - On a failure, it reports the last 400 characters of output.
104
+
105
+ `nodeFileStorage` keeps one file for each record, written atomically, and keeps history as JSON Lines (JSONL).
106
+ Only one process can own a storage folder.
107
+
108
+ ## WebAssembly or CLI
109
+
110
+ The following table gives the median time for each call. The project's benchmark measured the
111
+ times in a Linux container with a local disk. Each column heading gives the size of the store as the
112
+ number of paused instances and the number of completed instances.
113
+
114
+ | Path | Call | 0 / 0 | 100 / 1000 | 1000 / 1000 |
115
+ | ---- | ---- | ---: | ---: | ---: |
116
+ | wasm, in-process | `fire` enter / pause | 0.06 ms | 0.02 / 0.03 ms | 0.02 / 0.03 ms |
117
+ | wasm, in-process | view, stop | 0.02 ms | 0.006 ms | 0.006 ms |
118
+ | CLI, `smllm mcp` (one process, over stdio) | tool call | 0.2–0.4 ms | 0.1–0.6 ms | 0.1–2.3 ms |
119
+ | CLI, a process per call | `smllm fire`, `statusline` | 1.4–1.7 ms | 1.2–1.8 ms | 1.3–4.4 ms |
120
+
121
+ Use the WebAssembly engine in these cases:
122
+
123
+ - In a browser, a worker, or a serverless function, where there is no binary and no shell.
124
+ - For a model that answers in milliseconds.
125
+ - For guards and actions written in JavaScript.
126
+ - For storage of your own.
127
+
128
+ Use the CLI, from the `@six5536/smllm` npm package, when a Node app needs the CLI's behaviour as
129
+ it is. For tool calls, run `smllm mcp`. The CLI shares files between processes with locking, and
130
+ provides the status line, `smllm instance list`, and the Claude Code hooks.
131
+
132
+ The string API underneath, which takes and returns JSON text, is in `@six5536/smllm-wasm/raw` and
133
+ `@six5536/smllm-wasm/raw/bundler`.
134
+
135
+ ## Licence
136
+
137
+ smllm-wasm is licensed under the MIT licence. Its source is in the
138
+ [smllm repository](https://github.com/six5536/smllm).
@@ -0,0 +1,8 @@
1
+ import { Engine as Raw } from "../wasm/bundler/smllm_wasm.js";
2
+ import { Engine as Base } from "./wrap.js";
3
+ export type * from "./types.js";
4
+ export { memoryStorage } from "./storage.js";
5
+ /** smllm's engine over compiled machines (`smllm compile` output). */
6
+ export declare class Engine extends Base {
7
+ protected static Raw: typeof Raw;
8
+ }
@@ -0,0 +1,9 @@
1
+ // smllm-wasm for bundlers: the wasm loads on import. The string API is
2
+ // `smllm-wasm/raw/bundler`.
3
+ import { Engine as Raw } from "../wasm/bundler/smllm_wasm.js";
4
+ import { Engine as Base } from "./wrap.js";
5
+ export { memoryStorage } from "./storage.js";
6
+ /** smllm's engine over compiled machines (`smllm compile` output). */
7
+ export class Engine extends Base {
8
+ static Raw = Raw;
9
+ }
@@ -0,0 +1,10 @@
1
+ import init, { Engine as Raw, initSync } from "../wasm/web/smllm_wasm.js";
2
+ import { Engine as Base } from "./wrap.js";
3
+ export default init;
4
+ export { initSync };
5
+ export type * from "./types.js";
6
+ export { memoryStorage } from "./storage.js";
7
+ /** smllm's engine over compiled machines (`smllm compile` output). */
8
+ export declare class Engine extends Base {
9
+ protected static Raw: typeof Raw;
10
+ }
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // smllm-wasm for browsers and Node (the web build): call `init()` (or
2
+ // `initSync`) once, then use `Engine`. The string API is `smllm-wasm/raw`.
3
+ import init, { Engine as Raw, initSync } from "../wasm/web/smllm_wasm.js";
4
+ import { Engine as Base } from "./wrap.js";
5
+ export default init;
6
+ export { initSync };
7
+ export { memoryStorage } from "./storage.js";
8
+ /** smllm's engine over compiled machines (`smllm compile` output). */
9
+ export class Engine extends Base {
10
+ static Raw = Raw;
11
+ }
@@ -0,0 +1,12 @@
1
+ import type { Host } from "../types.js";
2
+ /** `timeoutSecs` default (DEC-5), as the CLI's. */
3
+ export declare const DEFAULT_TIMEOUT_SECS = 60;
4
+ /**
5
+ * A host that runs `command` guards and actions. `configDir` is the folder
6
+ * of the compiled config: a command's `cwd` is relative to it (D8-22).
7
+ * Spread it into your host: `new Engine(compiled, { ...nodeHost({ configDir }), ... })`.
8
+ */
9
+ export declare function nodeHost(options: {
10
+ configDir: string;
11
+ timeoutSecs?: number;
12
+ }): Host;
@@ -0,0 +1,106 @@
1
+ // The Node command host (HOST-15): `command` guards
2
+ // and actions run as the CLI runs them (DEC-4..DEC-7). Host methods are
3
+ // synchronous, so commands run with `spawnSync`. On unix the command is
4
+ // started detached, as the leader of its own process group, and a timeout
5
+ // kills that whole group, so what the command started dies with it; its
6
+ // output goes to a file, not a pipe, so a process it leaves running cannot
7
+ // hold the call open. On Windows the command's own process is killed, as the
8
+ // CLI does.
9
+ // @zen-component: HOST-JsPackage
10
+ import { spawnSync } from "node:child_process";
11
+ import { closeSync, mkdtempSync, openSync, readFileSync, rmSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join, resolve } from "node:path";
14
+ /** `timeoutSecs` default (DEC-5), as the CLI's. */
15
+ export const DEFAULT_TIMEOUT_SECS = 60;
16
+ const TAIL_CHARS = 400;
17
+ /** The last `TAIL_CHARS` characters of `text`, on one line (as the CLI's). */
18
+ function tail(text) {
19
+ const chars = [...text.trim()];
20
+ const cut = chars.length > TAIL_CHARS;
21
+ const s = chars.slice(-TAIL_CHARS).join("").split(/\s+/).filter(Boolean).join(" ");
22
+ return cut ? `…${s}` : s;
23
+ }
24
+ /** A command's failure text, `""` when it passed. */
25
+ function runCommand(params, env, sessionCwd, configDir, timeoutSecs) {
26
+ const run = params.run;
27
+ let argv;
28
+ if (typeof run === "string") {
29
+ argv = process.platform === "win32" ? ["cmd", "/C", run] : ["sh", "-c", run];
30
+ }
31
+ else if (Array.isArray(run) && run.length > 0 && run.every((a) => typeof a === "string")) {
32
+ argv = run;
33
+ }
34
+ else {
35
+ return "no run param";
36
+ }
37
+ // `params.cwd` is relative to the compiled config (D8-22); else the
38
+ // session's working directory, when there is one.
39
+ const cwd = typeof params.cwd === "string" ? resolve(configDir, params.cwd) : sessionCwd || undefined;
40
+ const secs = typeof params.timeoutSecs === "number" ? params.timeoutSecs : timeoutSecs;
41
+ const unix = process.platform !== "win32";
42
+ const dir = mkdtempSync(join(tmpdir(), "smllm-command-"));
43
+ const out = join(dir, "out");
44
+ const fd = openSync(out, "w");
45
+ try {
46
+ // `detached` is honoured by spawnSync as by spawn, though its typings
47
+ // omit it; the test that a timed-out grandchild dies guards this.
48
+ const options = {
49
+ cwd,
50
+ env: { ...process.env, ...env },
51
+ stdio: ["ignore", fd, fd],
52
+ timeout: secs * 1000,
53
+ killSignal: "SIGKILL",
54
+ // Its own session and process group (spawnSync honours it as spawn
55
+ // does), so a timeout can kill what it started too.
56
+ detached: unix,
57
+ windowsVerbatimArguments: argv[0] === "cmd",
58
+ windowsHide: true,
59
+ };
60
+ const r = spawnSync(argv[0], argv.slice(1), options);
61
+ const timedOut = r.error?.code === "ETIMEDOUT";
62
+ if (timedOut) {
63
+ if (unix && r.pid) {
64
+ // The leader is gone; the rest of its group goes too. A group id is
65
+ // not reused while members remain.
66
+ try {
67
+ process.kill(-r.pid, "SIGKILL");
68
+ }
69
+ catch {
70
+ // Nothing left in the group.
71
+ }
72
+ }
73
+ return `timed out after ${secs}s`;
74
+ }
75
+ if (r.error) {
76
+ return `could not start: ${r.error.message}`;
77
+ }
78
+ return failure(r.status, r.status === 0 ? "" : readFileSync(out, "utf8"));
79
+ }
80
+ finally {
81
+ closeSync(fd);
82
+ rmSync(dir, { recursive: true, force: true });
83
+ }
84
+ }
85
+ function failure(status, output) {
86
+ if (status === 0) {
87
+ return "";
88
+ }
89
+ const code = status === null ? "killed by a signal" : `exited ${status}`;
90
+ const t = tail(output);
91
+ return t ? `${code}: ${t}` : code;
92
+ }
93
+ /**
94
+ * A host that runs `command` guards and actions. `configDir` is the folder
95
+ * of the compiled config: a command's `cwd` is relative to it (D8-22).
96
+ * Spread it into your host: `new Engine(compiled, { ...nodeHost({ configDir }), ... })`.
97
+ */
98
+ export function nodeHost(options) {
99
+ const { configDir, timeoutSecs = DEFAULT_TIMEOUT_SECS } = options;
100
+ const exec = (kind, params, env, cwd) => kind === "command" ? runCommand(params, env, cwd, configDir, timeoutSecs) : `${kind} is not supported`;
101
+ return {
102
+ supports: (kind) => kind === "command",
103
+ check: (kind, params, env, cwd) => exec(kind, params, env, cwd) === "",
104
+ run: (kind, params, env, cwd) => exec(kind, params, env, cwd),
105
+ };
106
+ }
@@ -0,0 +1,2 @@
1
+ export { fileName, nodeFileStorage } from "./storage.js";
2
+ export { DEFAULT_TIMEOUT_SECS, nodeHost } from "./host.js";
@@ -0,0 +1,4 @@
1
+ // smllm-wasm for Node hosts: file storage and the command host.
2
+ // Import the engine itself from `smllm-wasm`.
3
+ export { fileName, nodeFileStorage } from "./storage.js";
4
+ export { DEFAULT_TIMEOUT_SECS, nodeHost } from "./host.js";
@@ -0,0 +1,9 @@
1
+ import type { Storage } from "../types.js";
2
+ /**
3
+ * A file name for `key`: `[a-z0-9-]` kept, every other byte %-escaped (upper
4
+ * case too, so a case-insensitive file system cannot merge two keys), cut
5
+ * with an FNV-1a hash when long.
6
+ */
7
+ export declare function fileName(key: string): string;
8
+ /** Storage in `dir`: `sessions/`, `bindings/`, `instances/` and `history/`. */
9
+ export declare function nodeFileStorage(dir: string): Storage;
@@ -0,0 +1,75 @@
1
+ // Node file storage (HOST-14): a file per record, written
2
+ // atomically (temp file, then rename), and history as JSONL, one line per
3
+ // entry in the CLI's format. One process owns a folder: several processes
4
+ // sharing state should use the CLI, whose store locks.
5
+ // @zen-component: HOST-JsPackage
6
+ import { appendFileSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ const SHELF = {
9
+ session: "sessions",
10
+ binding: "bindings",
11
+ instance: "instances",
12
+ };
13
+ /** Longest file name before `.json`; longer ones are cut and end in a hash. */
14
+ const MAX_NAME = 120;
15
+ /**
16
+ * A file name for `key`: `[a-z0-9-]` kept, every other byte %-escaped (upper
17
+ * case too, so a case-insensitive file system cannot merge two keys), cut
18
+ * with an FNV-1a hash when long.
19
+ */
20
+ export function fileName(key) {
21
+ let name = "";
22
+ for (const b of new TextEncoder().encode(key)) {
23
+ const c = String.fromCharCode(b);
24
+ name += /[a-z0-9-]/.test(c) ? c : `%${b.toString(16).padStart(2, "0")}`;
25
+ }
26
+ if (name.length > MAX_NAME) {
27
+ let h = 0xcbf29ce484222325n;
28
+ for (const b of new TextEncoder().encode(key)) {
29
+ h = ((h ^ BigInt(b)) * 0x100000001b3n) & 0xffffffffffffffffn;
30
+ }
31
+ name = `${name.slice(0, MAX_NAME - 17)}~${h.toString(16).padStart(16, "0")}`;
32
+ }
33
+ return name;
34
+ }
35
+ let written = 0;
36
+ /** Write `text` to `path` whole or not at all. */
37
+ function writeAtomic(path, text) {
38
+ const temp = `${path}.${process.pid}.${written++}.tmp`;
39
+ writeFileSync(temp, text);
40
+ renameSync(temp, path);
41
+ }
42
+ /** Storage in `dir`: `sessions/`, `bindings/`, `instances/` and `history/`. */
43
+ export function nodeFileStorage(dir) {
44
+ return {
45
+ load() {
46
+ const state = { sessions: {}, bindings: {}, instances: {} };
47
+ let any = false;
48
+ for (const shelf of Object.values(SHELF)) {
49
+ let names;
50
+ try {
51
+ names = readdirSync(join(dir, shelf)).filter((n) => n.endsWith(".json"));
52
+ }
53
+ catch {
54
+ continue;
55
+ }
56
+ for (const n of names) {
57
+ const { key, record } = JSON.parse(readFileSync(join(dir, shelf, n), "utf8"));
58
+ state[shelf][key] = record;
59
+ any = true;
60
+ }
61
+ }
62
+ return any ? state : undefined;
63
+ },
64
+ put(kind, key, record) {
65
+ const shelf = join(dir, SHELF[kind]);
66
+ mkdirSync(shelf, { recursive: true });
67
+ writeAtomic(join(shelf, `${fileName(key)}.json`), JSON.stringify({ key, record }));
68
+ },
69
+ history(machine, id, entry) {
70
+ const folder = join(dir, "history", fileName(machine));
71
+ mkdirSync(folder, { recursive: true });
72
+ appendFileSync(join(folder, `${fileName(id)}.jsonl`), `${JSON.stringify(entry)}\n`);
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,5 @@
1
+ import type { HistoryEntry, Storage } from "./types.js";
2
+ /** A storage that keeps everything in memory. `history` holds the log by `machine/id`. */
3
+ export declare function memoryStorage(): Storage & {
4
+ readonly log: Map<string, HistoryEntry[]>;
5
+ };
@@ -0,0 +1,26 @@
1
+ // In-memory storage (HOST-14): what `nodeFileStorage` does with files,
2
+ // without them; for tests, examples and hosts that persist elsewhere.
3
+ // @zen-component: HOST-JsPackage
4
+ /** A storage that keeps everything in memory. `history` holds the log by `machine/id`. */
5
+ export function memoryStorage() {
6
+ const state = { sessions: {}, bindings: {}, instances: {} };
7
+ const log = new Map();
8
+ let empty = true;
9
+ const shelf = {
10
+ session: state.sessions,
11
+ binding: state.bindings,
12
+ instance: state.instances,
13
+ };
14
+ return {
15
+ load: () => (empty ? undefined : structuredClone(state)),
16
+ put(kind, key, record) {
17
+ shelf[kind][key] = record;
18
+ empty = false;
19
+ },
20
+ history(machine, id, entry) {
21
+ const key = `${machine}/${id}`;
22
+ log.set(key, [...(log.get(key) ?? []), entry]);
23
+ },
24
+ log,
25
+ };
26
+ }
@@ -0,0 +1,109 @@
1
+ /** Where a session is after a call; all `null` in idle. */
2
+ export interface Location {
3
+ machine: string | null;
4
+ state: string | null;
5
+ instance: string | null;
6
+ ref: string | null;
7
+ }
8
+ /** The answer to a call: the `<smllm>` text for the agent, and where the session is. */
9
+ export interface Reply {
10
+ /** `false` when the event was rejected or the instance moved. */
11
+ ok: boolean;
12
+ session: string;
13
+ location: Location;
14
+ text: string;
15
+ }
16
+ /** The stop hook's decision (TURN-4..6). */
17
+ export type StopDecision = {
18
+ decision: "allow";
19
+ }
20
+ /** `block`: hold the agent and show it `text`; `runaway`: let it stop, show `text` to the user. */
21
+ | {
22
+ decision: "block" | "runaway";
23
+ text: string;
24
+ };
25
+ export interface InstanceStatus {
26
+ machine: string;
27
+ kind: string;
28
+ id: string;
29
+ ref: string | null;
30
+ label: string;
31
+ status: "active" | "interrupted" | "paused" | "completed";
32
+ }
33
+ /** Where a session is, as data (the `smllm statusline --json` object). */
34
+ export interface SessionStatus {
35
+ session: string;
36
+ idle: boolean;
37
+ machine: string | null;
38
+ state: string | null;
39
+ visit: number | null;
40
+ yielded: boolean;
41
+ instance: InstanceStatus | null;
42
+ interrupted: InstanceStatus | null;
43
+ paused: number;
44
+ }
45
+ /** One history entry, handed to `Host.history` as it happens. */
46
+ export interface HistoryEntry {
47
+ /** Unix ms. */
48
+ at: number;
49
+ session: string;
50
+ event: string;
51
+ from: string | null;
52
+ to: string | null;
53
+ params: Record<string, string>;
54
+ trace: string[];
55
+ }
56
+ /**
57
+ * What the engine needs from its host. Every method is optional:
58
+ * - `supports`, `check`, `run`: host guard/action kinds (`command`); none by default.
59
+ * A command's `params.cwd` is relative to the compiled config file (`nodeHost`'s `configDir`).
60
+ * - `read`: prompt files (`smllm compile` inlines them, so rarely needed).
61
+ * - `isMatch`: `new RegExp(pattern).test(value)` by default.
62
+ * - `now`, `random`: `Date.now()` and `crypto.getRandomValues` by default.
63
+ * - `history`: each history entry as it happens; dropped by default.
64
+ * - `put`: each session, binding and instance as it is saved (write-through storage); dropped by default.
65
+ * A method that throws is a host failure the engine reports, never a broken engine.
66
+ */
67
+ export interface Host {
68
+ supports?(kind: string): boolean;
69
+ check?(kind: string, params: Record<string, unknown>, env: Record<string, string>, cwd: string): boolean;
70
+ /** Nothing (or `""`) on success, else the failure detail. */
71
+ run?(kind: string, params: Record<string, unknown>, env: Record<string, string>, cwd: string): string | void;
72
+ read?(file: string): string | undefined;
73
+ isMatch?(pattern: string, value: string): boolean;
74
+ now?(): number;
75
+ random?(): number;
76
+ history?(machine: string, id: string, entry: HistoryEntry): void;
77
+ put?(kind: RecordKind, key: string, record: unknown): void;
78
+ }
79
+ /** What `put` hands on: a session (key: its key), a binding (`harness/hostSession`, record: the session key) or an instance (`machine/id`). */
80
+ export type RecordKind = "session" | "binding" | "instance";
81
+ /**
82
+ * Where an engine's state lives (HOST-14): `load` once, into `importState`;
83
+ * then each saved record through `put` and each history entry through
84
+ * `history`, as they happen, so persisting costs what changed.
85
+ */
86
+ export interface Storage {
87
+ load(): State | undefined;
88
+ put(kind: RecordKind, key: string, record: unknown): void;
89
+ history(machine: string, id: string, entry: HistoryEntry): void;
90
+ }
91
+ /** The agent-facing `smllm` tool: the same definition the MCP server offers. */
92
+ export interface Tool {
93
+ name: string;
94
+ description: string;
95
+ /** JSON Schema of `ToolArgs`. */
96
+ inputSchema: Record<string, unknown>;
97
+ }
98
+ /** The agent's tool arguments. */
99
+ export interface ToolArgs {
100
+ session?: string;
101
+ event?: string;
102
+ params?: Record<string, string>;
103
+ }
104
+ /** Sessions, bindings and instances by key, as `exportState` returns them and `importState` takes them. */
105
+ export interface State {
106
+ sessions: Record<string, unknown>;
107
+ bindings: Record<string, string>;
108
+ instances: Record<string, unknown>;
109
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ // Types of the typed smllm-wasm API, shared by the web and bundler entry
2
+ // points. They mirror what
3
+ // smllm-core serializes.
4
+ export {};
package/dist/wrap.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { Host, RecordKind, Reply, SessionStatus, State, Storage, StopDecision, Tool, ToolArgs } from "./types.js";
2
+ /** The host the wasm calls: strings in, strings out, every method there. */
3
+ export interface RawHost {
4
+ supports(kind: string): boolean;
5
+ check(kind: string, params: string, env: string, cwd: string): boolean;
6
+ run(kind: string, params: string, env: string, cwd: string): string;
7
+ read(file: string): string | undefined;
8
+ isMatch(pattern: string, value: string): boolean;
9
+ now(): number;
10
+ random(): number;
11
+ history(machine: string, id: string, entry: string): void;
12
+ put(kind: RecordKind, key: string, record: string): void;
13
+ }
14
+ /** A build's wasm-bindgen `Engine`: JSON text in and out. */
15
+ export interface RawEngine {
16
+ tool(): string;
17
+ callTool(args: string): string;
18
+ unsupported(): string;
19
+ bind(harness: string, hostSession: string | null | undefined, cwd: string): string;
20
+ view(key: string): string;
21
+ events(key: string): string;
22
+ status(key: string): string;
23
+ fire(key: string | null | undefined, event: string, params: string): string;
24
+ stop(key: string, stopHookActive: boolean): string;
25
+ promptSubmitted(key: string): void;
26
+ exportState(): string;
27
+ importState(state: string): void;
28
+ free(): void;
29
+ }
30
+ /** A build's wasm-bindgen `Engine` class. */
31
+ export type RawClass = new (compiled: string, host: RawHost) => RawEngine;
32
+ /** smllm's engine over compiled machines (`smllm compile` output). */
33
+ export declare class Engine {
34
+ #private;
35
+ /** The build's raw `Engine`: set by the entry module's subclass. */
36
+ protected static Raw: RawClass;
37
+ /**
38
+ * `storage`, when given, is loaded now and then receives every saved
39
+ * record and history entry (its `put` and `history` go before `host`'s).
40
+ */
41
+ constructor(compiled: string | object, host?: Host, storage?: Storage);
42
+ /** The agent-facing tool, to hand to an LLM's tool list (HOST-13). */
43
+ tool(): Tool;
44
+ /** Answer a tool call as the MCP server does; a malformed call is `ok: false`, never a throw. */
45
+ callTool(args?: ToolArgs): Reply;
46
+ /** Guard and action kinds the machines use that this host cannot run. */
47
+ unsupported(): string[];
48
+ /** Bind a harness session (reused when `hostSession` is already bound). */
49
+ bind(harness: string, hostSession?: string, cwd?: string): Reply;
50
+ /** Where the session is; changes nothing. */
51
+ view(key: string): Reply;
52
+ /** The events list of the session's state. */
53
+ events(key: string): Reply;
54
+ status(key: string): SessionStatus;
55
+ /** Fire an event; with no key, only `enter` (it starts a session). */
56
+ fire(key: string | undefined, event: string, params?: Record<string, string>): Reply;
57
+ stop(key: string, stopHookActive?: boolean): StopDecision;
58
+ promptSubmitted(key: string): void;
59
+ exportState(): State;
60
+ importState(state: State | string): void;
61
+ /** Free the wasm memory now, rather than when collected. */
62
+ free(): void;
63
+ [Symbol.dispose](): void;
64
+ }