@sweny-ai/core 0.1.75 → 0.1.77
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__/cascade.test.js +3 -0
- package/dist/__tests__/executor.test.js +899 -0
- package/dist/__tests__/requires.test.d.ts +1 -0
- package/dist/__tests__/requires.test.js +82 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +213 -0
- package/dist/__tests__/schema.test.js +75 -1
- package/dist/__tests__/spec-conformance.test.js +10 -0
- package/dist/__tests__/testing.test.js +18 -0
- package/dist/claude.d.ts +4 -0
- package/dist/claude.js +41 -0
- package/dist/executor.js +104 -43
- package/dist/executor.test.js +15 -0
- package/dist/requires.d.ts +7 -0
- package/dist/requires.js +34 -0
- package/dist/retry.d.ts +23 -0
- package/dist/retry.js +72 -0
- package/dist/schema.d.ts +603 -48
- package/dist/schema.js +97 -0
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +7 -0
- package/dist/types.d.ts +64 -0
- package/dist/verify.d.ts +1 -0
- package/dist/verify.js +1 -1
- package/dist/workflow-builder.test.js +6 -0
- package/package.json +3 -3
package/dist/executor.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
import { consoleLogger } from "./types.js";
|
|
16
16
|
import { resolveSources } from "./source-resolver.js";
|
|
17
17
|
import { evaluateVerify } from "./verify.js";
|
|
18
|
+
import { evaluateRequires } from "./requires.js";
|
|
19
|
+
import { buildRetryPreamble } from "./retry.js";
|
|
18
20
|
/**
|
|
19
21
|
* Execute a workflow from entry to completion.
|
|
20
22
|
*
|
|
@@ -96,6 +98,33 @@ export async function execute(workflow, input, options) {
|
|
|
96
98
|
input,
|
|
97
99
|
...Object.fromEntries([...results.entries()].map(([k, v]) => [k, v.data])),
|
|
98
100
|
};
|
|
101
|
+
// Pre-condition gate: evaluate `requires` against the cross-node context
|
|
102
|
+
// BEFORE invoking the LLM. Failure either marks the node failed (on_fail
|
|
103
|
+
// default) or skipped (on_fail: "skip") and skips execution entirely.
|
|
104
|
+
const requiresError = evaluateRequires(node.requires, context);
|
|
105
|
+
if (requiresError) {
|
|
106
|
+
const onFail = node.requires?.on_fail ?? "fail";
|
|
107
|
+
const result = onFail === "skip"
|
|
108
|
+
? {
|
|
109
|
+
status: "skipped",
|
|
110
|
+
data: { skipped_reason: requiresError.replace(/^requires failed:/, "requires not met:") },
|
|
111
|
+
toolCalls: [],
|
|
112
|
+
}
|
|
113
|
+
: {
|
|
114
|
+
status: "failed",
|
|
115
|
+
data: { error: requiresError },
|
|
116
|
+
toolCalls: [],
|
|
117
|
+
};
|
|
118
|
+
results.set(currentId, result);
|
|
119
|
+
trace.steps.push({ node: currentId, status: result.status, iteration });
|
|
120
|
+
safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
|
|
121
|
+
logger.warn(` requires ${onFail === "skip" ? "skipped" : "failed"}: ${requiresError}`, { node: currentId });
|
|
122
|
+
// Apply normal routing rules (dry run gate + resolveNext).
|
|
123
|
+
// TODO: dedupe with requires path — see advanceFromNode helper below
|
|
124
|
+
const next = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace);
|
|
125
|
+
currentId = next;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
99
128
|
// Wrap tool handlers to emit events + inject context
|
|
100
129
|
const trackedTools = tools.map((t) => ({
|
|
101
130
|
...t,
|
|
@@ -111,56 +140,59 @@ export async function execute(workflow, input, options) {
|
|
|
111
140
|
const effectiveRules = assembleCascaded("rules", currentId, workflow.nodes[currentId], inputRules.length, (workflow.rules ?? []).length, resolvedSources);
|
|
112
141
|
const effectiveContext = assembleCascaded("context", currentId, workflow.nodes[currentId], inputContext.length, (workflow.context ?? []).length, resolvedSources);
|
|
113
142
|
const instruction = buildNodeInstruction(resolvedInstruction, effectiveRules, effectiveContext, input, skillInstructions);
|
|
114
|
-
// Run Claude on this node
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
// Run Claude on this node, with optional verify-failure retry loop.
|
|
144
|
+
let attempt = 0;
|
|
145
|
+
let result;
|
|
146
|
+
let currentInstruction = instruction;
|
|
147
|
+
const retry = node.retry;
|
|
148
|
+
while (true) {
|
|
149
|
+
result = await claude.run({
|
|
150
|
+
instruction: currentInstruction,
|
|
151
|
+
context,
|
|
152
|
+
tools: trackedTools,
|
|
153
|
+
outputSchema: node.output,
|
|
154
|
+
maxTurns: node.max_turns,
|
|
155
|
+
onProgress: (message) => {
|
|
156
|
+
safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
// Retry only triggers on verify failure — bail on tool/API errors.
|
|
160
|
+
if (result.status !== "success")
|
|
161
|
+
break;
|
|
131
162
|
const verifyError = evaluateVerify(node.verify, result);
|
|
132
|
-
if (verifyError)
|
|
133
|
-
|
|
134
|
-
|
|
163
|
+
if (!verifyError)
|
|
164
|
+
break;
|
|
165
|
+
// Apply verify failure to the result.
|
|
166
|
+
result.status = "failed";
|
|
167
|
+
result.data = { ...result.data, error: verifyError };
|
|
168
|
+
if (!retry || attempt >= retry.max) {
|
|
135
169
|
logger.warn(` verify failed: ${verifyError}`, { node: currentId });
|
|
170
|
+
break;
|
|
136
171
|
}
|
|
172
|
+
// Build retry preamble + record attempt in trace.
|
|
173
|
+
trace.steps.push({ node: currentId, status: "failed", iteration, retryAttempt: attempt });
|
|
174
|
+
logger.warn(` verify failed (attempt ${attempt + 1}/${retry.max + 1}): ${verifyError}`, { node: currentId });
|
|
175
|
+
const preamble = await buildRetryPreamble({
|
|
176
|
+
retry,
|
|
177
|
+
verifyError,
|
|
178
|
+
toolCalls: result.toolCalls,
|
|
179
|
+
nodeInstruction: resolvedInstruction,
|
|
180
|
+
claude,
|
|
181
|
+
logger,
|
|
182
|
+
context,
|
|
183
|
+
});
|
|
184
|
+
currentInstruction = `${preamble}\n\n---\n\n${instruction}`;
|
|
185
|
+
safeObserve(observer, { type: "node:retry", node: currentId, attempt: attempt + 1, reason: verifyError, preamble }, logger);
|
|
186
|
+
attempt++;
|
|
137
187
|
}
|
|
138
188
|
results.set(currentId, result);
|
|
139
|
-
trace.steps.push(
|
|
189
|
+
trace.steps.push(attempt > 0
|
|
190
|
+
? { node: currentId, status: result.status, iteration, retryAttempt: attempt }
|
|
191
|
+
: { node: currentId, status: result.status, iteration });
|
|
140
192
|
safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
|
|
141
193
|
logger.info(` ✓ ${result.status}`, { node: currentId, toolCalls: result.toolCalls.length });
|
|
142
|
-
// Dry run
|
|
143
|
-
|
|
144
|
-
// conditional edges are action decisions (investigate→create_issue/skip).
|
|
145
|
-
// Enforced in the executor so it cannot be bypassed by LLM evaluation.
|
|
146
|
-
const isDryRun = input && typeof input === "object" && input.dryRun === true;
|
|
147
|
-
if (isDryRun) {
|
|
148
|
-
const outEdges = workflow.edges.filter((e) => e.from === currentId);
|
|
149
|
-
if (outEdges.some((e) => e.when)) {
|
|
150
|
-
safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "dry run" }, logger);
|
|
151
|
-
currentId = null;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Resolve next node via edge conditions
|
|
156
|
-
const prevId = currentId;
|
|
157
|
-
currentId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger);
|
|
158
|
-
// Record the routing decision in the trace
|
|
159
|
-
if (currentId) {
|
|
160
|
-
const edgeKey = `${prevId}→${currentId}`;
|
|
161
|
-
const reason = workflow.edges.find((e) => e.from === prevId && e.to === currentId)?.when ?? "only path";
|
|
162
|
-
trace.edges.push({ from: prevId, to: currentId, reason });
|
|
163
|
-
}
|
|
194
|
+
// Dry run gate + routing — shared with requires path via advanceFromNode helper.
|
|
195
|
+
currentId = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace);
|
|
164
196
|
}
|
|
165
197
|
safeObserve(observer, {
|
|
166
198
|
type: "workflow:end",
|
|
@@ -341,6 +373,35 @@ function resolveConfig(skills, overrides) {
|
|
|
341
373
|
}
|
|
342
374
|
return config;
|
|
343
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Apply the dry-run gate and then resolve the next node via edge conditions.
|
|
378
|
+
*
|
|
379
|
+
* Used in both the normal execution path and the requires-failure path so
|
|
380
|
+
* the dry-run guard + resolveNext + trace-edge recording logic lives in one place.
|
|
381
|
+
*
|
|
382
|
+
* Returns the next node ID, or null when execution should stop.
|
|
383
|
+
*/
|
|
384
|
+
async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace) {
|
|
385
|
+
// Dry run hard gate — stop at the first conditional routing decision.
|
|
386
|
+
// Unconditional edges are analysis flow (prepare→gather→investigate);
|
|
387
|
+
// conditional edges are action decisions (investigate→create_issue/skip).
|
|
388
|
+
// Enforced in the executor so it cannot be bypassed by LLM evaluation.
|
|
389
|
+
const isDryRun = input && typeof input === "object" && input.dryRun === true;
|
|
390
|
+
if (isDryRun) {
|
|
391
|
+
const outEdges = workflow.edges.filter((e) => e.from === currentId);
|
|
392
|
+
if (outEdges.some((e) => e.when)) {
|
|
393
|
+
safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "dry run" }, logger);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const prevId = currentId;
|
|
398
|
+
const nextId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger);
|
|
399
|
+
if (nextId) {
|
|
400
|
+
const reason = workflow.edges.find((e) => e.from === prevId && e.to === nextId)?.when ?? "only path";
|
|
401
|
+
trace.edges.push({ from: prevId, to: nextId, reason });
|
|
402
|
+
}
|
|
403
|
+
return nextId;
|
|
404
|
+
}
|
|
344
405
|
/**
|
|
345
406
|
* Resolve which edge to follow from the current node.
|
|
346
407
|
*
|
package/dist/executor.test.js
CHANGED
|
@@ -353,6 +353,9 @@ describe("skill instruction injection", () => {
|
|
|
353
353
|
async evaluate() {
|
|
354
354
|
return "";
|
|
355
355
|
},
|
|
356
|
+
async ask() {
|
|
357
|
+
return "";
|
|
358
|
+
},
|
|
356
359
|
};
|
|
357
360
|
await execute(workflow, {}, {
|
|
358
361
|
skills: createSkillMap([instructionSkill]),
|
|
@@ -405,6 +408,9 @@ describe("skill instruction injection", () => {
|
|
|
405
408
|
async evaluate() {
|
|
406
409
|
return "";
|
|
407
410
|
},
|
|
411
|
+
async ask() {
|
|
412
|
+
return "";
|
|
413
|
+
},
|
|
408
414
|
};
|
|
409
415
|
await execute(workflow, {}, {
|
|
410
416
|
skills: createSkillMap([skillA, skillB]),
|
|
@@ -448,6 +454,9 @@ describe("skill instruction injection", () => {
|
|
|
448
454
|
async evaluate() {
|
|
449
455
|
return "";
|
|
450
456
|
},
|
|
457
|
+
async ask() {
|
|
458
|
+
return "";
|
|
459
|
+
},
|
|
451
460
|
};
|
|
452
461
|
// Should NOT throw "none are configured"
|
|
453
462
|
const { results } = await execute(workflow, {}, {
|
|
@@ -487,6 +496,9 @@ describe("skill instruction injection", () => {
|
|
|
487
496
|
async evaluate() {
|
|
488
497
|
return "";
|
|
489
498
|
},
|
|
499
|
+
async ask() {
|
|
500
|
+
return "";
|
|
501
|
+
},
|
|
490
502
|
};
|
|
491
503
|
// No "my-rubric" in the skill map — executor must pull it from workflow.skills
|
|
492
504
|
const { results } = await execute(workflow, {}, {
|
|
@@ -537,6 +549,9 @@ describe("skill instruction injection", () => {
|
|
|
537
549
|
async evaluate() {
|
|
538
550
|
return "";
|
|
539
551
|
},
|
|
552
|
+
async ask() {
|
|
553
|
+
return "";
|
|
554
|
+
},
|
|
540
555
|
};
|
|
541
556
|
await execute(workflow, {}, {
|
|
542
557
|
skills: createSkillMap([callerSkill]),
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NodeRequires } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Evaluate a node's `requires` block against the cross-node context map.
|
|
4
|
+
* Returns null when all checks pass (or when `requires` is undefined),
|
|
5
|
+
* otherwise a single concatenated failure string.
|
|
6
|
+
*/
|
|
7
|
+
export declare function evaluateRequires(requires: NodeRequires | undefined, context: Record<string, unknown>): string | null;
|
package/dist/requires.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// ─── Requires: pre-condition gate ───────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Evaluates `node.requires` BEFORE the LLM runs. Same path grammar and
|
|
4
|
+
// resolver as verify — the only differences are the data root (cross-node
|
|
5
|
+
// context map instead of result.data) and the error prefix.
|
|
6
|
+
import { checkOutputRequired, checkOutputMatches } from "./verify.js";
|
|
7
|
+
/**
|
|
8
|
+
* Evaluate a node's `requires` block against the cross-node context map.
|
|
9
|
+
* Returns null when all checks pass (or when `requires` is undefined),
|
|
10
|
+
* otherwise a single concatenated failure string.
|
|
11
|
+
*/
|
|
12
|
+
export function evaluateRequires(requires, context) {
|
|
13
|
+
if (!requires)
|
|
14
|
+
return null;
|
|
15
|
+
const hasRequiredChecks = requires.output_required && requires.output_required.length > 0;
|
|
16
|
+
const hasMatchChecks = requires.output_matches && requires.output_matches.length > 0;
|
|
17
|
+
if (!hasRequiredChecks && !hasMatchChecks) {
|
|
18
|
+
return `requires failed:\n - Requires block present but no checks declared`;
|
|
19
|
+
}
|
|
20
|
+
const failures = [];
|
|
21
|
+
if (hasRequiredChecks) {
|
|
22
|
+
const e = checkOutputRequired(requires.output_required, context);
|
|
23
|
+
if (e)
|
|
24
|
+
failures.push(e);
|
|
25
|
+
}
|
|
26
|
+
if (hasMatchChecks) {
|
|
27
|
+
const e = checkOutputMatches(requires.output_matches, context);
|
|
28
|
+
if (e)
|
|
29
|
+
failures.push(e);
|
|
30
|
+
}
|
|
31
|
+
if (failures.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
return `requires failed:\n - ${failures.join("\n - ")}`;
|
|
34
|
+
}
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Claude, NodeRetry, ToolCall, Logger } from "./types.js";
|
|
2
|
+
export interface BuildRetryPreambleOptions {
|
|
3
|
+
retry: NodeRetry;
|
|
4
|
+
verifyError: string;
|
|
5
|
+
toolCalls: ToolCall[];
|
|
6
|
+
nodeInstruction: string;
|
|
7
|
+
claude: Claude;
|
|
8
|
+
logger: Logger;
|
|
9
|
+
context: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build the retry preamble for a single retry attempt.
|
|
13
|
+
*
|
|
14
|
+
* Resolution order:
|
|
15
|
+
* 1. retry.instruction is a string → static preamble + verify error
|
|
16
|
+
* 2. retry.instruction is { auto: true } → claude.ask(default prompt)
|
|
17
|
+
* 3. retry.instruction is { reflect: "..." } → claude.ask(author prompt)
|
|
18
|
+
* 4. Otherwise (omitted) → default preamble
|
|
19
|
+
*
|
|
20
|
+
* Reflection failure (throw or empty response) silently falls back to the
|
|
21
|
+
* default static preamble and logs a warning.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildRetryPreamble(opts: BuildRetryPreambleOptions): Promise<string>;
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// ─── Retry: preamble construction for verify-failure self-healing ──
|
|
2
|
+
//
|
|
3
|
+
// Builds the preamble that gets prepended to the node instruction on retry.
|
|
4
|
+
// In autonomous modes ({ auto: true } or { reflect: ... }), this calls
|
|
5
|
+
// claude.ask to generate a diagnosis. If reflection fails or returns empty,
|
|
6
|
+
// falls back to the default static preamble — reflection failure never
|
|
7
|
+
// escalates to a workflow failure.
|
|
8
|
+
import { isErrorOutput } from "./verify.js";
|
|
9
|
+
const DEFAULT_REFLECTION_PROMPT = "Briefly diagnose the failure and state your strategy for the retry. Keep your response to 2-4 sentences.";
|
|
10
|
+
const DEFAULT_PREAMBLE_HEADER = "## Previous attempt failed verification";
|
|
11
|
+
/**
|
|
12
|
+
* Build the retry preamble for a single retry attempt.
|
|
13
|
+
*
|
|
14
|
+
* Resolution order:
|
|
15
|
+
* 1. retry.instruction is a string → static preamble + verify error
|
|
16
|
+
* 2. retry.instruction is { auto: true } → claude.ask(default prompt)
|
|
17
|
+
* 3. retry.instruction is { reflect: "..." } → claude.ask(author prompt)
|
|
18
|
+
* 4. Otherwise (omitted) → default preamble
|
|
19
|
+
*
|
|
20
|
+
* Reflection failure (throw or empty response) silently falls back to the
|
|
21
|
+
* default static preamble and logs a warning.
|
|
22
|
+
*/
|
|
23
|
+
export async function buildRetryPreamble(opts) {
|
|
24
|
+
const { retry, verifyError, toolCalls, nodeInstruction, claude, logger, context } = opts;
|
|
25
|
+
const inst = retry.instruction;
|
|
26
|
+
if (typeof inst === "string") {
|
|
27
|
+
return `## Retry guidance\n\n${inst}\n\n${DEFAULT_PREAMBLE_HEADER}\n\n${verifyError}`;
|
|
28
|
+
}
|
|
29
|
+
if (inst && typeof inst === "object") {
|
|
30
|
+
const reflectPrompt = "reflect" in inst && typeof inst.reflect === "string" ? inst.reflect : DEFAULT_REFLECTION_PROMPT;
|
|
31
|
+
const askInstruction = buildReflectionPrompt(reflectPrompt, nodeInstruction, verifyError, toolCalls);
|
|
32
|
+
try {
|
|
33
|
+
const diagnosis = await claude.ask({ instruction: askInstruction, context });
|
|
34
|
+
const trimmed = diagnosis.trim();
|
|
35
|
+
if (trimmed.length > 0) {
|
|
36
|
+
return `## Reflection on previous attempt\n\n${trimmed}\n\n${DEFAULT_PREAMBLE_HEADER}\n\n${verifyError}`;
|
|
37
|
+
}
|
|
38
|
+
logger.warn("Retry reflection returned empty response; falling back to default preamble.");
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
42
|
+
logger.warn(`Retry reflection threw: ${msg}; falling back to default preamble.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return defaultPreamble(verifyError);
|
|
46
|
+
}
|
|
47
|
+
function defaultPreamble(verifyError) {
|
|
48
|
+
return `${DEFAULT_PREAMBLE_HEADER}\n\n${verifyError}\n\nFix the issue and try again.`;
|
|
49
|
+
}
|
|
50
|
+
function buildReflectionPrompt(reflectPrompt, nodeInstruction, verifyError, toolCalls) {
|
|
51
|
+
const summary = summarizeToolCalls(toolCalls);
|
|
52
|
+
return [
|
|
53
|
+
`You attempted to: ${nodeInstruction}`,
|
|
54
|
+
"",
|
|
55
|
+
`Verification failed with: ${verifyError}`,
|
|
56
|
+
"",
|
|
57
|
+
`You called these tools during the failed attempt:`,
|
|
58
|
+
summary,
|
|
59
|
+
"",
|
|
60
|
+
reflectPrompt,
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
function summarizeToolCalls(toolCalls) {
|
|
64
|
+
if (toolCalls.length === 0)
|
|
65
|
+
return "(no tools were called)";
|
|
66
|
+
return toolCalls
|
|
67
|
+
.map((c) => {
|
|
68
|
+
const status = isErrorOutput(c.output) ? "error" : "ok";
|
|
69
|
+
return ` - ${c.tool} (${status})`;
|
|
70
|
+
})
|
|
71
|
+
.join("\n");
|
|
72
|
+
}
|