@memberjunction/ai-agents 3.3.0 → 4.0.0
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/AgentDataPreloader.d.ts +117 -1
- package/dist/AgentDataPreloader.d.ts.map +1 -1
- package/dist/AgentDataPreloader.js +156 -58
- package/dist/AgentDataPreloader.js.map +1 -1
- package/dist/AgentRunner.d.ts +212 -0
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +354 -103
- package/dist/AgentRunner.js.map +1 -1
- package/dist/PayloadChangeAnalyzer.d.ts +68 -0
- package/dist/PayloadChangeAnalyzer.d.ts.map +1 -1
- package/dist/PayloadChangeAnalyzer.js +68 -32
- package/dist/PayloadChangeAnalyzer.js.map +1 -1
- package/dist/PayloadFeedbackManager.d.ts +57 -1
- package/dist/PayloadFeedbackManager.d.ts.map +1 -1
- package/dist/PayloadFeedbackManager.js +66 -21
- package/dist/PayloadFeedbackManager.js.map +1 -1
- package/dist/PayloadManager.d.ts +286 -1
- package/dist/PayloadManager.d.ts.map +1 -1
- package/dist/PayloadManager.js +423 -50
- package/dist/PayloadManager.js.map +1 -1
- package/dist/__tests__/action-changes.test.d.ts +13 -0
- package/dist/__tests__/action-changes.test.d.ts.map +1 -1
- package/dist/__tests__/action-changes.test.js +57 -4
- package/dist/__tests__/action-changes.test.js.map +1 -1
- package/dist/__tests__/agent-memory-features.test.d.ts +52 -0
- package/dist/__tests__/agent-memory-features.test.d.ts.map +1 -1
- package/dist/__tests__/agent-memory-features.test.js +96 -13
- package/dist/__tests__/agent-memory-features.test.js.map +1 -1
- package/dist/__tests__/agent-type-prompt-params.test.d.ts +12 -0
- package/dist/__tests__/agent-type-prompt-params.test.d.ts.map +1 -1
- package/dist/__tests__/agent-type-prompt-params.test.js +112 -20
- package/dist/__tests__/agent-type-prompt-params.test.js.map +1 -1
- package/dist/__tests__/chat-handling-option.test.d.ts +26 -0
- package/dist/__tests__/chat-handling-option.test.d.ts.map +1 -1
- package/dist/__tests__/chat-handling-option.test.js +41 -2
- package/dist/__tests__/chat-handling-option.test.js.map +1 -1
- package/dist/agent-context-injector.d.ts +114 -0
- package/dist/agent-context-injector.d.ts.map +1 -1
- package/dist/agent-context-injector.js +138 -19
- package/dist/agent-context-injector.js.map +1 -1
- package/dist/agent-types/base-agent-type.d.ts +354 -0
- package/dist/agent-types/base-agent-type.d.ts.map +1 -1
- package/dist/agent-types/base-agent-type.js +288 -17
- package/dist/agent-types/base-agent-type.js.map +1 -1
- package/dist/agent-types/flow-agent-type.d.ts +328 -2
- package/dist/agent-types/flow-agent-type.d.ts.map +1 -1
- package/dist/agent-types/flow-agent-type.js +503 -63
- package/dist/agent-types/flow-agent-type.js.map +1 -1
- package/dist/agent-types/index.d.ts +14 -3
- package/dist/agent-types/index.d.ts.map +1 -1
- package/dist/agent-types/index.js +14 -23
- package/dist/agent-types/index.js.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.d.ts +190 -0
- package/dist/agent-types/loop-agent-prompt-params.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.js +23 -6
- package/dist/agent-types/loop-agent-prompt-params.js.map +1 -1
- package/dist/agent-types/loop-agent-response-type.d.ts +62 -0
- package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-response-type.js +3 -2
- package/dist/agent-types/loop-agent-response-type.js.map +1 -1
- package/dist/agent-types/loop-agent-type.d.ts +139 -2
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +207 -35
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +1344 -1
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +2282 -211
- package/dist/base-agent.js.map +1 -1
- package/dist/index.d.ts +24 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -35
- package/dist/index.js.map +1 -1
- package/dist/memory-cleanup-agent.d.ts +53 -1
- package/dist/memory-cleanup-agent.d.ts.map +1 -1
- package/dist/memory-cleanup-agent.js +89 -21
- package/dist/memory-cleanup-agent.js.map +1 -1
- package/dist/memory-manager-agent.d.ts +61 -1
- package/dist/memory-manager-agent.d.ts.map +1 -1
- package/dist/memory-manager-agent.js +260 -116
- package/dist/memory-manager-agent.js.map +1 -1
- package/dist/services/AgentEmbeddingService.d.ts +16 -0
- package/dist/services/AgentEmbeddingService.d.ts.map +1 -0
- package/dist/services/AgentEmbeddingService.js +158 -0
- package/dist/services/AgentEmbeddingService.js.map +1 -0
- package/dist/types/AgentMatchResult.d.ts +18 -0
- package/dist/types/AgentMatchResult.d.ts.map +1 -0
- package/dist/types/AgentMatchResult.js +3 -0
- package/dist/types/AgentMatchResult.js.map +1 -0
- package/dist/types/payload-operations.d.ts +51 -0
- package/dist/types/payload-operations.d.ts.map +1 -1
- package/dist/types/payload-operations.js +54 -15
- package/dist/types/payload-operations.js.map +1 -1
- package/dist/utils/ConversationMessageResolver.d.ts +79 -1
- package/dist/utils/ConversationMessageResolver.d.ts.map +1 -1
- package/dist/utils/ConversationMessageResolver.js +99 -9
- package/dist/utils/ConversationMessageResolver.js.map +1 -1
- package/package.json +20 -19
package/dist/PayloadManager.js
CHANGED
|
@@ -1,40 +1,77 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Payload management for hierarchical agent execution.
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to control which parts of a payload
|
|
5
|
+
* are accessible to sub-agents (downstream) and which parts they can
|
|
6
|
+
* modify (upstream). It supports JSON path-based access control with
|
|
7
|
+
* wildcards and nested path support.
|
|
8
|
+
*
|
|
9
|
+
* ## Key Features:
|
|
10
|
+
* - Path-based access control for sub-agent data isolation
|
|
11
|
+
* - Operation-level permissions (add, update, delete) per path
|
|
12
|
+
* - Automatic detection of suspicious payload changes
|
|
13
|
+
* - Human-readable diff generation for audit trails
|
|
14
|
+
* - Configurable warning thresholds for content changes
|
|
15
|
+
*
|
|
16
|
+
* ## Path Syntax:
|
|
17
|
+
* - Exact paths: "customer.name"
|
|
18
|
+
* - Wildcards: "customer.*" (all properties under customer)
|
|
19
|
+
* - Array indices: "items[0].price" or "items[*].price"
|
|
20
|
+
* - Deep wildcards: "**.id" (all id fields at any depth)
|
|
21
|
+
* - Root wildcard: "*" (entire payload)
|
|
22
|
+
* - Operation control: "path:add,update" (specific operations only)
|
|
23
|
+
*
|
|
24
|
+
* ## Change Detection Rules:
|
|
25
|
+
* - Content truncation: Warns when text reduced by >70%
|
|
26
|
+
* - Key removal: Flags when non-empty keys are removed
|
|
27
|
+
* - Type changes: Detects object→primitive conversions
|
|
28
|
+
* - Pattern anomalies: Identifies placeholder replacements
|
|
29
|
+
*
|
|
30
|
+
* @module @memberjunction/ai-agents
|
|
31
|
+
* @author MemberJunction.com
|
|
32
|
+
* @since 3.0.0
|
|
33
|
+
*/
|
|
34
|
+
import { LogError, LogStatus } from '@memberjunction/core';
|
|
35
|
+
import { DeepDiffer } from '@memberjunction/global';
|
|
36
|
+
import _ from 'lodash';
|
|
37
|
+
import { PayloadChangeAnalyzer } from './PayloadChangeAnalyzer.js';
|
|
38
|
+
import { parsePathWithOperations, parsePathsWithOperations, isOperationAllowed } from './types/payload-operations.js';
|
|
39
|
+
/**
|
|
40
|
+
* Manages payload access control for agent hierarchies.
|
|
41
|
+
*
|
|
42
|
+
* This class handles extracting specific paths from payloads when sending
|
|
43
|
+
* data downstream to sub-agents, and merging results back upstream while
|
|
44
|
+
* respecting write permissions.
|
|
45
|
+
*
|
|
46
|
+
* Path syntax supports:
|
|
47
|
+
* - Exact paths: "customer.name"
|
|
48
|
+
* - Wildcards: "customer.*" (all properties under customer)
|
|
49
|
+
* - Array indices: "items[0].price" or "items[*].price"
|
|
50
|
+
* - Deep wildcards: "**.id" (all id fields at any depth)
|
|
51
|
+
* - Root wildcard: "*" (entire payload)
|
|
52
|
+
*/
|
|
53
|
+
export class PayloadManager {
|
|
54
|
+
/**
|
|
55
|
+
* Extracts only the allowed paths from a payload for downstream transmission.
|
|
56
|
+
*
|
|
57
|
+
* @param fullPayload The complete payload object
|
|
58
|
+
* @param downstreamPaths Array of path patterns defining what to extract
|
|
59
|
+
* @returns A new object containing only the allowed paths
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const payload = { customer: { id: 1, name: 'John', secret: 'xxx' }, order: { id: 2 } };
|
|
64
|
+
* const paths = ['customer.id', 'customer.name', 'order.*'];
|
|
65
|
+
* const result = extractDownstreamPayload(payload, paths);
|
|
66
|
+
* // Returns: { customer: { id: 1, name: 'John' }, order: { id: 2 } }
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
33
69
|
extractDownstreamPayload(subAgentName, fullPayload, downstreamPaths) {
|
|
34
70
|
if (!fullPayload)
|
|
35
71
|
return null;
|
|
36
72
|
if (!downstreamPaths || downstreamPaths.length === 0)
|
|
37
73
|
return {};
|
|
74
|
+
// Handle wildcard - return everything
|
|
38
75
|
if (downstreamPaths.includes('*')) {
|
|
39
76
|
return _.cloneDeep(fullPayload);
|
|
40
77
|
}
|
|
@@ -44,11 +81,28 @@ class PayloadManager {
|
|
|
44
81
|
this.extractPathPattern(fullPayload, pathPattern, result);
|
|
45
82
|
}
|
|
46
83
|
catch (error) {
|
|
47
|
-
|
|
84
|
+
LogError(`Failed to extract path pattern '${pathPattern}': ${error.message}`);
|
|
48
85
|
}
|
|
49
86
|
}
|
|
50
87
|
return result;
|
|
51
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Merges sub-agent results back into the parent payload, respecting write permissions.
|
|
91
|
+
*
|
|
92
|
+
* @param parentPayload The original parent payload
|
|
93
|
+
* @param subAgentPayload The payload returned by the sub-agent
|
|
94
|
+
* @param upstreamPaths Array of path patterns the sub-agent is allowed to write
|
|
95
|
+
* @returns A new merged payload object
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* const parent = { customer: { id: 1 }, analysis: { sentiment: null } };
|
|
100
|
+
* const subResult = { customer: { id: 2 }, analysis: { sentiment: 'positive', score: 0.9 } };
|
|
101
|
+
* const writePaths = ['analysis.*'];
|
|
102
|
+
* const merged = mergeUpstreamPayload(parent, subResult, writePaths);
|
|
103
|
+
* // Returns: { customer: { id: 1 }, analysis: { sentiment: 'positive', score: 0.9 } }
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
52
106
|
mergeUpstreamPayload(subAgentName, parentPayload, subAgentPayload, upstreamPaths, verbose) {
|
|
53
107
|
const blockedOperations = [];
|
|
54
108
|
if (!parentPayload && !subAgentPayload) {
|
|
@@ -71,7 +125,7 @@ class PayloadManager {
|
|
|
71
125
|
}
|
|
72
126
|
if (!upstreamPaths || upstreamPaths.length === 0) {
|
|
73
127
|
if (verbose) {
|
|
74
|
-
|
|
128
|
+
LogStatus('Warning: No upstream paths specified - sub-agent changes will be ignored');
|
|
75
129
|
}
|
|
76
130
|
return {
|
|
77
131
|
result: parentPayload,
|
|
@@ -81,10 +135,13 @@ class PayloadManager {
|
|
|
81
135
|
blockedOperations
|
|
82
136
|
};
|
|
83
137
|
}
|
|
138
|
+
// Start with a deep clone of the parent payload
|
|
84
139
|
const result = _.cloneDeep(parentPayload || {});
|
|
85
140
|
const counts = { additions: 0, updates: 0, deletions: 0 };
|
|
141
|
+
// Handle wildcard - merge everything
|
|
86
142
|
if (upstreamPaths.includes('*')) {
|
|
87
143
|
const merged = this.deepMerge(result, subAgentPayload);
|
|
144
|
+
// Count changes
|
|
88
145
|
this.countChanges(parentPayload, merged, counts);
|
|
89
146
|
return {
|
|
90
147
|
result: merged,
|
|
@@ -94,6 +151,7 @@ class PayloadManager {
|
|
|
94
151
|
blockedOperations
|
|
95
152
|
};
|
|
96
153
|
}
|
|
154
|
+
// Check each path in the sub-agent payload
|
|
97
155
|
const mergeResult = this.mergeAllowedPaths(result, subAgentPayload, upstreamPaths, subAgentName, verbose);
|
|
98
156
|
return {
|
|
99
157
|
result: result,
|
|
@@ -103,27 +161,42 @@ class PayloadManager {
|
|
|
103
161
|
blockedOperations: mergeResult.blockedOperations
|
|
104
162
|
};
|
|
105
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Extracts a path pattern from source object into destination.
|
|
166
|
+
*
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
106
169
|
extractPathPattern(source, pathPattern, destination) {
|
|
170
|
+
// Handle deep wildcards (**)
|
|
107
171
|
if (pathPattern.includes('**')) {
|
|
108
172
|
this.extractDeepWildcard(source, pathPattern, destination);
|
|
109
173
|
return;
|
|
110
174
|
}
|
|
175
|
+
// Handle regular paths with potential wildcards
|
|
111
176
|
const pathParts = this.parsePathParts(pathPattern);
|
|
112
177
|
this.extractPath(source, pathParts, destination, []);
|
|
113
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Recursively extracts paths based on parsed path parts.
|
|
181
|
+
*
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
114
184
|
extractPath(source, pathParts, destination, currentPath) {
|
|
115
185
|
if (!source || pathParts.length === 0)
|
|
116
186
|
return;
|
|
117
187
|
const [currentPart, ...remainingParts] = pathParts;
|
|
188
|
+
// Handle wildcards
|
|
118
189
|
if (currentPart === '*') {
|
|
119
190
|
if (_.isObject(source) && !_.isArray(source)) {
|
|
120
191
|
for (const key in source) {
|
|
121
192
|
if (source.hasOwnProperty(key)) {
|
|
122
193
|
const newPath = [...currentPath, key];
|
|
123
194
|
if (remainingParts.length === 0) {
|
|
195
|
+
// Terminal wildcard - copy the value
|
|
124
196
|
_.set(destination, newPath, _.cloneDeep(source[key]));
|
|
125
197
|
}
|
|
126
198
|
else {
|
|
199
|
+
// Non-terminal wildcard - continue recursion
|
|
127
200
|
this.extractPath(source[key], remainingParts, destination, newPath);
|
|
128
201
|
}
|
|
129
202
|
}
|
|
@@ -131,20 +204,29 @@ class PayloadManager {
|
|
|
131
204
|
}
|
|
132
205
|
return;
|
|
133
206
|
}
|
|
207
|
+
// Handle array notation
|
|
134
208
|
if (currentPart.includes('[')) {
|
|
135
209
|
this.extractArrayPath(source, currentPart, remainingParts, destination, currentPath);
|
|
136
210
|
return;
|
|
137
211
|
}
|
|
212
|
+
// Handle regular property
|
|
138
213
|
if (source.hasOwnProperty(currentPart)) {
|
|
139
214
|
const newPath = [...currentPath, currentPart];
|
|
140
215
|
if (remainingParts.length === 0) {
|
|
216
|
+
// Terminal property - copy the value
|
|
141
217
|
_.set(destination, newPath, _.cloneDeep(source[currentPart]));
|
|
142
218
|
}
|
|
143
219
|
else {
|
|
220
|
+
// Non-terminal property - continue recursion
|
|
144
221
|
this.extractPath(source[currentPart], remainingParts, destination, newPath);
|
|
145
222
|
}
|
|
146
223
|
}
|
|
147
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Handles array path extraction (e.g., items[0] or items[*]).
|
|
227
|
+
*
|
|
228
|
+
* @private
|
|
229
|
+
*/
|
|
148
230
|
extractArrayPath(source, arrayPart, remainingParts, destination, currentPath) {
|
|
149
231
|
const match = arrayPart.match(/^([^[]+)\[([^\]]+)\]$/);
|
|
150
232
|
if (!match)
|
|
@@ -155,6 +237,7 @@ class PayloadManager {
|
|
|
155
237
|
return;
|
|
156
238
|
const newPath = [...currentPath, propertyName];
|
|
157
239
|
if (indexPart === '*') {
|
|
240
|
+
// Extract all array elements
|
|
158
241
|
const destArray = [];
|
|
159
242
|
for (let i = 0; i < sourceArray.length; i++) {
|
|
160
243
|
if (remainingParts.length === 0) {
|
|
@@ -173,6 +256,7 @@ class PayloadManager {
|
|
|
173
256
|
}
|
|
174
257
|
}
|
|
175
258
|
else {
|
|
259
|
+
// Extract specific index
|
|
176
260
|
const index = parseInt(indexPart, 10);
|
|
177
261
|
if (!isNaN(index) && index >= 0 && index < sourceArray.length) {
|
|
178
262
|
if (remainingParts.length === 0) {
|
|
@@ -184,6 +268,11 @@ class PayloadManager {
|
|
|
184
268
|
}
|
|
185
269
|
}
|
|
186
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Handles deep wildcard extraction (e.g., **.id).
|
|
273
|
+
*
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
187
276
|
extractDeepWildcard(source, pathPattern, destination) {
|
|
188
277
|
const parts = pathPattern.split('**.');
|
|
189
278
|
if (parts.length !== 2 || parts[0] !== '')
|
|
@@ -210,19 +299,31 @@ class PayloadManager {
|
|
|
210
299
|
};
|
|
211
300
|
extractRecursive(source);
|
|
212
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Merges allowed paths from sub-agent payload into result.
|
|
304
|
+
*
|
|
305
|
+
* @private
|
|
306
|
+
*/
|
|
213
307
|
mergeAllowedPaths(result, subAgentPayload, upstreamPaths, subAgentName, verbose) {
|
|
308
|
+
// Get all paths from sub-agent payload
|
|
214
309
|
const subAgentPaths = this.getAllPaths(subAgentPayload);
|
|
310
|
+
// Track unauthorized changes for consolidated warning
|
|
215
311
|
const unauthorizedChanges = [];
|
|
312
|
+
// Check each path against allowed patterns
|
|
216
313
|
for (const actualPath of subAgentPaths) {
|
|
217
314
|
const subAgentValue = _.get(subAgentPayload, actualPath);
|
|
218
315
|
const originalValue = _.get(result, actualPath);
|
|
316
|
+
// Determine the operation type
|
|
219
317
|
const operation = originalValue === undefined ? 'add' : 'update';
|
|
318
|
+
// Check if the operation is allowed for this path
|
|
220
319
|
const isOperationAllowed = this.isOperationAllowedForPath(actualPath, operation, upstreamPaths);
|
|
221
320
|
if (isOperationAllowed) {
|
|
222
321
|
_.set(result, actualPath, _.cloneDeep(subAgentValue));
|
|
223
322
|
}
|
|
224
323
|
else {
|
|
324
|
+
// Only track if the sub-agent is trying to change the value
|
|
225
325
|
if (!_.isEqual(subAgentValue, originalValue)) {
|
|
326
|
+
// Check if any operation is allowed on this path
|
|
226
327
|
const isPathAllowed = this.isPathAllowed(actualPath, upstreamPaths);
|
|
227
328
|
unauthorizedChanges.push({
|
|
228
329
|
path: actualPath,
|
|
@@ -234,13 +335,17 @@ class PayloadManager {
|
|
|
234
335
|
}
|
|
235
336
|
}
|
|
236
337
|
}
|
|
338
|
+
// Also check for deletions (paths in result but not in subAgentPayload)
|
|
237
339
|
const resultPaths = this.getAllPaths(result);
|
|
238
340
|
for (const resultPath of resultPaths) {
|
|
239
341
|
const resultValue = _.get(result, resultPath);
|
|
240
342
|
const subAgentValue = _.get(subAgentPayload, resultPath);
|
|
343
|
+
// If the path exists in result but not in subAgentPayload, it's a deletion attempt
|
|
241
344
|
if (resultValue !== undefined && subAgentValue === undefined) {
|
|
345
|
+
// Check if delete operation is allowed
|
|
242
346
|
const isDeleteAllowed = this.isOperationAllowedForPath(resultPath, 'delete', upstreamPaths);
|
|
243
347
|
if (isDeleteAllowed) {
|
|
348
|
+
// Delete the path from result
|
|
244
349
|
_.unset(result, resultPath);
|
|
245
350
|
}
|
|
246
351
|
else {
|
|
@@ -255,15 +360,19 @@ class PayloadManager {
|
|
|
255
360
|
}
|
|
256
361
|
}
|
|
257
362
|
}
|
|
363
|
+
// Clean up any empty objects that resulted from property deletions in arrays
|
|
258
364
|
this.cleanupEmptyArrayElements(result);
|
|
365
|
+
// Initialize counts
|
|
259
366
|
const counts = { additions: 0, updates: 0, deletions: 0 };
|
|
260
367
|
const warnings = [];
|
|
261
368
|
const blockedOperations = [];
|
|
262
|
-
|
|
369
|
+
// Count successful operations
|
|
370
|
+
const parentPayload = arguments[2]; // The original parent payload parameter
|
|
263
371
|
for (const path of subAgentPaths) {
|
|
264
372
|
const mergedValue = _.get(result, path);
|
|
265
373
|
const subAgentValue = _.get(subAgentPayload, path);
|
|
266
374
|
if (_.isEqual(mergedValue, subAgentValue)) {
|
|
375
|
+
// This change was allowed and applied
|
|
267
376
|
const originalParentValue = _.get(parentPayload || {}, path);
|
|
268
377
|
if (originalParentValue === undefined) {
|
|
269
378
|
counts.additions++;
|
|
@@ -273,7 +382,9 @@ class PayloadManager {
|
|
|
273
382
|
}
|
|
274
383
|
}
|
|
275
384
|
}
|
|
385
|
+
// Output consolidated warning if there were unauthorized changes
|
|
276
386
|
if (unauthorizedChanges.length > 0) {
|
|
387
|
+
// Convert to blocked operations format
|
|
277
388
|
const now = new Date().toISOString();
|
|
278
389
|
for (const change of unauthorizedChanges) {
|
|
279
390
|
blockedOperations.push({
|
|
@@ -286,6 +397,7 @@ class PayloadManager {
|
|
|
286
397
|
});
|
|
287
398
|
}
|
|
288
399
|
warnings.push(`Sub-agent "${subAgentName}" attempted ${unauthorizedChanges.length} unauthorized operation(s)`);
|
|
400
|
+
// Only log to console in verbose mode
|
|
289
401
|
if (verbose) {
|
|
290
402
|
const groupedChanges = this.groupUnauthorizedChanges(unauthorizedChanges);
|
|
291
403
|
let warningMessage = `\n⚠️ Sub-agent "${subAgentName}" attempted ${unauthorizedChanges.length} unauthorized operation${unauthorizedChanges.length > 1 ? 's' : ''}:\n`;
|
|
@@ -298,14 +410,20 @@ class PayloadManager {
|
|
|
298
410
|
}
|
|
299
411
|
}
|
|
300
412
|
warningMessage += `\n ℹ️ Authorized paths: ${upstreamPaths.join(', ')}\n`;
|
|
301
|
-
|
|
413
|
+
LogStatus(warningMessage);
|
|
302
414
|
}
|
|
303
415
|
}
|
|
304
416
|
return { counts, warnings, blockedOperations };
|
|
305
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Count changes between two payloads for tracking operations
|
|
420
|
+
*
|
|
421
|
+
* @private
|
|
422
|
+
*/
|
|
306
423
|
countChanges(original, modified, counts) {
|
|
307
424
|
const originalPaths = this.getAllPaths(original || {});
|
|
308
425
|
const modifiedPaths = this.getAllPaths(modified || {});
|
|
426
|
+
// Check for additions and updates
|
|
309
427
|
for (const path of modifiedPaths) {
|
|
310
428
|
const originalValue = _.get(original, path);
|
|
311
429
|
const modifiedValue = _.get(modified, path);
|
|
@@ -316,15 +434,22 @@ class PayloadManager {
|
|
|
316
434
|
counts.updates++;
|
|
317
435
|
}
|
|
318
436
|
}
|
|
437
|
+
// Check for deletions
|
|
319
438
|
for (const path of originalPaths) {
|
|
320
439
|
if (!modifiedPaths.includes(path)) {
|
|
321
440
|
counts.deletions++;
|
|
322
441
|
}
|
|
323
442
|
}
|
|
324
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Groups unauthorized changes by their root path for cleaner display.
|
|
446
|
+
*
|
|
447
|
+
* @private
|
|
448
|
+
*/
|
|
325
449
|
groupUnauthorizedChanges(changes) {
|
|
326
450
|
const grouped = {};
|
|
327
451
|
for (const change of changes) {
|
|
452
|
+
// Extract the root category (first part of the path)
|
|
328
453
|
const rootCategory = change.path.split('.')[0] || 'root';
|
|
329
454
|
if (!grouped[rootCategory]) {
|
|
330
455
|
grouped[rootCategory] = [];
|
|
@@ -333,21 +458,33 @@ class PayloadManager {
|
|
|
333
458
|
}
|
|
334
459
|
return grouped;
|
|
335
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Formats a value for display in warning messages.
|
|
463
|
+
*
|
|
464
|
+
* @private
|
|
465
|
+
*/
|
|
336
466
|
formatValue(value) {
|
|
337
467
|
if (value === undefined)
|
|
338
468
|
return 'undefined';
|
|
339
469
|
if (value === null)
|
|
340
470
|
return 'null';
|
|
341
471
|
if (typeof value === 'string') {
|
|
472
|
+
// Clip long strings to 50 characters
|
|
342
473
|
const trimmed = value.length > 50 ? value.substring(0, 50) + '...' : value;
|
|
343
474
|
return `"${trimmed}"`;
|
|
344
475
|
}
|
|
345
476
|
if (typeof value === 'object') {
|
|
346
477
|
const str = JSON.stringify(value);
|
|
478
|
+
// Clip long JSON to 50 characters
|
|
347
479
|
return str.length > 50 ? str.substring(0, 50) + '...' : str;
|
|
348
480
|
}
|
|
349
481
|
return String(value);
|
|
350
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Gets all leaf paths from an object.
|
|
485
|
+
*
|
|
486
|
+
* @private
|
|
487
|
+
*/
|
|
351
488
|
getAllPaths(obj, currentPath = []) {
|
|
352
489
|
const paths = [];
|
|
353
490
|
if (!obj || typeof obj !== 'object') {
|
|
@@ -371,42 +508,63 @@ class PayloadManager {
|
|
|
371
508
|
}
|
|
372
509
|
return paths;
|
|
373
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Recursively cleans up empty objects from arrays after merge operations.
|
|
513
|
+
* This is necessary because property-level deletions can leave empty object shells in arrays.
|
|
514
|
+
*
|
|
515
|
+
* @private
|
|
516
|
+
*/
|
|
374
517
|
cleanupEmptyArrayElements(obj) {
|
|
375
518
|
if (!obj || typeof obj !== 'object') {
|
|
376
519
|
return;
|
|
377
520
|
}
|
|
378
521
|
if (Array.isArray(obj)) {
|
|
522
|
+
// Filter out empty objects from the array
|
|
523
|
+
// We need to modify the array in place to maintain references
|
|
379
524
|
let writeIndex = 0;
|
|
380
525
|
for (let readIndex = 0; readIndex < obj.length; readIndex++) {
|
|
381
526
|
const element = obj[readIndex];
|
|
527
|
+
// Keep the element if it's not an empty object
|
|
528
|
+
// An empty object is one that is an object with no own properties
|
|
382
529
|
const shouldKeep = !(element !== null &&
|
|
383
530
|
typeof element === 'object' &&
|
|
384
531
|
!Array.isArray(element) &&
|
|
385
532
|
Object.keys(element).length === 0);
|
|
386
533
|
if (shouldKeep) {
|
|
534
|
+
// First recurse into the element to clean up any nested arrays
|
|
387
535
|
this.cleanupEmptyArrayElements(element);
|
|
536
|
+
// Then keep the element in the array
|
|
388
537
|
if (writeIndex !== readIndex) {
|
|
389
538
|
obj[writeIndex] = element;
|
|
390
539
|
}
|
|
391
540
|
writeIndex++;
|
|
392
541
|
}
|
|
393
542
|
}
|
|
543
|
+
// Truncate the array to remove the empty slots at the end
|
|
394
544
|
obj.length = writeIndex;
|
|
395
545
|
}
|
|
396
546
|
else {
|
|
547
|
+
// For objects, recursively clean up any nested arrays
|
|
397
548
|
for (const key of Object.keys(obj)) {
|
|
398
549
|
this.cleanupEmptyArrayElements(obj[key]);
|
|
399
550
|
}
|
|
400
551
|
}
|
|
401
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Checks if a path matches any of the allowed patterns.
|
|
555
|
+
*
|
|
556
|
+
* @private
|
|
557
|
+
*/
|
|
402
558
|
isPathAllowed(actualPath, allowedPatterns) {
|
|
403
559
|
const normalizedPath = actualPath.replace(/\[(\d+)\]/g, '.$1');
|
|
404
560
|
for (const pattern of allowedPatterns) {
|
|
405
|
-
|
|
561
|
+
// Parse the pattern to extract path and operations
|
|
562
|
+
const parsedPattern = parsePathWithOperations(pattern);
|
|
406
563
|
const pathPattern = parsedPattern.path;
|
|
407
564
|
if (pathPattern === '*')
|
|
408
565
|
return true;
|
|
409
566
|
if (pathPattern.includes('**')) {
|
|
567
|
+
// Handle deep wildcards
|
|
410
568
|
const regex = pathPattern
|
|
411
569
|
.replace(/\./g, '\\.')
|
|
412
570
|
.replace(/\*\*/g, '.*')
|
|
@@ -416,15 +574,19 @@ class PayloadManager {
|
|
|
416
574
|
}
|
|
417
575
|
}
|
|
418
576
|
else {
|
|
577
|
+
// Handle regular patterns
|
|
578
|
+
// Check if pattern ends with .* which means "everything under this path"
|
|
419
579
|
if (pathPattern.endsWith('.*')) {
|
|
420
|
-
const basePath = pathPattern.slice(0, -2);
|
|
580
|
+
const basePath = pathPattern.slice(0, -2); // Remove the .*
|
|
421
581
|
const escapedBase = basePath.replace(/\./g, '\\.');
|
|
582
|
+
// Match the base path followed by a dot and anything after
|
|
422
583
|
const regex = `^${escapedBase}(\\..*)?$`;
|
|
423
584
|
if (new RegExp(regex).test(normalizedPath)) {
|
|
424
585
|
return true;
|
|
425
586
|
}
|
|
426
587
|
}
|
|
427
588
|
else {
|
|
589
|
+
// Handle other wildcard patterns
|
|
428
590
|
const regex = pathPattern
|
|
429
591
|
.replace(/\./g, '\\.')
|
|
430
592
|
.replace(/\*/g, '[^.]+')
|
|
@@ -437,9 +599,17 @@ class PayloadManager {
|
|
|
437
599
|
}
|
|
438
600
|
return false;
|
|
439
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* Checks if a specific operation is allowed for a path.
|
|
604
|
+
*
|
|
605
|
+
* @param actualPath The actual path to check
|
|
606
|
+
* @param operation The operation to check
|
|
607
|
+
* @param allowedPatterns Array of allowed patterns with optional operations
|
|
608
|
+
* @returns True if the operation is allowed on this path
|
|
609
|
+
*/
|
|
440
610
|
isOperationAllowedForPath(actualPath, operation, allowedPatterns) {
|
|
441
611
|
const normalizedPath = actualPath.replace(/\[(\d+)\]/g, '.$1');
|
|
442
|
-
const parsedPatterns =
|
|
612
|
+
const parsedPatterns = parsePathsWithOperations(allowedPatterns);
|
|
443
613
|
for (const parsedPattern of parsedPatterns) {
|
|
444
614
|
const pathPattern = parsedPattern.path;
|
|
445
615
|
let pathMatches = false;
|
|
@@ -447,6 +617,7 @@ class PayloadManager {
|
|
|
447
617
|
pathMatches = true;
|
|
448
618
|
}
|
|
449
619
|
else if (pathPattern.includes('**')) {
|
|
620
|
+
// Handle deep wildcards
|
|
450
621
|
const regex = pathPattern
|
|
451
622
|
.replace(/\./g, '\\.')
|
|
452
623
|
.replace(/\*\*/g, '.*')
|
|
@@ -454,6 +625,7 @@ class PayloadManager {
|
|
|
454
625
|
pathMatches = new RegExp(`^${regex}$`).test(normalizedPath);
|
|
455
626
|
}
|
|
456
627
|
else {
|
|
628
|
+
// Handle regular patterns
|
|
457
629
|
if (pathPattern.endsWith('.*')) {
|
|
458
630
|
const basePath = pathPattern.slice(0, -2);
|
|
459
631
|
const escapedBase = basePath.replace(/\./g, '\\.');
|
|
@@ -468,13 +640,20 @@ class PayloadManager {
|
|
|
468
640
|
pathMatches = new RegExp(`^${regex}$`).test(normalizedPath);
|
|
469
641
|
}
|
|
470
642
|
}
|
|
471
|
-
|
|
643
|
+
// If path matches, check if operation is allowed
|
|
644
|
+
if (pathMatches && isOperationAllowed(parsedPattern, operation)) {
|
|
472
645
|
return true;
|
|
473
646
|
}
|
|
474
647
|
}
|
|
475
648
|
return false;
|
|
476
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Parses a path string into parts, handling array notation.
|
|
652
|
+
*
|
|
653
|
+
* @private
|
|
654
|
+
*/
|
|
477
655
|
parsePathParts(path) {
|
|
656
|
+
// Split by dots but preserve array notation
|
|
478
657
|
const parts = [];
|
|
479
658
|
let current = '';
|
|
480
659
|
let inBracket = false;
|
|
@@ -502,6 +681,27 @@ class PayloadManager {
|
|
|
502
681
|
}
|
|
503
682
|
return parts;
|
|
504
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* Deep merges two objects, with source overriding destination.
|
|
686
|
+
*
|
|
687
|
+
* This method preserves existing nested properties in the destination while
|
|
688
|
+
* adding or updating properties from the source. It handles nested objects
|
|
689
|
+
* recursively, ensuring that partial updates don't wipe out existing data.
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```typescript
|
|
693
|
+
* const dest = { decision: { Y: 4, Z: 2 } };
|
|
694
|
+
* const src = { decision: { x: "string" } };
|
|
695
|
+
* const result = deepMerge(dest, src);
|
|
696
|
+
* // Returns: { decision: { x: "string", Y: 4, Z: 2 } }
|
|
697
|
+
* ```
|
|
698
|
+
*
|
|
699
|
+
* @param destination The target object to merge into
|
|
700
|
+
* @param source The source object to merge from
|
|
701
|
+
* @returns A new merged object with all properties from both objects
|
|
702
|
+
*
|
|
703
|
+
* @public
|
|
704
|
+
*/
|
|
505
705
|
deepMerge(destination, source) {
|
|
506
706
|
if (!source)
|
|
507
707
|
return destination;
|
|
@@ -511,35 +711,50 @@ class PayloadManager {
|
|
|
511
711
|
for (const key in source) {
|
|
512
712
|
if (source.hasOwnProperty(key)) {
|
|
513
713
|
if (_.isObject(source[key]) && !_.isArray(source[key]) && _.isObject(result[key]) && !_.isArray(result[key])) {
|
|
714
|
+
// Both are objects - recursive merge
|
|
514
715
|
result[key] = this.deepMerge(result[key], source[key]);
|
|
515
716
|
}
|
|
516
717
|
else {
|
|
718
|
+
// Otherwise, source overwrites destination
|
|
517
719
|
result[key] = _.cloneDeep(source[key]);
|
|
518
720
|
}
|
|
519
721
|
}
|
|
520
722
|
}
|
|
521
723
|
return result;
|
|
522
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Applies an AgentPayloadChangeRequest to a payload
|
|
727
|
+
*
|
|
728
|
+
* @param originalPayload The original payload to apply changes to
|
|
729
|
+
* @param changeRequest The change request from the AI agent
|
|
730
|
+
* @param options Configuration options for the operation
|
|
731
|
+
* @returns Result object with the modified payload, operation counts, and any warnings
|
|
732
|
+
*/
|
|
523
733
|
applyAgentChangeRequest(originalPayload, changeRequest, options) {
|
|
524
734
|
const warnings = [];
|
|
525
735
|
const blockedOperations = [];
|
|
526
736
|
const result = _.cloneDeep(originalPayload) || {};
|
|
527
737
|
const counts = { additions: 0, updates: 0, deletions: 0 };
|
|
738
|
+
// Process all changes recursively
|
|
528
739
|
this.processChangeRequest(result, originalPayload, changeRequest, [], counts, warnings, options?.allowedPaths, blockedOperations);
|
|
740
|
+
// Log if requested and in verbose mode
|
|
529
741
|
if (options?.logChanges && options?.verbose) {
|
|
530
742
|
this.logChangesSummary(counts, changeRequest.reasoning, options.agentName);
|
|
531
743
|
}
|
|
744
|
+
// Analyze changes if requested
|
|
532
745
|
let analysis;
|
|
533
|
-
if (options?.analyzeChanges !== false) {
|
|
534
|
-
const analyzer = new
|
|
746
|
+
if (options?.analyzeChanges !== false) { // Default to true
|
|
747
|
+
const analyzer = new PayloadChangeAnalyzer();
|
|
535
748
|
analysis = analyzer.analyzeChangeRequest(originalPayload, changeRequest, result);
|
|
749
|
+
// Add analysis warnings to the main warnings array
|
|
536
750
|
if (analysis.warnings.length > 0) {
|
|
537
751
|
warnings.push(...analysis.warnings.map(w => `[${w.severity}] ${w.message}`));
|
|
538
752
|
}
|
|
539
753
|
}
|
|
754
|
+
// Generate diff if requested
|
|
540
755
|
let diff;
|
|
541
|
-
if (options?.generateDiff !== false) {
|
|
542
|
-
const differ = new
|
|
756
|
+
if (options?.generateDiff !== false) { // Default to true
|
|
757
|
+
const differ = new DeepDiffer();
|
|
543
758
|
diff = differ.diff(originalPayload, result);
|
|
544
759
|
}
|
|
545
760
|
return {
|
|
@@ -553,6 +768,9 @@ class PayloadManager {
|
|
|
553
768
|
blockedOperations
|
|
554
769
|
};
|
|
555
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Process a change request recursively through the payload structure
|
|
773
|
+
*/
|
|
556
774
|
processChangeRequest(target, original, changeRequest, path, counts, warnings, allowedPaths, blockedOperations) {
|
|
557
775
|
if (Array.isArray(target)) {
|
|
558
776
|
this.processArrayChanges(target, original, changeRequest, path, counts, warnings, allowedPaths, blockedOperations);
|
|
@@ -561,49 +779,80 @@ class PayloadManager {
|
|
|
561
779
|
this.processObjectChanges(target, original, changeRequest, path, counts, warnings, allowedPaths, blockedOperations);
|
|
562
780
|
}
|
|
563
781
|
}
|
|
782
|
+
/**
|
|
783
|
+
* Process changes for array elements with support for deep merging object elements.
|
|
784
|
+
*
|
|
785
|
+
* For arrays containing objects, updates are merged deeply rather than replacing
|
|
786
|
+
* the entire element. This preserves existing properties while updating only the
|
|
787
|
+
* specified fields.
|
|
788
|
+
*
|
|
789
|
+
* Example:
|
|
790
|
+
* Original: [{id: 1, name: "Test", value: 100}]
|
|
791
|
+
* Update: [{value: 200}]
|
|
792
|
+
* Result: [{id: 1, name: "Test", value: 200}]
|
|
793
|
+
*
|
|
794
|
+
* Arrays of primitives still use replacement behavior for backwards compatibility.
|
|
795
|
+
*/
|
|
564
796
|
processArrayChanges(target, original, changeRequest, path, counts, warnings, allowedPaths, blockedOperations) {
|
|
565
797
|
const pathStr = path.join('.');
|
|
798
|
+
// Get change arrays for this path
|
|
566
799
|
const removeArray = pathStr ? _.get(changeRequest.removeElements, pathStr) : changeRequest.removeElements;
|
|
567
800
|
const updateArray = pathStr ? _.get(changeRequest.updateElements, pathStr) : changeRequest.updateElements;
|
|
568
801
|
const newArray = pathStr ? _.get(changeRequest.newElements, pathStr) : changeRequest.newElements;
|
|
802
|
+
// Build a new array with all changes applied
|
|
569
803
|
const newTargetArray = [];
|
|
804
|
+
// Process existing elements
|
|
570
805
|
for (let i = 0; i < target.length; i++) {
|
|
571
806
|
if (removeArray && removeArray[i] === '__DELETE__') {
|
|
572
807
|
counts.deletions++;
|
|
573
|
-
continue;
|
|
808
|
+
continue; // Skip deleted items
|
|
574
809
|
}
|
|
810
|
+
// Check for __DELETE__ in updateArray as well
|
|
575
811
|
if (updateArray && updateArray[i] === '__DELETE__') {
|
|
576
812
|
counts.deletions++;
|
|
577
|
-
continue;
|
|
813
|
+
continue; // Skip deleted items
|
|
578
814
|
}
|
|
815
|
+
// Use updated value if provided, otherwise keep original
|
|
579
816
|
let elementToAdd = target[i];
|
|
580
817
|
if (updateArray && this.isSignificantValue(updateArray[i])) {
|
|
818
|
+
// Check if both the update and current element are objects for deep merge
|
|
581
819
|
if (typeof updateArray[i] === 'object' && typeof elementToAdd === 'object' &&
|
|
582
820
|
!Array.isArray(updateArray[i]) && !Array.isArray(elementToAdd) &&
|
|
583
821
|
updateArray[i] !== null && elementToAdd !== null) {
|
|
822
|
+
// Deep merge for object elements - preserve existing properties
|
|
584
823
|
elementToAdd = _.cloneDeep(elementToAdd);
|
|
824
|
+
// Create a synthetic change request with just this update
|
|
825
|
+
// The updateElements should be the object itself at the root level
|
|
585
826
|
const elementChangeRequest = {
|
|
586
827
|
updateElements: updateArray[i]
|
|
587
828
|
};
|
|
588
|
-
this.processChangeRequest(elementToAdd, original ? original[i] : undefined, elementChangeRequest, [],
|
|
829
|
+
this.processChangeRequest(elementToAdd, original ? original[i] : undefined, elementChangeRequest, [], // Empty path since updateArray[i] is already at the element level
|
|
830
|
+
counts, warnings, allowedPaths, blockedOperations);
|
|
589
831
|
}
|
|
590
832
|
else {
|
|
833
|
+
// For primitives or arrays, replace entirely (existing behavior)
|
|
591
834
|
elementToAdd = updateArray[i];
|
|
592
835
|
counts.updates++;
|
|
593
836
|
}
|
|
594
837
|
}
|
|
595
838
|
else if (typeof elementToAdd === 'object' && elementToAdd !== null) {
|
|
839
|
+
// For objects/arrays that aren't being replaced, process nested changes
|
|
840
|
+
// IMPORTANT: Don't pass newElements for existing array elements -
|
|
841
|
+
// newElements items should be appended to the array, not merged into existing items
|
|
596
842
|
elementToAdd = _.cloneDeep(elementToAdd);
|
|
597
843
|
const changeRequestWithoutNew = {
|
|
598
844
|
updateElements: changeRequest.updateElements,
|
|
599
845
|
removeElements: changeRequest.removeElements,
|
|
600
846
|
replaceElements: changeRequest.replaceElements,
|
|
601
847
|
reasoning: changeRequest.reasoning
|
|
848
|
+
// Intentionally omit newElements
|
|
602
849
|
};
|
|
603
850
|
this.processChangeRequest(elementToAdd, original ? original[i] : undefined, changeRequestWithoutNew, [...path, i.toString()], counts, warnings, allowedPaths, blockedOperations);
|
|
604
851
|
}
|
|
605
852
|
newTargetArray.push(elementToAdd);
|
|
606
853
|
}
|
|
854
|
+
// Handle items in updateArray that are beyond target.length
|
|
855
|
+
// These should be treated as additions (AI put new items in updateElements)
|
|
607
856
|
if (updateArray && Array.isArray(updateArray)) {
|
|
608
857
|
for (let i = target.length; i < updateArray.length; i++) {
|
|
609
858
|
if (this.isSignificantValue(updateArray[i])) {
|
|
@@ -612,6 +861,7 @@ class PayloadManager {
|
|
|
612
861
|
}
|
|
613
862
|
}
|
|
614
863
|
}
|
|
864
|
+
// Add new elements
|
|
615
865
|
if (newArray && Array.isArray(newArray)) {
|
|
616
866
|
for (const item of newArray) {
|
|
617
867
|
if (this.isSignificantValue(item)) {
|
|
@@ -620,14 +870,22 @@ class PayloadManager {
|
|
|
620
870
|
}
|
|
621
871
|
}
|
|
622
872
|
}
|
|
873
|
+
// Replace array contents in-place
|
|
623
874
|
target.length = 0;
|
|
624
875
|
target.push(...newTargetArray);
|
|
625
876
|
}
|
|
877
|
+
/**
|
|
878
|
+
* Process changes for object properties
|
|
879
|
+
*/
|
|
626
880
|
processObjectChanges(target, original, changeRequest, path, counts, warnings, allowedPaths, blockedOperations) {
|
|
881
|
+
// Get all unique keys from change request
|
|
627
882
|
const changeKeys = this.getChangeKeys(changeRequest, path);
|
|
883
|
+
// Process changes for each key
|
|
628
884
|
for (const key of changeKeys) {
|
|
629
885
|
this.processKeyChange(target, original, changeRequest, [...path, key], key, counts, warnings, allowedPaths, blockedOperations);
|
|
630
886
|
}
|
|
887
|
+
// After processing all changes, check for "_DELETE_" values in updateElements
|
|
888
|
+
// This allows deletion within update operations at any depth
|
|
631
889
|
const pathStr = path.join('.');
|
|
632
890
|
const updateObj = pathStr ? _.get(changeRequest.updateElements, pathStr) : changeRequest.updateElements;
|
|
633
891
|
if (updateObj && typeof updateObj === 'object' && !Array.isArray(updateObj)) {
|
|
@@ -637,21 +895,28 @@ class PayloadManager {
|
|
|
637
895
|
keysToDelete.push(key);
|
|
638
896
|
}
|
|
639
897
|
}
|
|
898
|
+
// Delete the keys after iteration to avoid modification during iteration
|
|
640
899
|
for (const key of keysToDelete) {
|
|
641
900
|
delete target[key];
|
|
642
901
|
counts.deletions++;
|
|
643
902
|
}
|
|
644
903
|
}
|
|
904
|
+
// Recurse into all existing keys for nested changes
|
|
645
905
|
for (const key of Object.keys(target)) {
|
|
646
906
|
if (!changeKeys.has(key) && typeof target[key] === 'object' && target[key] !== null) {
|
|
647
907
|
this.processChangeRequest(target[key], original ? original[key] : undefined, changeRequest, [...path, key], counts, warnings, allowedPaths, blockedOperations);
|
|
648
908
|
}
|
|
649
909
|
}
|
|
650
910
|
}
|
|
911
|
+
/**
|
|
912
|
+
* Process a single key change (add, update, or delete)
|
|
913
|
+
*/
|
|
651
914
|
processKeyChange(target, original, changeRequest, keyPath, key, counts, warnings, allowedPaths, blockedOperations) {
|
|
652
915
|
const pathStr = keyPath.join('.');
|
|
916
|
+
// Check for replacement first (complete replacement - remove then add)
|
|
653
917
|
const replaceValue = _.get(changeRequest.replaceElements, pathStr);
|
|
654
918
|
if (replaceValue !== undefined) {
|
|
919
|
+
// Check if appropriate operation is allowed (delete if exists, add if not)
|
|
655
920
|
const operation = key in target ? 'update' : 'add';
|
|
656
921
|
if (allowedPaths && !this.isOperationAllowedForPath(pathStr, operation, allowedPaths)) {
|
|
657
922
|
const warning = `Operation denied: Cannot replace '${pathStr}' - operation '${operation}' not allowed`;
|
|
@@ -668,6 +933,7 @@ class PayloadManager {
|
|
|
668
933
|
}
|
|
669
934
|
return;
|
|
670
935
|
}
|
|
936
|
+
// Count the operations
|
|
671
937
|
if (key in target) {
|
|
672
938
|
counts.deletions++;
|
|
673
939
|
counts.additions++;
|
|
@@ -675,11 +941,14 @@ class PayloadManager {
|
|
|
675
941
|
else {
|
|
676
942
|
counts.additions++;
|
|
677
943
|
}
|
|
944
|
+
// Replace the value
|
|
678
945
|
target[key] = replaceValue;
|
|
679
|
-
return;
|
|
946
|
+
return; // Skip other operations since this is a complete replacement
|
|
680
947
|
}
|
|
948
|
+
// Check for deletion
|
|
681
949
|
const removeValue = _.get(changeRequest.removeElements, pathStr);
|
|
682
950
|
if (removeValue === '__DELETE__') {
|
|
951
|
+
// Check if delete operation is allowed
|
|
683
952
|
if (allowedPaths && !this.isOperationAllowedForPath(pathStr, 'delete', allowedPaths)) {
|
|
684
953
|
const warning = `Operation denied: Cannot delete '${pathStr}' - operation 'delete' not allowed`;
|
|
685
954
|
warnings.push(warning);
|
|
@@ -697,9 +966,13 @@ class PayloadManager {
|
|
|
697
966
|
}
|
|
698
967
|
delete target[key];
|
|
699
968
|
counts.deletions++;
|
|
969
|
+
// Don't return here - check if there's also a new value to add
|
|
970
|
+
// This handles the case where AI wants to replace by removing then adding
|
|
700
971
|
}
|
|
972
|
+
// Check for addition first
|
|
701
973
|
const newValue = _.get(changeRequest.newElements, pathStr);
|
|
702
974
|
if (newValue !== undefined && !(key in target)) {
|
|
975
|
+
// Check if add operation is allowed
|
|
703
976
|
if (allowedPaths && !this.isOperationAllowedForPath(pathStr, 'add', allowedPaths)) {
|
|
704
977
|
const warning = `Operation denied: Cannot add '${pathStr}' - operation 'add' not allowed`;
|
|
705
978
|
warnings.push(warning);
|
|
@@ -719,7 +992,12 @@ class PayloadManager {
|
|
|
719
992
|
counts.additions++;
|
|
720
993
|
return;
|
|
721
994
|
}
|
|
995
|
+
// Check for update
|
|
722
996
|
const updateValue = _.get(changeRequest.updateElements, pathStr);
|
|
997
|
+
// Be forgiving: if AI put an update in newElements by mistake, treat it as an update
|
|
998
|
+
// EXCEPTIONS where we should NOT treat newElements as updateElements:
|
|
999
|
+
// 1. Both are arrays - let processArrayChanges handle append semantics
|
|
1000
|
+
// 2. Both are objects - recurse to find nested arrays that need append semantics
|
|
723
1001
|
const bothAreArrays = Array.isArray(newValue) && Array.isArray(target[key]);
|
|
724
1002
|
const bothAreObjects = newValue !== undefined &&
|
|
725
1003
|
typeof newValue === 'object' && !Array.isArray(newValue) &&
|
|
@@ -734,6 +1012,7 @@ class PayloadManager {
|
|
|
734
1012
|
shouldTreatNewAsUpdate ? newValue :
|
|
735
1013
|
undefined;
|
|
736
1014
|
if (effectiveUpdateValue !== undefined && key in target) {
|
|
1015
|
+
// Check if update operation is allowed
|
|
737
1016
|
if (allowedPaths && !this.isOperationAllowedForPath(pathStr, 'update', allowedPaths)) {
|
|
738
1017
|
const warning = `Operation denied: Cannot update '${pathStr}' - operation 'update' not allowed`;
|
|
739
1018
|
warnings.push(warning);
|
|
@@ -749,19 +1028,25 @@ class PayloadManager {
|
|
|
749
1028
|
}
|
|
750
1029
|
return;
|
|
751
1030
|
}
|
|
1031
|
+
// If both values are objects or arrays, we need to recursively process the update
|
|
752
1032
|
if (typeof effectiveUpdateValue === 'object' && effectiveUpdateValue !== null &&
|
|
753
1033
|
typeof target[key] === 'object' && target[key] !== null) {
|
|
1034
|
+
// Recursively process nested updates for both objects and arrays
|
|
754
1035
|
this.processChangeRequest(target[key], original ? original[key] : undefined, changeRequest, keyPath, counts, warnings, allowedPaths, blockedOperations);
|
|
755
1036
|
}
|
|
756
1037
|
else {
|
|
1038
|
+
// For primitives or type mismatches, replace the value
|
|
757
1039
|
target[key] = effectiveUpdateValue;
|
|
758
1040
|
counts.updates++;
|
|
759
1041
|
}
|
|
1042
|
+
// Add a soft warning if we auto-corrected the placement (but not for arrays which use append semantics)
|
|
760
1043
|
if (updateValue === undefined && newValue !== undefined && shouldTreatNewAsUpdate) {
|
|
761
1044
|
warnings.push(`Auto-corrected: '${key}' was in newElements but already exists (treated as update)`);
|
|
762
1045
|
}
|
|
763
1046
|
}
|
|
764
1047
|
else if (updateValue !== undefined && !(key in target)) {
|
|
1048
|
+
// Be forgiving: if AI put an addition in updateElements by mistake, treat it as an addition
|
|
1049
|
+
// Check if add operation is allowed
|
|
765
1050
|
if (allowedPaths && !this.isOperationAllowedForPath(pathStr, 'add', allowedPaths)) {
|
|
766
1051
|
const warning = `Operation denied: Cannot add '${pathStr}' - operation 'add' not allowed`;
|
|
767
1052
|
warnings.push(warning);
|
|
@@ -783,15 +1068,25 @@ class PayloadManager {
|
|
|
783
1068
|
}
|
|
784
1069
|
else if (removeValue !== undefined && removeValue !== '__DELETE__' &&
|
|
785
1070
|
typeof target[key] === 'object' && target[key] !== null) {
|
|
1071
|
+
// Handle case where removeValue exists but isn't a direct deletion
|
|
1072
|
+
// This happens with arrays where removeValue is like [{}, '_DELETE_', {}]
|
|
1073
|
+
// We need to recurse to process the array removals
|
|
786
1074
|
this.processChangeRequest(target[key], original ? original[key] : undefined, changeRequest, keyPath, counts, warnings, allowedPaths, blockedOperations);
|
|
787
1075
|
}
|
|
788
1076
|
else if (newValue !== undefined && Array.isArray(newValue) && Array.isArray(target[key])) {
|
|
1077
|
+
// Handle array append: newElements has an array for an existing array key
|
|
1078
|
+
// Recurse to let processArrayChanges handle the append
|
|
789
1079
|
this.processChangeRequest(target[key], original ? original[key] : undefined, changeRequest, keyPath, counts, warnings, allowedPaths, blockedOperations);
|
|
790
1080
|
}
|
|
791
1081
|
else if (bothAreObjects) {
|
|
1082
|
+
// Handle object in newElements for existing object key
|
|
1083
|
+
// Recurse to find nested arrays that need append semantics
|
|
792
1084
|
this.processChangeRequest(target[key], original ? original[key] : undefined, changeRequest, keyPath, counts, warnings, allowedPaths, blockedOperations);
|
|
793
1085
|
}
|
|
794
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Get all unique keys from change request for a given path
|
|
1089
|
+
*/
|
|
795
1090
|
getChangeKeys(changeRequest, path) {
|
|
796
1091
|
const keys = new Set();
|
|
797
1092
|
const pathStr = path.join('.');
|
|
@@ -810,6 +1105,9 @@ class PayloadManager {
|
|
|
810
1105
|
addKeysFromObject(replaceObj);
|
|
811
1106
|
return keys;
|
|
812
1107
|
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Check if a value is significant (not empty object or undefined)
|
|
1110
|
+
*/
|
|
813
1111
|
isSignificantValue(value) {
|
|
814
1112
|
if (value === undefined)
|
|
815
1113
|
return false;
|
|
@@ -818,15 +1116,27 @@ class PayloadManager {
|
|
|
818
1116
|
}
|
|
819
1117
|
return true;
|
|
820
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Log a summary of changes applied
|
|
1121
|
+
*/
|
|
821
1122
|
logChangesSummary(counts, reasoning, agentName) {
|
|
822
1123
|
const agent = agentName ? ` by ${agentName}` : '';
|
|
823
1124
|
const reason = reasoning ? ` | Reason: ${reasoning}` : '';
|
|
824
|
-
|
|
1125
|
+
LogStatus(`Payload changes applied${agent}: ` +
|
|
825
1126
|
`+${counts.additions} additions, ~${counts.updates} updates, -${counts.deletions} deletions${reason}`);
|
|
826
1127
|
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Apply a change request to a sub-agent payload with upstream guardrails
|
|
1130
|
+
*
|
|
1131
|
+
* This method first applies the change request, then enforces upstream path restrictions
|
|
1132
|
+
* to ensure the sub-agent only modifies allowed paths.
|
|
1133
|
+
*/
|
|
827
1134
|
applySubAgentChangeRequest(parentPayload, changeRequest, upstreamPaths, subAgentName) {
|
|
1135
|
+
// First apply the full change request
|
|
828
1136
|
const changeResult = this.applyAgentChangeRequest(parentPayload, changeRequest, { validateChanges: true });
|
|
1137
|
+
// Then apply upstream guardrails
|
|
829
1138
|
const guardedResult = this.mergeUpstreamPayload(subAgentName, parentPayload, changeResult.result, upstreamPaths);
|
|
1139
|
+
// Count blocked changes (this is a simplified count)
|
|
830
1140
|
const blocked = this.countBlockedChanges(parentPayload, changeResult.result, guardedResult.result);
|
|
831
1141
|
return {
|
|
832
1142
|
...changeResult,
|
|
@@ -835,43 +1145,98 @@ class PayloadManager {
|
|
|
835
1145
|
blockedOperations: guardedResult.blockedOperations
|
|
836
1146
|
};
|
|
837
1147
|
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Count how many changes were blocked by guardrails
|
|
1150
|
+
*/
|
|
838
1151
|
countBlockedChanges(_original, intended, actual) {
|
|
1152
|
+
// This is a simplified implementation
|
|
1153
|
+
// A full implementation would do deep comparison
|
|
839
1154
|
const intendedStr = JSON.stringify(intended);
|
|
840
1155
|
const actualStr = JSON.stringify(actual);
|
|
841
1156
|
return intendedStr === actualStr ? 0 : 1;
|
|
842
1157
|
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Applies a payload scope transformation to extract only the scoped portion.
|
|
1160
|
+
*
|
|
1161
|
+
* @param payload The full payload to scope
|
|
1162
|
+
* @param scopePath The scope path (e.g., "/functionalRequirements" or "/PropA/SubProp1")
|
|
1163
|
+
* @returns The scoped portion of the payload
|
|
1164
|
+
*
|
|
1165
|
+
* @example
|
|
1166
|
+
* ```typescript
|
|
1167
|
+
* const payload = { functionalRequirements: { feature1: "..." }, technicalDesign: { ... } };
|
|
1168
|
+
* const scoped = applyPayloadScope(payload, "/functionalRequirements");
|
|
1169
|
+
* // Returns: { feature1: "..." }
|
|
1170
|
+
* ```
|
|
1171
|
+
*/
|
|
843
1172
|
applyPayloadScope(payload, scopePath) {
|
|
844
1173
|
if (!payload || !scopePath)
|
|
845
1174
|
return payload;
|
|
1175
|
+
// Remove leading slash and split path
|
|
846
1176
|
const pathParts = scopePath.startsWith('/')
|
|
847
1177
|
? scopePath.slice(1).split('/')
|
|
848
1178
|
: scopePath.split('/');
|
|
1179
|
+
// Navigate to the scoped portion
|
|
849
1180
|
let current = payload;
|
|
850
1181
|
for (const part of pathParts) {
|
|
851
1182
|
if (current && typeof current === 'object' && current !== null && part in current) {
|
|
852
1183
|
current = (current)[part];
|
|
853
1184
|
}
|
|
854
1185
|
else {
|
|
1186
|
+
// Path doesn't exist, return null
|
|
855
1187
|
return null;
|
|
856
1188
|
}
|
|
857
1189
|
}
|
|
1190
|
+
// Return a deep clone of the scoped portion
|
|
858
1191
|
return _.cloneDeep(current);
|
|
859
1192
|
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Reverses a payload scope transformation by wrapping the scoped content back into the full structure.
|
|
1195
|
+
*
|
|
1196
|
+
* @param scopedPayload The scoped payload to wrap
|
|
1197
|
+
* @param scopePath The scope path used for extraction
|
|
1198
|
+
* @returns A full payload structure with the scoped content at the correct path
|
|
1199
|
+
*
|
|
1200
|
+
* @example
|
|
1201
|
+
* ```typescript
|
|
1202
|
+
* const scoped = { feature1: "updated" };
|
|
1203
|
+
* const full = reversePayloadScope(scoped, "/functionalRequirements");
|
|
1204
|
+
* // Returns: { functionalRequirements: { feature1: "updated" } }
|
|
1205
|
+
* ```
|
|
1206
|
+
*/
|
|
860
1207
|
reversePayloadScope(scopedPayload, scopePath) {
|
|
861
1208
|
if (!scopePath)
|
|
862
1209
|
return scopedPayload;
|
|
1210
|
+
// Remove leading slash and split path
|
|
863
1211
|
const pathParts = scopePath.startsWith('/')
|
|
864
1212
|
? scopePath.slice(1).split('/')
|
|
865
1213
|
: scopePath.split('/');
|
|
1214
|
+
// Build the structure from the inside out
|
|
866
1215
|
let result = _.cloneDeep(scopedPayload);
|
|
867
1216
|
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
868
1217
|
result = { [pathParts[i]]: result };
|
|
869
1218
|
}
|
|
870
1219
|
return result;
|
|
871
1220
|
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Transforms paths in a change request to account for payload scoping.
|
|
1223
|
+
* Prepends the scope path to all paths in the change request.
|
|
1224
|
+
*
|
|
1225
|
+
* @param changeRequest The change request with paths relative to the scoped view
|
|
1226
|
+
* @param scopePath The scope path to prepend
|
|
1227
|
+
* @returns A new change request with transformed paths
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* ```typescript
|
|
1231
|
+
* const request = { updateElements: { "field1": "value" } };
|
|
1232
|
+
* const transformed = transformChangeRequestPaths(request, "/functionalRequirements");
|
|
1233
|
+
* // Returns: { updateElements: { "functionalRequirements.field1": "value" } }
|
|
1234
|
+
* ```
|
|
1235
|
+
*/
|
|
872
1236
|
transformChangeRequestPaths(changeRequest, scopePath) {
|
|
873
1237
|
if (!scopePath)
|
|
874
1238
|
return changeRequest;
|
|
1239
|
+
// Remove leading slash and convert to dot notation
|
|
875
1240
|
const pathPrefix = scopePath.startsWith('/')
|
|
876
1241
|
? scopePath.slice(1).replace(/\//g, '.')
|
|
877
1242
|
: scopePath.replace(/\//g, '.');
|
|
@@ -882,10 +1247,12 @@ class PayloadManager {
|
|
|
882
1247
|
for (const [key, value] of Object.entries(obj)) {
|
|
883
1248
|
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
884
1249
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1250
|
+
// For nested objects, check if it's a leaf value
|
|
885
1251
|
if (this.isLeafValue(value)) {
|
|
886
1252
|
result[newKey] = value;
|
|
887
1253
|
}
|
|
888
1254
|
else {
|
|
1255
|
+
// It's a nested path structure, recurse without adding to prefix
|
|
889
1256
|
Object.assign(result, transformObject(value, newKey));
|
|
890
1257
|
}
|
|
891
1258
|
}
|
|
@@ -903,17 +1270,23 @@ class PayloadManager {
|
|
|
903
1270
|
reasoning: changeRequest.reasoning
|
|
904
1271
|
};
|
|
905
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Helper to determine if a value is a leaf value (not a path structure)
|
|
1275
|
+
*/
|
|
906
1276
|
isLeafValue(value) {
|
|
907
1277
|
if (!value || typeof value !== 'object')
|
|
908
1278
|
return true;
|
|
1279
|
+
// Check if it looks like a data value rather than a path structure
|
|
1280
|
+
// This is a heuristic - may need refinement based on actual use cases
|
|
909
1281
|
const keys = Object.keys(value);
|
|
1282
|
+
// If it has common data properties, it's likely a value
|
|
910
1283
|
if (keys.some(k => ['id', 'name', 'value', 'data', 'type'].includes(k.toLowerCase()))) {
|
|
911
1284
|
return true;
|
|
912
1285
|
}
|
|
1286
|
+
// If all values are primitives, it's likely a value object
|
|
913
1287
|
return Object.values(value).every(v => v === null ||
|
|
914
1288
|
v === undefined ||
|
|
915
1289
|
typeof v !== 'object');
|
|
916
1290
|
}
|
|
917
1291
|
}
|
|
918
|
-
exports.PayloadManager = PayloadManager;
|
|
919
1292
|
//# sourceMappingURL=PayloadManager.js.map
|