@sweny-ai/core 0.1.99 → 0.1.100
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.d.ts +20 -0
- package/dist/claude.js +30 -7
- package/dist/executor.js +75 -1
- package/package.json +1 -1
package/dist/claude.d.ts
CHANGED
|
@@ -74,6 +74,26 @@ export declare class ClaudeClient implements Claude {
|
|
|
74
74
|
* `"42"`) from a tool that returned the string "42" (wrapper also sends
|
|
75
75
|
* `"42"`). Preserving the string is safer than guessing.
|
|
76
76
|
*/
|
|
77
|
+
/**
|
|
78
|
+
* Build the prompt used by `ClaudeClient.evaluate()` to pick a routing edge.
|
|
79
|
+
*
|
|
80
|
+
* Extracted as a pure function so the prompt body can be unit tested without
|
|
81
|
+
* spinning up an SDK query. The body is part of the routing contract: workflow
|
|
82
|
+
* authors and downstream maintainers rely on the model leaning on structured
|
|
83
|
+
* fields rather than prose narrative when picking an edge, especially in the
|
|
84
|
+
* fallback case where a source node did not declare an `output` schema (the
|
|
85
|
+
* executor's schema-strict filter only kicks in when a schema is present;
|
|
86
|
+
* see `buildRouteEvalEntry` in executor.ts).
|
|
87
|
+
*
|
|
88
|
+
* Any change to this prompt should keep:
|
|
89
|
+
* - The three "Evaluation rules" pointing the model at structured fields
|
|
90
|
+
* and away from prose narrative.
|
|
91
|
+
* - A terminal directive to return ONLY the choice ID.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildEvaluatePrompt(question: string, context: Record<string, unknown>, choices: {
|
|
94
|
+
id: string;
|
|
95
|
+
description: string;
|
|
96
|
+
}[]): string;
|
|
77
97
|
export declare function parseToolResultContent(content: unknown): unknown;
|
|
78
98
|
/**
|
|
79
99
|
* Produce a short, single-line description of a tool-error payload suitable
|
package/dist/claude.js
CHANGED
|
@@ -214,13 +214,7 @@ export class ClaudeClient {
|
|
|
214
214
|
}
|
|
215
215
|
async evaluate(opts) {
|
|
216
216
|
const { question, context, choices } = opts;
|
|
217
|
-
const
|
|
218
|
-
const prompt = [
|
|
219
|
-
question,
|
|
220
|
-
`\nContext:\n\`\`\`json\n${JSON.stringify(context, null, 2)}\n\`\`\``,
|
|
221
|
-
`\nChoices:\n${choiceList}`,
|
|
222
|
-
`\nRespond with ONLY the choice ID, nothing else.`,
|
|
223
|
-
].join("\n");
|
|
217
|
+
const prompt = buildEvaluatePrompt(question, context, choices);
|
|
224
218
|
const env = this.buildEnv();
|
|
225
219
|
let response = "";
|
|
226
220
|
try {
|
|
@@ -351,6 +345,35 @@ function coreToolToSdkTool(coreTool, defaultCtx) {
|
|
|
351
345
|
* `"42"`) from a tool that returned the string "42" (wrapper also sends
|
|
352
346
|
* `"42"`). Preserving the string is safer than guessing.
|
|
353
347
|
*/
|
|
348
|
+
/**
|
|
349
|
+
* Build the prompt used by `ClaudeClient.evaluate()` to pick a routing edge.
|
|
350
|
+
*
|
|
351
|
+
* Extracted as a pure function so the prompt body can be unit tested without
|
|
352
|
+
* spinning up an SDK query. The body is part of the routing contract: workflow
|
|
353
|
+
* authors and downstream maintainers rely on the model leaning on structured
|
|
354
|
+
* fields rather than prose narrative when picking an edge, especially in the
|
|
355
|
+
* fallback case where a source node did not declare an `output` schema (the
|
|
356
|
+
* executor's schema-strict filter only kicks in when a schema is present;
|
|
357
|
+
* see `buildRouteEvalEntry` in executor.ts).
|
|
358
|
+
*
|
|
359
|
+
* Any change to this prompt should keep:
|
|
360
|
+
* - The three "Evaluation rules" pointing the model at structured fields
|
|
361
|
+
* and away from prose narrative.
|
|
362
|
+
* - A terminal directive to return ONLY the choice ID.
|
|
363
|
+
*/
|
|
364
|
+
export function buildEvaluatePrompt(question, context, choices) {
|
|
365
|
+
const choiceList = choices.map((c) => `- "${c.id}": ${c.description}`).join("\n");
|
|
366
|
+
return [
|
|
367
|
+
question,
|
|
368
|
+
`\nContext:\n\`\`\`json\n${JSON.stringify(context, null, 2)}\n\`\`\``,
|
|
369
|
+
`\nChoices:\n${choiceList}`,
|
|
370
|
+
`\nEvaluation rules:`,
|
|
371
|
+
`1. Read each choice's condition literally and match against the structured fields in the context (e.g. status, counts, enum values, boolean flags).`,
|
|
372
|
+
`2. Ignore prose narrative fields ("summary", free-form rationale, conversational commentary). They are not the contract.`,
|
|
373
|
+
`3. When a field's value contradicts what a prose field claims, trust the field's value.`,
|
|
374
|
+
`\nRespond with ONLY the choice ID, nothing else.`,
|
|
375
|
+
].join("\n");
|
|
376
|
+
}
|
|
354
377
|
export function parseToolResultContent(content) {
|
|
355
378
|
if (typeof content !== "string")
|
|
356
379
|
return content;
|
package/dist/executor.js
CHANGED
|
@@ -315,6 +315,73 @@ function buildPriorNodeContext(result) {
|
|
|
315
315
|
// Spread evals first so a data-side `evals` key shadows it (back-compat).
|
|
316
316
|
return { evals: evalsByName, ...data };
|
|
317
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Build the prior-node entry shown to the LLM route evaluator.
|
|
320
|
+
*
|
|
321
|
+
* Differs from `buildPriorNodeContext` (which feeds downstream node `run()`
|
|
322
|
+
* calls) in one way: when the source node declared an `output` schema with
|
|
323
|
+
* a `properties` block, the data view is restricted to those declared
|
|
324
|
+
* properties. The `evals` namespace is always preserved when present.
|
|
325
|
+
*
|
|
326
|
+
* Why this exists. The route evaluator is a natural-language model. Anything
|
|
327
|
+
* it sees in the context can sway the decision, including prose narrative
|
|
328
|
+
* fields like the always-injected `summary` from the reference Claude
|
|
329
|
+
* client (claude.ts: `data: { summary: response, ...parsed }`). Real-world
|
|
330
|
+
* symptom: a node correctly emits `status: "pass"` but also adds a
|
|
331
|
+
* conversational `summary` like "the quality_retry_count is 1", and the
|
|
332
|
+
* evaluator pattern-matches the retry mention to flip the routing decision.
|
|
333
|
+
*
|
|
334
|
+
* The fix is to treat the `output` schema as the routing contract. If the
|
|
335
|
+
* author declared which fields matter, only those reach the route
|
|
336
|
+
* evaluator. When no schema is declared we fall back to the full data
|
|
337
|
+
* (back-compat for workflows without structured outputs).
|
|
338
|
+
*
|
|
339
|
+
* The downstream node prompt is untouched: nodes still see the full prior
|
|
340
|
+
* `data` including any prose `summary`, so workflows that consume the
|
|
341
|
+
* narrative in subsequent steps keep working.
|
|
342
|
+
*/
|
|
343
|
+
function buildRouteEvalEntry(result, sourceNode) {
|
|
344
|
+
const fullData = (result.data ?? {});
|
|
345
|
+
const declaredProps = getDeclaredOutputProperties(sourceNode?.output);
|
|
346
|
+
const dataView = declaredProps ? pickKeys(fullData, declaredProps) : fullData;
|
|
347
|
+
const evals = result.evals ?? [];
|
|
348
|
+
if (evals.length === 0)
|
|
349
|
+
return dataView;
|
|
350
|
+
const evalsByName = {};
|
|
351
|
+
for (const e of evals) {
|
|
352
|
+
evalsByName[e.name] = e;
|
|
353
|
+
}
|
|
354
|
+
// Spread evals first so a data-side `evals` key shadows it (back-compat).
|
|
355
|
+
return { evals: evalsByName, ...dataView };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Return the declared output property names for a node, or null when the
|
|
359
|
+
* node has no `output` schema or its schema does not declare a `properties`
|
|
360
|
+
* block. Null signals "no contract" and routing falls back to the full
|
|
361
|
+
* data view.
|
|
362
|
+
*
|
|
363
|
+
* Tolerant of malformed schemas because `output` is `JSONSchema =
|
|
364
|
+
* Record<string, unknown>` and is not parsed further by the executor.
|
|
365
|
+
*/
|
|
366
|
+
function getDeclaredOutputProperties(output) {
|
|
367
|
+
if (!output || typeof output !== "object")
|
|
368
|
+
return null;
|
|
369
|
+
const props = output.properties;
|
|
370
|
+
if (!props || typeof props !== "object")
|
|
371
|
+
return null;
|
|
372
|
+
const keys = Object.keys(props);
|
|
373
|
+
if (keys.length === 0)
|
|
374
|
+
return null;
|
|
375
|
+
return new Set(keys);
|
|
376
|
+
}
|
|
377
|
+
function pickKeys(obj, keys) {
|
|
378
|
+
const out = {};
|
|
379
|
+
for (const k of keys) {
|
|
380
|
+
if (k in obj)
|
|
381
|
+
out[k] = obj[k];
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
318
385
|
function nodeSourcesToArray(ns) {
|
|
319
386
|
if (!ns)
|
|
320
387
|
return { sources: [], only: false };
|
|
@@ -512,9 +579,16 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
512
579
|
// Claude evaluates which condition matches. Include input so conditions
|
|
513
580
|
// can reference workflow-level flags like dryRun, and expose evals so
|
|
514
581
|
// routing edges can read `priorNode.evals.X.pass`.
|
|
582
|
+
//
|
|
583
|
+
// Crucial difference from the run() context: when a prior node declared
|
|
584
|
+
// an `output` schema, the routing view of its data is restricted to the
|
|
585
|
+
// declared properties. Without this, non-schema fields (notably the
|
|
586
|
+
// always-injected `summary` prose from the reference Claude client) can
|
|
587
|
+
// sway the natural-language route evaluator and override the actual
|
|
588
|
+
// structured result. See `buildRouteEvalEntry` for the full rationale.
|
|
515
589
|
const context = {
|
|
516
590
|
input,
|
|
517
|
-
...Object.fromEntries([...results.entries()].map(([k, v]) => [k,
|
|
591
|
+
...Object.fromEntries([...results.entries()].map(([k, v]) => [k, buildRouteEvalEntry(v, workflow.nodes[k])])),
|
|
518
592
|
};
|
|
519
593
|
const choices = conditionalEdges.map((e) => ({
|
|
520
594
|
id: e.to,
|