@sweny-ai/core 0.1.213 → 0.1.215
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/executor.d.ts +4 -18
- package/dist/executor.js +64 -154
- package/dist/harness/acp.js +15 -8
- package/dist/loader.js +4 -2
- package/dist/output-contract-draft06.d.ts +7 -0
- package/dist/output-contract-draft06.js +210 -0
- package/dist/output-contract.d.ts +92 -0
- package/dist/output-contract.js +374 -0
- package/dist/schema.d.ts +16 -1
- package/dist/schema.js +17 -1
- package/dist/types.d.ts +6 -0
- package/package.json +3 -2
package/dist/executor.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* This replaces ~8k lines of engine + recipe step code.
|
|
14
14
|
*/
|
|
15
|
-
import type { Workflow, Node, Skill, Tool, Claude, Observer, NodeResult, Logger, ExecutionResult,
|
|
15
|
+
import type { Workflow, Node, Skill, Tool, Claude, Observer, NodeResult, Logger, ExecutionResult, EvalResult, Evaluator, ToolCall } from "./types.js";
|
|
16
16
|
import type { ToolAliases } from "./eval/index.js";
|
|
17
17
|
import type { HarnessPolicyMode } from "./harness/policy.js";
|
|
18
18
|
import type { AgentHarness } from "./harness/types.js";
|
|
@@ -20,6 +20,7 @@ import type { DeciderOperatorConfig } from "./decider.js";
|
|
|
20
20
|
import type { Budget } from "./budget.js";
|
|
21
21
|
import { type ActorInfo } from "./safe-outputs.js";
|
|
22
22
|
import { type ExecutionJournal } from "./journal.js";
|
|
23
|
+
import { COUNT_OF_KEYWORD, outputContractProblems } from "./output-contract.js";
|
|
23
24
|
export interface ExecuteOptions {
|
|
24
25
|
/** Registered skills (id → Skill) */
|
|
25
26
|
skills: Map<string, Skill>;
|
|
@@ -175,22 +176,8 @@ export declare function deciderRoutingState(workflow: Workflow, current: string,
|
|
|
175
176
|
* @internal
|
|
176
177
|
*/
|
|
177
178
|
export declare function buildBoundedContext(workflow: Workflow, nodeId: string, results: Map<string, NodeResult>, input: unknown, instruction: string): Record<string, unknown>;
|
|
178
|
-
|
|
179
|
-
export
|
|
180
|
-
field: string;
|
|
181
|
-
kind: "missing" | "invalid";
|
|
182
|
-
problem: string;
|
|
183
|
-
}
|
|
184
|
-
/** Keyword on an integer output property: the count sweny cross-checks against an array in the same output. */
|
|
185
|
-
export declare const COUNT_OF_KEYWORD = "x-sweny-count-of";
|
|
186
|
-
/**
|
|
187
|
-
* Check emitted data against the declared output contract (#357): every
|
|
188
|
-
* `required` field present; every present top-level property of its declared
|
|
189
|
-
* `type` and inside its `enum`; and every `x-sweny-count-of` integer equal to
|
|
190
|
-
* the count it declares over its array. Not full JSON Schema: formats,
|
|
191
|
-
* nested shapes and additionalProperties stay the author's concern.
|
|
192
|
-
*/
|
|
193
|
-
export declare function outputContractProblems(data: unknown, output: JSONSchema | undefined): ContractProblem[];
|
|
179
|
+
export { COUNT_OF_KEYWORD, outputContractProblems };
|
|
180
|
+
export type { ContractProblem } from "./output-contract.js";
|
|
194
181
|
/**
|
|
195
182
|
* Required tools the agent never called, when that is the only reason a node
|
|
196
183
|
* failed its evals and another turn could call them: every failure is a
|
|
@@ -216,4 +203,3 @@ export declare function isReadTool(tool: Pick<Tool, "access">): boolean;
|
|
|
216
203
|
* any side effect.
|
|
217
204
|
*/
|
|
218
205
|
export declare function guardStagedWrite(tool: Pick<Tool, "name" | "access">, staged: boolean): void;
|
|
219
|
-
export {};
|
package/dist/executor.js
CHANGED
|
@@ -37,6 +37,7 @@ import { redact, runSecretValues } from "./journal.js";
|
|
|
37
37
|
import { buildNodePolicy, hasNodePolicy, resolveNodePermissions } from "./node-policy.js";
|
|
38
38
|
import { applySafeOutputs, createEmitOutputTool, createWriteStageState, resolveActor, safeOutputsInstruction, unresolvedOutputs, SAFE_OUTPUT_SCREEN_INSTRUCTION, } from "./safe-outputs.js";
|
|
39
39
|
import { JournalWriteError } from "./journal.js";
|
|
40
|
+
import { COUNT_OF_KEYWORD, checkOutputContract, contractFailureMessage, contractPaths, listContractProblems, outputContractProblems, } from "./output-contract.js";
|
|
40
41
|
/**
|
|
41
42
|
* Default workflow-level step budget. Counts total node executions across the
|
|
42
43
|
* whole run (loop iterations included). Generous enough for legitimate deep
|
|
@@ -119,7 +120,7 @@ async function executeRun(workflow, input, options, results, trace, at) {
|
|
|
119
120
|
const nodeRunCounts = new Map(); // node → times executed
|
|
120
121
|
const maxSteps = options.max_steps ?? DEFAULT_MAX_STEPS;
|
|
121
122
|
let stepCount = 0; // total node executions across the run (loop iterations included)
|
|
122
|
-
validate(workflow, skills);
|
|
123
|
+
validate(workflow, skills, logger);
|
|
123
124
|
// Safe outputs (#365): run-wide caps and dedupe, the staged switch and the
|
|
124
125
|
// triggering actor, resolved once per run.
|
|
125
126
|
const writeState = createWriteStageState();
|
|
@@ -768,6 +769,8 @@ async function executeRun(workflow, input, options, results, trace, at) {
|
|
|
768
769
|
context,
|
|
769
770
|
tools: trackedTools,
|
|
770
771
|
outputSchema: harnessSchema(node.output),
|
|
772
|
+
// The executor checks the output and asks for one repair itself, the same on every harness.
|
|
773
|
+
...(node.output ? { callerRepairsOutput: true } : {}),
|
|
771
774
|
maxTurns: node.max_turns,
|
|
772
775
|
disallowedTools: node.disallowed_tools,
|
|
773
776
|
...(denyClasses.length > 0 ? { deny: denyClasses } : {}),
|
|
@@ -854,73 +857,69 @@ async function executeRun(workflow, input, options, results, trace, at) {
|
|
|
854
857
|
agentRunFailed = true;
|
|
855
858
|
break;
|
|
856
859
|
}
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
//
|
|
863
|
-
//
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
// stay the workflow author's problem and don't have the same
|
|
869
|
-
// routing-correctness blast radius.
|
|
870
|
-
// A missing required field is a contract violation handled on the same
|
|
871
|
-
// footing as an eval failure: an otherwise-recoverable one-off omission
|
|
872
|
-
// should self-heal on a second attempt when the node has `retry`
|
|
873
|
-
// configured. We synthesize an eval-shaped failure so the retry preamble
|
|
874
|
-
// and trace bookkeeping below treat it identically. When no retry is
|
|
875
|
-
// configured (or the budget is exhausted), it still hard-fails the node.
|
|
876
|
-
// Output contract (#357): required fields present, declared types and
|
|
877
|
-
// enums respected, declared counts consistent. One repair round trip
|
|
878
|
-
// first, for every harness: the agent is told exactly what is wrong.
|
|
879
|
-
const contract = outputContractProblems(result.data, node.output);
|
|
860
|
+
// Output contract (#357, #573): the emitted data is checked against the
|
|
861
|
+
// node's declared `output` JSON Schema at every depth (nested objects,
|
|
862
|
+
// arrays of objects, enums, `required` at any depth,
|
|
863
|
+
// additionalProperties), plus `x-sweny-count-of` counts. The check runs
|
|
864
|
+
// here, on the data every harness returns, so a violation reads the same
|
|
865
|
+
// whichever harness ran the node, each problem with its exact path and
|
|
866
|
+
// never the emitted value. One repair round trip first, for every
|
|
867
|
+
// harness: the agent is told exactly what is wrong. A check that cannot
|
|
868
|
+
// run (it threw) fails this node, never the run, and gets no repair.
|
|
869
|
+
const checked = checkOutputContract(result.data, node.output);
|
|
870
|
+
const contract = "problems" in checked ? checked.problems : [];
|
|
880
871
|
if (contract.length > 0 && !repaired) {
|
|
881
872
|
// Part of this node visit, like a retry of the same call: no extra
|
|
882
873
|
// max_steps slot. Its spend still counts (budget, journal usage).
|
|
883
874
|
repaired = true;
|
|
884
|
-
const listed = contract
|
|
885
|
-
|
|
875
|
+
const listed = listContractProblems(contract)
|
|
876
|
+
.map((line) => `- ${line}`)
|
|
877
|
+
.join("\n");
|
|
878
|
+
logger.warn(` output contract: ${contractPaths(contract).join(", ")}; asking the agent once to fix it`, {
|
|
886
879
|
node: currentId,
|
|
887
880
|
});
|
|
888
881
|
safeObserve(observer, {
|
|
889
882
|
type: "node:warning",
|
|
890
883
|
node: currentId,
|
|
891
884
|
reason: "output contract violated; one repair attempt",
|
|
892
|
-
fields: contract
|
|
885
|
+
fields: contractPaths(contract),
|
|
893
886
|
}, logger);
|
|
894
887
|
currentInstruction =
|
|
895
888
|
`Your previous output did not match this step's declared output schema:\n${listed}\n\n` +
|
|
896
|
-
`Do the step again and return the complete output with every listed field present and
|
|
889
|
+
`Do the step again and return the complete output, with every listed field present and matching the declared schema.` +
|
|
897
890
|
`\n\n---\n\n${currentInstruction}`;
|
|
898
891
|
continue;
|
|
899
892
|
}
|
|
900
|
-
// Still wrong after the repair. A field an expression
|
|
901
|
-
// node failure: routes that read it fall through to
|
|
902
|
-
// (decider or agent) at routing time.
|
|
903
|
-
//
|
|
893
|
+
// Still wrong after the repair. A problem under a field an expression
|
|
894
|
+
// routes on is not a node failure: routes that read it fall through to
|
|
895
|
+
// the next rung (decider or agent) at routing time. Every other problem
|
|
896
|
+
// fails the node, on the same footing as an eval failure: with `retry`
|
|
897
|
+
// configured, the reflection and retry path below gets another attempt.
|
|
898
|
+
// A schema that does not compile keeps the pre-#573 rule (deprecated):
|
|
899
|
+
// only a missing field fails the node.
|
|
904
900
|
const routedOn = routingFieldsOf(workflow, currentId);
|
|
905
901
|
const tolerated = contract.filter((p) => routedOn.has(p.field));
|
|
906
902
|
if (tolerated.length > 0) {
|
|
907
|
-
logger.warn(` output contract still violated for routing field(s) ${tolerated
|
|
903
|
+
logger.warn(` output contract still violated for routing field(s) ${contractPaths(tolerated).join(", ")}; ` +
|
|
908
904
|
`routes that read them fall through to the next rung`, { node: currentId });
|
|
909
905
|
safeObserve(observer, {
|
|
910
906
|
type: "node:warning",
|
|
911
907
|
node: currentId,
|
|
912
908
|
reason: "routing fields missing or invalid after repair; their routes fall through",
|
|
913
|
-
fields: tolerated
|
|
909
|
+
fields: contractPaths(tolerated),
|
|
914
910
|
}, logger);
|
|
915
911
|
}
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
912
|
+
const broken = contract.filter((p) => !routedOn.has(p.field) && (p.kind === "missing" || !p.fallback));
|
|
913
|
+
const contractError = "error" in checked
|
|
914
|
+
? `output validation error: ${checked.error}`
|
|
915
|
+
: broken.length > 0
|
|
916
|
+
? contractFailureMessage(broken)
|
|
917
|
+
: undefined;
|
|
919
918
|
let outcome;
|
|
920
|
-
if (
|
|
921
|
-
const
|
|
922
|
-
const failure = { name
|
|
923
|
-
outcome = { pass: false, error:
|
|
919
|
+
if (contractError !== undefined) {
|
|
920
|
+
const name = broken.some((p) => p.kind === "missing") ? "output_required" : "output_schema";
|
|
921
|
+
const failure = { name, kind: "value", pass: false, reasoning: contractError };
|
|
922
|
+
outcome = { pass: false, error: contractError, failures: [failure] };
|
|
924
923
|
// Still record the synthesized failure as the node's eval result so
|
|
925
924
|
// observers and downstream context see the contract violation.
|
|
926
925
|
result.evals = [failure];
|
|
@@ -1408,9 +1407,12 @@ export function deciderRoutingState(workflow, current, results, conditions, secr
|
|
|
1408
1407
|
const fields = fieldsOf(id);
|
|
1409
1408
|
if (fields.size > 0 && r.status === "success") {
|
|
1410
1409
|
const data = (r.data ?? {});
|
|
1410
|
+
// A field that breaks the node's contract by any rule (#573) is sent as null, like a mistyped one.
|
|
1411
|
+
const broken = invalidOutputFields(r, hasOwn(workflow.nodes, id) ? workflow.nodes[id] : undefined);
|
|
1411
1412
|
const output = Object.create(null);
|
|
1412
|
-
for (const [k, spec] of fields)
|
|
1413
|
-
output[k] = projectValue(spec, hasOwn(data, k) ? data[k] : undefined);
|
|
1413
|
+
for (const [k, spec] of fields) {
|
|
1414
|
+
output[k] = broken.has(k) ? null : projectValue(spec, hasOwn(data, k) ? data[k] : undefined);
|
|
1415
|
+
}
|
|
1414
1416
|
entry.output = output;
|
|
1415
1417
|
}
|
|
1416
1418
|
if (r.evals && r.evals.length > 0) {
|
|
@@ -1518,114 +1520,19 @@ export function buildBoundedContext(workflow, nodeId, results, input, instructio
|
|
|
1518
1520
|
.map(([k, v]) => [k, buildBoundedNodeEntry(v, workflow.nodes[k])])),
|
|
1519
1521
|
};
|
|
1520
1522
|
}
|
|
1523
|
+
// The output contract (#357, #573) lives in output-contract.ts; re-exported for existing importers.
|
|
1524
|
+
export { COUNT_OF_KEYWORD, outputContractProblems };
|
|
1521
1525
|
/**
|
|
1522
|
-
*
|
|
1523
|
-
*
|
|
1524
|
-
*
|
|
1526
|
+
* The top-level output fields of a node's result that are present but break
|
|
1527
|
+
* its contract (#573), at any depth and by any rule. Routing reads them as
|
|
1528
|
+
* unknown and the decider never sees their values. A check that cannot run
|
|
1529
|
+
* (it threw) marks every declared field, so nothing unchecked routes.
|
|
1525
1530
|
*/
|
|
1526
|
-
function
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
return [];
|
|
1532
|
-
return required.filter((r) => typeof r === "string");
|
|
1533
|
-
}
|
|
1534
|
-
/** Keyword on an integer output property: the count sweny cross-checks against an array in the same output. */
|
|
1535
|
-
export const COUNT_OF_KEYWORD = "x-sweny-count-of";
|
|
1536
|
-
function typeMatches(t, v) {
|
|
1537
|
-
switch (t) {
|
|
1538
|
-
case "string":
|
|
1539
|
-
return typeof v === "string";
|
|
1540
|
-
case "number":
|
|
1541
|
-
return typeof v === "number" && Number.isFinite(v);
|
|
1542
|
-
case "integer":
|
|
1543
|
-
return typeof v === "number" && Number.isInteger(v);
|
|
1544
|
-
case "boolean":
|
|
1545
|
-
return typeof v === "boolean";
|
|
1546
|
-
case "array":
|
|
1547
|
-
return Array.isArray(v);
|
|
1548
|
-
case "object":
|
|
1549
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1550
|
-
case "null":
|
|
1551
|
-
return v === null;
|
|
1552
|
-
default:
|
|
1553
|
-
return true; // unknown or absent type: not checked
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
function countOf(spec, data) {
|
|
1557
|
-
const items = hasOwn(data, spec.array) ? data[spec.array] : undefined;
|
|
1558
|
-
if (!Array.isArray(items))
|
|
1559
|
-
return undefined;
|
|
1560
|
-
let n = 0;
|
|
1561
|
-
for (const item of items) {
|
|
1562
|
-
if (!item || typeof item !== "object")
|
|
1563
|
-
continue;
|
|
1564
|
-
const it = item;
|
|
1565
|
-
const whereOk = Object.entries(spec.where ?? {}).every(([k, allowed]) => hasOwn(it, k) && Array.isArray(allowed) && allowed.some((a) => a === it[k]));
|
|
1566
|
-
const nonEmptyOk = (spec.non_empty ?? []).every((k) => hasOwn(it, k) && typeof it[k] === "string" && it[k].trim() !== "");
|
|
1567
|
-
if (whereOk && nonEmptyOk)
|
|
1568
|
-
n++;
|
|
1569
|
-
}
|
|
1570
|
-
return n;
|
|
1571
|
-
}
|
|
1572
|
-
/**
|
|
1573
|
-
* Check emitted data against the declared output contract (#357): every
|
|
1574
|
-
* `required` field present; every present top-level property of its declared
|
|
1575
|
-
* `type` and inside its `enum`; and every `x-sweny-count-of` integer equal to
|
|
1576
|
-
* the count it declares over its array. Not full JSON Schema: formats,
|
|
1577
|
-
* nested shapes and additionalProperties stay the author's concern.
|
|
1578
|
-
*/
|
|
1579
|
-
export function outputContractProblems(data, output) {
|
|
1580
|
-
if (!output || typeof output !== "object")
|
|
1581
|
-
return [];
|
|
1582
|
-
const obj = (data && typeof data === "object" ? data : {});
|
|
1583
|
-
const problems = [];
|
|
1584
|
-
for (const k of getDeclaredRequiredFields(output)) {
|
|
1585
|
-
if (!hasOwn(obj, k))
|
|
1586
|
-
problems.push({ field: k, kind: "missing", problem: "required but missing" });
|
|
1587
|
-
}
|
|
1588
|
-
const props = output.properties;
|
|
1589
|
-
if (!props || typeof props !== "object")
|
|
1590
|
-
return problems;
|
|
1591
|
-
for (const [k, raw] of Object.entries(props)) {
|
|
1592
|
-
if (!hasOwn(obj, k) || !raw || typeof raw !== "object")
|
|
1593
|
-
continue;
|
|
1594
|
-
const p = raw;
|
|
1595
|
-
const v = obj[k];
|
|
1596
|
-
const types = Array.isArray(p.type) ? p.type : p.type === undefined ? [] : [p.type];
|
|
1597
|
-
if (types.length > 0 && !types.some((t) => typeMatches(t, v))) {
|
|
1598
|
-
problems.push({ field: k, kind: "invalid", problem: `expected ${types.join(" or ")}, got ${JSON.stringify(v)}` });
|
|
1599
|
-
continue;
|
|
1600
|
-
}
|
|
1601
|
-
if (Array.isArray(p.enum) && !p.enum.some((e) => e === v)) {
|
|
1602
|
-
problems.push({
|
|
1603
|
-
field: k,
|
|
1604
|
-
kind: "invalid",
|
|
1605
|
-
problem: `expected one of ${JSON.stringify(p.enum)}, got ${JSON.stringify(v)}`,
|
|
1606
|
-
});
|
|
1607
|
-
continue;
|
|
1608
|
-
}
|
|
1609
|
-
const spec = p[COUNT_OF_KEYWORD];
|
|
1610
|
-
if (spec && typeof spec === "object" && typeof spec.array === "string") {
|
|
1611
|
-
const want = countOf(spec, obj);
|
|
1612
|
-
if (want === undefined) {
|
|
1613
|
-
problems.push({
|
|
1614
|
-
field: k,
|
|
1615
|
-
kind: "invalid",
|
|
1616
|
-
problem: `cannot be checked: '${spec.array}' is not a list`,
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
else if (v !== want) {
|
|
1620
|
-
problems.push({
|
|
1621
|
-
field: k,
|
|
1622
|
-
kind: "invalid",
|
|
1623
|
-
problem: `is ${JSON.stringify(v)} but '${spec.array}' has ${want} matching item(s)`,
|
|
1624
|
-
});
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
return problems;
|
|
1531
|
+
function invalidOutputFields(result, node) {
|
|
1532
|
+
const checked = checkOutputContract(result.data, node?.output);
|
|
1533
|
+
if ("error" in checked)
|
|
1534
|
+
return getDeclaredOutputProperties(node?.output) ?? new Set();
|
|
1535
|
+
return new Set(checked.problems.filter((p) => p.kind === "invalid").map((p) => p.field));
|
|
1629
1536
|
}
|
|
1630
1537
|
/**
|
|
1631
1538
|
* Required tools the agent never called, when that is the only reason a node
|
|
@@ -2300,9 +2207,8 @@ function resolveByExpressions(workflow, current, results, conditionalEdges, defa
|
|
|
2300
2207
|
if (r.status !== "success")
|
|
2301
2208
|
continue;
|
|
2302
2209
|
const node = hasOwn(workflow.nodes, id) ? workflow.nodes[id] : undefined;
|
|
2303
|
-
for (const
|
|
2304
|
-
|
|
2305
|
-
invalid.add(`${id}.${p.field}`);
|
|
2210
|
+
for (const f of invalidOutputFields(r, node))
|
|
2211
|
+
invalid.add(`${id}.${f}`);
|
|
2306
2212
|
scope[id] = buildPriorNodeContext(r);
|
|
2307
2213
|
}
|
|
2308
2214
|
const problems = [];
|
|
@@ -2368,7 +2274,7 @@ function resolveByExpressions(workflow, current, results, conditionalEdges, defa
|
|
|
2368
2274
|
/**
|
|
2369
2275
|
* Validate a workflow definition before execution.
|
|
2370
2276
|
*/
|
|
2371
|
-
function validate(workflow, skills) {
|
|
2277
|
+
function validate(workflow, skills, logger) {
|
|
2372
2278
|
for (const [id, def] of Object.entries(workflow.skills ?? {})) {
|
|
2373
2279
|
if (!def.instruction?.trim()) {
|
|
2374
2280
|
throw new Error(`Inline skill "${id}" must provide a non-empty instruction`);
|
|
@@ -2392,10 +2298,14 @@ function validate(workflow, skills) {
|
|
|
2392
2298
|
// reach execute() without the loader, so without this they got none of it
|
|
2393
2299
|
// (#326). Runs once per execute() call, before any node runs. Skill
|
|
2394
2300
|
// availability stays a warning below, so no knownSkills here.
|
|
2395
|
-
const
|
|
2301
|
+
const loadWarnings = [];
|
|
2302
|
+
const structural = validateWorkflow(workflow, undefined, { warnings: loadWarnings });
|
|
2396
2303
|
if (structural.length > 0) {
|
|
2397
2304
|
throw new Error(`Invalid workflow "${workflow.id}":\n${structural.map((e) => ` ${e.code}: ${e.message}`).join("\n")}`);
|
|
2398
2305
|
}
|
|
2306
|
+
// Deprecated but still loading (#573): said once per run, never silently.
|
|
2307
|
+
for (const w of loadWarnings)
|
|
2308
|
+
logger.warn(` ${w.code}: ${w.message}`, { node: w.nodeId });
|
|
2399
2309
|
// Check that each node has at least one available skill (if it lists any)
|
|
2400
2310
|
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
2401
2311
|
if (node.skills.length === 0)
|
package/dist/harness/acp.js
CHANGED
|
@@ -53,7 +53,8 @@ import { policyGate, resolveHarnessPolicy, TOOL_CLASSES } from "./policy.js";
|
|
|
53
53
|
import { gitCredentialPolicy } from "../git-credentials.js";
|
|
54
54
|
import { buildNodePrompt, reportCompletionUsage } from "./prompts.js";
|
|
55
55
|
import { makeAbort } from "./abort.js";
|
|
56
|
-
import { parseToolResultContent,
|
|
56
|
+
import { parseToolResultContent, summarizeToolError, tryParseJSON } from "./parse.js";
|
|
57
|
+
import { checkOutputContract, listContractProblems } from "../output-contract.js";
|
|
57
58
|
import { startToolBridge } from "./tool-bridge/server.js";
|
|
58
59
|
import { defaultSandboxWrapper, prepareAgentSpawn, wrappersFrom, } from "./sandbox-wrapper.js";
|
|
59
60
|
import { TOKEN_ENV } from "./tool-bridge/protocol.js";
|
|
@@ -775,18 +776,25 @@ export class AcpHarness {
|
|
|
775
776
|
const basePrompt = buildNodePrompt(req.instruction, req.context, req.outputSchema);
|
|
776
777
|
const schema = req.outputSchema;
|
|
777
778
|
const retry = { asked: false };
|
|
778
|
-
//
|
|
779
|
+
// The same check, at every depth, that sweny's executor applies to every
|
|
780
|
+
// harness's output (output-contract.ts). A check that cannot run asks nothing.
|
|
781
|
+
const mismatches = (text) => {
|
|
782
|
+
const checked = checkOutputContract(tryParseJSON(text, schema, { warn() { } }), schema);
|
|
783
|
+
return "problems" in checked ? listContractProblems(checked.problems) : [];
|
|
784
|
+
};
|
|
785
|
+
// One correction turn when the answer does not match the declared output,
|
|
786
|
+
// unless the caller (sweny's executor) runs the one repair itself: one repair loop on every harness.
|
|
779
787
|
const followUp = (turn) => {
|
|
780
|
-
if (!schema || retry.asked)
|
|
788
|
+
if (!schema || retry.asked || req.callerRepairsOutput)
|
|
781
789
|
return undefined;
|
|
782
|
-
const
|
|
783
|
-
const problems = schemaMismatches(parsed, schema);
|
|
790
|
+
const problems = mismatches(turn.text);
|
|
784
791
|
if (problems.length === 0)
|
|
785
792
|
return undefined;
|
|
786
793
|
retry.asked = true;
|
|
787
794
|
this.logger.info(` acp: the answer does not match the output shape (${problems.join("; ")}); asking once more`);
|
|
788
795
|
return [
|
|
789
|
-
|
|
796
|
+
"Your last reply did not match the required output:",
|
|
797
|
+
...problems.map((line) => `- ${line}`),
|
|
790
798
|
"Reply again, ending with ONLY a JSON object that matches this schema:",
|
|
791
799
|
"```json",
|
|
792
800
|
JSON.stringify(schema, null, 2),
|
|
@@ -838,8 +846,7 @@ export class AcpHarness {
|
|
|
838
846
|
const result = this.toResult(conv, schema, req.timeoutMs);
|
|
839
847
|
if (result.status === "success" && schema && retry.asked) {
|
|
840
848
|
const last = conv.turns.at(-1)?.text ?? "";
|
|
841
|
-
|
|
842
|
-
if (left.length > 0) {
|
|
849
|
+
if (mismatches(last).length > 0) {
|
|
843
850
|
extraDegraded.push(`structured_output: the answer still did not match the declared output after one retry`);
|
|
844
851
|
}
|
|
845
852
|
}
|
package/dist/loader.js
CHANGED
|
@@ -13,7 +13,7 @@ import * as fs from "node:fs";
|
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { parse as parseYaml } from "yaml";
|
|
15
15
|
import { ZodError } from "zod";
|
|
16
|
-
import { workflowZ, validateWorkflow, preflightLegacyVerify } from "./schema.js";
|
|
16
|
+
import { workflowZ, validateWorkflow, preflightLegacyVerify, } from "./schema.js";
|
|
17
17
|
import { approveDirectiveWarnings } from "./node-policy.js";
|
|
18
18
|
import { DEFAULT_MIGRATION_CONFIG, migrateWorkflow, migrationWarning, } from "./migrations.js";
|
|
19
19
|
/**
|
|
@@ -90,13 +90,15 @@ export function validateParsed(raw, options = {}) {
|
|
|
90
90
|
const msg = err instanceof Error ? err.message : String(err);
|
|
91
91
|
return { ok: false, errors: [{ message: msg, code: "SCHEMA" }] };
|
|
92
92
|
}
|
|
93
|
-
const
|
|
93
|
+
const schemaWarnings = [];
|
|
94
|
+
const structural = validateWorkflow(parsed, options.knownSkills, { warnings: schemaWarnings });
|
|
94
95
|
if (structural.length > 0) {
|
|
95
96
|
return {
|
|
96
97
|
ok: false,
|
|
97
98
|
errors: structural.map((e) => ({ message: e.message, code: e.code, nodeId: e.nodeId })),
|
|
98
99
|
};
|
|
99
100
|
}
|
|
101
|
+
warnings.push(...schemaWarnings.map((w) => `${w.code}: ${w.message}`));
|
|
100
102
|
warnings.push(...approveDirectiveWarnings(parsed));
|
|
101
103
|
return warnings.length > 0 ? { ok: true, workflow: parsed, warnings } : { ok: true, workflow: parsed };
|
|
102
104
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON Schema draft-06 meta-schema, as ajv ships it in
|
|
3
|
+
* `ajv/dist/refs/json-schema-draft-06.json` (ajv 8 adds it with
|
|
4
|
+
* `addMetaSchema` to support draft-06). Kept here as a module because core
|
|
5
|
+
* cannot import JSON files; a spec asserts it deep-equals ajv's copy.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DRAFT_06_META_SCHEMA: Record<string, unknown>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON Schema draft-06 meta-schema, as ajv ships it in
|
|
3
|
+
* `ajv/dist/refs/json-schema-draft-06.json` (ajv 8 adds it with
|
|
4
|
+
* `addMetaSchema` to support draft-06). Kept here as a module because core
|
|
5
|
+
* cannot import JSON files; a spec asserts it deep-equals ajv's copy.
|
|
6
|
+
*/
|
|
7
|
+
export const DRAFT_06_META_SCHEMA = {
|
|
8
|
+
$schema: "http://json-schema.org/draft-06/schema#",
|
|
9
|
+
$id: "http://json-schema.org/draft-06/schema#",
|
|
10
|
+
title: "Core schema meta-schema",
|
|
11
|
+
definitions: {
|
|
12
|
+
schemaArray: {
|
|
13
|
+
type: "array",
|
|
14
|
+
minItems: 1,
|
|
15
|
+
items: {
|
|
16
|
+
$ref: "#",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
nonNegativeInteger: {
|
|
20
|
+
type: "integer",
|
|
21
|
+
minimum: 0,
|
|
22
|
+
},
|
|
23
|
+
nonNegativeIntegerDefault0: {
|
|
24
|
+
allOf: [
|
|
25
|
+
{
|
|
26
|
+
$ref: "#/definitions/nonNegativeInteger",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
default: 0,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
simpleTypes: {
|
|
34
|
+
enum: ["array", "boolean", "integer", "null", "number", "object", "string"],
|
|
35
|
+
},
|
|
36
|
+
stringArray: {
|
|
37
|
+
type: "array",
|
|
38
|
+
items: {
|
|
39
|
+
type: "string",
|
|
40
|
+
},
|
|
41
|
+
uniqueItems: true,
|
|
42
|
+
default: [],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
type: ["object", "boolean"],
|
|
46
|
+
properties: {
|
|
47
|
+
$id: {
|
|
48
|
+
type: "string",
|
|
49
|
+
format: "uri-reference",
|
|
50
|
+
},
|
|
51
|
+
$schema: {
|
|
52
|
+
type: "string",
|
|
53
|
+
format: "uri",
|
|
54
|
+
},
|
|
55
|
+
$ref: {
|
|
56
|
+
type: "string",
|
|
57
|
+
format: "uri-reference",
|
|
58
|
+
},
|
|
59
|
+
title: {
|
|
60
|
+
type: "string",
|
|
61
|
+
},
|
|
62
|
+
description: {
|
|
63
|
+
type: "string",
|
|
64
|
+
},
|
|
65
|
+
default: {},
|
|
66
|
+
examples: {
|
|
67
|
+
type: "array",
|
|
68
|
+
items: {},
|
|
69
|
+
},
|
|
70
|
+
multipleOf: {
|
|
71
|
+
type: "number",
|
|
72
|
+
exclusiveMinimum: 0,
|
|
73
|
+
},
|
|
74
|
+
maximum: {
|
|
75
|
+
type: "number",
|
|
76
|
+
},
|
|
77
|
+
exclusiveMaximum: {
|
|
78
|
+
type: "number",
|
|
79
|
+
},
|
|
80
|
+
minimum: {
|
|
81
|
+
type: "number",
|
|
82
|
+
},
|
|
83
|
+
exclusiveMinimum: {
|
|
84
|
+
type: "number",
|
|
85
|
+
},
|
|
86
|
+
maxLength: {
|
|
87
|
+
$ref: "#/definitions/nonNegativeInteger",
|
|
88
|
+
},
|
|
89
|
+
minLength: {
|
|
90
|
+
$ref: "#/definitions/nonNegativeIntegerDefault0",
|
|
91
|
+
},
|
|
92
|
+
pattern: {
|
|
93
|
+
type: "string",
|
|
94
|
+
format: "regex",
|
|
95
|
+
},
|
|
96
|
+
additionalItems: {
|
|
97
|
+
$ref: "#",
|
|
98
|
+
},
|
|
99
|
+
items: {
|
|
100
|
+
anyOf: [
|
|
101
|
+
{
|
|
102
|
+
$ref: "#",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
$ref: "#/definitions/schemaArray",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
default: {},
|
|
109
|
+
},
|
|
110
|
+
maxItems: {
|
|
111
|
+
$ref: "#/definitions/nonNegativeInteger",
|
|
112
|
+
},
|
|
113
|
+
minItems: {
|
|
114
|
+
$ref: "#/definitions/nonNegativeIntegerDefault0",
|
|
115
|
+
},
|
|
116
|
+
uniqueItems: {
|
|
117
|
+
type: "boolean",
|
|
118
|
+
default: false,
|
|
119
|
+
},
|
|
120
|
+
contains: {
|
|
121
|
+
$ref: "#",
|
|
122
|
+
},
|
|
123
|
+
maxProperties: {
|
|
124
|
+
$ref: "#/definitions/nonNegativeInteger",
|
|
125
|
+
},
|
|
126
|
+
minProperties: {
|
|
127
|
+
$ref: "#/definitions/nonNegativeIntegerDefault0",
|
|
128
|
+
},
|
|
129
|
+
required: {
|
|
130
|
+
$ref: "#/definitions/stringArray",
|
|
131
|
+
},
|
|
132
|
+
additionalProperties: {
|
|
133
|
+
$ref: "#",
|
|
134
|
+
},
|
|
135
|
+
definitions: {
|
|
136
|
+
type: "object",
|
|
137
|
+
additionalProperties: {
|
|
138
|
+
$ref: "#",
|
|
139
|
+
},
|
|
140
|
+
default: {},
|
|
141
|
+
},
|
|
142
|
+
properties: {
|
|
143
|
+
type: "object",
|
|
144
|
+
additionalProperties: {
|
|
145
|
+
$ref: "#",
|
|
146
|
+
},
|
|
147
|
+
default: {},
|
|
148
|
+
},
|
|
149
|
+
patternProperties: {
|
|
150
|
+
type: "object",
|
|
151
|
+
additionalProperties: {
|
|
152
|
+
$ref: "#",
|
|
153
|
+
},
|
|
154
|
+
default: {},
|
|
155
|
+
},
|
|
156
|
+
dependencies: {
|
|
157
|
+
type: "object",
|
|
158
|
+
additionalProperties: {
|
|
159
|
+
anyOf: [
|
|
160
|
+
{
|
|
161
|
+
$ref: "#",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
$ref: "#/definitions/stringArray",
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
propertyNames: {
|
|
170
|
+
$ref: "#",
|
|
171
|
+
},
|
|
172
|
+
const: {},
|
|
173
|
+
enum: {
|
|
174
|
+
type: "array",
|
|
175
|
+
minItems: 1,
|
|
176
|
+
uniqueItems: true,
|
|
177
|
+
},
|
|
178
|
+
type: {
|
|
179
|
+
anyOf: [
|
|
180
|
+
{
|
|
181
|
+
$ref: "#/definitions/simpleTypes",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: "array",
|
|
185
|
+
items: {
|
|
186
|
+
$ref: "#/definitions/simpleTypes",
|
|
187
|
+
},
|
|
188
|
+
minItems: 1,
|
|
189
|
+
uniqueItems: true,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
format: {
|
|
194
|
+
type: "string",
|
|
195
|
+
},
|
|
196
|
+
allOf: {
|
|
197
|
+
$ref: "#/definitions/schemaArray",
|
|
198
|
+
},
|
|
199
|
+
anyOf: {
|
|
200
|
+
$ref: "#/definitions/schemaArray",
|
|
201
|
+
},
|
|
202
|
+
oneOf: {
|
|
203
|
+
$ref: "#/definitions/schemaArray",
|
|
204
|
+
},
|
|
205
|
+
not: {
|
|
206
|
+
$ref: "#",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
default: {},
|
|
210
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The node output contract (#357, #573): a node's emitted data checked
|
|
3
|
+
* against its declared `output` JSON Schema, at every depth.
|
|
4
|
+
*
|
|
5
|
+
* One standard validator (ajv) does the checking, so nested objects, arrays
|
|
6
|
+
* of objects, enums, `required` at any depth, `additionalProperties`,
|
|
7
|
+
* `oneOf` and the rest mean what JSON Schema says they mean. The check runs
|
|
8
|
+
* in the executor on the data every harness returns, so a violation reads
|
|
9
|
+
* the same whichever harness ran the node. Each problem carries the exact
|
|
10
|
+
* path of the value that broke the schema (`findings[1].severity`) and the
|
|
11
|
+
* rule it broke, never the value itself: output can carry secrets.
|
|
12
|
+
*
|
|
13
|
+
* Dialect: JSON Schema 2020-12, or the draft a schema names in `$schema`
|
|
14
|
+
* (draft-04, draft-06, draft-07, 2019-09). `format` is an annotation, as
|
|
15
|
+
* 2020-12 defines it by default, and is not asserted. Keywords ajv does not
|
|
16
|
+
* know (sweny's `x-` keywords among them) are ignored by ajv;
|
|
17
|
+
* `x-sweny-count-of` is checked here after the schema itself.
|
|
18
|
+
*
|
|
19
|
+
* A schema ajv cannot compile (deprecated, see {@link outputSchemaError})
|
|
20
|
+
* keeps the check sweny ran before #573: top-level `required`, and the
|
|
21
|
+
* `type` and `enum` of each top-level property.
|
|
22
|
+
*
|
|
23
|
+
* Output nested deeper than {@link MAX_OUTPUT_DEPTH} levels, or holding more
|
|
24
|
+
* than {@link MAX_OUTPUT_VALUES} values, is not checked: it is a problem in
|
|
25
|
+
* itself.
|
|
26
|
+
*
|
|
27
|
+
* No harness or skill imports.
|
|
28
|
+
*/
|
|
29
|
+
import type { JSONSchema } from "./types.js";
|
|
30
|
+
/** One way a node's emitted data breaks its declared output contract. */
|
|
31
|
+
export interface ContractProblem {
|
|
32
|
+
/**
|
|
33
|
+
* The top-level output field the problem sits under, or {@link ROOT_PATH}
|
|
34
|
+
* for the output object itself. Routing tolerance keys on it: a route that
|
|
35
|
+
* reads this field falls through instead of failing the node.
|
|
36
|
+
*/
|
|
37
|
+
field: string;
|
|
38
|
+
/** Where the broken value is, e.g. `findings[1].severity` ({@link ROOT_PATH} for the object itself). */
|
|
39
|
+
path: string;
|
|
40
|
+
/**
|
|
41
|
+
* `missing`: a top-level `required` field is absent. `invalid`: any other
|
|
42
|
+
* break, a `required` field missing at depth included.
|
|
43
|
+
*/
|
|
44
|
+
kind: "missing" | "invalid";
|
|
45
|
+
/** The rule that broke and what it expects. Never the emitted value. */
|
|
46
|
+
problem: string;
|
|
47
|
+
/**
|
|
48
|
+
* Found by the pre-#573 top-level check, because the schema does not
|
|
49
|
+
* compile. As before #573, only a missing field fails the node.
|
|
50
|
+
*/
|
|
51
|
+
fallback?: true;
|
|
52
|
+
}
|
|
53
|
+
/** The path that names the output object itself. */
|
|
54
|
+
export declare const ROOT_PATH = "(output)";
|
|
55
|
+
/** Keyword on an integer output property: the count sweny cross-checks against an array in the same output. */
|
|
56
|
+
export declare const COUNT_OF_KEYWORD = "x-sweny-count-of";
|
|
57
|
+
/** Deepest nesting checked: the output object is level 0. */
|
|
58
|
+
export declare const MAX_OUTPUT_DEPTH = 64;
|
|
59
|
+
/** Most values (objects, arrays and scalars together) checked in one output. */
|
|
60
|
+
export declare const MAX_OUTPUT_VALUES = 100000;
|
|
61
|
+
/**
|
|
62
|
+
* Why a declared output schema cannot be compiled, or undefined when it
|
|
63
|
+
* compiles (or there is none). Such a schema is deprecated: the node keeps
|
|
64
|
+
* the pre-#573 top-level check, and a future minor will refuse to load it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function outputSchemaError(output: JSONSchema | undefined): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Check emitted data against the declared output contract: the whole JSON
|
|
69
|
+
* Schema at every depth, then every `x-sweny-count-of` integer against the
|
|
70
|
+
* count it declares over its array. One problem per broken path, in schema
|
|
71
|
+
* order. Empty when the node declares no schema or the data conforms. A
|
|
72
|
+
* schema that does not compile gets the pre-#573 top-level check instead.
|
|
73
|
+
* Can throw (a hostile getter, an ajv fault): callers use
|
|
74
|
+
* {@link checkOutputContract}.
|
|
75
|
+
*/
|
|
76
|
+
export declare function outputContractProblems(data: unknown, output: JSONSchema | undefined): ContractProblem[];
|
|
77
|
+
/** The contract check that never throws: its problems, or why it could not run. */
|
|
78
|
+
export declare function checkOutputContract(data: unknown, output: JSONSchema | undefined): {
|
|
79
|
+
problems: ContractProblem[];
|
|
80
|
+
} | {
|
|
81
|
+
error: string;
|
|
82
|
+
};
|
|
83
|
+
/** The problems' paths, at most {@link MAX_LISTED} of them, for log lines and events. */
|
|
84
|
+
export declare function contractPaths(problems: ContractProblem[]): string[];
|
|
85
|
+
/** The problems as `path: problem` lines, at most {@link MAX_LISTED} of them. */
|
|
86
|
+
export declare function listContractProblems(problems: ContractProblem[]): string[];
|
|
87
|
+
/**
|
|
88
|
+
* The node error for a contract still broken after the repair request.
|
|
89
|
+
* Missing top-level fields are named together, the way the required-field
|
|
90
|
+
* gate always has; every other problem follows as `path: problem`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function contractFailureMessage(problems: ContractProblem[]): string;
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The node output contract (#357, #573): a node's emitted data checked
|
|
3
|
+
* against its declared `output` JSON Schema, at every depth.
|
|
4
|
+
*
|
|
5
|
+
* One standard validator (ajv) does the checking, so nested objects, arrays
|
|
6
|
+
* of objects, enums, `required` at any depth, `additionalProperties`,
|
|
7
|
+
* `oneOf` and the rest mean what JSON Schema says they mean. The check runs
|
|
8
|
+
* in the executor on the data every harness returns, so a violation reads
|
|
9
|
+
* the same whichever harness ran the node. Each problem carries the exact
|
|
10
|
+
* path of the value that broke the schema (`findings[1].severity`) and the
|
|
11
|
+
* rule it broke, never the value itself: output can carry secrets.
|
|
12
|
+
*
|
|
13
|
+
* Dialect: JSON Schema 2020-12, or the draft a schema names in `$schema`
|
|
14
|
+
* (draft-04, draft-06, draft-07, 2019-09). `format` is an annotation, as
|
|
15
|
+
* 2020-12 defines it by default, and is not asserted. Keywords ajv does not
|
|
16
|
+
* know (sweny's `x-` keywords among them) are ignored by ajv;
|
|
17
|
+
* `x-sweny-count-of` is checked here after the schema itself.
|
|
18
|
+
*
|
|
19
|
+
* A schema ajv cannot compile (deprecated, see {@link outputSchemaError})
|
|
20
|
+
* keeps the check sweny ran before #573: top-level `required`, and the
|
|
21
|
+
* `type` and `enum` of each top-level property.
|
|
22
|
+
*
|
|
23
|
+
* Output nested deeper than {@link MAX_OUTPUT_DEPTH} levels, or holding more
|
|
24
|
+
* than {@link MAX_OUTPUT_VALUES} values, is not checked: it is a problem in
|
|
25
|
+
* itself.
|
|
26
|
+
*
|
|
27
|
+
* No harness or skill imports.
|
|
28
|
+
*/
|
|
29
|
+
import { Ajv } from "ajv";
|
|
30
|
+
import { Ajv2019 } from "ajv/dist/2019.js";
|
|
31
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
32
|
+
import Ajv04Module from "ajv-draft-04";
|
|
33
|
+
import { DRAFT_06_META_SCHEMA } from "./output-contract-draft06.js";
|
|
34
|
+
/** The path that names the output object itself. */
|
|
35
|
+
export const ROOT_PATH = "(output)";
|
|
36
|
+
/** Keyword on an integer output property: the count sweny cross-checks against an array in the same output. */
|
|
37
|
+
export const COUNT_OF_KEYWORD = "x-sweny-count-of";
|
|
38
|
+
/** Deepest nesting checked: the output object is level 0. */
|
|
39
|
+
export const MAX_OUTPUT_DEPTH = 64;
|
|
40
|
+
/** Most values (objects, arrays and scalars together) checked in one output. */
|
|
41
|
+
export const MAX_OUTPUT_VALUES = 100_000;
|
|
42
|
+
/**
|
|
43
|
+
* Prose every harness puts in `data.summary` next to the parsed output. It is
|
|
44
|
+
* not the agent's structured answer, so it is checked only when the schema
|
|
45
|
+
* names it (a declared or required `summary`).
|
|
46
|
+
*/
|
|
47
|
+
const HARNESS_SUMMARY_KEY = "summary";
|
|
48
|
+
/** Problems listed in one message, log line or event before the rest are counted. */
|
|
49
|
+
const MAX_LISTED = 20;
|
|
50
|
+
const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
51
|
+
// ─── Compilation ─────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* `strict: false`: unknown keywords (descriptions, `x-` keywords) are not
|
|
54
|
+
* errors. `validateFormats: false`: `format` is an annotation (2020-12
|
|
55
|
+
* default), so no format plugin is needed. `allErrors`: report every
|
|
56
|
+
* problem at once, so one repair request names them all.
|
|
57
|
+
*/
|
|
58
|
+
const AJV_OPTIONS = { allErrors: true, strict: false, validateFormats: false, logger: false };
|
|
59
|
+
// ajv-draft-04 is CommonJS with only a default export; `.default` is the class under Node and bundlers alike.
|
|
60
|
+
const Ajv04 = Ajv04Module.default;
|
|
61
|
+
const DRAFT_04 = new Ajv04(AJV_OPTIONS);
|
|
62
|
+
const DRAFT_07 = new Ajv(AJV_OPTIONS).addMetaSchema(DRAFT_06_META_SCHEMA);
|
|
63
|
+
const DRAFT_2019 = new Ajv2019(AJV_OPTIONS);
|
|
64
|
+
const DRAFT_2020 = new Ajv2020(AJV_OPTIONS);
|
|
65
|
+
/** The validator for the dialect the schema names in `$schema`; 2020-12 when it names none. */
|
|
66
|
+
function validatorFor(schema) {
|
|
67
|
+
const dialect = String(schema.$schema);
|
|
68
|
+
if (/draft-04/.test(dialect))
|
|
69
|
+
return DRAFT_04;
|
|
70
|
+
if (/draft-0[67]/.test(dialect))
|
|
71
|
+
return DRAFT_07;
|
|
72
|
+
return /2019-09/.test(dialect) ? DRAFT_2019 : DRAFT_2020;
|
|
73
|
+
}
|
|
74
|
+
const INVALID_SCHEMA = "schema is invalid: ";
|
|
75
|
+
/** ajv repeats a meta-schema complaint once per branch it tried: say each once. */
|
|
76
|
+
function oneEach(message) {
|
|
77
|
+
const head = message.startsWith(INVALID_SCHEMA) ? INVALID_SCHEMA : "";
|
|
78
|
+
return head + [...new Set(message.slice(head.length).split(", "))].join(", ");
|
|
79
|
+
}
|
|
80
|
+
const compiled = new WeakMap();
|
|
81
|
+
/** Compile once per schema object. The schema is released from ajv afterwards, so two schemas may share an `$id`. */
|
|
82
|
+
function compile(schema) {
|
|
83
|
+
const hit = compiled.get(schema);
|
|
84
|
+
if (hit)
|
|
85
|
+
return hit;
|
|
86
|
+
const v = validatorFor(schema);
|
|
87
|
+
let out;
|
|
88
|
+
try {
|
|
89
|
+
out = { validate: v.compile(schema) };
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
out = { error: oneEach(err.message) };
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
v.removeSchema(schema);
|
|
96
|
+
}
|
|
97
|
+
compiled.set(schema, out);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Why a declared output schema cannot be compiled, or undefined when it
|
|
102
|
+
* compiles (or there is none). Such a schema is deprecated: the node keeps
|
|
103
|
+
* the pre-#573 top-level check, and a future minor will refuse to load it.
|
|
104
|
+
*/
|
|
105
|
+
export function outputSchemaError(output) {
|
|
106
|
+
if (!output || typeof output !== "object")
|
|
107
|
+
return undefined;
|
|
108
|
+
const c = compile(output);
|
|
109
|
+
return "error" in c ? c.error : undefined;
|
|
110
|
+
}
|
|
111
|
+
// ─── Paths and messages ──────────────────────────────────────────
|
|
112
|
+
function pointerSegments(pointer) {
|
|
113
|
+
if (pointer === "")
|
|
114
|
+
return [];
|
|
115
|
+
return pointer
|
|
116
|
+
.slice(1)
|
|
117
|
+
.split("/")
|
|
118
|
+
.map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
119
|
+
}
|
|
120
|
+
/** One step down the data. Every segment but a final missing or extra key exists, since ajv found it there. */
|
|
121
|
+
const step = (cur, seg) => cur[seg];
|
|
122
|
+
/**
|
|
123
|
+
* `["findings", "1", "severity"]` over the data -> `findings[1].severity`.
|
|
124
|
+
* `[n]` only where the value is an array; a key that is not an identifier is
|
|
125
|
+
* quoted (`["odd key"]`, `byIndex["0"]`).
|
|
126
|
+
*/
|
|
127
|
+
function renderPath(segments, data) {
|
|
128
|
+
if (segments.length === 0)
|
|
129
|
+
return ROOT_PATH;
|
|
130
|
+
let out = "";
|
|
131
|
+
let cur = data;
|
|
132
|
+
for (const seg of segments) {
|
|
133
|
+
if (Array.isArray(cur))
|
|
134
|
+
out += `[${seg}]`;
|
|
135
|
+
else if (/^[A-Za-z_$][\w$-]*$/.test(seg))
|
|
136
|
+
out += out === "" ? seg : `.${seg}`;
|
|
137
|
+
else
|
|
138
|
+
out += `[${JSON.stringify(seg)}]`;
|
|
139
|
+
cur = step(cur, seg);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/** A schema value (an enum list, a const) as JSON, cut to 80 characters. Never used on output data. */
|
|
144
|
+
function preview(v) {
|
|
145
|
+
const s = String(JSON.stringify(v));
|
|
146
|
+
return s.length > 80 ? `${s.slice(0, 77)}...` : s;
|
|
147
|
+
}
|
|
148
|
+
function toProblem(e, data) {
|
|
149
|
+
const segs = pointerSegments(e.instancePath);
|
|
150
|
+
const p = e.params;
|
|
151
|
+
let problem;
|
|
152
|
+
switch (e.keyword) {
|
|
153
|
+
case "required":
|
|
154
|
+
segs.push(String(p.missingProperty));
|
|
155
|
+
problem = "required but missing";
|
|
156
|
+
break;
|
|
157
|
+
case "dependentRequired":
|
|
158
|
+
case "dependencies":
|
|
159
|
+
segs.push(String(p.missingProperty));
|
|
160
|
+
problem = `required when '${String(p.property)}' is present, but missing`;
|
|
161
|
+
break;
|
|
162
|
+
case "additionalProperties":
|
|
163
|
+
segs.push(String(p.additionalProperty));
|
|
164
|
+
problem = "not allowed (additionalProperties: false)";
|
|
165
|
+
break;
|
|
166
|
+
case "unevaluatedProperties":
|
|
167
|
+
segs.push(String(p.unevaluatedProperty));
|
|
168
|
+
problem = "not allowed (unevaluatedProperties: false)";
|
|
169
|
+
break;
|
|
170
|
+
case "type":
|
|
171
|
+
problem = `expected ${[p.type].flat().join(" or ")}`;
|
|
172
|
+
break;
|
|
173
|
+
case "enum":
|
|
174
|
+
problem = `expected one of ${preview(p.allowedValues)}`;
|
|
175
|
+
break;
|
|
176
|
+
case "const":
|
|
177
|
+
problem = `expected ${preview(p.allowedValue)}`;
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
// ajv's wording names the rule and its limit, never the value.
|
|
181
|
+
problem = `${e.message}`;
|
|
182
|
+
}
|
|
183
|
+
const missingTopLevel = e.keyword === "required" && segs.length === 1;
|
|
184
|
+
return {
|
|
185
|
+
field: segs.length === 0 ? ROOT_PATH : segs[0],
|
|
186
|
+
path: renderPath(segs, data),
|
|
187
|
+
kind: missingTopLevel ? "missing" : "invalid",
|
|
188
|
+
problem,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// ─── The pre-#573 top-level check (schemas that do not compile) ──
|
|
192
|
+
function typeMatches(t, v) {
|
|
193
|
+
switch (t) {
|
|
194
|
+
case "string":
|
|
195
|
+
return typeof v === "string";
|
|
196
|
+
case "number":
|
|
197
|
+
return Number.isFinite(v);
|
|
198
|
+
case "integer":
|
|
199
|
+
return Number.isInteger(v);
|
|
200
|
+
case "boolean":
|
|
201
|
+
return typeof v === "boolean";
|
|
202
|
+
case "array":
|
|
203
|
+
return Array.isArray(v);
|
|
204
|
+
case "object":
|
|
205
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
206
|
+
case "null":
|
|
207
|
+
return v === null;
|
|
208
|
+
default:
|
|
209
|
+
return true; // unknown or absent type: not checked, as before
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Every `required` field present; every present top-level property of its declared `type` and in its `enum`. */
|
|
213
|
+
function topLevelProblems(output, data) {
|
|
214
|
+
const problems = [];
|
|
215
|
+
const required = Array.isArray(output.required) ? output.required : [];
|
|
216
|
+
for (const k of required) {
|
|
217
|
+
if (typeof k === "string" && !hasOwn(data, k)) {
|
|
218
|
+
problems.push({ field: k, path: k, kind: "missing", problem: "required but missing", fallback: true });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// This schema did not compile, so any part of it may be malformed: read it defensively.
|
|
222
|
+
for (const [k, raw] of Object.entries(output.properties ?? {})) {
|
|
223
|
+
if (!hasOwn(data, k))
|
|
224
|
+
continue;
|
|
225
|
+
const p = (raw ?? {});
|
|
226
|
+
// An absent or unknown type matches anything, as before #573.
|
|
227
|
+
const types = [p.type].flat();
|
|
228
|
+
let problem;
|
|
229
|
+
if (!types.some((t) => typeMatches(t, data[k])))
|
|
230
|
+
problem = `expected ${types.join(" or ")}`;
|
|
231
|
+
else if (Array.isArray(p.enum) && !p.enum.some((e) => e === data[k]))
|
|
232
|
+
problem = `expected one of ${preview(p.enum)}`;
|
|
233
|
+
if (problem)
|
|
234
|
+
problems.push({ field: k, path: renderPath([k], data), kind: "invalid", problem, fallback: true });
|
|
235
|
+
}
|
|
236
|
+
return problems;
|
|
237
|
+
}
|
|
238
|
+
function countOf(spec, data) {
|
|
239
|
+
const items = data[spec.array];
|
|
240
|
+
if (!Array.isArray(items))
|
|
241
|
+
return undefined;
|
|
242
|
+
let n = 0;
|
|
243
|
+
for (const item of items) {
|
|
244
|
+
if (!item || typeof item !== "object")
|
|
245
|
+
continue;
|
|
246
|
+
const it = item;
|
|
247
|
+
const whereOk = Object.entries(spec.where ?? {}).every(([k, allowed]) => Array.isArray(allowed) && allowed.some((a) => a === it[k]));
|
|
248
|
+
const nonEmptyOk = (spec.non_empty ?? []).every((k) => typeof it[k] === "string" && it[k].trim() !== "");
|
|
249
|
+
if (whereOk && nonEmptyOk)
|
|
250
|
+
n++;
|
|
251
|
+
}
|
|
252
|
+
return n;
|
|
253
|
+
}
|
|
254
|
+
function countProblems(output, data, alreadyBroken, fallback) {
|
|
255
|
+
const problems = [];
|
|
256
|
+
for (const [k, raw] of Object.entries(output.properties ?? {})) {
|
|
257
|
+
if (!hasOwn(data, k) || alreadyBroken.has(k))
|
|
258
|
+
continue;
|
|
259
|
+
const spec = raw?.[COUNT_OF_KEYWORD];
|
|
260
|
+
if (typeof spec?.array !== "string")
|
|
261
|
+
continue;
|
|
262
|
+
const want = countOf(spec, data);
|
|
263
|
+
const problem = want === undefined
|
|
264
|
+
? `cannot be checked: '${spec.array}' is not a list`
|
|
265
|
+
: data[k] !== want
|
|
266
|
+
? `does not match the count: '${spec.array}' has ${want} matching item(s)`
|
|
267
|
+
: undefined;
|
|
268
|
+
if (problem) {
|
|
269
|
+
problems.push({
|
|
270
|
+
field: k,
|
|
271
|
+
path: renderPath([k], data),
|
|
272
|
+
kind: "invalid",
|
|
273
|
+
problem,
|
|
274
|
+
...(fallback ? { fallback: true } : {}),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return problems;
|
|
279
|
+
}
|
|
280
|
+
// ─── The check ───────────────────────────────────────────────────
|
|
281
|
+
/** The data the schema is checked against: the harness's `summary` prose left out unless the schema names it. */
|
|
282
|
+
function structuredView(output, data) {
|
|
283
|
+
const declared = hasOwn(Object(output.properties), HARNESS_SUMMARY_KEY) ||
|
|
284
|
+
(Array.isArray(output.required) && output.required.includes(HARNESS_SUMMARY_KEY));
|
|
285
|
+
if (declared)
|
|
286
|
+
return data;
|
|
287
|
+
const { [HARNESS_SUMMARY_KEY]: _summary, ...rest } = data;
|
|
288
|
+
return rest;
|
|
289
|
+
}
|
|
290
|
+
/** Why the output is too big to check, without recursion, or undefined. */
|
|
291
|
+
function tooBig(data) {
|
|
292
|
+
const stack = [[data, 0]];
|
|
293
|
+
let values = 0;
|
|
294
|
+
while (stack.length > 0) {
|
|
295
|
+
const [v, depth] = stack.pop();
|
|
296
|
+
if (depth > MAX_OUTPUT_DEPTH)
|
|
297
|
+
return `not checked: the output nests deeper than ${MAX_OUTPUT_DEPTH} levels`;
|
|
298
|
+
if (++values > MAX_OUTPUT_VALUES)
|
|
299
|
+
return `not checked: the output holds more than ${MAX_OUTPUT_VALUES} values`;
|
|
300
|
+
if (v !== null && typeof v === "object")
|
|
301
|
+
for (const c of Object.values(v))
|
|
302
|
+
stack.push([c, depth + 1]);
|
|
303
|
+
}
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Check emitted data against the declared output contract: the whole JSON
|
|
308
|
+
* Schema at every depth, then every `x-sweny-count-of` integer against the
|
|
309
|
+
* count it declares over its array. One problem per broken path, in schema
|
|
310
|
+
* order. Empty when the node declares no schema or the data conforms. A
|
|
311
|
+
* schema that does not compile gets the pre-#573 top-level check instead.
|
|
312
|
+
* Can throw (a hostile getter, an ajv fault): callers use
|
|
313
|
+
* {@link checkOutputContract}.
|
|
314
|
+
*/
|
|
315
|
+
export function outputContractProblems(data, output) {
|
|
316
|
+
if (!output || typeof output !== "object")
|
|
317
|
+
return [];
|
|
318
|
+
const obj = structuredView(output, (data && typeof data === "object" ? data : {}));
|
|
319
|
+
const big = tooBig(obj);
|
|
320
|
+
if (big)
|
|
321
|
+
return [{ field: ROOT_PATH, path: ROOT_PATH, kind: "invalid", problem: big }];
|
|
322
|
+
const c = compile(output);
|
|
323
|
+
if ("error" in c) {
|
|
324
|
+
const problems = topLevelProblems(output, obj);
|
|
325
|
+
return [...problems, ...countProblems(output, obj, new Set(problems.map((p) => p.field)), true)];
|
|
326
|
+
}
|
|
327
|
+
const problems = [];
|
|
328
|
+
const seen = new Set();
|
|
329
|
+
c.validate(obj);
|
|
330
|
+
for (const e of c.validate.errors ?? []) {
|
|
331
|
+
const p = toProblem(e, obj);
|
|
332
|
+
if (seen.has(p.path))
|
|
333
|
+
continue;
|
|
334
|
+
seen.add(p.path);
|
|
335
|
+
problems.push(p);
|
|
336
|
+
}
|
|
337
|
+
return [...problems, ...countProblems(output, obj, new Set(problems.map((p) => p.field)), false)];
|
|
338
|
+
}
|
|
339
|
+
/** The contract check that never throws: its problems, or why it could not run. */
|
|
340
|
+
export function checkOutputContract(data, output) {
|
|
341
|
+
try {
|
|
342
|
+
return { problems: outputContractProblems(data, output) };
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function capped(lines) {
|
|
349
|
+
if (lines.length <= MAX_LISTED)
|
|
350
|
+
return lines;
|
|
351
|
+
return [...lines.slice(0, MAX_LISTED), `and ${lines.length - MAX_LISTED} more`];
|
|
352
|
+
}
|
|
353
|
+
/** The problems' paths, at most {@link MAX_LISTED} of them, for log lines and events. */
|
|
354
|
+
export function contractPaths(problems) {
|
|
355
|
+
return capped(problems.map((p) => p.path));
|
|
356
|
+
}
|
|
357
|
+
/** The problems as `path: problem` lines, at most {@link MAX_LISTED} of them. */
|
|
358
|
+
export function listContractProblems(problems) {
|
|
359
|
+
return capped(problems.map((p) => `${p.path}: ${p.problem}`));
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The node error for a contract still broken after the repair request.
|
|
363
|
+
* Missing top-level fields are named together, the way the required-field
|
|
364
|
+
* gate always has; every other problem follows as `path: problem`.
|
|
365
|
+
*/
|
|
366
|
+
export function contractFailureMessage(problems) {
|
|
367
|
+
const missing = problems.filter((p) => p.kind === "missing").map((p) => p.path);
|
|
368
|
+
const rest = problems.filter((p) => p.kind !== "missing");
|
|
369
|
+
const parts = [
|
|
370
|
+
...(missing.length > 0 ? [`required field(s) missing from emitted data: ${capped(missing).join(", ")}`] : []),
|
|
371
|
+
...listContractProblems(rest),
|
|
372
|
+
];
|
|
373
|
+
return `output schema violation: ${parts.join("; ")}`;
|
|
374
|
+
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1047,6 +1047,21 @@ export interface WorkflowError {
|
|
|
1047
1047
|
message: string;
|
|
1048
1048
|
nodeId?: string;
|
|
1049
1049
|
}
|
|
1050
|
+
/**
|
|
1051
|
+
* A finding that does not stop the workflow loading (#573): something that
|
|
1052
|
+
* works today and is deprecated. `OUTPUT_SCHEMA_FALLBACK`: a node's `output`
|
|
1053
|
+
* schema does not compile, so its output keeps the pre-#573 top-level check;
|
|
1054
|
+
* a future minor will refuse to load it.
|
|
1055
|
+
*/
|
|
1056
|
+
export interface WorkflowWarning {
|
|
1057
|
+
code: "OUTPUT_SCHEMA_FALLBACK";
|
|
1058
|
+
message: string;
|
|
1059
|
+
nodeId: string;
|
|
1060
|
+
}
|
|
1061
|
+
export interface ValidateWorkflowOptions {
|
|
1062
|
+
/** Receives warning-level findings. They never appear in the returned errors. */
|
|
1063
|
+
warnings?: WorkflowWarning[];
|
|
1064
|
+
}
|
|
1050
1065
|
/**
|
|
1051
1066
|
* Validate a workflow's graph structure.
|
|
1052
1067
|
*
|
|
@@ -1061,7 +1076,7 @@ export interface WorkflowError {
|
|
|
1061
1076
|
* Note: nodes with no outgoing edges are valid terminal nodes — the executor
|
|
1062
1077
|
* returns null when it reaches one, ending the workflow cleanly.
|
|
1063
1078
|
*/
|
|
1064
|
-
export declare function validateWorkflow(workflow: Workflow | z.infer<typeof workflowZ>, knownSkills?: Set<string
|
|
1079
|
+
export declare function validateWorkflow(workflow: Workflow | z.infer<typeof workflowZ>, knownSkills?: Set<string>, options?: ValidateWorkflowOptions): WorkflowError[];
|
|
1065
1080
|
export declare const workflowJsonSchema: {
|
|
1066
1081
|
readonly $schema: "https://json-schema.org/draft/2020-12/schema";
|
|
1067
1082
|
readonly $id: "https://spec.sweny.ai/schemas/workflow.json";
|
package/dist/schema.js
CHANGED
|
@@ -12,6 +12,7 @@ import { sourceZ } from "./sources.js";
|
|
|
12
12
|
import { resolveNodePermissions } from "./node-policy.js";
|
|
13
13
|
import { workflowInputsZ, WORKFLOW_INPUT_TYPES } from "./inputs.js";
|
|
14
14
|
import { checkExpression, isWhenExpression, WHEN_EXPR_MAX_LENGTH } from "./when.js";
|
|
15
|
+
import { outputSchemaError } from "./output-contract.js";
|
|
15
16
|
// Studio and other schema-only consumers label edges without importing the root entry.
|
|
16
17
|
export { isWhenExpression, whenLabel } from "./when.js";
|
|
17
18
|
export { sourceZ };
|
|
@@ -455,7 +456,7 @@ export function parseWorkflow(raw) {
|
|
|
455
456
|
* Note: nodes with no outgoing edges are valid terminal nodes — the executor
|
|
456
457
|
* returns null when it reaches one, ending the workflow cleanly.
|
|
457
458
|
*/
|
|
458
|
-
export function validateWorkflow(workflow, knownSkills) {
|
|
459
|
+
export function validateWorkflow(workflow, knownSkills, options = {}) {
|
|
459
460
|
const errors = [];
|
|
460
461
|
const nodeIds = new Set(Object.keys(workflow.nodes));
|
|
461
462
|
const entryExists = nodeIds.has(workflow.entry);
|
|
@@ -693,6 +694,21 @@ export function validateWorkflow(workflow, knownSkills) {
|
|
|
693
694
|
});
|
|
694
695
|
}
|
|
695
696
|
}
|
|
697
|
+
// Output schemas compile with the validator the executor checks outputs
|
|
698
|
+
// with (#573). One that does not compile still loads, as it did before: a
|
|
699
|
+
// warning, and the node keeps the pre-#573 top-level check. Deprecated.
|
|
700
|
+
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
701
|
+
const why = outputSchemaError(node.output);
|
|
702
|
+
if (why !== undefined) {
|
|
703
|
+
options.warnings?.push({
|
|
704
|
+
code: "OUTPUT_SCHEMA_FALLBACK",
|
|
705
|
+
message: `Node "${nodeId}" declares an output schema the validator cannot compile (${why}). ` +
|
|
706
|
+
`Its output gets only the old top-level check (required fields, top-level types and enums). ` +
|
|
707
|
+
`Deprecated: a future minor will refuse to load this workflow; fix the schema or name its draft in $schema.`,
|
|
708
|
+
nodeId,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
696
712
|
// Reject eval policies that are reserved in the vocabulary but not yet
|
|
697
713
|
// implemented. The Zod schema accepts the whole EVAL_POLICIES enum (the
|
|
698
714
|
// published JSON schema advertises them as reserved), but only `all_pass`
|
package/dist/types.d.ts
CHANGED
|
@@ -1003,6 +1003,12 @@ export interface Claude {
|
|
|
1003
1003
|
context: Record<string, unknown>;
|
|
1004
1004
|
tools: Tool[];
|
|
1005
1005
|
outputSchema?: JSONSchema;
|
|
1006
|
+
/**
|
|
1007
|
+
* The caller checks the output against `outputSchema` and asks for one
|
|
1008
|
+
* repair itself (sweny's executor, #573), so the harness must not run a
|
|
1009
|
+
* correction turn of its own: one repair loop on every harness.
|
|
1010
|
+
*/
|
|
1011
|
+
callerRepairsOutput?: boolean;
|
|
1006
1012
|
/** Called with status messages while Claude is working (tool name, etc.) */
|
|
1007
1013
|
onProgress?: (message: string) => void;
|
|
1008
1014
|
/** MCP servers declared by this node's resolved skills. Explicit client configs win. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sweny-ai/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.215",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sweny": "./dist/cli/main.js"
|
|
@@ -66,6 +66,8 @@
|
|
|
66
66
|
"@anthropic-ai/claude-agent-sdk": "^0.3.314",
|
|
67
67
|
"@clack/prompts": "^1.8.1",
|
|
68
68
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
69
|
+
"ajv": "^8.20.0",
|
|
70
|
+
"ajv-draft-04": "^1.0.0",
|
|
69
71
|
"chalk": "^5",
|
|
70
72
|
"commander": "^13",
|
|
71
73
|
"yaml": "^2.9.1",
|
|
@@ -75,7 +77,6 @@
|
|
|
75
77
|
"@anthropic-ai/claude-code": "^2.1.314",
|
|
76
78
|
"@stryker-mutator/core": "^10.1.1",
|
|
77
79
|
"@stryker-mutator/vitest-runner": "^10.1.1",
|
|
78
|
-
"ajv": "^8.20.0",
|
|
79
80
|
"esbuild": "^0.28.2",
|
|
80
81
|
"fast-check": "^3.23.2",
|
|
81
82
|
"prettier": "^3.9.10",
|