smithers-orchestrator 0.22.0 → 0.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smithers-orchestrator",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Public Smithers facade for durable coding-agent workflows and Gateway operation",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -61,27 +61,33 @@
61
61
  "incur": "^0.4.1",
62
62
  "react": "^19.2.5",
63
63
  "zod": "^4.3.6",
64
- "@smithers-orchestrator/agents": "0.22.0",
65
- "@smithers-orchestrator/cli": "0.22.0",
66
- "@smithers-orchestrator/components": "0.22.0",
67
- "@smithers-orchestrator/control-plane": "0.22.0",
68
- "@smithers-orchestrator/driver": "0.22.0",
69
- "@smithers-orchestrator/engine": "0.22.0",
70
- "@smithers-orchestrator/gateway-client": "0.22.0",
71
- "@smithers-orchestrator/errors": "0.22.0",
72
- "@smithers-orchestrator/gateway-react": "0.22.0",
73
- "@smithers-orchestrator/graph": "0.22.0",
74
- "@smithers-orchestrator/db": "0.22.0",
75
- "@smithers-orchestrator/memory": "0.22.0",
76
- "@smithers-orchestrator/observability": "0.22.0",
77
- "@smithers-orchestrator/openapi": "0.22.0",
78
- "@smithers-orchestrator/react-reconciler": "0.22.0",
79
- "@smithers-orchestrator/sandbox": "0.22.0",
80
- "@smithers-orchestrator/scorers": "0.22.0",
81
- "@smithers-orchestrator/scheduler": "0.22.0",
82
- "@smithers-orchestrator/server": "0.22.0",
83
- "@smithers-orchestrator/time-travel": "0.22.0",
84
- "@smithers-orchestrator/vcs": "0.22.0"
64
+ "@smithers-orchestrator/components": "0.23.0",
65
+ "@smithers-orchestrator/cli": "0.23.0",
66
+ "@smithers-orchestrator/db": "0.23.0",
67
+ "@smithers-orchestrator/control-plane": "0.23.0",
68
+ "@smithers-orchestrator/driver": "0.23.0",
69
+ "@smithers-orchestrator/engine": "0.23.0",
70
+ "@smithers-orchestrator/errors": "0.23.0",
71
+ "@smithers-orchestrator/gateway-react": "0.23.0",
72
+ "@smithers-orchestrator/memory": "0.23.0",
73
+ "@smithers-orchestrator/graph": "0.23.0",
74
+ "@smithers-orchestrator/openapi": "0.23.0",
75
+ "@smithers-orchestrator/observability": "0.23.0",
76
+ "@smithers-orchestrator/react-reconciler": "0.23.0",
77
+ "@smithers-orchestrator/sandbox": "0.23.0",
78
+ "@smithers-orchestrator/scheduler": "0.23.0",
79
+ "@smithers-orchestrator/scorers": "0.23.0",
80
+ "@smithers-orchestrator/server": "0.23.0",
81
+ "@smithers-orchestrator/time-travel": "0.23.0",
82
+ "@smithers-orchestrator/tool-context": "0.23.0",
83
+ "@smithers-orchestrator/gateway-client": "0.23.0",
84
+ "@smithers-orchestrator/agents": "0.23.0",
85
+ "@smithers-orchestrator/vcs": "0.23.0"
86
+ },
87
+ "optionalDependencies": {
88
+ "@electric-sql/pglite": "0.5.1",
89
+ "@electric-sql/pglite-socket": "0.2.1",
90
+ "pg": "^8.13.1"
85
91
  },
86
92
  "devDependencies": {
87
93
  "@types/bun": "latest",
@@ -0,0 +1,19 @@
1
+ import { createSmithers } from "../index.js";
2
+ import { z } from "zod";
3
+ import type { AgentLike } from "@smithers-orchestrator/agents";
4
+
5
+ const agent = {
6
+ generate: async () => ({ ok: true }),
7
+ } satisfies AgentLike;
8
+
9
+ const { Task, outputs } = createSmithers({
10
+ next: z.object({
11
+ ok: z.boolean(),
12
+ }),
13
+ });
14
+
15
+ const _TaskForkJsx = (
16
+ <Task id="next" output={outputs.next} agent={agent} fork="source">
17
+ Continue from the source task.
18
+ </Task>
19
+ );
package/src/create.js CHANGED
@@ -14,6 +14,8 @@ import { Approval as BaseApproval, Workflow as BaseWorkflow, Task as BaseTask, S
14
14
  import { zodToTable } from "@smithers-orchestrator/db/zodToTable";
15
15
  import { zodToCreateTableSQL, syncZodTableSchema } from "@smithers-orchestrator/db/zodToCreateTableSQL";
16
16
  import { camelToSnake } from "@smithers-orchestrator/db/utils/camelToSnake";
17
+ import { SmithersDb } from "@smithers-orchestrator/db/adapter";
18
+ import { POSTGRES } from "@smithers-orchestrator/db/dialect";
17
19
  import { resolve } from "node:path";
18
20
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
19
21
  /** @typedef {import("@smithers-orchestrator/components").ApprovalProps<any, any>} ApprovalProps */
@@ -173,43 +175,14 @@ function mergeAlertPolicies(base, override) {
173
175
  return merged;
174
176
  }
175
177
  /**
176
- * Schema-driven API users define only Zod schemas, the framework owns the entire storage layer.
177
- *
178
- * @template {Record<string, import("zod").ZodObject<any>>} Schemas
179
- * @param {Schemas} schemas
180
- * @param {CreateSmithersOptions} [opts]
181
- * @returns {import("./CreateSmithersApi.ts").CreateSmithersApi<Schemas>}
182
- *
183
- * @example
184
- * ```ts
185
- * const { Workflow, Task, smithers, outputs } = createSmithers({
186
- * discover: discoverOutputSchema,
187
- * research: researchOutputSchema,
188
- * });
178
+ * Generate the Drizzle table metadata, schema registry, and output refs shared by
179
+ * every backend. The Drizzle tables carry only column/name metadata — the actual
180
+ * storage is created per-dialect by the caller; dialect-aware engine reads consult
181
+ * these objects via getTableColumns/getTableName, never to issue SQLite queries.
189
182
  *
190
- * export default smithers((ctx) => (
191
- * <Workflow name="my-workflow">
192
- * <Task id="discover" output={outputs.discover} agent={myAgent}>...</Task>
193
- * </Workflow>
194
- * ));
195
- * ```
183
+ * @param {Record<string, any>} schemas
196
184
  */
197
- export function createSmithers(schemas, opts) {
198
- const dbPath = opts?.dbPath ?? "./smithers.db";
199
- const absDbPath = resolve(process.cwd(), dbPath);
200
- if (process.env.SMITHERS_HOT === "1") {
201
- const sig = computeSchemaSig(schemas, absDbPath);
202
- const cached = hotCache.get(absDbPath);
203
- if (cached) {
204
- if (cached.schemaSig !== sig) {
205
- throw new SmithersError("SCHEMA_CHANGE_HOT", "[smithers hot] Schema change detected; restart required to apply schema changes.");
206
- }
207
- cached.setModuleAlertPolicy(opts?.alertPolicy);
208
- return cached.api;
209
- }
210
- // Will cache after creating the API below
211
- }
212
- // 1. Generate Drizzle tables from Zod schemas
185
+ function prepareSmithersTables(schemas) {
213
186
  const tables = {};
214
187
  const inputTable = schemas.input
215
188
  ? zodToTable("input", schemas.input, { isInput: true })
@@ -223,77 +196,38 @@ export function createSmithers(schemas, opts) {
223
196
  const tableName = camelToSnake(name);
224
197
  tables[name] = zodToTable(tableName, zodSchema);
225
198
  }
226
- // 2. Create SQLite db
227
- const sqlite = new Database(dbPath);
228
- sqlite.run(`PRAGMA journal_mode = ${opts?.journalMode ?? "WAL"}`);
229
- // 30s timeout: concurrent worktrees each spawn agent processes that all write
230
- // to smithers.db simultaneously. 5s is too short and causes SQLITE_IOERR_VNODE
231
- // on macOS when the VFS can't acquire the WAL shared-memory lock in time.
232
- sqlite.run("PRAGMA busy_timeout = 30000");
233
- // NORMAL is safe in WAL mode (no data loss on crash) and reduces fsync
234
- // stalls that contribute to WAL checkpoint contention across processes.
235
- sqlite.run("PRAGMA synchronous = NORMAL");
236
- // Ensure no exclusive lock is held, allowing multiple readers/writers.
237
- sqlite.run("PRAGMA locking_mode = NORMAL");
238
- sqlite.run("PRAGMA foreign_keys = ON");
239
- // Register a process-exit hook to explicitly close the Database.
240
- // bun:sqlite's GC finalizer calls sqlite3_close() which fatally aborts if
241
- // Drizzle's cached prepared statements haven't been finalized first.
242
- // Calling close() ourselves lets sqlite3 finalize everything gracefully.
243
- let dbClosed = false;
244
- const closeDb = () => {
245
- if (dbClosed)
246
- return;
247
- dbClosed = true;
248
- try {
249
- sqlite.close();
250
- }
251
- catch { }
252
- process.removeListener("exit", closeDb);
253
- };
254
- process.once("exit", closeDb);
255
- // 3. Auto-create tables, and ALTER any existing tables to add columns the
256
- // current schema introduced (CREATE TABLE IF NOT EXISTS would silently
257
- // skip the columns and a later upsert would fail with "no column named X").
258
- if (schemas.input) {
259
- syncZodTableSchema(sqlite, "input", schemas.input, { isInput: true });
260
- }
261
- else {
262
- sqlite.exec(`CREATE TABLE IF NOT EXISTS "input" (run_id TEXT PRIMARY KEY, payload TEXT)`);
263
- try {
264
- const cols = sqlite.query(`PRAGMA table_info("input")`).all();
265
- const hasPayload = cols.some((col) => col?.name === "payload");
266
- if (!hasPayload) {
267
- sqlite.run(`ALTER TABLE "input" ADD COLUMN payload TEXT`);
268
- }
269
- }
270
- catch {
271
- // ignore - older SQLite or permission issues; input payload remains best-effort
272
- }
273
- }
274
- for (const [name, zodSchema] of Object.entries(schemas)) {
275
- if (name === "input")
276
- continue;
277
- const tableName = camelToSnake(name);
278
- syncZodTableSchema(sqlite, tableName, zodSchema);
279
- }
280
- // 4. Create Drizzle instance with all tables in the schema
281
199
  const drizzleSchema = { input: inputTable };
282
200
  for (const [key, table] of Object.entries(tables)) {
283
201
  drizzleSchema[key] = table;
284
202
  }
285
- const db = drizzle(sqlite, { schema: drizzleSchema });
286
- // 5. Build schema registry for engine resolution of string output keys
287
203
  const schemaRegistry = new Map();
288
204
  for (const [name, zodSchema] of Object.entries(schemas)) {
289
205
  if (name === "input")
290
206
  continue;
291
207
  schemaRegistry.set(name, { table: tables[name], zodSchema });
292
208
  }
293
- // 6. Build output refs and reverse lookup: ZodObject reference → schema key name
294
209
  const { outputs, zodToKeyName, ambiguousZodSchemas } = prepareOutputSchemas(schemas);
295
- // 7. Context + hooks
296
- const { SmithersContext: RuntimeSmithersContext, useCtx, } = createSmithersContext();
210
+ return { tables, inputTable, drizzleSchema, schemaRegistry, outputs, zodToKeyName, ambiguousZodSchemas };
211
+ }
212
+ /**
213
+ * Construct the public createSmithers API object around a prepared database
214
+ * handle and shared table metadata. Backend-agnostic: `db` is either a Drizzle
215
+ * bun:sqlite instance or a Postgres descriptor; every engine read/write below it
216
+ * is dialect-aware.
217
+ *
218
+ * @param {{
219
+ * db: unknown;
220
+ * tables: Record<string, unknown>;
221
+ * schemaRegistry: Map<string, unknown>;
222
+ * outputs: Record<string, unknown>;
223
+ * zodToKeyName: Map<unknown, string>;
224
+ * ambiguousZodSchemas: Set<unknown>;
225
+ * opts?: CreateSmithersOptions;
226
+ * }} config
227
+ */
228
+ function buildSmithersApi(config) {
229
+ const { db, tables, schemaRegistry, outputs, zodToKeyName, ambiguousZodSchemas, opts } = config;
230
+ const { SmithersContext: RuntimeSmithersContext, useCtx } = createSmithersContext();
297
231
  const ctxRef = { current: null };
298
232
  let moduleAlertPolicy = opts?.alertPolicy;
299
233
  /**
@@ -352,9 +286,8 @@ export function createSmithers(schemas, opts) {
352
286
  });
353
287
  }
354
288
  /**
355
- * @param {(ctx: SmithersCtx<Schemas>) => React.ReactElement} build
289
+ * @param {(ctx: SmithersCtx<any>) => React.ReactElement} build
356
290
  * @param {SmithersWorkflowOptions} [smithersOpts]
357
- * @returns {SmithersWorkflow<Schemas>}
358
291
  */
359
292
  function boundSmithers(build, smithersOpts) {
360
293
  const workflowOpts = {
@@ -402,9 +335,116 @@ export function createSmithers(schemas, opts) {
402
335
  useCtx,
403
336
  smithers: boundSmithers,
404
337
  db,
405
- tables: tables,
338
+ tables,
406
339
  outputs,
407
340
  };
341
+ return { api, setModuleAlertPolicy };
342
+ }
343
+ /**
344
+ * Schema-driven API — users define only Zod schemas, the framework owns the entire storage layer.
345
+ *
346
+ * @template {Record<string, import("zod").ZodObject<any>>} Schemas
347
+ * @param {Schemas} schemas
348
+ * @param {CreateSmithersOptions} [opts]
349
+ * @returns {import("./CreateSmithersApi.ts").CreateSmithersApi<Schemas>}
350
+ *
351
+ * @example
352
+ * ```ts
353
+ * const { Workflow, Task, smithers, outputs } = createSmithers({
354
+ * discover: discoverOutputSchema,
355
+ * research: researchOutputSchema,
356
+ * });
357
+ *
358
+ * export default smithers((ctx) => (
359
+ * <Workflow name="my-workflow">
360
+ * <Task id="discover" output={outputs.discover} agent={myAgent}>...</Task>
361
+ * </Workflow>
362
+ * ));
363
+ * ```
364
+ */
365
+ export function createSmithers(schemas, opts) {
366
+ const dbPath = opts?.dbPath ?? "./smithers.db";
367
+ const absDbPath = resolve(process.cwd(), dbPath);
368
+ if (process.env.SMITHERS_HOT === "1") {
369
+ const sig = computeSchemaSig(schemas, absDbPath);
370
+ const cached = hotCache.get(absDbPath);
371
+ if (cached) {
372
+ if (cached.schemaSig !== sig) {
373
+ throw new SmithersError("SCHEMA_CHANGE_HOT", "[smithers hot] Schema change detected; restart required to apply schema changes.");
374
+ }
375
+ cached.setModuleAlertPolicy(opts?.alertPolicy);
376
+ return cached.api;
377
+ }
378
+ // Will cache after creating the API below
379
+ }
380
+ // 1. Generate Drizzle tables + schema metadata from Zod schemas.
381
+ const { tables, drizzleSchema, schemaRegistry, outputs, zodToKeyName, ambiguousZodSchemas } = prepareSmithersTables(schemas);
382
+ // 2. Create SQLite db
383
+ const sqlite = new Database(dbPath);
384
+ sqlite.run(`PRAGMA journal_mode = ${opts?.journalMode ?? "WAL"}`);
385
+ // 30s timeout: concurrent worktrees each spawn agent processes that all write
386
+ // to smithers.db simultaneously. 5s is too short and causes SQLITE_IOERR_VNODE
387
+ // on macOS when the VFS can't acquire the WAL shared-memory lock in time.
388
+ sqlite.run("PRAGMA busy_timeout = 30000");
389
+ // NORMAL is safe in WAL mode (no data loss on crash) and reduces fsync
390
+ // stalls that contribute to WAL checkpoint contention across processes.
391
+ sqlite.run("PRAGMA synchronous = NORMAL");
392
+ // Ensure no exclusive lock is held, allowing multiple readers/writers.
393
+ sqlite.run("PRAGMA locking_mode = NORMAL");
394
+ sqlite.run("PRAGMA foreign_keys = ON");
395
+ // Register a process-exit hook to explicitly close the Database.
396
+ // bun:sqlite's GC finalizer calls sqlite3_close() which fatally aborts if
397
+ // Drizzle's cached prepared statements haven't been finalized first.
398
+ // Calling close() ourselves lets sqlite3 finalize everything gracefully.
399
+ let dbClosed = false;
400
+ const closeDb = () => {
401
+ if (dbClosed)
402
+ return;
403
+ dbClosed = true;
404
+ try {
405
+ sqlite.close();
406
+ }
407
+ catch { }
408
+ process.removeListener("exit", closeDb);
409
+ };
410
+ process.once("exit", closeDb);
411
+ // 3. Auto-create tables, and ALTER any existing tables to add columns the
412
+ // current schema introduced (CREATE TABLE IF NOT EXISTS would silently
413
+ // skip the columns and a later upsert would fail with "no column named X").
414
+ if (schemas.input) {
415
+ syncZodTableSchema(sqlite, "input", schemas.input, { isInput: true });
416
+ }
417
+ else {
418
+ sqlite.exec(`CREATE TABLE IF NOT EXISTS "input" (run_id TEXT PRIMARY KEY, payload TEXT)`);
419
+ try {
420
+ const cols = sqlite.query(`PRAGMA table_info("input")`).all();
421
+ const hasPayload = cols.some((col) => col?.name === "payload");
422
+ if (!hasPayload) {
423
+ sqlite.run(`ALTER TABLE "input" ADD COLUMN payload TEXT`);
424
+ }
425
+ }
426
+ catch {
427
+ // ignore - older SQLite or permission issues; input payload remains best-effort
428
+ }
429
+ }
430
+ for (const [name, zodSchema] of Object.entries(schemas)) {
431
+ if (name === "input")
432
+ continue;
433
+ const tableName = camelToSnake(name);
434
+ syncZodTableSchema(sqlite, tableName, zodSchema);
435
+ }
436
+ // 4. Create Drizzle instance with all tables in the schema
437
+ const db = drizzle(sqlite, { schema: drizzleSchema });
438
+ // 5. Build the public API around the prepared db + table metadata.
439
+ const { api, setModuleAlertPolicy } = buildSmithersApi({
440
+ db,
441
+ tables,
442
+ schemaRegistry,
443
+ outputs,
444
+ zodToKeyName,
445
+ ambiguousZodSchemas,
446
+ opts,
447
+ });
408
448
  if (process.env.SMITHERS_HOT === "1") {
409
449
  const sig = computeSchemaSig(schemas, absDbPath);
410
450
  hotCache.set(absDbPath, {
@@ -415,3 +455,116 @@ export function createSmithers(schemas, opts) {
415
455
  }
416
456
  return api;
417
457
  }
458
+ /**
459
+ * PostgreSQL/PGlite-backed equivalent of {@link createSmithers}. Asynchronous
460
+ * because connecting and provisioning schema over the wire is async (unlike the
461
+ * synchronous bun:sqlite path). Boots a node-postgres connection (`provider:
462
+ * "postgres"`) or an embedded PGlite over a local socket (`provider: "pglite"`),
463
+ * provisions the durable engine schema + the per-Zod-schema output tables with
464
+ * Postgres-typed DDL, and returns the same createSmithers API surface plus a
465
+ * `close()` teardown for the connection.
466
+ *
467
+ * @template {Record<string, import("zod").ZodObject<any>>} Schemas
468
+ * @param {Schemas} schemas
469
+ * @param {CreateSmithersOptions & ({ provider: "postgres"; connectionString?: string; connection?: object } | { provider: "pglite"; dataDir?: string })} opts
470
+ * @returns {Promise<import("./CreateSmithersApi.ts").CreateSmithersApi<Schemas> & { close: () => Promise<void> }>}
471
+ */
472
+ export async function createSmithersPostgres(schemas, opts) {
473
+ const provider = opts?.provider ?? "postgres";
474
+ // 1. Generate Drizzle tables + schema metadata from Zod schemas (shared).
475
+ const { tables, drizzleSchema, schemaRegistry, outputs, zodToKeyName, ambiguousZodSchemas } = prepareSmithersTables(schemas);
476
+ // 2. Boot the Postgres/PGlite connection.
477
+ const pgModule = await import("pg");
478
+ const pg = pgModule.default ?? pgModule;
479
+ // BIGINT (ms timestamps, counters) → JS number, matching SQLite's behavior.
480
+ pg.types.setTypeParser(20, (value) => (value === null ? null : Number(value)));
481
+ /** @type {Array<() => Promise<void>>} */
482
+ const teardown = [];
483
+ let connectionString = opts?.connectionString;
484
+ if (provider === "pglite") {
485
+ const { PGlite } = await import("@electric-sql/pglite");
486
+ const { PGLiteSocketServer } = await import("@electric-sql/pglite-socket");
487
+ const pglite = await PGlite.create(opts?.dataDir || undefined);
488
+ const port = await findFreePgPort();
489
+ const server = new PGLiteSocketServer({ db: pglite, host: "127.0.0.1", port, maxConnections: 5 });
490
+ await server.start();
491
+ teardown.push(async () => {
492
+ await server.stop().catch(() => {});
493
+ await pglite.close().catch(() => {});
494
+ });
495
+ connectionString = `postgres://postgres@127.0.0.1:${port}/postgres`;
496
+ }
497
+ const client = new pg.Client(connectionString ? { connectionString } : opts?.connection);
498
+ /** @type {{ api: import("./CreateSmithersApi.ts").CreateSmithersApi<Schemas> }} */
499
+ let built;
500
+ try {
501
+ await client.connect();
502
+ teardown.push(async () => {
503
+ await client.end().catch(() => {});
504
+ });
505
+ // 3. Postgres descriptor consumed by the engine + adapter. The Drizzle table
506
+ // objects (snake_case columns identical to the DDL below) are attached only
507
+ // for column/name metadata; the engine's reads/writes against this descriptor
508
+ // are dialect-aware and go through the @effect/sql adapter or raw $n queries.
509
+ const descriptor = { dialect: "postgres", connection: client, schema: drizzleSchema };
510
+ const adapter = new SmithersDb(descriptor);
511
+ // 4. Durable engine schema (idempotent), then the input + output tables with
512
+ // Postgres-typed DDL derived from the Zod schemas.
513
+ await adapter.internalStorage.ensureSchema();
514
+ if (schemas.input) {
515
+ await client.query({ text: zodToCreateTableSQL("input", schemas.input, { isInput: true, dialect: POSTGRES }) });
516
+ }
517
+ else {
518
+ await client.query({ text: `CREATE TABLE IF NOT EXISTS "input" (run_id TEXT PRIMARY KEY, payload TEXT)` });
519
+ }
520
+ for (const [name, zodSchema] of Object.entries(schemas)) {
521
+ if (name === "input")
522
+ continue;
523
+ const tableName = camelToSnake(name);
524
+ await client.query({ text: zodToCreateTableSQL(tableName, zodSchema, { dialect: POSTGRES }) });
525
+ }
526
+ // 5. Build the public API around the descriptor + table metadata.
527
+ built = buildSmithersApi({
528
+ db: descriptor,
529
+ tables,
530
+ schemaRegistry,
531
+ outputs,
532
+ zodToKeyName,
533
+ ambiguousZodSchemas,
534
+ opts,
535
+ });
536
+ }
537
+ catch (e) {
538
+ // Drain any teardown registered so far (socket server, pg client) so a
539
+ // failure after boot does not leak the port / connection.
540
+ for (const fn of teardown.reverse()) {
541
+ await fn().catch(() => {});
542
+ }
543
+ throw e;
544
+ }
545
+ const { api } = built;
546
+ return {
547
+ ...api,
548
+ close: async () => {
549
+ for (const fn of teardown.reverse()) {
550
+ await fn();
551
+ }
552
+ },
553
+ };
554
+ }
555
+ /**
556
+ * @returns {Promise<number>}
557
+ */
558
+ async function findFreePgPort() {
559
+ const net = await import("node:net");
560
+ return new Promise((resolveFn, reject) => {
561
+ const srv = net.createServer();
562
+ srv.unref();
563
+ srv.on("error", reject);
564
+ srv.listen(0, "127.0.0.1", () => {
565
+ const address = srv.address();
566
+ const port = typeof address === "object" && address ? address.port : 0;
567
+ srv.close(() => resolveFn(port));
568
+ });
569
+ });
570
+ }
package/src/index.js CHANGED
@@ -175,7 +175,7 @@ export { AnthropicAgent, OpenAIAgent, HermesAgent, AmpAgent, AntigravityAgent, C
175
175
  // VCS
176
176
  export { runJj, getJjPointer, revertToJjPointer, isJjRepo, workspaceAdd, workspaceList, workspaceClose, } from "@smithers-orchestrator/vcs/jj";
177
177
  // Core API
178
- export { createSmithers } from "./create.js";
178
+ export { createSmithers, createSmithersPostgres } from "./create.js";
179
179
  export {
180
180
  fragment,
181
181
  renderFrame,
@@ -1,29 +1,10 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
-
3
- const storage = new AsyncLocalStorage();
4
-
5
- export function runWithToolContext(ctx, fn) {
6
- return storage.run(ctx, fn);
7
- }
8
-
9
- export function getToolContext() {
10
- return storage.getStore();
11
- }
12
-
13
- export function getToolIdempotencyKey(ctx = getToolContext()) {
14
- if (!ctx) {
15
- return null;
16
- }
17
- if (typeof ctx.idempotencyKey === "string" && ctx.idempotencyKey.length > 0) {
18
- return ctx.idempotencyKey;
19
- }
20
- if (!ctx.runId || !ctx.nodeId) {
21
- return null;
22
- }
23
- return `smithers:${ctx.runId}:${ctx.nodeId}:${ctx.iteration ?? 0}`;
24
- }
25
-
26
- export function nextToolSeq(ctx) {
27
- ctx.seq = (ctx.seq ?? 0) + 1;
28
- return ctx.seq;
29
- }
1
+ // The tool-execution context now lives in its own low-level package so both the
2
+ // engine and smithers can depend on it without a cycle (the engine needs
3
+ // runWithToolContext to give in-process agent tools their run context). This file
4
+ // stays as a re-export so existing relative importers keep working.
5
+ export {
6
+ runWithToolContext,
7
+ getToolContext,
8
+ getToolIdempotencyKey,
9
+ nextToolSeq,
10
+ } from "@smithers-orchestrator/tool-context";
@@ -26,6 +26,8 @@ function defaultToolContext() {
26
26
  maxOutputBytes: 200_000,
27
27
  timeoutMs: 60_000,
28
28
  seq: 0,
29
+ // No-op unless the engine populated a real durability handle (flag on).
30
+ durabilitySnapshot: async () => ({ skipped: true }),
29
31
  };
30
32
  }
31
33
 
@@ -48,14 +50,28 @@ export function defineTool(options) {
48
50
  inputSchema: zodSchema(options.schema),
49
51
  execute: async (args) => {
50
52
  const toolContext = getToolContext();
53
+ // Merge the ambient context OVER the defaults, so a partial context from the
54
+ // engine (run/node/cwd + durabilitySnapshot) overrides what it sets and keeps
55
+ // sane defaults for the rest.
51
56
  const definedContext = {
52
- ...(toolContext ?? defaultToolContext()),
57
+ ...defaultToolContext(),
58
+ ...(toolContext ?? {}),
53
59
  idempotencyKey: getToolIdempotencyKey(toolContext),
54
60
  toolName: options.name,
55
61
  sideEffect,
56
62
  idempotent,
57
63
  };
58
- return options.execute(args, definedContext);
64
+ const result = await options.execute(args, definedContext);
65
+ // Strict Tier 1 snapshot at this tool boundary, before the agent proceeds.
66
+ // No-op by default; never delays past one jj snapshot and never fails the tool.
67
+ if (sideEffect && typeof definedContext.durabilitySnapshot === "function") {
68
+ try {
69
+ await definedContext.durabilitySnapshot(options.name, definedContext.toolUseId);
70
+ } catch {
71
+ /* snapshot failures never fail the tool */
72
+ }
73
+ }
74
+ return result;
59
75
  },
60
76
  });
61
77