smithers-orchestrator 0.21.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 +28 -22
- package/src/__type-tests__/task-fork-jsx.test.tsx +19 -0
- package/src/create.js +251 -97
- package/src/external/create-external-smithers.js +2 -1
- package/src/index.d.ts +3 -2
- package/src/index.js +7 -2
- package/src/tools/bash.js +21 -14
- package/src/tools/context.js +10 -29
- package/src/tools/defineTool.js +18 -2
- package/src/tools/utils.js +28 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smithers-orchestrator",
|
|
3
|
-
"version": "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/components": "0.
|
|
65
|
-
"@smithers-orchestrator/
|
|
66
|
-
"@smithers-orchestrator/
|
|
67
|
-
"@smithers-orchestrator/
|
|
68
|
-
"@smithers-orchestrator/
|
|
69
|
-
"@smithers-orchestrator/
|
|
70
|
-
"@smithers-orchestrator/
|
|
71
|
-
"@smithers-orchestrator/
|
|
72
|
-
"@smithers-orchestrator/
|
|
73
|
-
"@smithers-orchestrator/
|
|
74
|
-
"@smithers-orchestrator/
|
|
75
|
-
"@smithers-orchestrator/
|
|
76
|
-
"@smithers-orchestrator/
|
|
77
|
-
"@smithers-orchestrator/
|
|
78
|
-
"@smithers-orchestrator/
|
|
79
|
-
"@smithers-orchestrator/
|
|
80
|
-
"@smithers-orchestrator/
|
|
81
|
-
"@smithers-orchestrator/
|
|
82
|
-
"@smithers-orchestrator/
|
|
83
|
-
"@smithers-orchestrator/
|
|
84
|
-
"@smithers-orchestrator/
|
|
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
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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,76 +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
|
-
};
|
|
253
|
-
process.on("exit", closeDb);
|
|
254
|
-
// 3. Auto-create tables, and ALTER any existing tables to add columns the
|
|
255
|
-
// current schema introduced (CREATE TABLE IF NOT EXISTS would silently
|
|
256
|
-
// skip the columns and a later upsert would fail with "no column named X").
|
|
257
|
-
if (schemas.input) {
|
|
258
|
-
syncZodTableSchema(sqlite, "input", schemas.input, { isInput: true });
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
sqlite.exec(`CREATE TABLE IF NOT EXISTS "input" (run_id TEXT PRIMARY KEY, payload TEXT)`);
|
|
262
|
-
try {
|
|
263
|
-
const cols = sqlite.query(`PRAGMA table_info("input")`).all();
|
|
264
|
-
const hasPayload = cols.some((col) => col?.name === "payload");
|
|
265
|
-
if (!hasPayload) {
|
|
266
|
-
sqlite.run(`ALTER TABLE "input" ADD COLUMN payload TEXT`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
catch {
|
|
270
|
-
// ignore - older SQLite or permission issues; input payload remains best-effort
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
for (const [name, zodSchema] of Object.entries(schemas)) {
|
|
274
|
-
if (name === "input")
|
|
275
|
-
continue;
|
|
276
|
-
const tableName = camelToSnake(name);
|
|
277
|
-
syncZodTableSchema(sqlite, tableName, zodSchema);
|
|
278
|
-
}
|
|
279
|
-
// 4. Create Drizzle instance with all tables in the schema
|
|
280
199
|
const drizzleSchema = { input: inputTable };
|
|
281
200
|
for (const [key, table] of Object.entries(tables)) {
|
|
282
201
|
drizzleSchema[key] = table;
|
|
283
202
|
}
|
|
284
|
-
const db = drizzle(sqlite, { schema: drizzleSchema });
|
|
285
|
-
// 5. Build schema registry for engine resolution of string output keys
|
|
286
203
|
const schemaRegistry = new Map();
|
|
287
204
|
for (const [name, zodSchema] of Object.entries(schemas)) {
|
|
288
205
|
if (name === "input")
|
|
289
206
|
continue;
|
|
290
207
|
schemaRegistry.set(name, { table: tables[name], zodSchema });
|
|
291
208
|
}
|
|
292
|
-
// 6. Build output refs and reverse lookup: ZodObject reference → schema key name
|
|
293
209
|
const { outputs, zodToKeyName, ambiguousZodSchemas } = prepareOutputSchemas(schemas);
|
|
294
|
-
|
|
295
|
-
|
|
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();
|
|
296
231
|
const ctxRef = { current: null };
|
|
297
232
|
let moduleAlertPolicy = opts?.alertPolicy;
|
|
298
233
|
/**
|
|
@@ -351,9 +286,8 @@ export function createSmithers(schemas, opts) {
|
|
|
351
286
|
});
|
|
352
287
|
}
|
|
353
288
|
/**
|
|
354
|
-
* @param {(ctx: SmithersCtx<
|
|
289
|
+
* @param {(ctx: SmithersCtx<any>) => React.ReactElement} build
|
|
355
290
|
* @param {SmithersWorkflowOptions} [smithersOpts]
|
|
356
|
-
* @returns {SmithersWorkflow<Schemas>}
|
|
357
291
|
*/
|
|
358
292
|
function boundSmithers(build, smithersOpts) {
|
|
359
293
|
const workflowOpts = {
|
|
@@ -401,9 +335,116 @@ export function createSmithers(schemas, opts) {
|
|
|
401
335
|
useCtx,
|
|
402
336
|
smithers: boundSmithers,
|
|
403
337
|
db,
|
|
404
|
-
tables
|
|
338
|
+
tables,
|
|
405
339
|
outputs,
|
|
406
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
|
+
});
|
|
407
448
|
if (process.env.SMITHERS_HOT === "1") {
|
|
408
449
|
const sig = computeSchemaSig(schemas, absDbPath);
|
|
409
450
|
hotCache.set(absDbPath, {
|
|
@@ -414,3 +455,116 @@ export function createSmithers(schemas, opts) {
|
|
|
414
455
|
}
|
|
415
456
|
return api;
|
|
416
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
|
+
}
|
|
@@ -127,8 +127,9 @@ export function createExternalSmithers(config) {
|
|
|
127
127
|
sqlite.close();
|
|
128
128
|
}
|
|
129
129
|
catch { }
|
|
130
|
+
process.removeListener("exit", closeDb);
|
|
130
131
|
};
|
|
131
|
-
process.
|
|
132
|
+
process.once("exit", closeDb);
|
|
132
133
|
const inputTable = sqliteTable("input", {
|
|
133
134
|
runId: text("run_id").primaryKey(),
|
|
134
135
|
payload: text("payload", { mode: "json" }).$type(),
|
package/src/index.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ import { WorkflowProps } from '@smithers-orchestrator/components/components/Work
|
|
|
56
56
|
import * as _smithers_server_gateway from '@smithers-orchestrator/server/gateway';
|
|
57
57
|
export { Gateway } from '@smithers-orchestrator/server/gateway';
|
|
58
58
|
import * as _smithers_agents from '@smithers-orchestrator/agents';
|
|
59
|
-
export { AmpAgent, AnthropicAgent, AntigravityAgent, ClaudeCodeAgent, CodexAgent, ForgeAgent, GeminiAgent, KimiAgent, OpenAIAgent, OpenCodeAgent, PiAgent } from '@smithers-orchestrator/agents';
|
|
59
|
+
export { AmpAgent, AnthropicAgent, AntigravityAgent, ClaudeCodeAgent, CodexAgent, ForgeAgent, GeminiAgent, HermesAgent, KimiAgent, OpenAIAgent, OpenCodeAgent, PiAgent } from '@smithers-orchestrator/agents';
|
|
60
60
|
import * as _smithers_scorers from '@smithers-orchestrator/scorers';
|
|
61
61
|
export { aggregateScores, createScorer, faithfulnessScorer, latencyScorer, llmJudge, relevancyScorer, runScorersAsync, runScorersBatch, schemaAdherenceScorer, smithersScorers, toxicityScorer } from '@smithers-orchestrator/scorers';
|
|
62
62
|
import * as _smithers_agents_capability_registry from '@smithers-orchestrator/agents/capability-registry';
|
|
@@ -331,6 +331,7 @@ type MemoryStore = _smithers_memory.MemoryStore;
|
|
|
331
331
|
type MemoryThread = _smithers_memory.MemoryThread;
|
|
332
332
|
type MessageHistoryConfig = _smithers_memory.MessageHistoryConfig;
|
|
333
333
|
type OpenAIAgentOptions<CALL_OPTIONS = never, TOOLS = ai.ToolSet> = _smithers_agents.OpenAIAgentOptions<CALL_OPTIONS, TOOLS>;
|
|
334
|
+
type HermesAgentOptions<CALL_OPTIONS = never, TOOLS = ai.ToolSet> = _smithers_agents.HermesAgentOptions<CALL_OPTIONS, TOOLS>;
|
|
334
335
|
type OpenCodeAgentOptions = _smithers_agents.OpenCodeAgentOptions;
|
|
335
336
|
type OpenApiAuth = _smithers_openapi.OpenApiAuth;
|
|
336
337
|
type OpenApiSpec = _smithers_openapi.OpenApiSpec;
|
|
@@ -407,4 +408,4 @@ type XmlElement = _smithers_graph_XmlNode.XmlElement;
|
|
|
407
408
|
type XmlNode = _smithers_graph_XmlNode.XmlNode;
|
|
408
409
|
type XmlText = _smithers_graph_XmlNode.XmlText;
|
|
409
410
|
|
|
410
|
-
export { type AgentCapabilityRegistry, type AgentLike, type AgentToolDescriptor, type AggregateOptions, type AggregateScore, type AnthropicAgentOptions, type ApprovalAutoApprove, type ApprovalDecision, type ApprovalMode, type ApprovalOption, type ApprovalProps, type ApprovalRanking, type ApprovalRequest, type ApprovalSelection, type ColumnDef, type ConnectRequest, type ContinueAsNewProps, type CreateScorerConfig, type CreateSmithersApi, type DepsSpec, type EventFrame, type ExternalSmithersConfig, type GatewayAuthConfig, type GatewayDefaults, type GatewayOptions, type GatewayTokenGrant, type GraphSnapshot, type HelloResponse, type HostContainer, type HostNodeJson, type InferDeps, type InferOutputEntry, type InferRow, type JjRevertResult, type KanbanProps, type KnownSmithersErrorCode, type LlmJudgeConfig, type MemoryFact, type MemoryLayerConfig, type MemoryMessage, type MemoryNamespace, type MemoryNamespaceKind, type MemoryProcessor, type MemoryProcessorConfig, type MemoryServiceApi, type MemoryStore, type MemoryThread, type MessageHistoryConfig, type OpenAIAgentOptions, type OpenApiAuth, type OpenApiSpec, type OpenApiToolsOptions, type OpenCodeAgentOptions, type OutputAccessor, type OutputKey, type OutputTarget, type PiAgentOptions, type PiExtensionUiRequest, type PiExtensionUiResponse, type PollerProps, type RequestFrame, type ResolvedSmithersObservabilityOptions, type ResponseFrame, type RevertOptions, type RevertResult, type RunJjOptions, type RunJjResult, type RunOptions, type RunResult, type RunStatus, type SagaProps, type SagaStepDef, type SagaStepProps, type SamplingConfig, type SandboxProps, type SandboxRuntime, type SandboxVolumeMount, type SandboxWorkspaceSpec, type SchemaRegistryEntry, type ScoreResult, type ScoreRow, type Scorer, type ScorerBinding, type ScorerContext, type ScorerFn, type ScorerInput, type ScorersMap, type SemanticRecallConfig, type SerializedCtx, type ServeOptions, type ServerOptions, type SignalProps, type SmithersAlertLabels, type SmithersAlertPolicy, type SmithersAlertPolicyDefaults, type SmithersAlertPolicyRule, type SmithersAlertReaction, type SmithersAlertReactionKind, type SmithersAlertReactionRef, type SmithersAlertSeverity, type SmithersCtx, type SmithersError, type SmithersErrorCode, type SmithersEvent, type SmithersLogFormat, type SmithersObservabilityOptions, type SmithersObservabilityService, type SmithersWorkflow, type SmithersWorkflowOptions, type TaskDescriptor, type TaskMemoryConfig, type TaskProps, type TimeTravelOptions, type TimeTravelResult, type TimerProps, type TryCatchFinallyProps, type WaitForEventProps, type WorkingMemoryConfig, type WorkspaceAddOptions, type WorkspaceInfo, type WorkspaceResult, type XmlElement, type XmlNode, type XmlText, bash, createExternalSmithers, createSmithers, defineTool, edit, getDefinedToolMetadata, grep, mdxPlugin, read, tools, write };
|
|
411
|
+
export { type AgentCapabilityRegistry, type AgentLike, type AgentToolDescriptor, type AggregateOptions, type AggregateScore, type AnthropicAgentOptions, type ApprovalAutoApprove, type ApprovalDecision, type ApprovalMode, type ApprovalOption, type ApprovalProps, type ApprovalRanking, type ApprovalRequest, type ApprovalSelection, type ColumnDef, type ConnectRequest, type ContinueAsNewProps, type CreateScorerConfig, type CreateSmithersApi, type DepsSpec, type EventFrame, type ExternalSmithersConfig, type GatewayAuthConfig, type GatewayDefaults, type GatewayOptions, type GatewayTokenGrant, type GraphSnapshot, type HelloResponse, type HermesAgentOptions, type HostContainer, type HostNodeJson, type InferDeps, type InferOutputEntry, type InferRow, type JjRevertResult, type KanbanProps, type KnownSmithersErrorCode, type LlmJudgeConfig, type MemoryFact, type MemoryLayerConfig, type MemoryMessage, type MemoryNamespace, type MemoryNamespaceKind, type MemoryProcessor, type MemoryProcessorConfig, type MemoryServiceApi, type MemoryStore, type MemoryThread, type MessageHistoryConfig, type OpenAIAgentOptions, type OpenApiAuth, type OpenApiSpec, type OpenApiToolsOptions, type OpenCodeAgentOptions, type OutputAccessor, type OutputKey, type OutputTarget, type PiAgentOptions, type PiExtensionUiRequest, type PiExtensionUiResponse, type PollerProps, type RequestFrame, type ResolvedSmithersObservabilityOptions, type ResponseFrame, type RevertOptions, type RevertResult, type RunJjOptions, type RunJjResult, type RunOptions, type RunResult, type RunStatus, type SagaProps, type SagaStepDef, type SagaStepProps, type SamplingConfig, type SandboxProps, type SandboxRuntime, type SandboxVolumeMount, type SandboxWorkspaceSpec, type SchemaRegistryEntry, type ScoreResult, type ScoreRow, type Scorer, type ScorerBinding, type ScorerContext, type ScorerFn, type ScorerInput, type ScorersMap, type SemanticRecallConfig, type SerializedCtx, type ServeOptions, type ServerOptions, type SignalProps, type SmithersAlertLabels, type SmithersAlertPolicy, type SmithersAlertPolicyDefaults, type SmithersAlertPolicyRule, type SmithersAlertReaction, type SmithersAlertReactionKind, type SmithersAlertReactionRef, type SmithersAlertSeverity, type SmithersCtx, type SmithersError, type SmithersErrorCode, type SmithersEvent, type SmithersLogFormat, type SmithersObservabilityOptions, type SmithersObservabilityService, type SmithersWorkflow, type SmithersWorkflowOptions, type TaskDescriptor, type TaskMemoryConfig, type TaskProps, type TimeTravelOptions, type TimeTravelResult, type TimerProps, type TryCatchFinallyProps, type WaitForEventProps, type WorkingMemoryConfig, type WorkspaceAddOptions, type WorkspaceInfo, type WorkspaceResult, type XmlElement, type XmlNode, type XmlText, bash, createExternalSmithers, createSmithers, defineTool, edit, getDefinedToolMetadata, grep, mdxPlugin, read, tools, write };
|
package/src/index.js
CHANGED
|
@@ -68,6 +68,11 @@
|
|
|
68
68
|
* @template [TOOLS=import("ai").ToolSet]
|
|
69
69
|
* @typedef {import("@smithers-orchestrator/agents").OpenAIAgentOptions<CALL_OPTIONS, TOOLS>} OpenAIAgentOptions
|
|
70
70
|
*/
|
|
71
|
+
/**
|
|
72
|
+
* @template [CALL_OPTIONS=never]
|
|
73
|
+
* @template [TOOLS=import("ai").ToolSet]
|
|
74
|
+
* @typedef {import("@smithers-orchestrator/agents").HermesAgentOptions<CALL_OPTIONS, TOOLS>} HermesAgentOptions
|
|
75
|
+
*/
|
|
71
76
|
/** @typedef {import("@smithers-orchestrator/openapi").OpenApiAuth} OpenApiAuth */
|
|
72
77
|
/** @typedef {import("@smithers-orchestrator/openapi").OpenApiSpec} OpenApiSpec */
|
|
73
78
|
/** @typedef {import("@smithers-orchestrator/openapi").OpenApiToolsOptions} OpenApiToolsOptions */
|
|
@@ -166,11 +171,11 @@ export { knownSmithersErrorCodes } from "@smithers-orchestrator/errors/knownSmit
|
|
|
166
171
|
// Components
|
|
167
172
|
export { Approval, ApprovalGate, Aspects, Branch, CheckSuite, ClassifyAndRoute, ContentPipeline, ContinueAsNew, Debate, DecisionTable, DriftDetector, EscalationChain, GatherAndSynthesize, HumanTask, Kanban, Loop, MergeQueue, Optimizer, Panel, Parallel, Poller, Ralph, ReviewLoop, Runbook, Saga, Sandbox, ScanFixVerify, Sequence, Signal, Subflow, SuperSmithers, Supervisor, Task, Timer, TryCatchFinally, WaitForEvent, Workflow, Worktree, approvalDecisionSchema, approvalRankingSchema, approvalSelectionSchema, continueAsNew, } from "@smithers-orchestrator/components";
|
|
168
173
|
// Agents
|
|
169
|
-
export { AnthropicAgent, OpenAIAgent, AmpAgent, AntigravityAgent, ClaudeCodeAgent, CodexAgent, GeminiAgent, PiAgent, KimiAgent, ForgeAgent, OpenCodeAgent, } from "@smithers-orchestrator/agents";
|
|
174
|
+
export { AnthropicAgent, OpenAIAgent, HermesAgent, AmpAgent, AntigravityAgent, ClaudeCodeAgent, CodexAgent, GeminiAgent, PiAgent, KimiAgent, ForgeAgent, OpenCodeAgent, } from "@smithers-orchestrator/agents";
|
|
170
175
|
// VCS
|
|
171
176
|
export { runJj, getJjPointer, revertToJjPointer, isJjRepo, workspaceAdd, workspaceList, workspaceClose, } from "@smithers-orchestrator/vcs/jj";
|
|
172
177
|
// Core API
|
|
173
|
-
export { createSmithers } from "./create.js";
|
|
178
|
+
export { createSmithers, createSmithersPostgres } from "./create.js";
|
|
174
179
|
export {
|
|
175
180
|
fragment,
|
|
176
181
|
renderFrame,
|
package/src/tools/bash.js
CHANGED
|
@@ -100,29 +100,36 @@ function validateBashInvocation(cmd, args, opts, ctx) {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function tokenExecutableName(token) {
|
|
104
|
+
const stripped = token.split(/[/\\]/).pop() ?? token;
|
|
105
|
+
return stripped;
|
|
106
|
+
}
|
|
107
|
+
|
|
103
108
|
function assertNetworkAllowed(cmd, args, allowNetwork) {
|
|
104
109
|
if (allowNetwork) {
|
|
105
110
|
return;
|
|
106
111
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
const tokens = [cmd, ...(args ?? [])].flatMap((part) =>
|
|
113
|
+
String(part).split(/\s+/).filter(Boolean),
|
|
114
|
+
);
|
|
115
|
+
const executables = new Set(tokens.map(tokenExecutableName));
|
|
116
|
+
const networkExecutables = ["curl", "wget", "npm", "bun", "pip"];
|
|
117
|
+
const hasNetworkExecutable = networkExecutables.some((name) =>
|
|
118
|
+
executables.has(name),
|
|
119
|
+
);
|
|
120
|
+
const urlSchemes = ["http://", "https://"];
|
|
121
|
+
const hasUrlToken = tokens.some((token) =>
|
|
122
|
+
urlSchemes.some((scheme) => token.startsWith(scheme)),
|
|
123
|
+
);
|
|
124
|
+
if (hasNetworkExecutable || hasUrlToken) {
|
|
118
125
|
throw new SmithersError(
|
|
119
126
|
"TOOL_NETWORK_DISABLED",
|
|
120
127
|
"Network access is disabled for bash tool",
|
|
121
128
|
);
|
|
122
129
|
}
|
|
123
|
-
if (
|
|
124
|
-
const gitRemoteOps = ["push", "pull", "fetch", "clone", "remote"];
|
|
125
|
-
if (
|
|
130
|
+
if (executables.has("git")) {
|
|
131
|
+
const gitRemoteOps = new Set(["push", "pull", "fetch", "clone", "remote"]);
|
|
132
|
+
if (tokens.some((token) => gitRemoteOps.has(token))) {
|
|
126
133
|
throw new SmithersError(
|
|
127
134
|
"TOOL_GIT_REMOTE_DISABLED",
|
|
128
135
|
"Git remote operations are disabled for bash tool",
|
package/src/tools/context.js
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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";
|
package/src/tools/defineTool.js
CHANGED
|
@@ -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
|
-
...
|
|
57
|
+
...defaultToolContext(),
|
|
58
|
+
...(toolContext ?? {}),
|
|
53
59
|
idempotencyKey: getToolIdempotencyKey(toolContext),
|
|
54
60
|
toolName: options.name,
|
|
55
61
|
sideEffect,
|
|
56
62
|
idempotent,
|
|
57
63
|
};
|
|
58
|
-
|
|
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
|
|
package/src/tools/utils.js
CHANGED
|
@@ -27,12 +27,25 @@ export function sha256Hex(value) {
|
|
|
27
27
|
return createHash("sha256").update(value).digest("hex");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function utf8BoundaryLength(buf, byteLength) {
|
|
31
|
+
let end = Math.min(byteLength, buf.length);
|
|
32
|
+
if (end >= buf.length) {
|
|
33
|
+
return buf.length;
|
|
34
|
+
}
|
|
35
|
+
// If the cut lands on a continuation byte, back up to the lead byte of the
|
|
36
|
+
// partial sequence so we never decode an incomplete codepoint.
|
|
37
|
+
while (end > 0 && (buf[end] & 0xc0) === 0x80) {
|
|
38
|
+
end -= 1;
|
|
39
|
+
}
|
|
40
|
+
return end;
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
export function truncateToBytes(text, maxBytes) {
|
|
31
44
|
const buf = Buffer.from(text, "utf8");
|
|
32
45
|
if (buf.length <= maxBytes) {
|
|
33
46
|
return text;
|
|
34
47
|
}
|
|
35
|
-
return buf.subarray(0, maxBytes).toString("utf8");
|
|
48
|
+
return buf.subarray(0, utf8BoundaryLength(buf, maxBytes)).toString("utf8");
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
export async function resolveToolPath(rootDir, inputPath) {
|
|
@@ -72,8 +85,11 @@ function appendLimited(chunks, state, chunk, maxBytes) {
|
|
|
72
85
|
state.storedBytes += buffer.length;
|
|
73
86
|
return;
|
|
74
87
|
}
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
const accepted = utf8BoundaryLength(buffer, remaining);
|
|
89
|
+
if (accepted > 0) {
|
|
90
|
+
chunks.push(buffer.subarray(0, accepted));
|
|
91
|
+
state.storedBytes += accepted;
|
|
92
|
+
}
|
|
77
93
|
state.truncated = true;
|
|
78
94
|
}
|
|
79
95
|
|
|
@@ -91,7 +107,12 @@ export function captureProcess(
|
|
|
91
107
|
return new Promise((resolve, reject) => {
|
|
92
108
|
const stdoutChunks = [];
|
|
93
109
|
const stderrChunks = [];
|
|
94
|
-
const
|
|
110
|
+
const stdoutState = {
|
|
111
|
+
storedBytes: 0,
|
|
112
|
+
totalBytes: 0,
|
|
113
|
+
truncated: false,
|
|
114
|
+
};
|
|
115
|
+
const stderrState = {
|
|
95
116
|
storedBytes: 0,
|
|
96
117
|
totalBytes: 0,
|
|
97
118
|
truncated: false,
|
|
@@ -149,10 +170,10 @@ export function captureProcess(
|
|
|
149
170
|
}
|
|
150
171
|
|
|
151
172
|
child.stdout.on("data", (chunk) => {
|
|
152
|
-
appendLimited(stdoutChunks,
|
|
173
|
+
appendLimited(stdoutChunks, stdoutState, chunk, maxOutputBytes);
|
|
153
174
|
});
|
|
154
175
|
child.stderr.on("data", (chunk) => {
|
|
155
|
-
appendLimited(stderrChunks,
|
|
176
|
+
appendLimited(stderrChunks, stderrState, chunk, maxOutputBytes);
|
|
156
177
|
});
|
|
157
178
|
child.on("error", (error) => {
|
|
158
179
|
finish(() =>
|
|
@@ -167,7 +188,7 @@ export function captureProcess(
|
|
|
167
188
|
});
|
|
168
189
|
child.on("close", (exitCode, signal) => {
|
|
169
190
|
finish(() => {
|
|
170
|
-
resolve({ exitCode: exitCode ?? (signal ? 1 : 0), signal, stdout: Buffer.concat(stdoutChunks).toString("utf8"), stderr: Buffer.concat(stderrChunks).toString("utf8"), truncated:
|
|
191
|
+
resolve({ exitCode: exitCode ?? (signal ? 1 : 0), signal, stdout: Buffer.concat(stdoutChunks).toString("utf8"), stderr: Buffer.concat(stderrChunks).toString("utf8"), truncated: stdoutState.truncated || stderrState.truncated, totalBytes: stdoutState.totalBytes + stderrState.totalBytes });
|
|
171
192
|
});
|
|
172
193
|
});
|
|
173
194
|
});
|