@zenera/engine 0.0.0-stage → 1.1.39

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.
@@ -0,0 +1,118 @@
1
+ export interface SessionPaths {
2
+ id: string;
3
+ dir: string;
4
+ /** what the agent can see and write */
5
+ workspace: string;
6
+ data: string;
7
+ /** the live, resumable state — rewritten after every run */
8
+ state: string;
9
+ blobs: string;
10
+ runs: string;
11
+ lock: string;
12
+ meta: string;
13
+ }
14
+ export declare function sessionPaths(projectDir: string, id: string): SessionPaths;
15
+ /**
16
+ * The same layout, rooted wherever a host keeps its sessions and named however
17
+ * it names them: a server stores one per user and thread, not per project.
18
+ */
19
+ export declare function sessionPathsAt(dir: string): SessionPaths;
20
+ export interface SessionMeta {
21
+ version: 1;
22
+ id: string;
23
+ createdAt: string;
24
+ /**
25
+ * Absolute path the agent's file tools are rooted at. Recorded so resuming
26
+ * never re-asks and never silently moves — a session that quietly changed
27
+ * what "the workspace" meant between turns would be unexplainable.
28
+ */
29
+ workspace: string;
30
+ lastRunAt?: string;
31
+ title?: string;
32
+ }
33
+ export declare function createSession(projectDir: string, id: string, workspace: string): SessionPaths;
34
+ export declare function createSessionAt(dir: string, workspace: string): SessionPaths;
35
+ export declare function readSessionMeta(p: SessionPaths): Promise<SessionMeta>;
36
+ export declare function writeSessionMeta(p: SessionPaths, meta: SessionMeta): void;
37
+ export interface SessionSummary {
38
+ id: string;
39
+ createdAt?: string;
40
+ runs: number;
41
+ lastRunAt?: string;
42
+ busy: boolean;
43
+ title?: string;
44
+ }
45
+ /** Newest first — the order a picker wants. */
46
+ export declare function listSessions(projectDir: string): Promise<SessionSummary[]>;
47
+ export declare function newestSession(projectDir: string): string | undefined;
48
+ export declare function requireSession(projectDir: string, id: string): SessionPaths;
49
+ export interface Held {
50
+ release(): void;
51
+ }
52
+ export declare function acquire(p: SessionPaths): Held;
53
+ export interface RunPaths {
54
+ id: string;
55
+ dir: string;
56
+ input: string;
57
+ output: string;
58
+ state: string;
59
+ report: string;
60
+ /** the same run as one Mermaid flowchart, for a model to read */
61
+ graph: string;
62
+ meta: string;
63
+ }
64
+ export declare function runPaths(session: SessionPaths, id: string): RunPaths;
65
+ export declare function createRun(session: SessionPaths): RunPaths;
66
+ /**
67
+ * The same paths, reached from a directory instead of from a pair of ids.
68
+ *
69
+ * `zen run --json` hands back a run directory, and a caller holding one has no
70
+ * reason to take it apart again to ask a second question about the same run.
71
+ * The layout is fixed, so the ids can be read back out of the path — and both
72
+ * still go through `isStamp`, so a path from anywhere cannot name a directory
73
+ * this layout would never have produced.
74
+ *
75
+ * The session comes back too, not only the run: blobs are stored per session,
76
+ * so anything that resolves a payload needs both halves — and the project, so
77
+ * a command reached this way can still read the project's config.
78
+ */
79
+ export declare function runPathsAt(dir: string): {
80
+ project: string;
81
+ session: SessionPaths;
82
+ run: RunPaths;
83
+ };
84
+ export interface RunMeta {
85
+ version: 1;
86
+ id: string;
87
+ session: string;
88
+ startedAt: string;
89
+ finishedAt: string;
90
+ durationMs: number;
91
+ agent: string;
92
+ stopReason: string;
93
+ turns: number;
94
+ usage: unknown;
95
+ workspace: string;
96
+ memory?: string;
97
+ error?: string;
98
+ }
99
+ export declare function writeRunMeta(p: RunPaths, meta: RunMeta): void;
100
+ /**
101
+ * Partial, because a run killed before it recorded anything still has a
102
+ * directory, and every field here is worth having on its own.
103
+ */
104
+ export declare function readRunMeta(p: RunPaths): Promise<Partial<RunMeta>>;
105
+ export interface RunSummary {
106
+ id: string;
107
+ startedAt?: string;
108
+ finishedAt?: string;
109
+ agent?: string;
110
+ stopReason?: string;
111
+ error?: string;
112
+ }
113
+ /** Newest first — the order a picker wants. */
114
+ export declare function listRuns(session: SessionPaths): Promise<RunSummary[]>;
115
+ export declare function newestRun(session: SessionPaths): string | undefined;
116
+ /** A path to show a human: relative when that is shorter, absolute otherwise. */
117
+ export declare function display(path: string, from?: string): string;
118
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1,197 @@
1
+ import { claimLock, ownLock } from '@zenera/neo';
2
+ import { existsSync, mkdirSync, rmSync } from 'node:fs';
3
+ import { basename, dirname, join, relative, resolve } from 'node:path';
4
+ import { EngineError, EXIT, invalidError, usageError } from "./errors.js";
5
+ import { readJson, writeJson } from "./home.js";
6
+ import { isStamp, stamp, stampInstant } from "./ids.js";
7
+ import { isBusy, runIds, sessionIds, sessionsDir } from "./projects.js";
8
+ export function sessionPaths(projectDir, id) {
9
+ if (!isStamp(id)) {
10
+ throw usageError(`"${id}" is not a session id`, 'ids look like 20260825-143012-a7f3');
11
+ }
12
+ return sessionPathsAt(join(sessionsDir(projectDir), id));
13
+ }
14
+ /**
15
+ * The same layout, rooted wherever a host keeps its sessions and named however
16
+ * it names them: a server stores one per user and thread, not per project.
17
+ */
18
+ export function sessionPathsAt(dir) {
19
+ const data = join(dir, '.data');
20
+ return {
21
+ id: basename(dir),
22
+ dir,
23
+ workspace: join(dir, 'workspace'),
24
+ data,
25
+ state: join(data, 'state.json'),
26
+ blobs: join(data, 'blobs'),
27
+ runs: join(dir, 'runs'),
28
+ lock: join(dir, '.lock'),
29
+ meta: join(data, 'session.json'),
30
+ };
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Creating and finding
34
+ // ---------------------------------------------------------------------------
35
+ export function createSession(projectDir, id, workspace) {
36
+ return createSessionAt(sessionPaths(projectDir, id).dir, workspace);
37
+ }
38
+ export function createSessionAt(dir, workspace) {
39
+ const p = sessionPathsAt(dir);
40
+ mkdirSync(p.data, { recursive: true });
41
+ mkdirSync(p.runs, { recursive: true });
42
+ mkdirSync(resolve(workspace), { recursive: true });
43
+ const meta = {
44
+ version: 1,
45
+ id: p.id,
46
+ createdAt: new Date().toISOString(),
47
+ workspace: resolve(workspace),
48
+ };
49
+ writeJson(p.meta, meta, 0o644);
50
+ return p;
51
+ }
52
+ export async function readSessionMeta(p) {
53
+ const meta = await readJson(p.meta, {});
54
+ return {
55
+ version: 1,
56
+ id: p.id,
57
+ createdAt: meta.createdAt ?? new Date().toISOString(),
58
+ // Sessions from before the workspace was recorded fall back to their
59
+ // own directory, which is where it would have been.
60
+ workspace: meta.workspace ?? p.workspace,
61
+ lastRunAt: meta.lastRunAt,
62
+ title: meta.title,
63
+ };
64
+ }
65
+ export function writeSessionMeta(p, meta) {
66
+ writeJson(p.meta, meta, 0o644);
67
+ }
68
+ /** Newest first — the order a picker wants. */
69
+ export async function listSessions(projectDir) {
70
+ const out = [];
71
+ for (const id of sessionIds(projectDir).reverse()) {
72
+ const p = sessionPaths(projectDir, id);
73
+ const meta = await readSessionMeta(p);
74
+ const ids = runIds(p.dir);
75
+ out.push({
76
+ id,
77
+ createdAt: meta.createdAt,
78
+ runs: ids.length,
79
+ lastRunAt: meta.lastRunAt,
80
+ busy: isBusy(p.dir),
81
+ title: meta.title,
82
+ });
83
+ }
84
+ return out;
85
+ }
86
+ export function newestSession(projectDir) {
87
+ return sessionIds(projectDir).at(-1);
88
+ }
89
+ export function requireSession(projectDir, id) {
90
+ const p = sessionPaths(projectDir, id);
91
+ if (!existsSync(p.dir)) {
92
+ throw invalidError(`no session ${id}`, 'see: zen list --sessions');
93
+ }
94
+ return p;
95
+ }
96
+ export function acquire(p) {
97
+ claimLock(p.lock, ownLock(), (held) => new EngineError(`session ${p.id} is already running (pid ${held.pid})`, EXIT.failed, 'wait for it, or start another with --new'), 2);
98
+ let released = false;
99
+ const release = () => {
100
+ if (released) {
101
+ return;
102
+ }
103
+ released = true;
104
+ rmSync(p.lock, { force: true });
105
+ };
106
+ return { release };
107
+ }
108
+ export function runPaths(session, id) {
109
+ if (!isStamp(id)) {
110
+ throw usageError(`"${id}" is not a run id`);
111
+ }
112
+ const dir = join(session.runs, id);
113
+ return {
114
+ id,
115
+ dir,
116
+ input: join(dir, 'input.md'),
117
+ output: join(dir, 'output.md'),
118
+ state: join(dir, 'state.json'),
119
+ report: join(dir, 'report.html'),
120
+ graph: join(dir, 'graph.mmd'),
121
+ meta: join(dir, 'meta.json'),
122
+ };
123
+ }
124
+ export function createRun(session) {
125
+ const p = runPaths(session, stamp());
126
+ mkdirSync(p.dir, { recursive: true });
127
+ return p;
128
+ }
129
+ /**
130
+ * The same paths, reached from a directory instead of from a pair of ids.
131
+ *
132
+ * `zen run --json` hands back a run directory, and a caller holding one has no
133
+ * reason to take it apart again to ask a second question about the same run.
134
+ * The layout is fixed, so the ids can be read back out of the path — and both
135
+ * still go through `isStamp`, so a path from anywhere cannot name a directory
136
+ * this layout would never have produced.
137
+ *
138
+ * The session comes back too, not only the run: blobs are stored per session,
139
+ * so anything that resolves a payload needs both halves — and the project, so
140
+ * a command reached this way can still read the project's config.
141
+ */
142
+ export function runPathsAt(dir) {
143
+ const full = resolve(dir);
144
+ const runId = basename(full);
145
+ const sessionDir = dirname(dirname(full));
146
+ const wrong = () => usageError(`${display(full)} is not a run directory`, 'runs live at <project>/sessions/<session>/runs/<run>');
147
+ if (!isStamp(runId) || !isStamp(basename(sessionDir)) || basename(dirname(full)) !== 'runs') {
148
+ throw wrong();
149
+ }
150
+ const project = dirname(dirname(sessionDir));
151
+ const session = sessionPaths(project, basename(sessionDir));
152
+ const run = runPaths(session, runId);
153
+ if (resolve(run.dir) !== full) {
154
+ throw wrong();
155
+ }
156
+ if (!existsSync(run.state)) {
157
+ throw invalidError(`run ${runId} has no state.json`, 'only a run that got far enough to save state can be inspected');
158
+ }
159
+ return { project, session, run };
160
+ }
161
+ export function writeRunMeta(p, meta) {
162
+ writeJson(p.meta, meta, 0o644);
163
+ }
164
+ /**
165
+ * Partial, because a run killed before it recorded anything still has a
166
+ * directory, and every field here is worth having on its own.
167
+ */
168
+ export async function readRunMeta(p) {
169
+ return await readJson(p.meta, {});
170
+ }
171
+ /** Newest first — the order a picker wants. */
172
+ export async function listRuns(session) {
173
+ const out = [];
174
+ for (const id of runIds(session.dir).reverse()) {
175
+ // A run killed before it recorded anything still has an id, and the id
176
+ // says when it started, so it is listable without its meta.
177
+ const meta = await readRunMeta(runPaths(session, id));
178
+ out.push({
179
+ id,
180
+ startedAt: meta.startedAt ?? stampInstant(id),
181
+ finishedAt: meta.finishedAt,
182
+ agent: meta.agent,
183
+ stopReason: meta.stopReason,
184
+ error: meta.error,
185
+ });
186
+ }
187
+ return out;
188
+ }
189
+ export function newestRun(session) {
190
+ return runIds(session.dir).at(-1);
191
+ }
192
+ /** A path to show a human: relative when that is shorter, absolute otherwise. */
193
+ export function display(path, from = process.cwd()) {
194
+ const rel = relative(from, path);
195
+ return !rel.startsWith('..') && rel.length < path.length ? rel || '.' : path;
196
+ }
197
+ //# sourceMappingURL=session.js.map
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
- "name": "@zenera/engine",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@zenera/engine",
3
+ "version": "1.1.39",
4
+ "description": "Runs a @zenera/neo project on this machine: keyring, sandbox, sessions and the record each turn leaves behind.",
5
+ "keywords": [
6
+ "agents",
7
+ "ai-agents",
8
+ "agent-runtime",
9
+ "sandbox",
10
+ "llm"
11
+ ],
12
+ "license": "MIT",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "author": "Andrey Ryabov",
17
+ "homepage": "https://github.com/zenera-ai/ZeneraNeo#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/zenera-ai/ZeneraNeo.git",
21
+ "directory": "packages/engine"
22
+ },
23
+ "bugs": "https://github.com/zenera-ai/ZeneraNeo/issues",
24
+ "type": "module",
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "!dist/**/*.map"
37
+ ],
38
+ "engines": {
39
+ "node": ">=24"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -b",
43
+ "clean": "tsc -b --clean && rm -rf dist",
44
+ "prepack": "tsc -b"
45
+ },
46
+ "dependencies": {
47
+ "@zenera/neo": "^1.1.39"
48
+ }
49
+ }