agent-work-layer 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-10-10
4
+
5
+ Both packages: `agent-work-layer` 0.3.0 and `agent-work-layer-mcp` 0.3.0.
6
+
7
+ ### Added
8
+
9
+ - `SqliteStore`: a `DeskStore` backed by SQLite through Node's built-in `node:sqlite` (Node 22.13+), with an atomic CONFLICT check (#1).
10
+
11
+ ### Changed
12
+
13
+ - `agent-work-layer-mcp` now depends on `agent-work-layer` ^0.3.0.
14
+
15
+ ## 0.2.1 — 2026-10-09
16
+
17
+ Documentation only; `agent-work-layer` alone (`agent-work-layer-mcp` stays at 0.2.0).
18
+
19
+ - README: diagram of a handoff moving a job from one worker to the next.
20
+
3
21
  ## 0.2.0 — 2026-10-09
4
22
 
5
23
  ### Breaking
package/README.md CHANGED
@@ -21,7 +21,7 @@ Agent Cards say who an agent is. A2A carries messages between agents. MCP connec
21
21
 
22
22
  <p align="center"><img src="docs/fields.svg" alt="Each Work Report field answers a Work Order field: evidence proves criteria, cost stays within budget, tools used stay within allowed tools" width="760"></p>
23
23
 
24
- More diagrams: [message sequence](docs/sequence.svg) · [lifecycle](docs/lifecycle.svg) · [delegation](docs/delegation.svg)
24
+ More diagrams: [message sequence](docs/sequence.svg) · [lifecycle](docs/lifecycle.svg) · [delegation](docs/delegation.svg) · [handoff](docs/handoff.svg)
25
25
 
26
26
  ## Install
27
27
 
@@ -40,7 +40,7 @@ Node.js 20.11 or later. ESM only. Types included.
40
40
  ```ts
41
41
  import { WorkDesk } from "agent-work-layer";
42
42
 
43
- const desk = new WorkDesk(); // in-memory; use FileStore or your own DeskStore to persist
43
+ const desk = new WorkDesk(); // in-memory; use FileStore, SqliteStore or your own DeskStore to persist
44
44
 
45
45
  // Manager issues a job
46
46
  const { order } = await desk.create(
@@ -92,6 +92,8 @@ The desk enforces the spec as you go:
92
92
 
93
93
  When a worker fails, gets blocked or is cancelled, it can say what the next worker should know. The manager passes that along in the next order:
94
94
 
95
+ <p align="center"><img src="docs/handoff.svg" alt="Worker A reports failed with a handoff; the manager reissues the job to Worker B with the same handoff: summary, decisions, what was tried, open questions and next step" width="760"></p>
96
+
95
97
  ```ts
96
98
  await desk.submitReport(order.id, {
97
99
  status: "failed",
@@ -192,7 +194,7 @@ Any active state ──cancel──▶ cancelled · deadline or budget excee
192
194
  | Export | Purpose |
193
195
  |---|---|
194
196
  | `WorkDesk` | Runs the lifecycle: `create`, `accept`, `decline`, `start`, `submitReport`, `answer`, `review`, `cancel`, `get`, `list`, `waitForChange` |
195
- | `MemoryStore`, `FileStore`, `DeskStore` | Storage. Implement `DeskStore` (4 methods) to use a database |
197
+ | `MemoryStore`, `FileStore`, `SqliteStore`, `DeskStore` | Storage. Implement `DeskStore` (4 methods) to use a database |
196
198
  | `validateOrder`, `validateReport`, `validateChildOrder`, `hasInvalid` | Spec checks, returning `Finding[]` |
197
199
  | `isWorkOrder`, `isWorkReport` | Type guards |
198
200
  | `toolAllowed`, `patternCovered` | Tool pattern matching |
@@ -209,6 +211,16 @@ Any active state ──cancel──▶ cancelled · deadline or budget excee
209
211
 
210
212
  ### Custom storage
211
213
 
214
+ `MemoryStore` keeps records in memory, `FileStore` writes one JSON file per order, and `SqliteStore` keeps them in a SQLite database using Node's built-in `node:sqlite` (Node 22.13+, no extra dependency):
215
+
216
+ ```ts
217
+ import { WorkDesk, SqliteStore } from "agent-work-layer";
218
+
219
+ const desk = new WorkDesk({ store: new SqliteStore("./awl.db") }); // or ":memory:"
220
+ ```
221
+
222
+ To use another database, implement `DeskStore`:
223
+
212
224
  ```ts
213
225
  import type { DeskStore, OrderRecord } from "agent-work-layer";
214
226
 
package/dist/generated.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Generated by scripts/generate.ts from package.json and schemas/*.json. Do not edit.
2
2
  /** This package's version. */
3
- export const VERSION = "0.2.0";
3
+ export const VERSION = "0.3.0";
4
4
  /** The Work Order JSON Schema (schemas/work-order.schema.json). */
5
5
  export const workOrderSchema = {
6
6
  "$schema": "https://json-schema.org/draft/2020-12/schema",
package/dist/index.d.ts CHANGED
@@ -11,6 +11,6 @@ export { STATES, TERMINAL_STATES, ROLE_FOR, isTerminal, nextState, allowedAction
11
11
  export type { Role } from "./lifecycle.ts";
12
12
  export { WorkDesk, summarize, lastSeq } from "./desk.ts";
13
13
  export type { DeskOptions, ListFilter, WaitOptions, SubmitResult } from "./desk.ts";
14
- export { MemoryStore, FileStore } from "./store.ts";
14
+ export { MemoryStore, FileStore, SqliteStore } from "./store.ts";
15
15
  export type { DeskStore } from "./store.ts";
16
16
  export { VERSION } from "./version.ts";
package/dist/index.js CHANGED
@@ -8,5 +8,5 @@ export { AwlError, isAwlError } from "./errors.js";
8
8
  export { validateOrder, validateReport, validateChildOrder, hasInvalid, isWorkOrder, isWorkReport, toolAllowed, patternCovered, workOrderSchema, workReportSchema, } from "./validate.js";
9
9
  export { STATES, TERMINAL_STATES, ROLE_FOR, isTerminal, nextState, allowedActions } from "./lifecycle.js";
10
10
  export { WorkDesk, summarize, lastSeq } from "./desk.js";
11
- export { MemoryStore, FileStore } from "./store.js";
11
+ export { MemoryStore, FileStore, SqliteStore } from "./store.js";
12
12
  export { VERSION } from "./version.js";
package/dist/store.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { OrderRecord } from "./types.ts";
2
+ import type { DatabaseSync } from "node:sqlite";
2
3
  export interface DeskStore {
3
4
  /** Returns the record, or null when there is none. */
4
5
  read(id: string): Promise<OrderRecord | null>;
@@ -33,3 +34,18 @@ export declare class FileStore implements DeskStore {
33
34
  update(record: OrderRecord, expectedSeq: number): Promise<void>;
34
35
  list(): Promise<OrderRecord[]>;
35
36
  }
37
+ /**
38
+ * One SQLite database, using Node's built-in `node:sqlite` (Node 22.13+). Pass a file path,
39
+ * or ":memory:" for a throwaway database. Several processes may share a file; the CONFLICT
40
+ * check is atomic (`UPDATE ... WHERE last_seq = ?`).
41
+ */
42
+ export declare class SqliteStore implements DeskStore {
43
+ readonly db: DatabaseSync;
44
+ constructor(path?: string);
45
+ read(id: string): Promise<OrderRecord | null>;
46
+ insert(record: OrderRecord): Promise<void>;
47
+ update(record: OrderRecord, expectedSeq: number): Promise<void>;
48
+ list(): Promise<OrderRecord[]>;
49
+ /** Closes the database. */
50
+ close(): void;
51
+ }
package/dist/store.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Storage for desks. Implement DeskStore to back a desk with a database.
2
2
  import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
3
4
  import { join } from "node:path";
4
5
  import { AwlError } from "./errors.js";
5
6
  const lastSeq = (r) => r.events.at(-1)?.seq ?? 0;
@@ -82,3 +83,53 @@ export class FileStore {
82
83
  return records.filter((r) => r !== null);
83
84
  }
84
85
  }
86
+ /**
87
+ * One SQLite database, using Node's built-in `node:sqlite` (Node 22.13+). Pass a file path,
88
+ * or ":memory:" for a throwaway database. Several processes may share a file; the CONFLICT
89
+ * check is atomic (`UPDATE ... WHERE last_seq = ?`).
90
+ */
91
+ export class SqliteStore {
92
+ db;
93
+ constructor(path = ":memory:") {
94
+ // Loaded here so importing the package does not need node:sqlite.
95
+ const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
96
+ this.db = new DatabaseSync(path);
97
+ this.db.exec(`
98
+ PRAGMA busy_timeout = 5000;
99
+ CREATE TABLE IF NOT EXISTS orders (
100
+ id TEXT PRIMARY KEY,
101
+ last_seq INTEGER NOT NULL,
102
+ record TEXT NOT NULL
103
+ ) STRICT;
104
+ `);
105
+ }
106
+ async read(id) {
107
+ const row = this.db.prepare("SELECT record FROM orders WHERE id = ?").get(id);
108
+ return row ? JSON.parse(row.record) : null;
109
+ }
110
+ async insert(record) {
111
+ const { changes } = this.db
112
+ .prepare("INSERT INTO orders (id, last_seq, record) VALUES (?, ?, ?) ON CONFLICT (id) DO NOTHING")
113
+ .run(record.order.id, lastSeq(record), JSON.stringify(record));
114
+ if (!changes)
115
+ throw new AwlError("ALREADY_EXISTS", `order "${record.order.id}" already exists`);
116
+ }
117
+ async update(record, expectedSeq) {
118
+ const { changes } = this.db
119
+ .prepare("UPDATE orders SET last_seq = ?, record = ? WHERE id = ? AND last_seq = ?")
120
+ .run(lastSeq(record), JSON.stringify(record), record.order.id, expectedSeq);
121
+ if (changes)
122
+ return;
123
+ if (!(await this.read(record.order.id)))
124
+ throw new AwlError("NOT_FOUND", `no order "${record.order.id}"`);
125
+ throw new AwlError("CONFLICT", `order "${record.order.id}" changed since it was read`);
126
+ }
127
+ async list() {
128
+ const rows = this.db.prepare("SELECT record FROM orders ORDER BY id").all();
129
+ return rows.map((r) => JSON.parse(r.record));
130
+ }
131
+ /** Closes the database. */
132
+ close() {
133
+ this.db.close();
134
+ }
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-work-layer",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Work Orders and Work Reports for AI agents: a typed SDK, validator and lifecycle desk. The MCP server is in agent-work-layer-mcp.",
5
5
  "keywords": [
6
6
  "ai",