@sweny-ai/core 0.1.73 → 0.1.75
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/dist/__tests__/executor.test.js +196 -1
- package/dist/__tests__/schema.test.js +68 -1
- package/dist/__tests__/verify.test.d.ts +1 -0
- package/dist/__tests__/verify.test.js +517 -0
- package/dist/executor.js +10 -30
- package/dist/schema.d.ts +284 -0
- package/dist/schema.js +21 -2
- package/dist/types.d.ts +31 -12
- package/dist/verify.d.ts +23 -0
- package/dist/verify.js +309 -0
- package/package.json +2 -2
package/dist/verify.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// ─── Verify: path resolver + check evaluators ───────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Evaluates `node.verify` post-conditions after the LLM finishes.
|
|
4
|
+
// All checks are AND-ed; failures are aggregated into one error string.
|
|
5
|
+
const SEGMENT_RE = /^([a-zA-Z_][a-zA-Z0-9_]*)(\[\*\])?$/;
|
|
6
|
+
const PREFIX_RE = /^([a-z]+):/;
|
|
7
|
+
function parsePath(path) {
|
|
8
|
+
if (path.length === 0)
|
|
9
|
+
return "empty path";
|
|
10
|
+
let mode = "all";
|
|
11
|
+
let body = path;
|
|
12
|
+
const prefixMatch = PREFIX_RE.exec(path);
|
|
13
|
+
if (prefixMatch) {
|
|
14
|
+
const prefix = prefixMatch[1];
|
|
15
|
+
if (prefix !== "all" && prefix !== "any") {
|
|
16
|
+
return `unknown prefix '${prefix}' (expected 'all' or 'any')`;
|
|
17
|
+
}
|
|
18
|
+
mode = prefix;
|
|
19
|
+
body = path.slice(prefixMatch[0].length);
|
|
20
|
+
if (body.length === 0)
|
|
21
|
+
return "empty path after prefix";
|
|
22
|
+
}
|
|
23
|
+
const parts = body.split(".");
|
|
24
|
+
const segments = [];
|
|
25
|
+
for (const part of parts) {
|
|
26
|
+
const m = SEGMENT_RE.exec(part);
|
|
27
|
+
if (!m)
|
|
28
|
+
return `malformed segment '${part}'`;
|
|
29
|
+
segments.push({ name: m[1], wildcard: m[2] === "[*]" });
|
|
30
|
+
}
|
|
31
|
+
return { mode, segments };
|
|
32
|
+
}
|
|
33
|
+
export function resolvePath(data, path) {
|
|
34
|
+
const parsed = parsePath(path);
|
|
35
|
+
if (typeof parsed === "string")
|
|
36
|
+
return { ok: false, reason: parsed };
|
|
37
|
+
let current = [data];
|
|
38
|
+
for (const seg of parsed.segments) {
|
|
39
|
+
const next = [];
|
|
40
|
+
for (const v of current) {
|
|
41
|
+
if (v === null)
|
|
42
|
+
return { ok: false, reason: `null encountered before segment '${seg.name}'` };
|
|
43
|
+
if (typeof v !== "object") {
|
|
44
|
+
return { ok: false, reason: `expected object before segment '${seg.name}', got ${typeof v}` };
|
|
45
|
+
}
|
|
46
|
+
const obj = v;
|
|
47
|
+
if (!(seg.name in obj)) {
|
|
48
|
+
return { ok: false, reason: `missing segment '${seg.name}'` };
|
|
49
|
+
}
|
|
50
|
+
const child = obj[seg.name];
|
|
51
|
+
if (seg.wildcard) {
|
|
52
|
+
if (!Array.isArray(child)) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
reason: `expected array at '${seg.name}', got ${child === null ? "null" : typeof child}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
for (const el of child)
|
|
59
|
+
next.push(el);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
next.push(child);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
current = next;
|
|
66
|
+
if (current.length === 0) {
|
|
67
|
+
return { ok: true, mode: parsed.mode, values: [] };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, mode: parsed.mode, values: current };
|
|
71
|
+
}
|
|
72
|
+
// A tool call "succeeded" when its output does not carry a non-null `error` field.
|
|
73
|
+
// `{ error: null }` and `{ error: false }` are treated as success — many real
|
|
74
|
+
// tools include the key with a null sentinel.
|
|
75
|
+
function isErrorOutput(output) {
|
|
76
|
+
if (!output || typeof output !== "object")
|
|
77
|
+
return false;
|
|
78
|
+
const err = output.error;
|
|
79
|
+
return err !== undefined && err !== null && err !== false;
|
|
80
|
+
}
|
|
81
|
+
function succeededTools(toolCalls) {
|
|
82
|
+
const names = new Set();
|
|
83
|
+
for (const c of toolCalls) {
|
|
84
|
+
if (!isErrorOutput(c.output))
|
|
85
|
+
names.add(c.tool);
|
|
86
|
+
}
|
|
87
|
+
return names;
|
|
88
|
+
}
|
|
89
|
+
function calledList(toolCalls) {
|
|
90
|
+
return toolCalls.map((c) => c.tool).join(", ") || "none";
|
|
91
|
+
}
|
|
92
|
+
export function checkAnyToolCalled(required, toolCalls) {
|
|
93
|
+
const succeeded = succeededTools(toolCalls);
|
|
94
|
+
if (required.some((t) => succeeded.has(t)))
|
|
95
|
+
return null;
|
|
96
|
+
return `any_tool_called: required one of [${required.join(", ")}] to succeed, called: [${calledList(toolCalls)}]`;
|
|
97
|
+
}
|
|
98
|
+
export function checkAllToolsCalled(required, toolCalls) {
|
|
99
|
+
const succeeded = succeededTools(toolCalls);
|
|
100
|
+
const missing = required.filter((t) => !succeeded.has(t));
|
|
101
|
+
if (missing.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
return `all_tools_called: missing successful calls to [${missing.join(", ")}], called: [${calledList(toolCalls)}]`;
|
|
104
|
+
}
|
|
105
|
+
export function checkNoToolCalled(forbidden, toolCalls) {
|
|
106
|
+
const calledNames = new Set(toolCalls.map((c) => c.tool));
|
|
107
|
+
const violated = forbidden.filter((t) => calledNames.has(t));
|
|
108
|
+
if (violated.length === 0)
|
|
109
|
+
return null;
|
|
110
|
+
return `no_tool_called: forbidden tools were invoked: [${violated.join(", ")}], called: [${calledList(toolCalls)}]`;
|
|
111
|
+
}
|
|
112
|
+
// Structural deep equality for JSON-shaped values. Handles object key-order
|
|
113
|
+
// (compares by key set, not insertion order), NaN (NaN === NaN here), arrays
|
|
114
|
+
// (element-wise), and treats `undefined` distinctly from missing keys. Does
|
|
115
|
+
// not handle Date/Map/Set/RegExp — verify only ever sees agent-produced JSON.
|
|
116
|
+
function deepEqual(a, b) {
|
|
117
|
+
if (Object.is(a, b))
|
|
118
|
+
return true;
|
|
119
|
+
if (typeof a !== typeof b)
|
|
120
|
+
return false;
|
|
121
|
+
if (a === null || b === null)
|
|
122
|
+
return false;
|
|
123
|
+
if (typeof a !== "object")
|
|
124
|
+
return false;
|
|
125
|
+
if (Array.isArray(a)) {
|
|
126
|
+
if (!Array.isArray(b) || a.length !== b.length)
|
|
127
|
+
return false;
|
|
128
|
+
for (let i = 0; i < a.length; i++) {
|
|
129
|
+
if (!deepEqual(a[i], b[i]))
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(b))
|
|
135
|
+
return false;
|
|
136
|
+
const ao = a;
|
|
137
|
+
const bo = b;
|
|
138
|
+
const aKeys = Object.keys(ao);
|
|
139
|
+
const bKeys = Object.keys(bo);
|
|
140
|
+
if (aKeys.length !== bKeys.length)
|
|
141
|
+
return false;
|
|
142
|
+
for (const k of aKeys) {
|
|
143
|
+
if (!Object.prototype.hasOwnProperty.call(bo, k))
|
|
144
|
+
return false;
|
|
145
|
+
if (!deepEqual(ao[k], bo[k]))
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function formatValue(v) {
|
|
151
|
+
if (v === null)
|
|
152
|
+
return "null";
|
|
153
|
+
if (v === undefined)
|
|
154
|
+
return "undefined";
|
|
155
|
+
return JSON.stringify(v);
|
|
156
|
+
}
|
|
157
|
+
export function checkOutputRequired(paths, data) {
|
|
158
|
+
const failures = [];
|
|
159
|
+
for (const path of paths) {
|
|
160
|
+
const r = resolvePath(data, path);
|
|
161
|
+
if (!r.ok) {
|
|
162
|
+
failures.push(`'${path}' ${r.reason}`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (r.mode === "all") {
|
|
166
|
+
if (r.values.length > 0 && !r.values.every((v) => v !== null && v !== undefined)) {
|
|
167
|
+
failures.push(`'${path}' resolved to null/undefined value(s)`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
if (r.values.length === 0) {
|
|
172
|
+
failures.push(`'${path}' (any:) resolved to no elements`);
|
|
173
|
+
}
|
|
174
|
+
else if (!r.values.some((v) => v !== null && v !== undefined)) {
|
|
175
|
+
failures.push(`'${path}' (any:) all resolved values are null/undefined`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (failures.length === 0)
|
|
180
|
+
return null;
|
|
181
|
+
return `output_required: ${failures.join("; ")}`;
|
|
182
|
+
}
|
|
183
|
+
function compileMatch(m) {
|
|
184
|
+
if (m.matches !== undefined) {
|
|
185
|
+
// Throws synchronously if the regex source is invalid; surfaced as a
|
|
186
|
+
// verify failure rather than a crash because the executor catches.
|
|
187
|
+
return { match: m, regex: new RegExp(m.matches) };
|
|
188
|
+
}
|
|
189
|
+
return { match: m };
|
|
190
|
+
}
|
|
191
|
+
function applyOperator(c, value) {
|
|
192
|
+
const m = c.match;
|
|
193
|
+
if (m.equals !== undefined) {
|
|
194
|
+
if (deepEqual(value, m.equals))
|
|
195
|
+
return { ok: true };
|
|
196
|
+
return { ok: false, reason: `equals ${formatValue(m.equals)}, got ${formatValue(value)}` };
|
|
197
|
+
}
|
|
198
|
+
if (m.in !== undefined) {
|
|
199
|
+
if (m.in.some((x) => deepEqual(value, x)))
|
|
200
|
+
return { ok: true };
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
reason: `in [${m.in.map(formatValue).join(", ")}], got ${formatValue(value)}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (c.regex) {
|
|
207
|
+
const s = typeof value === "string" ? value : JSON.stringify(value);
|
|
208
|
+
if (c.regex.test(s))
|
|
209
|
+
return { ok: true };
|
|
210
|
+
return { ok: false, reason: `matches /${m.matches}/, got ${formatValue(value)}` };
|
|
211
|
+
}
|
|
212
|
+
return { ok: false, reason: "no operator declared (zod should have caught this)" };
|
|
213
|
+
}
|
|
214
|
+
export function checkOutputMatches(matches, data) {
|
|
215
|
+
const failures = [];
|
|
216
|
+
for (const m of matches) {
|
|
217
|
+
let compiled;
|
|
218
|
+
try {
|
|
219
|
+
compiled = compileMatch(m);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
failures.push(`'${m.path}' invalid regex /${m.matches}/: ${err.message}`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const r = resolvePath(data, m.path);
|
|
226
|
+
if (!r.ok) {
|
|
227
|
+
failures.push(`'${m.path}' ${r.reason}`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (r.mode === "all") {
|
|
231
|
+
if (r.values.length === 0)
|
|
232
|
+
continue;
|
|
233
|
+
const offenders = [];
|
|
234
|
+
for (const v of r.values) {
|
|
235
|
+
const res = applyOperator(compiled, v);
|
|
236
|
+
if (!res.ok)
|
|
237
|
+
offenders.push(formatValue(v));
|
|
238
|
+
}
|
|
239
|
+
if (offenders.length > 0) {
|
|
240
|
+
const opDescription = describeOperator(compiled);
|
|
241
|
+
failures.push(`'${m.path}' ${opDescription}, got [${offenders.join(", ")}]`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
if (r.values.length === 0) {
|
|
246
|
+
failures.push(`'${m.path}' (any:) resolved to no elements`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const anyOk = r.values.some((v) => applyOperator(compiled, v).ok);
|
|
250
|
+
if (!anyOk) {
|
|
251
|
+
const opDescription = describeOperator(compiled);
|
|
252
|
+
failures.push(`'${m.path}' (any:) no element satisfies ${opDescription}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (failures.length === 0)
|
|
257
|
+
return null;
|
|
258
|
+
return `output_matches: ${failures.join("; ")}`;
|
|
259
|
+
}
|
|
260
|
+
function describeOperator(c) {
|
|
261
|
+
const m = c.match;
|
|
262
|
+
if (m.equals !== undefined)
|
|
263
|
+
return `equals ${formatValue(m.equals)}`;
|
|
264
|
+
if (m.in !== undefined)
|
|
265
|
+
return `in [${m.in.map(formatValue).join(", ")}]`;
|
|
266
|
+
if (m.matches !== undefined)
|
|
267
|
+
return `matches /${m.matches}/`;
|
|
268
|
+
return "no operator";
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Evaluate every declared check and return a concatenated error string,
|
|
272
|
+
* or null when the node passes verification (or has no verify block).
|
|
273
|
+
*
|
|
274
|
+
* Deterministic and side-effect free — the executor calls this synchronously
|
|
275
|
+
* after the LLM finishes.
|
|
276
|
+
*/
|
|
277
|
+
export function evaluateVerify(verify, result) {
|
|
278
|
+
if (!verify)
|
|
279
|
+
return null;
|
|
280
|
+
const failures = [];
|
|
281
|
+
if (verify.any_tool_called && verify.any_tool_called.length > 0) {
|
|
282
|
+
const e = checkAnyToolCalled(verify.any_tool_called, result.toolCalls);
|
|
283
|
+
if (e)
|
|
284
|
+
failures.push(e);
|
|
285
|
+
}
|
|
286
|
+
if (verify.all_tools_called && verify.all_tools_called.length > 0) {
|
|
287
|
+
const e = checkAllToolsCalled(verify.all_tools_called, result.toolCalls);
|
|
288
|
+
if (e)
|
|
289
|
+
failures.push(e);
|
|
290
|
+
}
|
|
291
|
+
if (verify.no_tool_called && verify.no_tool_called.length > 0) {
|
|
292
|
+
const e = checkNoToolCalled(verify.no_tool_called, result.toolCalls);
|
|
293
|
+
if (e)
|
|
294
|
+
failures.push(e);
|
|
295
|
+
}
|
|
296
|
+
if (verify.output_required && verify.output_required.length > 0) {
|
|
297
|
+
const e = checkOutputRequired(verify.output_required, result.data);
|
|
298
|
+
if (e)
|
|
299
|
+
failures.push(e);
|
|
300
|
+
}
|
|
301
|
+
if (verify.output_matches && verify.output_matches.length > 0) {
|
|
302
|
+
const e = checkOutputMatches(verify.output_matches, result.data);
|
|
303
|
+
if (e)
|
|
304
|
+
failures.push(e);
|
|
305
|
+
}
|
|
306
|
+
if (failures.length === 0)
|
|
307
|
+
return null;
|
|
308
|
+
return `verify failed:\n - ${failures.join("\n - ")}`;
|
|
309
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sweny-ai/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.75",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sweny": "./dist/cli/main.js"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"test:integration": "vitest run --config vitest.integration.config.ts"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
55
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.114",
|
|
56
56
|
"@clack/prompts": "^1.2.0",
|
|
57
57
|
"chalk": "^5",
|
|
58
58
|
"commander": "^13",
|