mcpgraph 0.1.15 → 0.1.16
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/README.md +26 -1
- package/dist/api.d.ts +13 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +40 -0
- package/dist/api.js.map +1 -1
- package/dist/config/expression-validator.d.ts +12 -0
- package/dist/config/expression-validator.d.ts.map +1 -0
- package/dist/config/expression-validator.js +77 -0
- package/dist/config/expression-validator.js.map +1 -0
- package/dist/config/parser.d.ts.map +1 -1
- package/dist/config/parser.js +9 -0
- package/dist/config/parser.js.map +1 -1
- package/dist/execution/context.d.ts +27 -3
- package/dist/execution/context.d.ts.map +1 -1
- package/dist/execution/context.js +52 -9
- package/dist/execution/context.js.map +1 -1
- package/dist/execution/executor.d.ts.map +1 -1
- package/dist/execution/executor.js +11 -12
- package/dist/execution/executor.js.map +1 -1
- package/dist/execution/nodes/entry-executor.d.ts.map +1 -1
- package/dist/execution/nodes/entry-executor.js +1 -2
- package/dist/execution/nodes/entry-executor.js.map +1 -1
- package/dist/execution/nodes/exit-executor.d.ts +1 -1
- package/dist/execution/nodes/exit-executor.d.ts.map +1 -1
- package/dist/execution/nodes/exit-executor.js +9 -7
- package/dist/execution/nodes/exit-executor.js.map +1 -1
- package/dist/execution/nodes/mcp-tool-executor.d.ts +1 -1
- package/dist/execution/nodes/mcp-tool-executor.d.ts.map +1 -1
- package/dist/execution/nodes/mcp-tool-executor.js +5 -4
- package/dist/execution/nodes/mcp-tool-executor.js.map +1 -1
- package/dist/execution/nodes/switch-executor.d.ts +1 -1
- package/dist/execution/nodes/switch-executor.d.ts.map +1 -1
- package/dist/execution/nodes/switch-executor.js +13 -8
- package/dist/execution/nodes/switch-executor.js.map +1 -1
- package/dist/execution/nodes/transform-executor.d.ts +1 -1
- package/dist/execution/nodes/transform-executor.d.ts.map +1 -1
- package/dist/execution/nodes/transform-executor.js +5 -4
- package/dist/execution/nodes/transform-executor.js.map +1 -1
- package/dist/expressions/json-logic.d.ts +11 -2
- package/dist/expressions/json-logic.d.ts.map +1 -1
- package/dist/expressions/json-logic.js +98 -16
- package/dist/expressions/json-logic.js.map +1 -1
- package/dist/expressions/jsonata-extensions.d.ts +10 -0
- package/dist/expressions/jsonata-extensions.d.ts.map +1 -0
- package/dist/expressions/jsonata-extensions.js +69 -0
- package/dist/expressions/jsonata-extensions.js.map +1 -0
- package/dist/expressions/jsonata.d.ts +8 -1
- package/dist/expressions/jsonata.d.ts.map +1 -1
- package/dist/expressions/jsonata.js +72 -12
- package/dist/expressions/jsonata.js.map +1 -1
- package/dist/types/execution.d.ts +1 -1
- package/dist/types/execution.d.ts.map +1 -1
- package/docs/design.md +5 -0
- package/docs/execution-context-redesign.md +284 -0
- package/docs/introspection-debugging.md +28 -1
- package/examples/api-usage.ts +54 -1
- package/examples/loop_example.yaml +84 -0
- package/package.json +1 -1
|
@@ -3,18 +3,48 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import jsonata from "jsonata";
|
|
5
5
|
import { logger } from "../logger.js";
|
|
6
|
-
|
|
6
|
+
import { registerHistoryFunctions } from "./jsonata-extensions.js";
|
|
7
|
+
/**
|
|
8
|
+
* Validate JSONata expression syntax without evaluating it
|
|
9
|
+
* @param expression - JSONata expression string to validate
|
|
10
|
+
* @throws Error if syntax is invalid
|
|
11
|
+
*/
|
|
12
|
+
export function validateJsonataSyntax(expression) {
|
|
7
13
|
try {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
jsonata(expression);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
// Extract detailed error message
|
|
18
|
+
let errorMessage;
|
|
19
|
+
if (error instanceof Error) {
|
|
20
|
+
errorMessage = error.message || error.toString();
|
|
21
|
+
if (errorMessage === '[object Object]' || !errorMessage) {
|
|
22
|
+
errorMessage = error.toString();
|
|
23
|
+
if (error.stack) {
|
|
24
|
+
errorMessage = error.stack.split('\n')[0] || errorMessage;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (error && typeof error === 'object') {
|
|
29
|
+
const errorObj = error;
|
|
30
|
+
if (errorObj.message && typeof errorObj.message === 'string') {
|
|
31
|
+
errorMessage = errorObj.message;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
errorMessage = JSON.stringify(error, null, 2);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
errorMessage = String(error);
|
|
17
39
|
}
|
|
40
|
+
throw new Error(`Invalid JSONata syntax: ${errorMessage}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function evaluateJsonata(expression, context, history, currentIndex) {
|
|
44
|
+
try {
|
|
45
|
+
const expr = jsonata(expression);
|
|
46
|
+
// Register history access functions
|
|
47
|
+
registerHistoryFunctions(expr, history, currentIndex);
|
|
18
48
|
const result = await expr.evaluate(context);
|
|
19
49
|
// Log for debugging
|
|
20
50
|
logger.debug(`JSONata expression: ${expression}`);
|
|
@@ -27,10 +57,40 @@ export async function evaluateJsonata(expression, context, previousNodeId) {
|
|
|
27
57
|
return result;
|
|
28
58
|
}
|
|
29
59
|
catch (error) {
|
|
30
|
-
|
|
60
|
+
// Extract detailed error message
|
|
61
|
+
let errorMessage;
|
|
62
|
+
if (error instanceof Error) {
|
|
63
|
+
errorMessage = error.message || error.toString();
|
|
64
|
+
// If message is still unhelpful, try to get more details
|
|
65
|
+
if (errorMessage === '[object Object]' || !errorMessage) {
|
|
66
|
+
errorMessage = error.toString();
|
|
67
|
+
// Try to get stack trace or other properties
|
|
68
|
+
if (error.stack) {
|
|
69
|
+
errorMessage = error.stack.split('\n')[0] || errorMessage;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (error && typeof error === 'object') {
|
|
74
|
+
// Try to extract meaningful information from error object
|
|
75
|
+
const errorObj = error;
|
|
76
|
+
if (errorObj.message && typeof errorObj.message === 'string') {
|
|
77
|
+
errorMessage = errorObj.message;
|
|
78
|
+
}
|
|
79
|
+
else if (errorObj.code && typeof errorObj.code === 'string') {
|
|
80
|
+
errorMessage = `Error code: ${errorObj.code}`;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
errorMessage = JSON.stringify(error, null, 2);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
errorMessage = String(error);
|
|
88
|
+
}
|
|
89
|
+
logger.error(`JSONata evaluation error: ${errorMessage}`);
|
|
31
90
|
logger.error(`Expression: ${expression}`);
|
|
91
|
+
logger.error(`Context keys: ${Object.keys(context).join(', ')}`);
|
|
32
92
|
logger.error(`Context: ${JSON.stringify(context, null, 2)}`);
|
|
33
|
-
throw
|
|
93
|
+
throw new Error(`JSONata evaluation failed: ${errorMessage}\nExpression: ${expression}`);
|
|
34
94
|
}
|
|
35
95
|
}
|
|
36
96
|
//# sourceMappingURL=jsonata.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonata.js","sourceRoot":"","sources":["../../src/expressions/jsonata.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"jsonata.js","sourceRoot":"","sources":["../../src/expressions/jsonata.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,12 +5,12 @@ import type { NodeDefinition } from "./config.js";
|
|
|
5
5
|
import type { ExecutionContext } from "../execution/context.js";
|
|
6
6
|
export type ExecutionStatus = "not_started" | "running" | "paused" | "finished" | "error" | "stopped";
|
|
7
7
|
export interface NodeExecutionRecord {
|
|
8
|
+
executionIndex: number;
|
|
8
9
|
nodeId: string;
|
|
9
10
|
nodeType: string;
|
|
10
11
|
startTime: number;
|
|
11
12
|
endTime: number;
|
|
12
13
|
duration: number;
|
|
13
|
-
input: unknown;
|
|
14
14
|
output: unknown;
|
|
15
15
|
error?: Error;
|
|
16
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execution.d.ts","sourceRoot":"","sources":["../../src/types/execution.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAEtG,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,
|
|
1
|
+
{"version":3,"file":"execution.d.ts","sourceRoot":"","sources":["../../src/types/execution.ts"],"names":[],"mappings":"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"}
|
package/docs/design.md
CHANGED
|
@@ -131,10 +131,15 @@ The YAML configuration centers around MCP server and tool definitions:
|
|
|
131
131
|
- Note: Entry and exit nodes are defined in the nodes section with a `tool` field indicating which tool they belong to
|
|
132
132
|
3. **Nodes**: The directed graph of nodes that execute when tools are called. Node types include:
|
|
133
133
|
- **`entry`**: Entry point for a tool's graph execution. Receives tool arguments and initializes execution context.
|
|
134
|
+
- **Output**: The tool input arguments (passed through as-is)
|
|
134
135
|
- **`mcp`**: Calls an MCP tool on an internal or external MCP server using `callTool`
|
|
136
|
+
- **Output**: The MCP tool's response (parsed from the tool's content)
|
|
135
137
|
- **`transform`**: Applies JSONata expressions to transform data between nodes
|
|
138
|
+
- **Output**: The result of evaluating the JSONata expression
|
|
136
139
|
- **`switch`**: Uses JSON Logic to conditionally route to different nodes based on data
|
|
140
|
+
- **Output**: The node ID of the target node that was routed to (string)
|
|
137
141
|
- **`exit`**: Exit point for a tool's graph execution. Extracts and returns the final result to the MCP tool caller
|
|
142
|
+
- **Output**: The output from the previous node in the execution history
|
|
138
143
|
|
|
139
144
|
### Example YAML Structure: count_files Tool
|
|
140
145
|
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Execution Context & History Redesign
|
|
2
|
+
|
|
3
|
+
## Rationale
|
|
4
|
+
|
|
5
|
+
The current execution context design has several limitations that become apparent when considering loops, debugging, and observability:
|
|
6
|
+
|
|
7
|
+
### Current Issues
|
|
8
|
+
|
|
9
|
+
1. **No execution history**: Only current context is maintained; no record of past executions
|
|
10
|
+
2. **Previous node tracking is a hack**: Tracked separately (`previousNodeId`) instead of being derived from history
|
|
11
|
+
3. **Loops overwrite data**: Same node executing multiple times overwrites previous outputs in context
|
|
12
|
+
4. **Context structure doesn't distinguish iterations**: Using node ID as key doesn't handle multiple executions of the same node
|
|
13
|
+
5. **Can't reconstruct historical context**: No way to see what context was available to a node at the time it executed (for debugging)
|
|
14
|
+
|
|
15
|
+
### Goals
|
|
16
|
+
|
|
17
|
+
1. **Single source of truth**: Execution history should be the authoritative record
|
|
18
|
+
2. **Handle loops gracefully**: Multiple executions of the same node should be accessible
|
|
19
|
+
3. **Derive previous node from history**: No separate tracking needed
|
|
20
|
+
4. **Time-travel debugging**: Reconstruct context at any point in execution
|
|
21
|
+
5. **Powerful JSONata access**: Access all executions, not just latest
|
|
22
|
+
6. **Backward compatibility**: Existing expressions should continue to work
|
|
23
|
+
|
|
24
|
+
## Current Implementation
|
|
25
|
+
|
|
26
|
+
### What We Have Now
|
|
27
|
+
|
|
28
|
+
- **Execution History**: `NodeExecutionRecord[]` stored in `ExecutionContext`
|
|
29
|
+
- Each record: `nodeId`, `nodeType`, `startTime`, `endTime`, `duration`, `input`, `output`, `error`
|
|
30
|
+
- Used for telemetry and debugging hooks
|
|
31
|
+
|
|
32
|
+
- **Current Context**: Separate `data` object keyed by node ID
|
|
33
|
+
- `this.data[nodeId] = output` (overwrites on loops)
|
|
34
|
+
- Used for JSONata/JSON Logic evaluation
|
|
35
|
+
|
|
36
|
+
- **Previous Node Tracking**: Separate `previousNodeId` variable passed around
|
|
37
|
+
|
|
38
|
+
### Problems
|
|
39
|
+
|
|
40
|
+
1. **Redundancy**: Both history and context store node outputs
|
|
41
|
+
2. **Input storage**: Storing `input` (full context) is redundant - can be derived from history
|
|
42
|
+
3. **Loop handling**: Context overwrites, history has multiple records but can't distinguish them
|
|
43
|
+
4. **Previous node**: Tracked separately instead of derived from history
|
|
44
|
+
|
|
45
|
+
## Proposed Design
|
|
46
|
+
|
|
47
|
+
### Core Insight
|
|
48
|
+
|
|
49
|
+
**If execution is sequential (no parallelism), the execution history is just an ordered array of node executions where:**
|
|
50
|
+
- Each execution's input is the context built from all previous executions
|
|
51
|
+
- Previous node is just `history[index - 1]`
|
|
52
|
+
- Context is built from history once per node execution (when node starts)
|
|
53
|
+
- History is the single source of truth
|
|
54
|
+
|
|
55
|
+
### Execution History Structure
|
|
56
|
+
|
|
57
|
+
**Structure:**
|
|
58
|
+
```typescript
|
|
59
|
+
interface NodeExecutionRecord {
|
|
60
|
+
executionIndex: number; // Position in overall execution history (0, 1, 2, ...) - unique identifier
|
|
61
|
+
nodeId: string;
|
|
62
|
+
nodeType: string;
|
|
63
|
+
startTime: number;
|
|
64
|
+
endTime: number;
|
|
65
|
+
duration: number;
|
|
66
|
+
output: unknown; // Only store output, derive input from history
|
|
67
|
+
error?: Error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class ExecutionContext {
|
|
71
|
+
private history: NodeExecutionRecord[] = [];
|
|
72
|
+
|
|
73
|
+
getData(): Record<string, unknown> {
|
|
74
|
+
// Build context from history - called once per node execution
|
|
75
|
+
// The context is built when the node starts and used throughout its execution
|
|
76
|
+
return this.buildContextFromHistory();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getPreviousNode(currentIndex: number): NodeExecutionRecord | null {
|
|
80
|
+
return currentIndex > 0 ? this.history[currentIndex - 1] : null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Key Points:**
|
|
86
|
+
- History array is the single source of truth
|
|
87
|
+
- No separate `data` object
|
|
88
|
+
- No `input` field in records (derivable from history)
|
|
89
|
+
- Context built fresh from history once per node execution
|
|
90
|
+
- Previous node derived from history index
|
|
91
|
+
|
|
92
|
+
### Context Structure for JSONata Access
|
|
93
|
+
|
|
94
|
+
We use a **flat context structure** with **history access functions**:
|
|
95
|
+
|
|
96
|
+
**Context Building:**
|
|
97
|
+
```typescript
|
|
98
|
+
private buildContextFromHistory(): Record<string, unknown> {
|
|
99
|
+
const context: Record<string, unknown> = {};
|
|
100
|
+
// Walk backwards, most recent execution of each node wins
|
|
101
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
102
|
+
const record = this.history[i];
|
|
103
|
+
if (!(record.nodeId in context)) {
|
|
104
|
+
context[record.nodeId] = record.output;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return context;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Example Context Object:**
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"entry_count_files": { "directory": "/path/to/dir" },
|
|
115
|
+
"list_directory_node": "[FILE] file1.txt\n[FILE] file2.txt",
|
|
116
|
+
"count_files_node": { "count": 2 }
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**If same node executes multiple times (loop):**
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"entry_loop": { "value": 0 },
|
|
124
|
+
"increment_node": { "value": 3 } // Only latest execution
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**JSONata Access:**
|
|
129
|
+
- `$.node_name` → latest output (backward compatible, simple notation)
|
|
130
|
+
- `$.node_name.foo` → property access
|
|
131
|
+
|
|
132
|
+
**Custom JSONata Functions (access history directly):**
|
|
133
|
+
- `$previousNode()` → previous node's output (from history)
|
|
134
|
+
- `$previousNode(index)` → node that executed N steps before current
|
|
135
|
+
- `$executionCount(nodeName)` → count of executions for a node
|
|
136
|
+
- `$nodeExecution(nodeName, index)` → specific execution by index (0 = first, -1 = last)
|
|
137
|
+
- `$nodeExecutions(nodeName)` → array of all executions for a node
|
|
138
|
+
|
|
139
|
+
**Example Usage:**
|
|
140
|
+
```jsonata
|
|
141
|
+
// Get previous node's output
|
|
142
|
+
$previousNode()
|
|
143
|
+
|
|
144
|
+
// Get execution count
|
|
145
|
+
$executionCount("increment_node") // Returns 3 if executed 3 times
|
|
146
|
+
|
|
147
|
+
// Get first execution
|
|
148
|
+
$nodeExecution("increment_node", 0) // Returns { "value": 1 }
|
|
149
|
+
|
|
150
|
+
// Get all executions
|
|
151
|
+
$nodeExecutions("increment_node") // Returns [{ "value": 1 }, { "value": 2 }, { "value": 3 }]
|
|
152
|
+
|
|
153
|
+
// Get second-to-last execution
|
|
154
|
+
$nodeExecution("increment_node", -2) // Returns { "value": 2 }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Implementation Note:**
|
|
158
|
+
Functions receive the execution history array and current execution index as parameters, allowing them to query history directly without exposing it in the context structure.
|
|
159
|
+
|
|
160
|
+
**Benefits:**
|
|
161
|
+
- Simple, flat context structure (backward compatible)
|
|
162
|
+
- Fast to build
|
|
163
|
+
- Clean notation: `$.node_name` for latest
|
|
164
|
+
- History access is explicit and clear
|
|
165
|
+
- No namespace conflicts (no special keys in context)
|
|
166
|
+
- Functions can provide powerful queries beyond simple data access
|
|
167
|
+
- History queries are separate from context structure
|
|
168
|
+
|
|
169
|
+
## Design Decisions
|
|
170
|
+
|
|
171
|
+
### Input Storage
|
|
172
|
+
|
|
173
|
+
**Decision**: Don't store `input` in `NodeExecutionRecord` - derive from history when needed.
|
|
174
|
+
|
|
175
|
+
**Rationale**: No redundancy, can always derive input by building context from history up to that point.
|
|
176
|
+
|
|
177
|
+
### Previous Node Resolution
|
|
178
|
+
|
|
179
|
+
**Decision**: `$previousNode()` is a custom JSONata function that queries the history array.
|
|
180
|
+
|
|
181
|
+
**Rationale**: Cleaner, more flexible, and keeps history access explicit.
|
|
182
|
+
|
|
183
|
+
### Entry Node Handling
|
|
184
|
+
|
|
185
|
+
**Decision**: Entry node's input is the tool input. When building context for the entry node, tool input is available as the entry node's output (stored in history).
|
|
186
|
+
|
|
187
|
+
**Rationale**: Consistent with other nodes - tool input is the input to the entry node.
|
|
188
|
+
|
|
189
|
+
### Execution Index
|
|
190
|
+
|
|
191
|
+
**Decision**: Add `executionIndex` field to `NodeExecutionRecord` to uniquely identify each execution.
|
|
192
|
+
|
|
193
|
+
**Rationale**:
|
|
194
|
+
- Provides a unique identifier for each execution (even when same node executes multiple times)
|
|
195
|
+
- Enables API endpoints to reference specific executions (e.g., "get context for execution at index 5")
|
|
196
|
+
- Makes it easy for debuggers to reference and query specific executions
|
|
197
|
+
- The index represents the position in the overall execution history array (0, 1, 2, ...)
|
|
198
|
+
|
|
199
|
+
## Implementation Plan
|
|
200
|
+
|
|
201
|
+
### Phase 1: Refactor ExecutionContext
|
|
202
|
+
|
|
203
|
+
1. Remove `data` object, keep only `history`
|
|
204
|
+
2. Remove `input` from `NodeExecutionRecord` (derive from history)
|
|
205
|
+
3. Add `executionIndex` to `NodeExecutionRecord` (set when adding to history)
|
|
206
|
+
4. Implement `buildContextFromHistory(upToIndex?: number)` method:
|
|
207
|
+
- If `upToIndex` is provided, build context from history up to that index (for debugging)
|
|
208
|
+
- If not provided, build context from entire history (for current execution)
|
|
209
|
+
5. Update `getData()` to build context from history once per node execution
|
|
210
|
+
|
|
211
|
+
### Phase 2: Update Node Executors
|
|
212
|
+
|
|
213
|
+
1. Remove `previousNodeId` parameter from all node executors
|
|
214
|
+
2. Update `addHistory()` to include `executionIndex` (current history length)
|
|
215
|
+
3. Update `addHistory()` calls to not pass `input` (or derive it)
|
|
216
|
+
4. Update exit node to get previous node from history instead of `previousNodeId`
|
|
217
|
+
|
|
218
|
+
### Phase 3: Update Expression Evaluation
|
|
219
|
+
|
|
220
|
+
1. Context stays flat - no changes needed to context structure
|
|
221
|
+
2. Add custom JSONata functions that receive history and current index:
|
|
222
|
+
- `$previousNode()` - returns previous node's output (from history)
|
|
223
|
+
- `$previousNode(index)` - returns node that executed N steps before current
|
|
224
|
+
- `$executionCount(nodeName)` - count of executions for a node
|
|
225
|
+
- `$nodeExecution(nodeName, index)` - specific execution by index (0 = first, -1 = last)
|
|
226
|
+
- `$nodeExecutions(nodeName)` - array of all executions for a node
|
|
227
|
+
3. Update `evaluateJsonLogic()` to work with flat context and new functions
|
|
228
|
+
4. Functions need access to:
|
|
229
|
+
- Execution history array
|
|
230
|
+
- Current execution index (to determine "previous")
|
|
231
|
+
|
|
232
|
+
### Phase 4: Update Hooks and Telemetry
|
|
233
|
+
|
|
234
|
+
1. Update hooks to derive input from history when needed
|
|
235
|
+
2. Verify telemetry still works correctly (it already uses history structure, but verify after removing `input` field)
|
|
236
|
+
3. Update introspection/debugging docs
|
|
237
|
+
|
|
238
|
+
### Phase 5: Add Debugging API Endpoint
|
|
239
|
+
|
|
240
|
+
1. Add `getContextForExecution(executionIndex: number)` method to API:
|
|
241
|
+
- Takes an `executionIndex` to identify a specific execution
|
|
242
|
+
- Builds context from history up to that execution index
|
|
243
|
+
- Returns the context that was available to that node when it executed
|
|
244
|
+
- Useful for time-travel debugging - "what context did this node see?"
|
|
245
|
+
2. Add helper method `getExecutionByIndex(executionIndex: number)` to easily access a specific record
|
|
246
|
+
|
|
247
|
+
### Phase 6: Testing & Documentation
|
|
248
|
+
|
|
249
|
+
1. Add tests for loop scenarios
|
|
250
|
+
2. Add tests for `$previousNode()` and other custom functions
|
|
251
|
+
3. Add tests for `getContextForExecution()` API endpoint
|
|
252
|
+
4. Update examples to show new capabilities
|
|
253
|
+
5. Update documentation
|
|
254
|
+
|
|
255
|
+
## Open Questions
|
|
256
|
+
|
|
257
|
+
1. **Performance**: Is building context from history too slow? (Answer: No - we build it once per node execution when the node starts, and it's a simple loop through the history array)
|
|
258
|
+
|
|
259
|
+
2. **Backward Compatibility**: Do we need to support old flat context structure? (Answer: Yes - the flat context structure maintains full backward compatibility - `$.node_name` works exactly as before)
|
|
260
|
+
|
|
261
|
+
3. **History Persistence**: Should history be persisted across executions? (Answer: Not in scope for this redesign, but structure supports it)
|
|
262
|
+
|
|
263
|
+
4. **Parallel Execution**: If we add parallel execution later, how does this design handle it? (Answer: Would need execution IDs or iteration numbers, but structure can accommodate)
|
|
264
|
+
|
|
265
|
+
## Next Steps
|
|
266
|
+
|
|
267
|
+
1. **Implementation**: Phase 1 (refactor ExecutionContext)
|
|
268
|
+
- Remove `data` object
|
|
269
|
+
- Remove `input` from `NodeExecutionRecord`
|
|
270
|
+
- Add `executionIndex` to `NodeExecutionRecord`
|
|
271
|
+
- Implement `buildContextFromHistory(upToIndex?)` method
|
|
272
|
+
2. **Implementation**: Phase 2 (update node executors)
|
|
273
|
+
- Remove `previousNodeId` parameter from all node executors
|
|
274
|
+
- Update `addHistory()` to include `executionIndex` (current history length)
|
|
275
|
+
- Update exit node to get previous node from history instead of `previousNodeId`
|
|
276
|
+
3. **Implementation**: Phase 3 (add history functions)
|
|
277
|
+
- Design function signatures (how to pass history/index to functions)
|
|
278
|
+
- Implement `$previousNode()`, `$executionCount()`, `$nodeExecution()`, `$nodeExecutions()`
|
|
279
|
+
4. **Implementation**: Phase 5 (add debugging API)
|
|
280
|
+
- Add `getContextForExecution(executionIndex: number)` to API
|
|
281
|
+
- Add `getExecutionByIndex(executionIndex: number)` helper
|
|
282
|
+
5. **Testing**: Ensure all existing tests pass
|
|
283
|
+
6. **Testing**: Add loop tests, new function tests, and API endpoint tests
|
|
284
|
+
|
|
@@ -364,17 +364,19 @@ Execution history provides a complete record of all node executions with detaile
|
|
|
364
364
|
|
|
365
365
|
```typescript
|
|
366
366
|
interface NodeExecutionRecord {
|
|
367
|
+
executionIndex: number; // Position in overall execution history (0, 1, 2, ...) - unique identifier
|
|
367
368
|
nodeId: string; // ID of the executed node
|
|
368
369
|
nodeType: string; // Type of node (entry, exit, transform, mcp, switch)
|
|
369
370
|
startTime: number; // Timestamp when node started (milliseconds)
|
|
370
371
|
endTime: number; // Timestamp when node ended (milliseconds)
|
|
371
372
|
duration: number; // Execution duration (milliseconds)
|
|
372
|
-
input: unknown; // Input data for the node
|
|
373
373
|
output: unknown; // Output data from the node
|
|
374
374
|
error?: Error; // Error object if node failed
|
|
375
375
|
}
|
|
376
376
|
```
|
|
377
377
|
|
|
378
|
+
**Note**: The `input` field has been removed. Input context can be derived by building context from history up to the execution index using `getContextForExecution(executionIndex)`.
|
|
379
|
+
|
|
378
380
|
### Accessing History
|
|
379
381
|
|
|
380
382
|
```typescript
|
|
@@ -398,6 +400,31 @@ if (state) {
|
|
|
398
400
|
}
|
|
399
401
|
```
|
|
400
402
|
|
|
403
|
+
### Time-Travel Debugging
|
|
404
|
+
|
|
405
|
+
You can get the context that was available to a specific execution using `getContextForExecution()`:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// Get context for a specific execution
|
|
409
|
+
const context = api.getContextForExecution(5);
|
|
410
|
+
if (context) {
|
|
411
|
+
console.log('Context available to execution #5:', context);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Get a specific execution record
|
|
415
|
+
const record = api.getExecutionByIndex(5);
|
|
416
|
+
if (record) {
|
|
417
|
+
console.log(`Execution #5: ${record.nodeId} executed at ${record.startTime}`);
|
|
418
|
+
console.log(`Output:`, record.output);
|
|
419
|
+
|
|
420
|
+
// Get the context that was available to this execution
|
|
421
|
+
const inputContext = api.getContextForExecution(record.executionIndex);
|
|
422
|
+
console.log('Input context:', inputContext);
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Note**: Both methods require an active execution with a controller (hooks/breakpoints enabled). They return `null` if no execution is in progress or the index is invalid.
|
|
427
|
+
|
|
401
428
|
## Telemetry
|
|
402
429
|
|
|
403
430
|
Telemetry provides aggregated performance metrics and execution statistics.
|
package/examples/api-usage.ts
CHANGED
|
@@ -70,7 +70,7 @@ async function introspectionExample() {
|
|
|
70
70
|
if (result.executionHistory) {
|
|
71
71
|
console.log('\nExecution History:');
|
|
72
72
|
for (const record of result.executionHistory) {
|
|
73
|
-
console.log(` ${record.nodeId} (${record.nodeType}): ${record.duration}ms`);
|
|
73
|
+
console.log(` [${record.executionIndex}] ${record.nodeId} (${record.nodeType}): ${record.duration}ms`);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -88,6 +88,57 @@ async function introspectionExample() {
|
|
|
88
88
|
await api.close();
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// Example: Time-travel debugging with getContextForExecution
|
|
92
|
+
async function timeTravelDebuggingExample() {
|
|
93
|
+
const api = new McpGraphApi('examples/count_files.yaml');
|
|
94
|
+
|
|
95
|
+
let executionIndexToInspect: number | null = null;
|
|
96
|
+
|
|
97
|
+
const { promise, controller } = api.executeTool('count_files', {
|
|
98
|
+
directory: './tests/files',
|
|
99
|
+
}, {
|
|
100
|
+
hooks: {
|
|
101
|
+
onNodeComplete: async (nodeId, node, input, output, duration) => {
|
|
102
|
+
// When count_files_node completes, inspect the context that was available to list_directory_node
|
|
103
|
+
if (nodeId === 'count_files_node') {
|
|
104
|
+
// Find the execution index of list_directory_node
|
|
105
|
+
const state = api.getExecutionState();
|
|
106
|
+
if (state) {
|
|
107
|
+
const listDirRecord = state.executionHistory.find(r => r.nodeId === 'list_directory_node');
|
|
108
|
+
if (listDirRecord) {
|
|
109
|
+
executionIndexToInspect = listDirRecord.executionIndex;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await promise;
|
|
118
|
+
|
|
119
|
+
if (executionIndexToInspect !== null && controller) {
|
|
120
|
+
// Get the context that was available to list_directory_node when it executed
|
|
121
|
+
const context = api.getContextForExecution(executionIndexToInspect);
|
|
122
|
+
if (context) {
|
|
123
|
+
console.log('\nTime-Travel Debugging:');
|
|
124
|
+
console.log(`Context available to execution #${executionIndexToInspect} (list_directory_node):`);
|
|
125
|
+
console.log(JSON.stringify(context, null, 2));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Get the execution record itself
|
|
129
|
+
const record = api.getExecutionByIndex(executionIndexToInspect);
|
|
130
|
+
if (record) {
|
|
131
|
+
console.log(`\nExecution Record #${executionIndexToInspect}:`);
|
|
132
|
+
console.log(` Node: ${record.nodeId}`);
|
|
133
|
+
console.log(` Type: ${record.nodeType}`);
|
|
134
|
+
console.log(` Duration: ${record.duration}ms`);
|
|
135
|
+
console.log(` Output: ${JSON.stringify(record.output).substring(0, 100)}...`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await api.close();
|
|
140
|
+
}
|
|
141
|
+
|
|
91
142
|
// Example: Validate config without creating an API instance
|
|
92
143
|
function validateConfigExample() {
|
|
93
144
|
const errors = McpGraphApi.validateConfig('examples/count_files.yaml');
|
|
@@ -118,6 +169,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
118
169
|
|
|
119
170
|
if (exampleToRun === 'introspection') {
|
|
120
171
|
introspectionExample().catch(console.error);
|
|
172
|
+
} else if (exampleToRun === 'debugging') {
|
|
173
|
+
timeTravelDebuggingExample().catch(console.error);
|
|
121
174
|
} else {
|
|
122
175
|
example().catch(console.error);
|
|
123
176
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
version: "1.0"
|
|
2
|
+
|
|
3
|
+
# MCP Server Metadata
|
|
4
|
+
server:
|
|
5
|
+
name: "loopExample"
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
description: "Example with a loop using history functions"
|
|
8
|
+
|
|
9
|
+
# Tool Definitions
|
|
10
|
+
tools:
|
|
11
|
+
- name: "sum_to_n"
|
|
12
|
+
description: "Sums numbers from 1 to n using a loop"
|
|
13
|
+
inputSchema:
|
|
14
|
+
type: "object"
|
|
15
|
+
properties:
|
|
16
|
+
n:
|
|
17
|
+
type: "number"
|
|
18
|
+
description: "The number to sum to"
|
|
19
|
+
required:
|
|
20
|
+
- n
|
|
21
|
+
outputSchema:
|
|
22
|
+
type: "object"
|
|
23
|
+
properties:
|
|
24
|
+
sum:
|
|
25
|
+
type: "number"
|
|
26
|
+
description: "The sum from 1 to n"
|
|
27
|
+
|
|
28
|
+
# Graph Nodes
|
|
29
|
+
nodes:
|
|
30
|
+
# Entry node: Receives tool arguments
|
|
31
|
+
- id: "entry_sum"
|
|
32
|
+
type: "entry"
|
|
33
|
+
tool: "sum_to_n"
|
|
34
|
+
next: "increment_node"
|
|
35
|
+
|
|
36
|
+
# Increment counter and add to sum
|
|
37
|
+
# Uses $nodeExecution() to get the previous iteration of this node
|
|
38
|
+
# First iteration: $executionCount("increment_node") = 0, so we initialize
|
|
39
|
+
# Subsequent iterations: $nodeExecution("increment_node", -1) gets the last execution
|
|
40
|
+
- id: "increment_node"
|
|
41
|
+
type: "transform"
|
|
42
|
+
transform:
|
|
43
|
+
expr: |
|
|
44
|
+
$executionCount("increment_node") = 0
|
|
45
|
+
? {
|
|
46
|
+
"counter": 1,
|
|
47
|
+
"sum": 1,
|
|
48
|
+
"target": $.entry_sum.n
|
|
49
|
+
}
|
|
50
|
+
: {
|
|
51
|
+
"counter": $nodeExecution("increment_node", -1).counter + 1,
|
|
52
|
+
"sum": $nodeExecution("increment_node", -1).sum + $nodeExecution("increment_node", -1).counter + 1,
|
|
53
|
+
"target": $.entry_sum.n
|
|
54
|
+
}
|
|
55
|
+
next: "check_condition"
|
|
56
|
+
|
|
57
|
+
# Check if we should continue looping
|
|
58
|
+
- id: "check_condition"
|
|
59
|
+
type: "switch"
|
|
60
|
+
conditions:
|
|
61
|
+
- rule:
|
|
62
|
+
"<": [{"var": "$.increment_node.counter"}, {"var": "$.increment_node.target"}]
|
|
63
|
+
target: "increment_node"
|
|
64
|
+
- target: "exit_sum"
|
|
65
|
+
|
|
66
|
+
# Exit node: Extracts the sum from increment_node
|
|
67
|
+
# Uses $previousNode() to verify it returns the switch node's output (target node ID)
|
|
68
|
+
# The output matches the tool's outputSchema (just the sum)
|
|
69
|
+
- id: "exit_sum"
|
|
70
|
+
type: "transform"
|
|
71
|
+
transform:
|
|
72
|
+
expr: |
|
|
73
|
+
$previousNode() = "exit_sum" ? {
|
|
74
|
+
"sum": $.increment_node.sum
|
|
75
|
+
} : {
|
|
76
|
+
"sum": 0,
|
|
77
|
+
"error": "previousNode check failed"
|
|
78
|
+
}
|
|
79
|
+
next: "exit_sum_final"
|
|
80
|
+
|
|
81
|
+
# Final exit node
|
|
82
|
+
- id: "exit_sum_final"
|
|
83
|
+
type: "exit"
|
|
84
|
+
tool: "sum_to_n"
|