@sweny-ai/core 0.1.168 → 0.1.170

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.js CHANGED
@@ -419,6 +419,7 @@ export async function execute(workflow, input, options) {
419
419
  const attemptBudget = budgetOn ? nodeBudget.attempt(signal) : undefined;
420
420
  try {
421
421
  result = await claude.run({
422
+ nodeId: currentId,
422
423
  instruction: currentInstruction,
423
424
  context,
424
425
  tools: trackedTools,
package/dist/testing.d.ts CHANGED
@@ -1,20 +1,18 @@
1
1
  /**
2
2
  * Test Helpers
3
3
  *
4
- * MockClaude for running workflows without an API key.
4
+ * MockHarness for running workflows without an API key.
5
5
  * File-based skill for local testing.
6
6
  *
7
7
  * @example
8
8
  * ```ts
9
- * import { MockClaude, fileSkill } from '@sweny-ai/core/testing'
9
+ * import { MockHarness } from '@sweny-ai/core/testing'
10
10
  *
11
- * const claude = new MockClaude({
12
- * gather: {
13
- * toolCalls: [{ tool: 'fs_read_json', input: { path: 'logs.json' } }],
14
- * data: { logs: [...] },
15
- * },
16
- * investigate: {
17
- * data: { root_cause: 'NPE in handler', severity: 'high' },
11
+ * const harness = new MockHarness({
12
+ * strict: true,
13
+ * responses: {
14
+ * gather: { data: { logs: [] } },
15
+ * investigate: { data: { root_cause: 'NPE in handler', severity: 'high' } },
18
16
  * },
19
17
  * })
20
18
  * ```
@@ -33,11 +31,13 @@ export interface MockNodeResponse {
33
31
  status?: "success" | "skipped" | "failed";
34
32
  }
35
33
  export interface MockHarnessOptions {
34
+ /** Refuse incomplete or ambiguous fixtures. Does not isolate supplied tool handlers. */
35
+ strict?: boolean;
36
36
  /** Node ID → scripted response */
37
37
  responses: Record<string, MockNodeResponse>;
38
38
  /** Route decisions: "fromNode" → chosen target node ID */
39
39
  routes?: Record<string, string>;
40
- /** Workflow definition — enables instruction-based node matching (required for branching workflows) */
40
+ /** Enables instruction matching for manual calls without nodeId; execute() supplies node IDs. */
41
41
  workflow?: Workflow;
42
42
  /** Scripted handler for `ask()` calls. */
43
43
  ask?: (instruction: string, context: Record<string, unknown>) => string;
@@ -57,6 +57,7 @@ export declare class MockHarness implements Claude, AgentHarness {
57
57
  private responses;
58
58
  private routes;
59
59
  private instructionMap;
60
+ private strict;
60
61
  private askFn?;
61
62
  constructor(opts: MockHarnessOptions);
62
63
  /** Returns the order in which nodes were executed */
@@ -70,6 +71,7 @@ export declare class MockHarness implements Claude, AgentHarness {
70
71
  prompt: string;
71
72
  }): Promise<string | null>;
72
73
  run(opts: {
74
+ nodeId?: string;
73
75
  instruction: string;
74
76
  context: Record<string, unknown>;
75
77
  tools: Tool[];
@@ -92,9 +94,11 @@ export declare class MockHarness implements Claude, AgentHarness {
92
94
  * Identify which node is being executed.
93
95
  *
94
96
  * Strategy:
95
- * 1. If a workflow was provided, match by instruction text (accurate for branching)
96
- * 2. Otherwise, fall back to sequential key matching (works for linear DAGs)
97
+ * Manual calls without nodeId can match a unique workflow instruction.
98
+ * Legacy mode also permits heuristic/sequential matching for compatibility.
99
+ * Strict mode never guesses from response order or instruction substrings.
97
100
  */
101
+ private fixtureFailure;
98
102
  private identifyNode;
99
103
  }
100
104
  /** @deprecated Use {@link MockHarnessOptions}. */
package/dist/testing.js CHANGED
@@ -1,20 +1,18 @@
1
1
  /**
2
2
  * Test Helpers
3
3
  *
4
- * MockClaude for running workflows without an API key.
4
+ * MockHarness for running workflows without an API key.
5
5
  * File-based skill for local testing.
6
6
  *
7
7
  * @example
8
8
  * ```ts
9
- * import { MockClaude, fileSkill } from '@sweny-ai/core/testing'
9
+ * import { MockHarness } from '@sweny-ai/core/testing'
10
10
  *
11
- * const claude = new MockClaude({
12
- * gather: {
13
- * toolCalls: [{ tool: 'fs_read_json', input: { path: 'logs.json' } }],
14
- * data: { logs: [...] },
15
- * },
16
- * investigate: {
17
- * data: { root_cause: 'NPE in handler', severity: 'high' },
11
+ * const harness = new MockHarness({
12
+ * strict: true,
13
+ * responses: {
14
+ * gather: { data: { logs: [] } },
15
+ * investigate: { data: { root_cause: 'NPE in handler', severity: 'high' } },
18
16
  * },
19
17
  * })
20
18
  * ```
@@ -47,9 +45,11 @@ export class MockHarness {
47
45
  callOrder = [];
48
46
  responses;
49
47
  routes;
50
- instructionMap; // instruction text → node ID
48
+ instructionMap; // instruction text → matching node IDs
49
+ strict;
51
50
  askFn;
52
51
  constructor(opts) {
52
+ this.strict = opts.strict ?? false;
53
53
  this.responses = opts.responses;
54
54
  this.routes = opts.routes ?? {};
55
55
  this.askFn = opts.ask;
@@ -59,7 +59,9 @@ export class MockHarness {
59
59
  for (const [id, node] of Object.entries(opts.workflow.nodes)) {
60
60
  // Source can be a string or an object — only index string instructions
61
61
  if (typeof node.instruction === "string") {
62
- this.instructionMap.set(node.instruction, id);
62
+ const ids = this.instructionMap.get(node.instruction) ?? [];
63
+ ids.push(id);
64
+ this.instructionMap.set(node.instruction, ids);
63
65
  }
64
66
  }
65
67
  }
@@ -80,19 +82,29 @@ export class MockHarness {
80
82
  return { ...result, harness: { id: "mock", version: "mock" }, degraded: [] };
81
83
  }
82
84
  async runScripted(opts) {
83
- // Identify which node this is by matching instruction text against responses
84
- // The executor wraps tools with event tracking, so we can find the node
85
- // by checking which response key's instruction appears
86
- const nodeId = this.identifyNode(opts.instruction);
85
+ // Executor identity is authoritative. Text matching is for older manual callers.
86
+ const nodeId = opts.nodeId ?? this.identifyNode(opts.instruction);
87
+ if (nodeId === null) {
88
+ return this.fixtureFailure("ambiguous or unknown instruction; pass nodeId or provide a workflow with unique instructions");
89
+ }
87
90
  this.callOrder.push(nodeId);
88
- const response = this.responses[nodeId];
91
+ const response = Object.hasOwn(this.responses, nodeId) ? this.responses[nodeId] : undefined;
89
92
  if (!response) {
93
+ if (this.strict)
94
+ return this.fixtureFailure(`node "${nodeId}" has no scripted response`);
90
95
  return {
91
96
  status: "success",
92
97
  data: { summary: `Mock: no scripted response for "${nodeId}"` },
93
98
  toolCalls: [],
94
99
  };
95
100
  }
101
+ // Check the entire script before executing any handler, so an incomplete
102
+ // fixture cannot perform an earlier write before discovering a missing tool.
103
+ if (this.strict) {
104
+ const missing = response.toolCalls?.find((tc) => !opts.tools.some((tool) => tool.name === tc.tool));
105
+ if (missing)
106
+ return this.fixtureFailure(`node "${nodeId}" requests unavailable tool "${missing.tool}"`);
107
+ }
96
108
  // Execute scripted tool calls
97
109
  const toolCalls = [];
98
110
  if (response.toolCalls) {
@@ -136,16 +148,29 @@ export class MockHarness {
136
148
  * Identify which node is being executed.
137
149
  *
138
150
  * Strategy:
139
- * 1. If a workflow was provided, match by instruction text (accurate for branching)
140
- * 2. Otherwise, fall back to sequential key matching (works for linear DAGs)
151
+ * Manual calls without nodeId can match a unique workflow instruction.
152
+ * Legacy mode also permits heuristic/sequential matching for compatibility.
153
+ * Strict mode never guesses from response order or instruction substrings.
141
154
  */
155
+ fixtureFailure(reason) {
156
+ return {
157
+ status: "failed",
158
+ data: { error: `MockHarness fixture error: ${reason}`, refused: true },
159
+ toolCalls: [],
160
+ };
161
+ }
142
162
  identifyNode(instruction) {
143
163
  // 1. Instruction-based matching (accurate for branching workflows)
144
164
  if (this.instructionMap.size > 0) {
145
- const nodeId = this.instructionMap.get(instruction);
146
- if (nodeId && nodeId in this.responses)
165
+ const matches = this.instructionMap.get(instruction);
166
+ if (this.strict)
167
+ return matches?.length === 1 ? matches[0] : null;
168
+ const nodeId = matches?.at(-1);
169
+ if (nodeId && Object.hasOwn(this.responses, nodeId))
147
170
  return nodeId;
148
171
  }
172
+ if (this.strict)
173
+ return null;
149
174
  // 2. Check if any response key appears literally in the instruction
150
175
  const keys = Object.keys(this.responses);
151
176
  for (const key of keys) {
package/dist/types.d.ts CHANGED
@@ -800,6 +800,8 @@ export interface Claude {
800
800
  readonly defaultJudgeModel?: string;
801
801
  /** Run a node: give Claude an instruction, context, and tools */
802
802
  run(opts: {
803
+ /** Stable workflow node identity, preserved across prompt wrapping and retries. */
804
+ nodeId?: string;
803
805
  instruction: string;
804
806
  context: Record<string, unknown>;
805
807
  tools: Tool[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.168",
3
+ "version": "0.1.170",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"