@smithers-orchestrator/engine 0.16.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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +50 -0
  3. package/src/AlertHumanRequestOptions.ts +8 -0
  4. package/src/AlertRuntimeServices.ts +10 -0
  5. package/src/ChildWorkflowDefinition.ts +5 -0
  6. package/src/ChildWorkflowExecuteOptions.ts +14 -0
  7. package/src/ContinuationRequest.ts +3 -0
  8. package/src/HijackState.ts +19 -0
  9. package/src/HumanRequestKind.ts +1 -0
  10. package/src/HumanRequestStatus.ts +1 -0
  11. package/src/PlanNode.ts +29 -0
  12. package/src/RalphMeta.ts +7 -0
  13. package/src/RalphState.ts +4 -0
  14. package/src/RalphStateMap.ts +3 -0
  15. package/src/ScheduleResult.ts +15 -0
  16. package/src/SignalRunOptions.ts +5 -0
  17. package/src/alert-runtime.js +22 -0
  18. package/src/approvals.js +220 -0
  19. package/src/child-workflow.js +163 -0
  20. package/src/effect/ApprovalDeferredResolution.ts +13 -0
  21. package/src/effect/ApprovalDurableDeferredResolution.ts +11 -0
  22. package/src/effect/ApprovalPayload.ts +7 -0
  23. package/src/effect/ApprovalResult.ts +6 -0
  24. package/src/effect/BuilderNode.ts +52 -0
  25. package/src/effect/BuilderStepHandle.ts +47 -0
  26. package/src/effect/CancelPayload.ts +3 -0
  27. package/src/effect/CancelResult.ts +4 -0
  28. package/src/effect/DeferredResolution.ts +7 -0
  29. package/src/effect/DiffBundle.ts +7 -0
  30. package/src/effect/ExecuteTaskActivityOptions.ts +7 -0
  31. package/src/effect/FilePatch.ts +6 -0
  32. package/src/effect/GetRunPayload.ts +3 -0
  33. package/src/effect/GetRunResult.ts +3 -0
  34. package/src/effect/LegacyExecuteTaskFn.ts +24 -0
  35. package/src/effect/ListRunsPayload.ts +6 -0
  36. package/src/effect/RunStatusSchema.ts +9 -0
  37. package/src/effect/RunSummary.ts +23 -0
  38. package/src/effect/SignalPayload.ts +7 -0
  39. package/src/effect/SignalResult.ts +6 -0
  40. package/src/effect/SmithersSqliteOptions.ts +3 -0
  41. package/src/effect/SqlMessageStorageEventHistoryQuery.ts +7 -0
  42. package/src/effect/TaggedWorkerError.ts +46 -0
  43. package/src/effect/TaskActivityContext.ts +4 -0
  44. package/src/effect/TaskActivityRetryOptions.ts +4 -0
  45. package/src/effect/TaskBridgeToolConfig.ts +6 -0
  46. package/src/effect/TaskFailure.ts +3 -0
  47. package/src/effect/TaskResult.ts +5 -0
  48. package/src/effect/UnknownWorkerError.ts +5 -0
  49. package/src/effect/WaitForEventDurableDeferredResolution.ts +11 -0
  50. package/src/effect/WorkerDispatchKind.ts +1 -0
  51. package/src/effect/WorkerTask.ts +14 -0
  52. package/src/effect/WorkerTaskError.ts +4 -0
  53. package/src/effect/WorkerTaskKind.ts +1 -0
  54. package/src/effect/WorkflowPatchDecisionRecord.ts +4 -0
  55. package/src/effect/WorkflowPatchDecisions.ts +1 -0
  56. package/src/effect/WorkflowVersioningRuntime.ts +7 -0
  57. package/src/effect/activity-bridge.js +131 -0
  58. package/src/effect/bridge-utils.js +45 -0
  59. package/src/effect/builder.js +837 -0
  60. package/src/effect/compute-task-bridge.js +734 -0
  61. package/src/effect/deferred-bridge.js +63 -0
  62. package/src/effect/deferred-state-bridge.js +1343 -0
  63. package/src/effect/diff-bundle.js +352 -0
  64. package/src/effect/durable-deferred-bridge.js +282 -0
  65. package/src/effect/entity-worker.js +154 -0
  66. package/src/effect/http-runner.js +86 -0
  67. package/src/effect/rpc-schema.js +101 -0
  68. package/src/effect/single-runner.js +189 -0
  69. package/src/effect/sql-message-storage.js +817 -0
  70. package/src/effect/static-task-bridge.js +308 -0
  71. package/src/effect/versioning.js +123 -0
  72. package/src/effect/workflow-bridge.js +260 -0
  73. package/src/effect/workflow-make-bridge.js +233 -0
  74. package/src/engine.js +6933 -0
  75. package/src/events.js +237 -0
  76. package/src/external/json-schema-to-zod.js +214 -0
  77. package/src/getDefinedToolMetadata.js +10 -0
  78. package/src/hot/HotReloadEvent.ts +21 -0
  79. package/src/hot/HotWorkflowController.js +220 -0
  80. package/src/hot/OverlayOptions.ts +4 -0
  81. package/src/hot/WatchTreeOptions.ts +6 -0
  82. package/src/hot/index.js +9 -0
  83. package/src/hot/overlay.js +177 -0
  84. package/src/hot/watch.js +174 -0
  85. package/src/human-requests.js +120 -0
  86. package/src/index.d.ts +1597 -0
  87. package/src/index.js +41 -0
  88. package/src/runtime-owner.js +36 -0
  89. package/src/scheduler.js +31 -0
  90. package/src/signals.js +82 -0
@@ -0,0 +1,4 @@
1
+ export type OverlayOptions = {
2
+ /** Directory basenames to exclude from overlay */
3
+ exclude?: string[];
4
+ };
@@ -0,0 +1,6 @@
1
+ export type WatchTreeOptions = {
2
+ /** Patterns to ignore (directory basenames) */
3
+ ignore?: string[];
4
+ /** Debounce interval in ms (default: 100) */
5
+ debounceMs?: number;
6
+ };
@@ -0,0 +1,9 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./HotReloadEvent.ts").HotReloadEvent} HotReloadEvent */
3
+ /** @typedef {import("./OverlayOptions.ts").OverlayOptions} OverlayOptions */
4
+ /** @typedef {import("./WatchTreeOptions.ts").WatchTreeOptions} WatchTreeOptions */
5
+ // @smithers-type-exports-end
6
+
7
+ export { WatchTree } from "./watch.js";
8
+ export { buildOverlay, cleanupGenerations, resolveOverlayEntry } from "./overlay.js";
9
+ export { HotWorkflowController } from "./HotWorkflowController.js";
@@ -0,0 +1,177 @@
1
+ import { readdir, mkdir, link, copyFile, rm } from "node:fs/promises";
2
+ import { resolve, relative, join, dirname } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { Effect } from "effect";
5
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
6
+ /** @typedef {import("./OverlayOptions.ts").OverlayOptions} OverlayOptions */
7
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
8
+
9
+ const DEFAULT_EXCLUDE = [
10
+ "node_modules",
11
+ ".git",
12
+ ".jj",
13
+ ".smithers",
14
+ ".DS_Store",
15
+ ];
16
+ /**
17
+ * Build a generation overlay by hardlinking (or copying) the hot root
18
+ * tree into a new generation directory.
19
+ *
20
+ * Returns the absolute path to the overlay directory.
21
+ *
22
+ * @param {string} hotRoot
23
+ * @param {string} outDir
24
+ * @param {number} generation
25
+ * @param {OverlayOptions} [opts]
26
+ * @returns {Effect.Effect<string, SmithersError>}
27
+ */
28
+ export function buildOverlayEffect(hotRoot, outDir, generation, opts) {
29
+ const exclude = new Set(opts?.exclude ?? DEFAULT_EXCLUDE);
30
+ const genDir = join(outDir, `gen-${generation}`);
31
+ return Effect.gen(function* () {
32
+ yield* Effect.tryPromise({
33
+ try: () => mkdir(genDir, { recursive: true }),
34
+ catch: (cause) => toSmithersError(cause, "create hot overlay generation dir", {
35
+ code: "HOT_OVERLAY_FAILED",
36
+ details: { hotRoot, outDir, generation },
37
+ }),
38
+ });
39
+ yield* mirrorTreeEffect(hotRoot, genDir, exclude);
40
+ return genDir;
41
+ }).pipe(Effect.annotateLogs({
42
+ hotRoot,
43
+ outDir,
44
+ generation,
45
+ excludeCount: exclude.size,
46
+ }), Effect.withLogSpan("hot:build-overlay"));
47
+ }
48
+ /**
49
+ * @param {string} hotRoot
50
+ * @param {string} outDir
51
+ * @param {number} generation
52
+ * @param {OverlayOptions} [opts]
53
+ * @returns {Promise<string>}
54
+ */
55
+ export async function buildOverlay(hotRoot, outDir, generation, opts) {
56
+ return Effect.runPromise(buildOverlayEffect(hotRoot, outDir, generation, opts));
57
+ }
58
+ /**
59
+ * Recursively mirror `src` into `dest`, using hardlinks where possible
60
+ * and falling back to copy. Skips excluded directory basenames.
61
+ */
62
+ function mirrorTreeEffect(src, dest, exclude) {
63
+ return Effect.gen(function* () {
64
+ const entries = yield* Effect.tryPromise({
65
+ try: () => readdir(src, { withFileTypes: true }),
66
+ catch: (cause) => toSmithersError(cause, "read hot overlay source dir", {
67
+ code: "HOT_OVERLAY_FAILED",
68
+ details: { src, dest },
69
+ }),
70
+ });
71
+ for (const entry of entries) {
72
+ if (exclude.has(entry.name))
73
+ continue;
74
+ if (entry.name.startsWith("."))
75
+ continue;
76
+ const srcPath = join(src, entry.name);
77
+ const destPath = join(dest, entry.name);
78
+ if (entry.isDirectory()) {
79
+ yield* Effect.tryPromise({
80
+ try: () => mkdir(destPath, { recursive: true }),
81
+ catch: (cause) => toSmithersError(cause, "create mirrored hot overlay dir", {
82
+ code: "HOT_OVERLAY_FAILED",
83
+ details: { srcPath, destPath },
84
+ }),
85
+ });
86
+ yield* mirrorTreeEffect(srcPath, destPath, exclude);
87
+ }
88
+ else if (entry.isFile()) {
89
+ const linked = yield* Effect.either(Effect.tryPromise({
90
+ try: () => link(srcPath, destPath),
91
+ catch: (cause) => toSmithersError(cause, "hardlink overlay file", {
92
+ code: "HOT_OVERLAY_FAILED",
93
+ details: { srcPath, destPath },
94
+ }),
95
+ }));
96
+ if (linked._tag === "Left") {
97
+ yield* Effect.tryPromise({
98
+ try: () => mkdir(dirname(destPath), { recursive: true }),
99
+ catch: (cause) => toSmithersError(cause, "create overlay file parent dir", {
100
+ code: "HOT_OVERLAY_FAILED",
101
+ details: { srcPath, destPath },
102
+ }),
103
+ });
104
+ yield* Effect.tryPromise({
105
+ try: () => copyFile(srcPath, destPath),
106
+ catch: (cause) => toSmithersError(cause, "copy overlay file", {
107
+ code: "HOT_OVERLAY_FAILED",
108
+ details: { srcPath, destPath },
109
+ }),
110
+ });
111
+ }
112
+ }
113
+ }
114
+ });
115
+ }
116
+ /**
117
+ * Remove old generation directories, keeping only the last `keepLast`.
118
+ *
119
+ * @param {string} outDir
120
+ * @param {number} keepLast
121
+ * @returns {Effect.Effect<void, SmithersError>}
122
+ */
123
+ export function cleanupGenerationsEffect(outDir, keepLast) {
124
+ return Effect.gen(function* () {
125
+ if (!existsSync(outDir))
126
+ return;
127
+ const entries = yield* Effect.tryPromise({
128
+ try: () => readdir(outDir, { withFileTypes: true }),
129
+ catch: (cause) => toSmithersError(cause, "read hot overlay generations", {
130
+ code: "HOT_OVERLAY_FAILED",
131
+ details: { outDir, keepLast },
132
+ }),
133
+ });
134
+ const genDirs = entries
135
+ .filter((e) => e.isDirectory() && e.name.startsWith("gen-"))
136
+ .map((e) => {
137
+ const num = parseInt(e.name.slice(4), 10);
138
+ return { name: e.name, num: isNaN(num) ? -1 : num };
139
+ })
140
+ .filter((e) => e.num >= 0)
141
+ .sort((a, b) => a.num - b.num);
142
+ const toRemove = genDirs.slice(0, Math.max(0, genDirs.length - keepLast));
143
+ for (const dir of toRemove) {
144
+ yield* Effect.either(Effect.tryPromise({
145
+ try: () => rm(join(outDir, dir.name), { recursive: true, force: true }),
146
+ catch: (cause) => toSmithersError(cause, "remove stale hot overlay generation", {
147
+ code: "HOT_OVERLAY_FAILED",
148
+ details: { outDir, generationDir: dir.name },
149
+ }),
150
+ }));
151
+ }
152
+ }).pipe(Effect.annotateLogs({
153
+ outDir,
154
+ keepLast,
155
+ }), Effect.withLogSpan("hot:cleanup-generations"));
156
+ }
157
+ /**
158
+ * @param {string} outDir
159
+ * @param {number} keepLast
160
+ * @returns {Promise<void>}
161
+ */
162
+ export async function cleanupGenerations(outDir, keepLast) {
163
+ await Effect.runPromise(cleanupGenerationsEffect(outDir, keepLast));
164
+ }
165
+ /**
166
+ * Resolve the overlay entry path given the original entry path,
167
+ * the hot root, and the overlay generation directory.
168
+ *
169
+ * @param {string} entryPath
170
+ * @param {string} hotRoot
171
+ * @param {string} genDir
172
+ * @returns {string}
173
+ */
174
+ export function resolveOverlayEntry(entryPath, hotRoot, genDir) {
175
+ const rel = relative(hotRoot, entryPath);
176
+ return resolve(genDir, rel);
177
+ }
@@ -0,0 +1,174 @@
1
+ import { watch } from "node:fs";
2
+ import { readdir, stat } from "node:fs/promises";
3
+ import { resolve, relative } from "node:path";
4
+ import { Effect } from "effect";
5
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
6
+ import { logDebug, logInfo } from "@smithers-orchestrator/observability/logging";
7
+ /** @typedef {import("./WatchTreeOptions.ts").WatchTreeOptions} WatchTreeOptions */
8
+
9
+ const DEFAULT_IGNORE = [
10
+ "node_modules",
11
+ ".git",
12
+ ".jj",
13
+ ".smithers",
14
+ ];
15
+ export class WatchTree {
16
+ watchers = [];
17
+ rootDir;
18
+ ignore;
19
+ debounceMs;
20
+ changedFiles = new Set();
21
+ debounceTimer = null;
22
+ waitResolve = null;
23
+ closed = false;
24
+ /**
25
+ * @param {string} rootDir
26
+ * @param {WatchTreeOptions} [opts]
27
+ */
28
+ constructor(rootDir, opts) {
29
+ this.rootDir = resolve(rootDir);
30
+ this.ignore = opts?.ignore ?? DEFAULT_IGNORE;
31
+ this.debounceMs = opts?.debounceMs ?? 100;
32
+ }
33
+ /** Start watching. Call once. */
34
+ async start() {
35
+ await Effect.runPromise(this.startEffect());
36
+ }
37
+ /**
38
+ * Returns a promise that resolves with changed file paths
39
+ * the next time file changes are detected (after debounce).
40
+ * Can be called repeatedly.
41
+ */
42
+ wait() {
43
+ // If there are already buffered changes, resolve immediately
44
+ if (this.changedFiles.size > 0) {
45
+ const files = [...this.changedFiles];
46
+ this.changedFiles.clear();
47
+ return Promise.resolve(files);
48
+ }
49
+ return Effect.runPromise(this.waitEffect());
50
+ }
51
+ /** Stop all watchers and clean up. */
52
+ close() {
53
+ this.closed = true;
54
+ if (this.debounceTimer)
55
+ clearTimeout(this.debounceTimer);
56
+ for (const w of this.watchers) {
57
+ try {
58
+ w.close();
59
+ }
60
+ catch { }
61
+ }
62
+ this.watchers = [];
63
+ // Resolve any pending wait with empty array
64
+ if (this.waitResolve) {
65
+ this.waitResolve([]);
66
+ this.waitResolve = null;
67
+ }
68
+ logInfo("closed hot watch tree", {
69
+ rootDir: this.rootDir,
70
+ }, "hot:watch");
71
+ }
72
+ startEffect() {
73
+ return Effect.tryPromise({
74
+ try: () => this.watchDir(this.rootDir),
75
+ catch: (cause) => toSmithersError(cause, "start hot watch tree"),
76
+ }).pipe(Effect.annotateLogs({
77
+ rootDir: this.rootDir,
78
+ debounceMs: this.debounceMs,
79
+ }), Effect.withLogSpan("hot:watch-start"));
80
+ }
81
+ waitEffect() {
82
+ return Effect.async((resume) => {
83
+ if (this.changedFiles.size > 0) {
84
+ const files = [...this.changedFiles];
85
+ this.changedFiles.clear();
86
+ resume(Effect.succeed(files));
87
+ return;
88
+ }
89
+ this.waitResolve = (files) => {
90
+ resume(Effect.succeed(files));
91
+ };
92
+ return Effect.sync(() => {
93
+ if (this.waitResolve) {
94
+ this.waitResolve = null;
95
+ }
96
+ });
97
+ }).pipe(Effect.annotateLogs({
98
+ rootDir: this.rootDir,
99
+ }), Effect.withLogSpan("hot:watch-wait"));
100
+ }
101
+ /**
102
+ * @param {string} name
103
+ * @returns {boolean}
104
+ */
105
+ shouldIgnore(name) {
106
+ return this.ignore.includes(name) || name.startsWith(".");
107
+ }
108
+ /**
109
+ * @param {string} dir
110
+ * @returns {Promise<void>}
111
+ */
112
+ async watchDir(dir) {
113
+ if (this.closed)
114
+ return;
115
+ const baseName = dir.split("/").pop() ?? "";
116
+ if (baseName && this.shouldIgnore(baseName) && dir !== this.rootDir)
117
+ return;
118
+ try {
119
+ const watcher = watch(dir, (eventType, filename) => {
120
+ if (!filename || this.closed)
121
+ return;
122
+ // Ignore hidden files and ignored dirs
123
+ const parts = filename.split("/");
124
+ if (parts.some((p) => this.shouldIgnore(p)))
125
+ return;
126
+ const fullPath = resolve(dir, filename);
127
+ logDebug("hot watch tree observed file change", {
128
+ rootDir: this.rootDir,
129
+ eventType,
130
+ fullPath,
131
+ }, "hot:watch");
132
+ this.onFileChange(fullPath);
133
+ });
134
+ this.watchers.push(watcher);
135
+ // Recursively watch subdirectories
136
+ const entries = await readdir(dir, { withFileTypes: true });
137
+ for (const entry of entries) {
138
+ if (entry.isDirectory() && !this.shouldIgnore(entry.name)) {
139
+ await this.watchDir(resolve(dir, entry.name));
140
+ }
141
+ }
142
+ }
143
+ catch {
144
+ // Directory may have been deleted; ignore
145
+ }
146
+ }
147
+ /**
148
+ * @param {string} filePath
149
+ */
150
+ onFileChange(filePath) {
151
+ this.changedFiles.add(filePath);
152
+ // Debounce: reset timer on each change
153
+ if (this.debounceTimer)
154
+ clearTimeout(this.debounceTimer);
155
+ this.debounceTimer = setTimeout(() => {
156
+ this.flush();
157
+ }, this.debounceMs);
158
+ }
159
+ flush() {
160
+ if (this.changedFiles.size === 0)
161
+ return;
162
+ const files = [...this.changedFiles];
163
+ this.changedFiles.clear();
164
+ logInfo("flushing hot watch changes", {
165
+ rootDir: this.rootDir,
166
+ changedFileCount: files.length,
167
+ changedFiles: files.join(","),
168
+ }, "hot:watch");
169
+ if (this.waitResolve) {
170
+ this.waitResolve(files);
171
+ this.waitResolve = null;
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,120 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./HumanRequestKind.ts").HumanRequestKind} HumanRequestKind */
3
+ /** @typedef {import("./HumanRequestStatus.ts").HumanRequestStatus} HumanRequestStatus */
4
+ // @smithers-type-exports-end
5
+
6
+ import { jsonSchemaToZod } from "./external/json-schema-to-zod.js";
7
+ /**
8
+ * @typedef {{ ok: true; } | { ok: false; code: "HUMAN_REQUEST_SCHEMA_INVALID" | "HUMAN_REQUEST_VALIDATION_FAILED"; message: string; }} HumanRequestSchemaValidation
9
+ */
10
+
11
+ /** @type {readonly ["ask", "confirm", "select", "json"]} */
12
+ export const HUMAN_REQUEST_KINDS = ["ask", "confirm", "select", "json"];
13
+ /** @type {readonly ["pending", "answered", "cancelled", "expired"]} */
14
+ export const HUMAN_REQUEST_STATUSES = [
15
+ "pending",
16
+ "answered",
17
+ "cancelled",
18
+ "expired",
19
+ ];
20
+ /**
21
+ * @param {string} runId
22
+ * @param {string} nodeId
23
+ * @param {number} iteration
24
+ * @returns {string}
25
+ */
26
+ export function buildHumanRequestId(runId, nodeId, iteration) {
27
+ return `human:${runId}:${nodeId}:${iteration}`;
28
+ }
29
+ /**
30
+ * @param {Record<string, unknown> | null | undefined} meta
31
+ * @returns {boolean}
32
+ */
33
+ export function isHumanTaskMeta(meta) {
34
+ return Boolean(meta?.humanTask);
35
+ }
36
+ /**
37
+ * @param {Record<string, unknown> | null | undefined} meta
38
+ * @param {string} fallback
39
+ * @returns {string}
40
+ */
41
+ export function getHumanTaskPrompt(meta, fallback) {
42
+ const prompt = meta?.prompt;
43
+ return typeof prompt === "string" && prompt.trim().length > 0
44
+ ? prompt
45
+ : fallback;
46
+ }
47
+ /**
48
+ * @param {{ timeoutAtMs?: number | null } | null | undefined} request
49
+ * @returns {boolean}
50
+ */
51
+ export function isHumanRequestPastTimeout(request, nowMs = Date.now()) {
52
+ return (typeof request?.timeoutAtMs === "number" &&
53
+ Number.isFinite(request.timeoutAtMs) &&
54
+ request.timeoutAtMs <= nowMs);
55
+ }
56
+ /**
57
+ * @param {{ issues?: Array<{ path?: PropertyKey[]; message?: string }> }} error
58
+ */
59
+ function formatValidationIssues(error) {
60
+ const issues = error.issues ?? [];
61
+ if (issues.length === 0) {
62
+ return "unknown validation error";
63
+ }
64
+ return issues
65
+ .map((issue) => {
66
+ const path = Array.isArray(issue.path) && issue.path.length > 0
67
+ ? issue.path.join(".")
68
+ : "(root)";
69
+ return `${path}: ${issue.message ?? "invalid value"}`;
70
+ })
71
+ .join("; ");
72
+ }
73
+ /**
74
+ * @param {{ requestId: string; schemaJson: string | null }} request
75
+ * @param {unknown} value
76
+ * @returns {HumanRequestSchemaValidation}
77
+ */
78
+ export function validateHumanRequestValue(request, value) {
79
+ if (!request.schemaJson) {
80
+ return { ok: true };
81
+ }
82
+ let schema;
83
+ try {
84
+ schema = JSON.parse(request.schemaJson);
85
+ }
86
+ catch (err) {
87
+ return {
88
+ ok: false,
89
+ code: "HUMAN_REQUEST_SCHEMA_INVALID",
90
+ message: `Stored schema for ${request.requestId} is not valid JSON: ${err?.message ?? String(err)}`,
91
+ };
92
+ }
93
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
94
+ return {
95
+ ok: false,
96
+ code: "HUMAN_REQUEST_SCHEMA_INVALID",
97
+ message: `Stored schema for ${request.requestId} is not a JSON object.`,
98
+ };
99
+ }
100
+ let validator;
101
+ try {
102
+ validator = jsonSchemaToZod(schema);
103
+ }
104
+ catch (err) {
105
+ return {
106
+ ok: false,
107
+ code: "HUMAN_REQUEST_SCHEMA_INVALID",
108
+ message: `Stored schema for ${request.requestId} could not be loaded for validation: ${err?.message ?? String(err)}`,
109
+ };
110
+ }
111
+ const result = validator.safeParse(value);
112
+ if (!result.success) {
113
+ return {
114
+ ok: false,
115
+ code: "HUMAN_REQUEST_VALIDATION_FAILED",
116
+ message: `Human request ${request.requestId} does not match the stored schema: ${formatValidationIssues(result.error)}`,
117
+ };
118
+ }
119
+ return { ok: true };
120
+ }