@sweny-ai/core 0.1.101 → 0.1.102
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/claude.js +1 -0
- package/dist/executor.js +102 -14
- package/dist/types.d.ts +17 -0
- package/package.json +1 -1
package/dist/claude.js
CHANGED
|
@@ -372,6 +372,7 @@ export function buildEvaluatePrompt(question, context, choices) {
|
|
|
372
372
|
`1. Read each choice's condition literally and match against the structured fields in the context (e.g. status, counts, enum values, boolean flags).`,
|
|
373
373
|
`2. Ignore prose narrative fields ("summary", free-form rationale, conversational commentary). They are not the contract.`,
|
|
374
374
|
`3. When a field's value contradicts what a prose field claims, trust the field's value.`,
|
|
375
|
+
`4. A field whose value is explicitly null means the source node DECLARED that field but did NOT emit a value. Treat null as "unknown" and do NOT match it against any specific value (do not match "is 0", "is N", "is true", "is false", or "is undefined" against a null field). Prefer a default/fallback edge when the field needed for a decision is null.`,
|
|
375
376
|
`\nRespond with ONLY the choice ID, nothing else.`,
|
|
376
377
|
].join("\n");
|
|
377
378
|
}
|
package/dist/executor.js
CHANGED
|
@@ -174,6 +174,27 @@ export async function execute(workflow, input, options) {
|
|
|
174
174
|
// Retry only triggers on eval failure. Bail on tool/API errors.
|
|
175
175
|
if (result.status !== "success")
|
|
176
176
|
break;
|
|
177
|
+
// Required-field gate: if the source node's declared output schema
|
|
178
|
+
// marks fields as `required`, those fields are the routing contract.
|
|
179
|
+
// An agent that omits a required field has violated the contract; we
|
|
180
|
+
// fail the node loudly rather than letting routing guess against a
|
|
181
|
+
// half-built data view. See the offload release-notes field bug:
|
|
182
|
+
// an optional quality_retry_count was declared in the schema but
|
|
183
|
+
// omitted by the agent, the route view silently dropped the key,
|
|
184
|
+
// and an "is 0 OR is undefined" condition ghost-matched.
|
|
185
|
+
//
|
|
186
|
+
// We only check declared `required` fields, not full JSON Schema
|
|
187
|
+
// validation. Type mismatches, formats, additionalProperties, etc.
|
|
188
|
+
// stay the workflow author's problem and don't have the same
|
|
189
|
+
// routing-correctness blast radius.
|
|
190
|
+
const missingRequired = findMissingRequiredFields(result.data, node.output);
|
|
191
|
+
if (missingRequired.length > 0) {
|
|
192
|
+
const msg = `output schema violation: required field(s) missing from emitted data: ${missingRequired.join(", ")}`;
|
|
193
|
+
result.status = "failed";
|
|
194
|
+
result.data = { ...(result.data ?? {}), error: msg };
|
|
195
|
+
logger.warn(` ${msg}`, { node: currentId });
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
177
198
|
const evalResults = await evaluateAll(node.eval, result, {
|
|
178
199
|
aliases: toolAliases,
|
|
179
200
|
claude,
|
|
@@ -344,16 +365,44 @@ function buildPriorNodeContext(result) {
|
|
|
344
365
|
function buildRouteEvalEntry(result, sourceNode) {
|
|
345
366
|
const fullData = (result.data ?? {});
|
|
346
367
|
const declaredProps = getDeclaredOutputProperties(sourceNode?.output);
|
|
347
|
-
|
|
368
|
+
// When the source node declared an `output.properties` block, the routing
|
|
369
|
+
// view is contract-shaped: every declared property is present in the
|
|
370
|
+
// view, with an explicit `null` when the agent did not emit it. The
|
|
371
|
+
// alternative (silently dropping the key) lets the LLM evaluator
|
|
372
|
+
// ghost-match conditions like "is 0", "is N", or "is undefined" against
|
|
373
|
+
// a structurally-absent field. That was the offload release-notes field
|
|
374
|
+
// bug: an optional `quality_retry_count` declared in the schema but
|
|
375
|
+
// omitted by the agent, evaluated against a condition like
|
|
376
|
+
// `quality_retry_count is 0 OR is undefined`, took the wrong branch.
|
|
377
|
+
//
|
|
378
|
+
// Returning `missing` separately lets the caller emit a loud warning so
|
|
379
|
+
// operators see the contract violation in the log stream.
|
|
380
|
+
let dataView;
|
|
381
|
+
const missing = [];
|
|
382
|
+
if (declaredProps) {
|
|
383
|
+
dataView = {};
|
|
384
|
+
for (const k of declaredProps) {
|
|
385
|
+
if (k in fullData) {
|
|
386
|
+
dataView[k] = fullData[k];
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
dataView[k] = null;
|
|
390
|
+
missing.push(k);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
dataView = fullData;
|
|
396
|
+
}
|
|
348
397
|
const evals = result.evals ?? [];
|
|
349
398
|
if (evals.length === 0)
|
|
350
|
-
return dataView;
|
|
399
|
+
return { view: dataView, missing };
|
|
351
400
|
const evalsByName = {};
|
|
352
401
|
for (const e of evals) {
|
|
353
402
|
evalsByName[e.name] = e;
|
|
354
403
|
}
|
|
355
404
|
// Spread evals first so a data-side `evals` key shadows it (back-compat).
|
|
356
|
-
return { evals: evalsByName, ...dataView };
|
|
405
|
+
return { view: { evals: evalsByName, ...dataView }, missing };
|
|
357
406
|
}
|
|
358
407
|
/**
|
|
359
408
|
* Return the declared output property names for a node, or null when the
|
|
@@ -375,13 +424,34 @@ function getDeclaredOutputProperties(output) {
|
|
|
375
424
|
return null;
|
|
376
425
|
return new Set(keys);
|
|
377
426
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
427
|
+
/**
|
|
428
|
+
* Return the declared `required` field names for a node's output schema,
|
|
429
|
+
* or an empty array when no required block is declared. Tolerant of
|
|
430
|
+
* malformed schemas (mirrors getDeclaredOutputProperties).
|
|
431
|
+
*/
|
|
432
|
+
function getDeclaredRequiredFields(output) {
|
|
433
|
+
if (!output || typeof output !== "object")
|
|
434
|
+
return [];
|
|
435
|
+
const required = output.required;
|
|
436
|
+
if (!Array.isArray(required))
|
|
437
|
+
return [];
|
|
438
|
+
return required.filter((r) => typeof r === "string");
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Validate a node's emitted data against the declared output schema's
|
|
442
|
+
* required fields. Returns the list of required-but-missing field names.
|
|
443
|
+
*
|
|
444
|
+
* This is intentionally narrow: we do NOT do full JSON Schema validation.
|
|
445
|
+
* The routing impact comes from required fields being absent, so that's
|
|
446
|
+
* what we check. Type mismatches, format violations, additionalProperties
|
|
447
|
+
* etc. stay the workflow author's problem.
|
|
448
|
+
*/
|
|
449
|
+
function findMissingRequiredFields(data, output) {
|
|
450
|
+
const required = getDeclaredRequiredFields(output);
|
|
451
|
+
if (required.length === 0)
|
|
452
|
+
return [];
|
|
453
|
+
const obj = (data ?? {});
|
|
454
|
+
return required.filter((k) => !(k in obj));
|
|
385
455
|
}
|
|
386
456
|
function nodeSourcesToArray(ns) {
|
|
387
457
|
if (!ns)
|
|
@@ -587,10 +657,28 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
587
657
|
// always-injected `summary` prose from the reference Claude client) can
|
|
588
658
|
// sway the natural-language route evaluator and override the actual
|
|
589
659
|
// structured result. See `buildRouteEvalEntry` for the full rationale.
|
|
590
|
-
const context = {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
660
|
+
const context = { input };
|
|
661
|
+
for (const [k, v] of results.entries()) {
|
|
662
|
+
const { view, missing } = buildRouteEvalEntry(v, workflow.nodes[k]);
|
|
663
|
+
context[k] = view;
|
|
664
|
+
if (missing.length > 0) {
|
|
665
|
+
// Loud signal for the operator. The view itself already replaces the
|
|
666
|
+
// missing keys with explicit `null`, so the LLM evaluator cannot
|
|
667
|
+
// ghost-match "is N" or "is undefined" against a structurally-absent
|
|
668
|
+
// field. The warning surfaces the contract violation in the log
|
|
669
|
+
// stream and to observer-based telemetry.
|
|
670
|
+
logger?.warn(` route eval: source node '${k}' declared properties not in emitted data: ${missing.join(", ")}`, {
|
|
671
|
+
node: k,
|
|
672
|
+
fields: missing,
|
|
673
|
+
});
|
|
674
|
+
safeObserve(observer, {
|
|
675
|
+
type: "node:warning",
|
|
676
|
+
node: k,
|
|
677
|
+
reason: `declared output properties missing from emitted data; routing view filled them with null`,
|
|
678
|
+
fields: missing,
|
|
679
|
+
}, logger);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
594
682
|
const choices = conditionalEdges.map((e) => ({
|
|
595
683
|
id: e.to,
|
|
596
684
|
description: e.when,
|
package/dist/types.d.ts
CHANGED
|
@@ -409,6 +409,23 @@ export type ExecutionEvent = {
|
|
|
409
409
|
attempt: number;
|
|
410
410
|
reason: string;
|
|
411
411
|
preamble: string;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Non-fatal contract violation surfaced during node execution. Currently
|
|
415
|
+
* emitted when a source node declares output properties that the agent
|
|
416
|
+
* did not emit. The route eval view fills the missing keys with `null`
|
|
417
|
+
* so the LLM evaluator can't ghost-match conditions like "is undefined"
|
|
418
|
+
* or "is 0", but the operator still wants a loud signal in the log
|
|
419
|
+
* stream.
|
|
420
|
+
*
|
|
421
|
+
* `fields` lists the declared property names that were missing from the
|
|
422
|
+
* emitted data. `reason` is a short human-readable summary.
|
|
423
|
+
*/
|
|
424
|
+
| {
|
|
425
|
+
type: "node:warning";
|
|
426
|
+
node: string;
|
|
427
|
+
reason: string;
|
|
428
|
+
fields: string[];
|
|
412
429
|
} | {
|
|
413
430
|
type: "route";
|
|
414
431
|
from: string;
|