agentxchain 2.91.0 → 2.93.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.
@@ -217,7 +217,16 @@ async function executeParallelTurns(root, config, state, maxConcurrent, callback
217
217
 
218
218
  // If selectRole returns a role we already tried (or assigned), try
219
219
  // other eligible roles from the routing before giving up.
220
+ // Exception: when delegation queue is driving resolution, do not fill
221
+ // extra slots with non-delegation roles via the fallback — those roles
222
+ // would execute without delegation context and corrupt the lifecycle.
220
223
  if (triedRoles.has(roleId)) {
224
+ const hasPendingDelegations = Array.isArray(state?.delegation_queue) &&
225
+ state.delegation_queue.some(d => d.status === 'pending' || d.status === 'active');
226
+ const hasPendingReview = !!state?.pending_delegation_review;
227
+ if (hasPendingDelegations || hasPendingReview) {
228
+ break;
229
+ }
221
230
  const phase = state.phase;
222
231
  const allowed = config?.routing?.[phase]?.allowed_next_roles || [];
223
232
  const alternateFound = allowed.some((alt) => {
@@ -248,6 +257,13 @@ async function executeParallelTurns(root, config, state, maxConcurrent, callback
248
257
  turnsToDispatch.push({ turn: assignResult.turn, state: assignResult.state });
249
258
  emit({ type: 'turn_assigned', turn: assignResult.turn, role: roleId, state: assignResult.state });
250
259
 
260
+ // Delegation review is a coordination checkpoint — do not fill additional
261
+ // slots alongside it. The review must execute alone so it can assess all
262
+ // delegation results before the run continues.
263
+ if (assignResult.turn.delegation_review) {
264
+ break;
265
+ }
266
+
251
267
  // Reload state after assignment to get accurate active count
252
268
  state = loadState(root, config);
253
269
  activeCount = getActiveTurnCount(state);
@@ -76,6 +76,16 @@
76
76
  "rationale": {
77
77
  "type": "string",
78
78
  "minLength": 1
79
+ },
80
+ "durability": {
81
+ "type": "string",
82
+ "enum": ["run", "repo"],
83
+ "description": "Decision persistence scope. 'run' (default) dies with the run; 'repo' persists across runs as a binding constraint."
84
+ },
85
+ "overrides": {
86
+ "type": "string",
87
+ "pattern": "^DEC-\\d+$",
88
+ "description": "ID of an active repo-durable decision this decision supersedes."
79
89
  }
80
90
  }
81
91
  }
@@ -325,6 +325,8 @@ function checkPlaceholder(errors, fieldPath, value) {
325
325
  }
326
326
  }
327
327
 
328
+ const VALID_DURABILITIES = ['run', 'repo'];
329
+
328
330
  function validateDecision(dec, index) {
329
331
  const errors = [];
330
332
  const prefix = `decisions[${index}]`;
@@ -344,6 +346,17 @@ function validateDecision(dec, index) {
344
346
  if (typeof dec.rationale !== 'string' || !dec.rationale.trim()) {
345
347
  errors.push(`${prefix}.rationale must be a non-empty string.`);
346
348
  }
349
+ if (dec.durability !== undefined && !VALID_DURABILITIES.includes(dec.durability)) {
350
+ errors.push(`${prefix}.durability must be one of: ${VALID_DURABILITIES.join(', ')}.`);
351
+ }
352
+ if (dec.overrides !== undefined) {
353
+ if (typeof dec.overrides !== 'string' || !/^DEC-\d+$/.test(dec.overrides)) {
354
+ errors.push(`${prefix}.overrides must match pattern DEC-NNN.`);
355
+ }
356
+ if (dec.overrides === dec.id) {
357
+ errors.push(`${prefix}.overrides cannot reference itself.`);
358
+ }
359
+ }
347
360
  return errors;
348
361
  }
349
362