@zenera/engine 1.1.54 → 1.1.56

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/engine.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { AgentRunner, Workspace, type AgentEvent, type AgentProject, type AgentState, type Input, type MemoryIndex, type RunIdentity, type RunResult } from '@zenera/neo';
1
+ import { AgentRunner, Workspace, type AgentEvent, type AgentProject, type AgentState, type Input, type ResumeAnswer, type MemoryIndex, type RunIdentity, type RunResult, type SandboxFactory } from '@zenera/neo';
2
2
  import { type ModelIssue } from './audit.ts';
3
3
  import { type Logger } from './errors.ts';
4
- import { type Project } from './projects.ts';
5
4
  import { type ZenPlugin } from './plugins.ts';
5
+ import { type Project } from './projects.ts';
6
6
  import { type Preflight, type SandboxSetup } from './sandbox.ts';
7
7
  import { type Held, type RunPaths, type SessionPaths } from './session.ts';
8
8
  /** Who a run acts for — re-exported so a host needs only this package. */
@@ -27,10 +27,9 @@ export interface EngineOptions {
27
27
  /** recall only: nothing is written and several runs may share one graph */
28
28
  memoryReadOnly?: boolean;
29
29
  /**
30
- * An already-open graph to remember into instead of opening `memoryDir`. A
31
- * memory directory is locked by whoever opens it, so a host running several
32
- * sessions over one graph opens it once and hands the same index to each.
33
- * Name its directory in `memoryDir` too: that is what is mounted at `/memory`.
30
+ * An already-open graph to remember into instead of opening `memoryDir`.
31
+ * Sessions may each open the same directory; handing them one index saves
32
+ * every session loading the graph again.
34
33
  */
35
34
  memory?: MemoryIndex;
36
35
  /** whether credentials reach the sandbox — `--no-keys` sets this false */
@@ -54,6 +53,13 @@ export interface EngineOptions {
54
53
  onIssue?: (issue: ModelIssue) => void;
55
54
  /** readies the container engine when the project can shell out; nothing when unset */
56
55
  preflight?: Preflight;
56
+ /**
57
+ * Opens the session's sandbox — where its commands run and its files
58
+ * live. Podman on this machine when unset, which is what `zen run` is; a
59
+ * host with a sandbox service passes its own, and the file tools then read
60
+ * the files where that sandbox keeps them.
61
+ */
62
+ newSandbox?: SandboxFactory;
57
63
  /** draws each finished run as `graph.mmd`; no graph when unset */
58
64
  graph?: GraphRenderer;
59
65
  /**
@@ -148,4 +154,11 @@ export declare function envelope(engine: Engine, outcome: RunOutcome): Record<st
148
154
  * command.
149
155
  */
150
156
  export declare function runEngine(engine: Engine, input: Input, onEvent?: (event: AgentEvent) => void, signal?: AbortSignal): Promise<RunOutcome>;
157
+ /**
158
+ * Continues a session whose last run stopped `suspended`: `answers` answer its
159
+ * suspended calls (re-run, a result, or cancelled - see `ResumeAnswer`). The
160
+ * run is recorded like any other turn; its input reads as the answers given.
161
+ * Calls left unanswered keep it suspended.
162
+ */
163
+ export declare function resumeEngine(engine: Engine, answers: Record<string, ResumeAnswer>, onEvent?: (event: AgentEvent) => void, signal?: AbortSignal): Promise<RunOutcome>;
151
164
  //# sourceMappingURL=engine.d.ts.map
package/dist/engine.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AgentRunner, FilePayloadStore, SANDBOX_MOUNT, SKILLS_MOUNT, Workspace, assertState, buildRunReport, compactIdentity, exaTools, fileTools, identityFromEnv, identityTools, lastText, loadProject, memoryDir, readProjectConfig, renderReportHtml, sandboxTools, turns, } from '@zenera/neo';
1
+ import { AgentRunner, FilePayloadStore, MemoryError, SKILLS_MOUNT, Workspace, assertState, buildRunReport, compactIdentity, exaTools, fileTools, identityFromEnv, identityTools, lastText, loadProject, memoryDir, readProjectConfig, renderReportHtml, sandboxTools, turns, } from '@zenera/neo';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { writeFile } from 'node:fs/promises';
4
4
  import { resolve } from 'node:path';
@@ -8,8 +8,8 @@ import { loadProjectEnv } from "./env.js";
8
8
  import { EXIT, EngineError, invalidError, stderrLogger, usageError, } from "./errors.js";
9
9
  import { readJson, writeJson } from "./home.js";
10
10
  import { KeyStore, assertUsable } from "./keys.js";
11
- import { projectMounts } from "./projects.js";
12
11
  import { envPlugins, loadPlugins, mergePlugins, } from "./plugins.js";
12
+ import { projectMounts } from "./projects.js";
13
13
  import { buildSandbox, prepareHome, teardown, usesSandbox, } from "./sandbox.js";
14
14
  import { acquire, createRun, readSessionMeta, writeRunMeta, writeSessionMeta, } from "./session.js";
15
15
  /**
@@ -52,7 +52,7 @@ export async function openEngine(opts) {
52
52
  // resolved against them. They are always registered, whether or not anyone
53
53
  // selects them: a project that names `sandbox:*` against an empty tool list
54
54
  // fails to load with "unknown tool", which would be a lie.
55
- let sandbox;
55
+ let setup;
56
56
  let project;
57
57
  let files;
58
58
  let memory;
@@ -66,8 +66,8 @@ export async function openEngine(opts) {
66
66
  }
67
67
  memory = memoryDir(root, config, opts.memoryDir);
68
68
  // Assets and the skill catalog are mounted for both, under one name.
69
- const mounts = projectMounts(root, config, opts.memoryDir);
70
- sandbox = buildSandbox({
69
+ const mounts = projectMounts(root, config);
70
+ setup = buildSandbox({
71
71
  config,
72
72
  root,
73
73
  session: opts.session,
@@ -78,14 +78,15 @@ export async function openEngine(opts) {
78
78
  env: env?.names,
79
79
  mounts,
80
80
  identity: user,
81
+ newSandbox: opts.newSandbox,
81
82
  ...(opts.disposable ? { persist: false } : {}),
82
83
  });
83
- const workspaceOptions = {
84
+ const workspaceOptions = setup.sandbox.files({
84
85
  root: workspace,
85
86
  readOnly: opts.readOnly,
86
- mount: sandbox.spec.workdir ?? SANDBOX_MOUNT,
87
+ mount: setup.workdir,
87
88
  mounts,
88
- };
89
+ });
89
90
  // The same view the file tools have, so a path the agent just wrote
90
91
  // resolves the same way when it asks memory to keep a copy.
91
92
  files = new Workspace(workspaceOptions);
@@ -94,7 +95,7 @@ export async function openEngine(opts) {
94
95
  // one name for it: whatever `run_command` prints a path as,
95
96
  // `read_file` accepts.
96
97
  ...fileTools(workspaceOptions),
97
- ...sandboxTools(sandbox.pool),
98
+ ...sandboxTools(setup.sandbox),
98
99
  // Registered whether or not a key exists: the credential is
99
100
  // read when a tool is called, so a project that names `exa:*`
100
101
  // loads on a machine that cannot yet search, and says so on the
@@ -113,12 +114,12 @@ export async function openEngine(opts) {
113
114
  memoryDir: opts.memoryDir,
114
115
  memoryReadOnly: opts.memoryReadOnly,
115
116
  payloads,
116
- resolveFile: (path) => files.within(path),
117
+ files,
117
118
  });
118
119
  }
119
120
  catch (err) {
120
121
  await plugins?.close();
121
- throw invalidError(err instanceof Error ? err.message : String(err), `while loading ${opts.project.dir}`);
122
+ throw invalidError(err instanceof Error ? err.message : String(err), err instanceof MemoryError && err.hint ? err.hint : `while loading ${opts.project.dir}`);
122
123
  }
123
124
  const loaded = plugins;
124
125
  let state;
@@ -129,8 +130,8 @@ export async function openEngine(opts) {
129
130
  // container engine costs an error instead of a round trip. Nothing is
130
131
  // started: the container itself waits until something actually runs.
131
132
  if (usesSandbox(project)) {
132
- prepareHome(sandbox);
133
- await opts.preflight?.(sandbox, opts.yes);
133
+ prepareHome(setup);
134
+ await opts.preflight?.(setup, opts.yes);
134
135
  }
135
136
  // `recordRequests` is what makes the report show the exact bytes sent
136
137
  // rather than a reconstruction. It costs state size, and a CLI run is
@@ -155,18 +156,18 @@ export async function openEngine(opts) {
155
156
  runner,
156
157
  name: opts.project.name,
157
158
  workspace,
158
- workspaceMount: sandbox.spec.workdir ?? SANDBOX_MOUNT,
159
+ workspaceMount: setup.workdir,
159
160
  files,
160
161
  readOnly: Boolean(opts.readOnly),
161
162
  session: opts.session,
162
163
  memory,
163
164
  state,
164
- sandbox,
165
+ sandbox: setup,
165
166
  lock,
166
167
  graph: opts.graph,
167
168
  user,
168
169
  close: async () => {
169
- await teardown(sandbox.pool, logger);
170
+ await teardown(setup, logger);
170
171
  // A host-supplied graph is the host's to release.
171
172
  if (!opts.memory) {
172
173
  project.close();
@@ -275,6 +276,35 @@ export async function runEngine(engine, input, onEvent, signal) {
275
276
  }
276
277
  return outcome;
277
278
  }
279
+ /**
280
+ * Continues a session whose last run stopped `suspended`: `answers` answer its
281
+ * suspended calls (re-run, a result, or cancelled - see `ResumeAnswer`). The
282
+ * run is recorded like any other turn; its input reads as the answers given.
283
+ * Calls left unanswered keep it suspended.
284
+ */
285
+ export async function resumeEngine(engine, answers, onEvent, signal) {
286
+ if (!engine.state?.suspended?.length) {
287
+ throw usageError('this session is not waiting on anything to resume');
288
+ }
289
+ const startedAt = new Date();
290
+ const stream = engine.runner.resume(engine.state, { answers, signal });
291
+ if (onEvent) {
292
+ for await (const event of stream) {
293
+ onEvent(event);
294
+ }
295
+ }
296
+ const result = await stream.final();
297
+ const durationMs = Date.now() - startedAt.getTime();
298
+ engine.state = result.state;
299
+ const input = `(resumed: ${Object.entries(answers)
300
+ .map(([id, a]) => `${id} ${a.status}`)
301
+ .join(', ')})`;
302
+ const outcome = await record(engine, input, result, startedAt, durationMs);
303
+ if (result.stopReason === 'failed') {
304
+ throw new EngineError(result.state.error ?? 'the run failed', EXIT.failed, `report: ${outcome.run.report}`);
305
+ }
306
+ return outcome;
307
+ }
278
308
  /**
279
309
  * Everything a finished turn leaves behind. The session state is written first:
280
310
  * if the report fails to render, the conversation is still resumable, which is
@@ -1,4 +1,4 @@
1
- import { type ProjectConfig, type SandboxMount } from '@zenera/neo';
1
+ import { type ProjectConfig, type Mount } from '@zenera/neo';
2
2
  /** Whether a directory is a project: whether the loader has something to read. */
3
3
  export declare function isProjectDir(dir: string): boolean;
4
4
  /** A project resolved on disk. */
@@ -16,7 +16,7 @@ export interface Project {
16
16
  * and does not edit — and the paths are resolved, because the podman machine on
17
17
  * macOS shares the real path or nothing.
18
18
  */
19
- export declare function projectMounts(root: string, config: ProjectConfig, memoryAt?: string): SandboxMount[];
19
+ export declare function projectMounts(root: string, config: ProjectConfig): Mount[];
20
20
  /** Walks up from `start` looking for a project configuration. */
21
21
  export declare function findUp(start: string): Promise<Project | undefined>;
22
22
  export declare function openDir(dir: string): Promise<Project>;
package/dist/projects.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ASSETS_MOUNT, assetsDir, FILES_DIR, MEMORY_MOUNT, memoryDir, pidAlive, skillDirs, skillMounts, SKILLS_MOUNT, } from '@zenera/neo';
2
- import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
1
+ import { ASSETS_MOUNT, assetsDir, pidAlive, skillDirs, skillMounts, SKILLS_MOUNT, } from '@zenera/neo';
2
+ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
3
3
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
4
4
  import { invalidError, usageError } from "./errors.js";
5
5
  import { paths, readJson, writeJson } from "./home.js";
@@ -35,25 +35,27 @@ export function isProjectDir(dir) {
35
35
  * and does not edit — and the paths are resolved, because the podman machine on
36
36
  * macOS shares the real path or nothing.
37
37
  */
38
- export function projectMounts(root, config, memoryAt) {
38
+ export function projectMounts(root, config) {
39
39
  const mounts = [];
40
40
  const assets = assetsDir(root, config);
41
41
  if (assets) {
42
- mounts.push({ host: realpathSync(assets), at: ASSETS_MOUNT, readOnly: true });
42
+ mounts.push({
43
+ host: realpathSync(assets),
44
+ at: ASSETS_MOUNT,
45
+ readOnly: true,
46
+ source: { kind: 'assets' },
47
+ });
43
48
  }
44
49
  // The catalog goes in whole, at fixed names, before anything is loaded: a
45
50
  // container's mounts are decided when it is created, so a skill folder
46
51
  // cannot be added at the moment `skill_load` asks for it.
47
52
  for (const dir of skillMounts(skillDirs(root, config), SKILLS_MOUNT)) {
48
- mounts.push({ host: realpathSync(dir.path), at: dir.at, readOnly: true });
49
- }
50
- const memory = memoryDir(root, config, memoryAt);
51
- if (memory) {
52
- // Created here because the mount is decided before the project is
53
- // loaded, and podman would refuse a source that does not exist yet.
54
- const files = join(memory, FILES_DIR);
55
- mkdirSync(files, { recursive: true });
56
- mounts.push({ host: realpathSync(files), at: MEMORY_MOUNT, readOnly: true });
53
+ mounts.push({
54
+ host: realpathSync(dir.path),
55
+ at: dir.at,
56
+ readOnly: true,
57
+ source: { kind: 'skills' },
58
+ });
57
59
  }
58
60
  return mounts;
59
61
  }
package/dist/sandbox.d.ts CHANGED
@@ -1,18 +1,21 @@
1
- import { SandboxPool, type AgentProject, type ProjectConfig, type RunIdentity, type SandboxMount, type SandboxSpec } from '@zenera/neo';
1
+ import { type AgentProject, type ProjectConfig, type RunIdentity, type Sandbox, type SandboxFactory, type Mount, type SandboxSpec } from '@zenera/neo';
2
2
  import { type Logger } from './errors.ts';
3
3
  import { type ResolvedBuild } from './image.ts';
4
4
  import type { SessionPaths } from './session.ts';
5
5
  export interface SandboxSetup {
6
- pool: SandboxPool;
6
+ /** where the session's commands run, and the files they see; podman for `zen run` */
7
+ sandbox: Sandbox;
7
8
  /** the resolved base spec, for `zn sandbox status` and the image pre-warm */
8
9
  spec: SandboxSpec;
10
+ /** where the workspace is in the sandbox, and so the name the file tools answer with */
11
+ workdir: string;
9
12
  image: string;
10
13
  /** the Dockerfile behind `image`, when it came from one */
11
14
  build?: ResolvedBuild;
12
15
  /** host side of the container's `$HOME`, created only if it is ever needed */
13
16
  home: string;
14
17
  /** every tree bound into the container besides the workspace itself */
15
- mounts: readonly SandboxMount[];
18
+ mounts: readonly Mount[];
16
19
  }
17
20
  export interface SandboxInputs {
18
21
  config: ProjectConfig;
@@ -26,7 +29,7 @@ export interface SandboxInputs {
26
29
  * and its skill catalog. The same array goes to the file tools, so a path
27
30
  * a command prints is a path `read_file` accepts.
28
31
  */
29
- mounts?: readonly SandboxMount[];
32
+ mounts?: readonly Mount[];
30
33
  /** `--image` */
31
34
  image?: string;
32
35
  /** `--no-keys`: refuse to forward credentials, whatever the config says */
@@ -40,13 +43,15 @@ export interface SandboxInputs {
40
43
  * promise it does not keep.
41
44
  */
42
45
  env?: readonly string[];
43
- /** `false` overrides every `persist:` in the config, the agents' included */
46
+ /** `false` overrides the config's `persist:` */
44
47
  persist?: boolean;
45
48
  /**
46
49
  * Who the run is acting for. Set on every command as `ZEN_USER_*`, never
47
50
  * on the container — see `execEnv` in the library for why.
48
51
  */
49
52
  identity?: RunIdentity;
53
+ /** opens the sandbox; podman on this machine when unset */
54
+ newSandbox?: SandboxFactory;
50
55
  }
51
56
  export declare function buildSandbox(opts: SandboxInputs): SandboxSetup;
52
57
  /**
@@ -67,12 +72,17 @@ export type Preflight = (setup: SandboxSetup, yes?: boolean) => Promise<void>;
67
72
  /**
68
73
  * Where the container's `$HOME` lives must exist before podman mounts it — and
69
74
  * the `zenera` libraries go into it, written fresh on every open so they match
70
- * the engine that set the variables they read.
75
+ * the engine that set the variables they read. Done for every sandbox: it is a
76
+ * small copy into the session folder, and a sandbox that does not bind it
77
+ * supplies its own `$HOME` (`source: home`) with the same libraries.
71
78
  */
72
79
  export declare function prepareHome(setup: SandboxSetup): void;
73
80
  /** Where each one lands, inside the container. */
74
81
  export declare const RUNTIME_PYTHON = "/home/agent/.zenera/python";
75
82
  export declare const RUNTIME_NODE = "/home/agent/.node_modules/zenera";
76
- /** Best-effort teardown: losing a container must never lose a run. */
77
- export declare function teardown(pool: SandboxPool, logger?: Logger): Promise<void>;
83
+ /**
84
+ * Best-effort teardown: losing a container must never lose a run. Closing is
85
+ * also where a sandbox that keeps its files elsewhere persists them.
86
+ */
87
+ export declare function teardown(setup: SandboxSetup, logger?: Logger): Promise<void>;
78
88
  //# sourceMappingURL=sandbox.d.ts.map
package/dist/sandbox.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_SANDBOX_IMAGE, GATEWAY_URL_ENV, SANDBOX_GROUP, SandboxPool, identityEnv, isIdentityEnv, } from '@zenera/neo';
1
+ import { DEFAULT_SANDBOX_IMAGE, GATEWAY_URL_ENV, SANDBOX_GROUP, WORKSPACE_MOUNT, identityEnv, podmanSandbox, isIdentityEnv, } from '@zenera/neo';
2
2
  import { cpSync, mkdirSync, rmSync } from 'node:fs';
3
3
  import { basename, join, relative } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
@@ -48,7 +48,12 @@ function forwarded() {
48
48
  for (const cred of credentials()) {
49
49
  if (cred.holds === 'file') {
50
50
  const at = `${KEY_MOUNT}/${basename(cred.value)}`;
51
- mounts.push({ host: cred.value, at, readOnly: true });
51
+ mounts.push({
52
+ host: cred.value,
53
+ at,
54
+ readOnly: true,
55
+ source: { kind: 'credential', name: cred.env },
56
+ });
52
57
  env[cred.env] = at;
53
58
  continue;
54
59
  }
@@ -75,7 +80,10 @@ export function buildSandbox(opts) {
75
80
  ...(opts.persist === false ? { persist: false } : {}),
76
81
  };
77
82
  const home = join(opts.session.data, 'sandbox', 'home');
78
- const mounts = [{ host: home, at: HOME }, ...(opts.mounts ?? [])];
83
+ const mounts = [
84
+ { host: home, at: HOME, source: { kind: 'home' } },
85
+ ...(opts.mounts ?? []),
86
+ ];
79
87
  const keys = base.keys === false ? undefined : forwarded();
80
88
  if (keys) {
81
89
  mounts.push(...keys.mounts);
@@ -86,24 +94,8 @@ export function buildSandbox(opts) {
86
94
  // `ZENERA_AGENT` tells a `zen` run by `run_command` that no person is there.
87
95
  const extra = { HOME, PYTHONDONTWRITEBYTECODE: '1', ZENERA_AGENT: '1', ...(keys?.env ?? {}) };
88
96
  const spec = toSpec(base, extra, secretsOf(keys?.secrets, opts.env));
89
- const agents = {};
90
- for (const agent of opts.config.agents) {
91
- if (agent.sandbox) {
92
- const merged = merge(base, agent.sandbox);
93
- if (opts.persist === false) {
94
- merged.persist = false;
95
- }
96
- // An agent that opts out gets neither the variables nor the mount,
97
- // but the mount is on the pool and cannot be taken back per agent —
98
- // so the variable is what actually decides, and without it the file
99
- // sitting there is not a credential anything will look for.
100
- const own = merged.keys === false ? undefined : keys;
101
- agents[agent.name] = toSpec(merged, { HOME, PYTHONDONTWRITEBYTECODE: '1', ZENERA_AGENT: '1', ...(own?.env ?? {}) }, own && secretsOf(own.secrets, opts.env));
102
- }
103
- }
104
- const pool = new SandboxPool({
97
+ const sandbox = (opts.newSandbox ?? podmanSandbox)({
105
98
  ...spec,
106
- agents,
107
99
  root: opts.workspace,
108
100
  key: opts.session.id,
109
101
  readOnly: opts.readOnly,
@@ -111,23 +103,15 @@ export function buildSandbox(opts) {
111
103
  execEnv: identityEnv(opts.identity),
112
104
  execPaths: RUNTIME_PATHS,
113
105
  });
114
- return { pool, spec, image: spec.image ?? DEFAULT_SANDBOX_IMAGE, build, home, mounts };
115
- }
116
- /**
117
- * An agent's overrides on the project's block. `image` and `build` answer the
118
- * same question, so naming either one drops the other: a plain spread would
119
- * leave an agent's `image` sitting next to the project's `build`, and the
120
- * schema forbids exactly that combination when it is written down.
121
- */
122
- function merge(base, agent) {
123
- const merged = { ...base, ...agent };
124
- if (agent.image !== undefined) {
125
- delete merged.build;
126
- }
127
- if (agent.build !== undefined) {
128
- delete merged.image;
129
- }
130
- return merged;
106
+ return {
107
+ sandbox,
108
+ spec,
109
+ workdir: spec.workdir ?? WORKSPACE_MOUNT,
110
+ image: spec.image ?? DEFAULT_SANDBOX_IMAGE,
111
+ build,
112
+ home,
113
+ mounts,
114
+ };
131
115
  }
132
116
  /**
133
117
  * The two sources of forwarded names, as one list: the credentials the keyring
@@ -189,7 +173,9 @@ export function usesSandbox(project) {
189
173
  /**
190
174
  * Where the container's `$HOME` lives must exist before podman mounts it — and
191
175
  * the `zenera` libraries go into it, written fresh on every open so they match
192
- * the engine that set the variables they read.
176
+ * the engine that set the variables they read. Done for every sandbox: it is a
177
+ * small copy into the session folder, and a sandbox that does not bind it
178
+ * supplies its own `$HOME` (`source: home`) with the same libraries.
193
179
  */
194
180
  export function prepareHome(setup) {
195
181
  mkdirSync(setup.home, { recursive: true });
@@ -226,10 +212,13 @@ function installRuntime(home) {
226
212
  cpSync(join(RUNTIME_SOURCE, from), target, { recursive: true });
227
213
  }
228
214
  }
229
- /** Best-effort teardown: losing a container must never lose a run. */
230
- export async function teardown(pool, logger = stderrLogger) {
215
+ /**
216
+ * Best-effort teardown: losing a container must never lose a run. Closing is
217
+ * also where a sandbox that keeps its files elsewhere persists them.
218
+ */
219
+ export async function teardown(setup, logger = stderrLogger) {
231
220
  try {
232
- await pool.dispose();
221
+ await setup.sandbox.close();
233
222
  }
234
223
  catch (err) {
235
224
  logger.warn(`could not clean up the sandbox: ${err instanceof Error ? err.message : String(err)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/engine",
3
- "version": "1.1.54",
3
+ "version": "1.1.56",
4
4
  "description": "Runs a @zenera/neo project on this machine: keyring, sandbox, sessions and the record each turn leaves behind.",
5
5
  "keywords": [
6
6
  "agents",
@@ -45,6 +45,6 @@
45
45
  "prepack": "tsc -b"
46
46
  },
47
47
  "dependencies": {
48
- "@zenera/neo": "^1.1.54"
48
+ "@zenera/neo": "^1.1.56"
49
49
  }
50
50
  }