surf-cli 2.19.0 → 2.20.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.
@@ -0,0 +1,65 @@
1
+ const crypto = require("node:crypto");
2
+ const { getPrivateStateRoot } = require("./private-state.cjs");
3
+ const { resolveTypeSafeCredential } = require("./semantic-credentials.cjs");
4
+ const { createJevEvaluator } = require("./semantic-provider.cjs");
5
+ const { createSemanticWorkflowRuntime, WORKFLOW_POLICY } = require("./semantic-workflow.cjs");
6
+ const { createSemanticWorkflowStateStore } = require("./semantic-workflow-state.cjs");
7
+
8
+ function createConcreteSemanticExecutor({ request, workflow, inputs = {}, env = process.env, clock = () => Date.now(), evaluate, attemptStore }) {
9
+ if (typeof request !== "function") throw new TypeError("semantic workflow browser request is required");
10
+ if (!evaluate) {
11
+ const credential = resolveTypeSafeCredential(env);
12
+ if (!credential) {
13
+ const error = new Error("TypeSafe API key is not configured; run `surf semantic auth set` or set TYPESAFE_API_KEY");
14
+ error.code = "provider_not_configured";
15
+ throw error;
16
+ }
17
+ evaluate = createJevEvaluator({ apiKey: credential.apiKey, env });
18
+ }
19
+ const digest = crypto.createHash("sha256").update(JSON.stringify(workflow)).digest("hex");
20
+ const createAttemptStore = attemptStore ? undefined : ({ runId, workflowDigest }) =>
21
+ createSemanticWorkflowStateStore({ root: getPrivateStateRoot(env), clock, runId, workflowDigest });
22
+ const runtime = createSemanticWorkflowRuntime({
23
+ request,
24
+ evaluate,
25
+ attemptStore,
26
+ createAttemptStore,
27
+ now: clock,
28
+ });
29
+ const context = runtime.createContext({
30
+ workflowDigest: digest,
31
+ deadlineMs: workflow.semantic?.deadlineMs,
32
+ maxProviderCalls: workflow.semantic?.maxProviderCalls,
33
+ inputs,
34
+ });
35
+ let failed = false;
36
+ const execute = async (step) => {
37
+ const result = await runtime.executeStep({ id: step.id, as: step.as, ...step.args }, context);
38
+ if (result.kind !== "success") failed = true;
39
+ return {
40
+ ...result,
41
+ semantic: {
42
+ runId: context.runId,
43
+ stepId: step.id,
44
+ checkpoint: context.runId,
45
+ ...(context.model ? { model: context.model } : {}),
46
+ ...(result.reason ? { reason: result.reason } : {}),
47
+ ...(result.write ? { write: result.write } : {}),
48
+ ...(result.coverage ? { coverage: result.coverage } : {}),
49
+ usage: { ...context.usage },
50
+ limits: {
51
+ ...context.limits,
52
+ maxSearchObservations: step.args.search?.maxObservations ?? WORKFLOW_POLICY.defaultSearchObservations,
53
+ maxSearchObservationsCeiling: WORKFLOW_POLICY.maxSearchObservations,
54
+ },
55
+ },
56
+ ...(result.binding || result.coverage || result.probability !== undefined
57
+ ? { publicResult: { binding: result.binding, coverage: result.coverage, probability: result.probability } }
58
+ : {}),
59
+ };
60
+ };
61
+ execute.close = async () => runtime.closeContext(context, failed ? { reason: "workflow_failed", state: "failed" } : { state: "completed" });
62
+ return execute;
63
+ }
64
+
65
+ module.exports = { createConcreteSemanticExecutor };
@@ -0,0 +1,271 @@
1
+ const crypto = require("crypto");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const {
5
+ atomicWriteJson,
6
+ ensurePrivateDir,
7
+ readPrivateJson,
8
+ removePrivateFile,
9
+ writePrivateFileExclusive,
10
+ } = require("./private-state.cjs");
11
+
12
+ const VERSION = 1;
13
+ const TERMINAL_ATTEMPT_STATES = new Set([
14
+ "not_dispatched",
15
+ "outcome_unknown",
16
+ "acknowledged_unverified",
17
+ "verified",
18
+ ]);
19
+ const TERMINAL_RUN_STATES = new Set(["completed", "failed", "cancelled", "outcome_unknown"]);
20
+ const TRANSITIONS = Object.freeze({
21
+ reserved: new Set(["not_dispatched", "dispatch_intent"]),
22
+ dispatch_intent: new Set(["outcome_unknown", "acknowledged_unverified", "verified"]),
23
+ });
24
+
25
+ function boundedString(value, name, max = 256) {
26
+ if (typeof value !== "string" || value.length < 1 || value.length > max || /[\u0000-\u001f\u007f]/.test(value)) {
27
+ throw new Error(`${name} must be a bounded non-control string`);
28
+ }
29
+ return value;
30
+ }
31
+
32
+ function safeId(value, name) {
33
+ const result = boundedString(value, name, 128);
34
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(result)) throw new Error(`${name} contains unsupported characters`);
35
+ return result;
36
+ }
37
+
38
+ function timestamp(clock) {
39
+ const value = clock();
40
+ const date = value instanceof Date ? value : new Date(value);
41
+ if (!Number.isFinite(date.getTime())) throw new Error("clock returned an invalid time");
42
+ return date.toISOString();
43
+ }
44
+
45
+ function fingerprint(value) {
46
+ return crypto.createHash("sha256").update(String(value)).digest("hex");
47
+ }
48
+
49
+ function fingerprintOptional(value) {
50
+ return value === undefined || value === null || value === "" ? undefined : fingerprint(value);
51
+ }
52
+
53
+ function sanitizeBudgets(budgets = {}) {
54
+ if (budgets === null || typeof budgets !== "object" || Array.isArray(budgets)) {
55
+ throw new Error("budgets must be an object");
56
+ }
57
+ const entries = Object.entries(budgets);
58
+ if (entries.length > 16) throw new Error("too many budget counters");
59
+ return Object.fromEntries(entries.map(([name, value]) => {
60
+ if (!/^[A-Za-z][A-Za-z0-9]{0,31}$/.test(name)) throw new Error(`invalid budget name: ${name}`);
61
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
62
+ throw new Error(`budget ${name} must be a non-negative finite number`);
63
+ }
64
+ return [name, value];
65
+ }));
66
+ }
67
+
68
+ function sanitizeTarget(target = {}) {
69
+ if (target === null || typeof target !== "object" || Array.isArray(target)) {
70
+ throw new Error("target provenance must be an object");
71
+ }
72
+ const entries = Object.entries(target);
73
+ if (entries.length > 12) throw new Error("target provenance has too many fields");
74
+ return Object.fromEntries(entries.map(([name, value]) => {
75
+ if (!/^[A-Za-z][A-Za-z0-9]{0,31}$/.test(name)) throw new Error(`invalid target provenance field: ${name}`);
76
+ if (!["string", "number", "boolean"].includes(typeof value)) {
77
+ throw new Error(`target provenance ${name} must be scalar`);
78
+ }
79
+ return [name, fingerprint(boundedString(String(value), `target provenance ${name}`, 1024))];
80
+ }));
81
+ }
82
+
83
+ function createSemanticWorkflowStateStore(options = {}) {
84
+ const root = path.resolve(boundedString(options.root, "root", 4096));
85
+ const clock = options.clock || Date.now;
86
+ if (typeof clock !== "function") throw new Error("clock must be a function");
87
+ const runId = safeId(options.runId, "runId");
88
+ const workflowDigest = boundedString(options.workflowDigest, "workflowDigest", 256);
89
+ const runKey = fingerprint(`${runId}\0${workflowDigest}`);
90
+ const directory = path.join(root, "semantic-workflows", runKey);
91
+ const ownerPath = path.join(directory, "owner.json");
92
+ const runPath = path.join(directory, "run.json");
93
+ const checkpointPath = path.join(directory, "checkpoint.json");
94
+ const attemptsDirectory = path.join(directory, "attempts");
95
+ const ownerId = crypto.randomUUID();
96
+ let owned = false;
97
+ let released = false;
98
+
99
+ function requireOwner() {
100
+ if (!owned || released) throw new Error("semantic workflow run is not owned by this store");
101
+ const owner = readPrivateJson(ownerPath, null, { root });
102
+ if (!owner || owner.ownerId !== ownerId) throw new Error("semantic workflow run ownership was lost");
103
+ }
104
+
105
+ function attemptPath(attemptId) {
106
+ return path.join(attemptsDirectory, `${safeId(attemptId, "attemptId")}.json`);
107
+ }
108
+
109
+ function loadAttempt(attemptId) {
110
+ const attempt = readPrivateJson(attemptPath(attemptId), null, { root });
111
+ if (!attempt) throw new Error(`write attempt is missing: ${attemptId}`);
112
+ return attempt;
113
+ }
114
+
115
+ function persistAttempt(attempt) {
116
+ atomicWriteJson(attemptPath(attempt.attemptId), attempt, { root });
117
+ return attempt;
118
+ }
119
+
120
+ function acquire() {
121
+ if (owned && !released) throw new Error("semantic workflow run is already owned by this store");
122
+ if (released) throw new Error("released semantic workflow store cannot be reacquired");
123
+ ensurePrivateDir(attemptsDirectory, root);
124
+ const createdAt = timestamp(clock);
125
+ const owner = { version: VERSION, ownerId, runId, workflowDigest, createdAt };
126
+ try {
127
+ writePrivateFileExclusive(ownerPath, `${JSON.stringify(owner, null, 2)}\n`, { root, encoding: "utf8" });
128
+ } catch (error) {
129
+ if (error?.code === "EEXIST") throw new Error(`semantic workflow run is already owned: ${runId}`);
130
+ throw error;
131
+ }
132
+ try {
133
+ const existingRun = readPrivateJson(runPath, null, { root });
134
+ if (existingRun) throw new Error(`semantic workflow run already exists in state ${existingRun.state}`);
135
+ atomicWriteJson(runPath, { version: VERSION, runId, workflowDigest, state: "running", createdAt, updatedAt: createdAt }, { root });
136
+ } catch (error) {
137
+ try { removePrivateFile(ownerPath, { root }); } catch {}
138
+ throw error;
139
+ }
140
+ owned = true;
141
+ return { runId, workflowDigest, ownerId };
142
+ }
143
+
144
+ function reserve({ attemptId = crypto.randomUUID(), stepId, operation, target = {}, budgets = {} } = {}) {
145
+ requireOwner();
146
+ const id = safeId(attemptId, "attemptId");
147
+ const now = timestamp(clock);
148
+ const attempt = {
149
+ version: VERSION,
150
+ runId,
151
+ workflowDigest,
152
+ attemptId: id,
153
+ stepId: safeId(stepId, "stepId"),
154
+ operation: safeId(operation, "operation"),
155
+ target: sanitizeTarget(target),
156
+ budgets: sanitizeBudgets(budgets),
157
+ state: "reserved",
158
+ createdAt: now,
159
+ updatedAt: now,
160
+ };
161
+ writePrivateFileExclusive(attemptPath(id), `${JSON.stringify(attempt, null, 2)}\n`, { root, encoding: "utf8" });
162
+ return attempt;
163
+ }
164
+
165
+ function transition(attemptId, nextState, details = {}) {
166
+ requireOwner();
167
+ const current = loadAttempt(attemptId);
168
+ if (!TRANSITIONS[current.state]?.has(nextState)) {
169
+ throw new Error(`invalid write attempt transition: ${current.state} -> ${nextState}`);
170
+ }
171
+ const updated = {
172
+ ...current,
173
+ state: nextState,
174
+ updatedAt: timestamp(clock),
175
+ budgets: sanitizeBudgets(details.budgets ?? current.budgets),
176
+ };
177
+ const reasonFingerprint = fingerprintOptional(details.reason);
178
+ const errorFingerprint = fingerprintOptional(details.error);
179
+ if (reasonFingerprint) updated.reasonFingerprint = reasonFingerprint;
180
+ if (errorFingerprint) updated.errorFingerprint = errorFingerprint;
181
+ return persistAttempt(updated);
182
+ }
183
+
184
+ function dispatchIntent(attemptId, details = {}) {
185
+ return transition(attemptId, "dispatch_intent", details);
186
+ }
187
+
188
+ function terminal(attemptId, state, details = {}) {
189
+ if (!TERMINAL_ATTEMPT_STATES.has(state)) throw new Error(`invalid terminal write attempt state: ${state}`);
190
+ return transition(attemptId, state, details);
191
+ }
192
+
193
+ function checkpoint({ completedSteps = [], reason, error, budgets = {} } = {}) {
194
+ requireOwner();
195
+ if (!Array.isArray(completedSteps) || completedSteps.length > 32) {
196
+ throw new Error("completedSteps must be an array of at most 32 step IDs");
197
+ }
198
+ const value = {
199
+ version: VERSION,
200
+ runId,
201
+ workflowDigest,
202
+ completedSteps: completedSteps.map((stepId) => safeId(stepId, "completed step ID")),
203
+ budgets: sanitizeBudgets(budgets),
204
+ updatedAt: timestamp(clock),
205
+ };
206
+ const reasonFingerprint = fingerprintOptional(reason);
207
+ const errorFingerprint = fingerprintOptional(error);
208
+ if (reasonFingerprint) value.reasonFingerprint = reasonFingerprint;
209
+ if (errorFingerprint) value.errorFingerprint = errorFingerprint;
210
+ atomicWriteJson(checkpointPath, value, { root });
211
+ return value;
212
+ }
213
+
214
+ function read() {
215
+ const run = readPrivateJson(runPath, null, { root });
216
+ const savedCheckpoint = readPrivateJson(checkpointPath, null, { root });
217
+ let attempts = [];
218
+ try {
219
+ attempts = fs.readdirSync(attemptsDirectory)
220
+ .filter((name) => name.endsWith(".json"))
221
+ .sort()
222
+ .map((name) => readPrivateJson(path.join(attemptsDirectory, name), null, { root }))
223
+ .filter(Boolean)
224
+ .map((attempt) => attempt.state === "dispatch_intent"
225
+ ? { ...attempt, effectiveState: "outcome_unknown" }
226
+ : { ...attempt, effectiveState: attempt.state });
227
+ } catch (error) {
228
+ if (error?.code !== "ENOENT") throw error;
229
+ }
230
+ return { run, checkpoint: savedCheckpoint, attempts };
231
+ }
232
+
233
+ function release({ state, reason, error, budgets = {} } = {}) {
234
+ requireOwner();
235
+ if (!TERMINAL_RUN_STATES.has(state)) throw new Error(`invalid terminal run state: ${state}`);
236
+ const snapshot = read();
237
+ const nonterminal = snapshot.attempts.find((attempt) => !TERMINAL_ATTEMPT_STATES.has(attempt.state));
238
+ if (nonterminal) throw new Error(`cannot release run with nonterminal attempt: ${nonterminal.attemptId}`);
239
+ const current = snapshot.run;
240
+ if (!current || current.state !== "running") throw new Error("semantic workflow run record is not running");
241
+ const updated = {
242
+ ...current,
243
+ state,
244
+ budgets: sanitizeBudgets(budgets),
245
+ updatedAt: timestamp(clock),
246
+ };
247
+ const reasonFingerprint = fingerprintOptional(reason);
248
+ const errorFingerprint = fingerprintOptional(error);
249
+ if (reasonFingerprint) updated.reasonFingerprint = reasonFingerprint;
250
+ if (errorFingerprint) updated.errorFingerprint = errorFingerprint;
251
+ atomicWriteJson(runPath, updated, { root });
252
+ removePrivateFile(ownerPath, { root });
253
+ released = true;
254
+ return updated;
255
+ }
256
+
257
+ return {
258
+ acquire,
259
+ reserve,
260
+ dispatchIntent,
261
+ terminal,
262
+ checkpoint,
263
+ read,
264
+ inspect: read,
265
+ release,
266
+ };
267
+ }
268
+
269
+ module.exports = {
270
+ createSemanticWorkflowStateStore,
271
+ };