agentxchain 2.86.0 → 2.88.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/package.json
CHANGED
|
@@ -124,6 +124,12 @@ export function writeDispatchBundle(root, state, config, opts = {}) {
|
|
|
124
124
|
if (turn.conflict_context) {
|
|
125
125
|
assignment.conflict_context = turn.conflict_context;
|
|
126
126
|
}
|
|
127
|
+
if (turn.delegation_context) {
|
|
128
|
+
assignment.delegation_context = turn.delegation_context;
|
|
129
|
+
}
|
|
130
|
+
if (turn.delegation_review) {
|
|
131
|
+
assignment.delegation_review = turn.delegation_review;
|
|
132
|
+
}
|
|
127
133
|
if (warnings.length > 0) {
|
|
128
134
|
assignment.advisory_warnings = warnings.map((message) => ({ code: 'advisory_scope_overlap', message }));
|
|
129
135
|
}
|
|
@@ -547,6 +553,52 @@ function renderContext(state, config, root, turn, role) {
|
|
|
547
553
|
lines.push('');
|
|
548
554
|
}
|
|
549
555
|
|
|
556
|
+
// Delegation context (when this turn is a delegated sub-task)
|
|
557
|
+
if (turn.delegation_context) {
|
|
558
|
+
const dc = turn.delegation_context;
|
|
559
|
+
lines.push('## Delegation Context');
|
|
560
|
+
lines.push('');
|
|
561
|
+
lines.push('You are executing a delegated sub-task.');
|
|
562
|
+
lines.push('');
|
|
563
|
+
lines.push(`- **Delegated by:** ${dc.parent_role} (turn ${dc.parent_turn_id})`);
|
|
564
|
+
lines.push(`- **Charter:** ${dc.charter}`);
|
|
565
|
+
if (Array.isArray(dc.acceptance_contract) && dc.acceptance_contract.length > 0) {
|
|
566
|
+
lines.push('- **Acceptance contract:**');
|
|
567
|
+
for (const req of dc.acceptance_contract) {
|
|
568
|
+
lines.push(` - ${req}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
lines.push('');
|
|
572
|
+
lines.push('Focus exclusively on the charter above. Do not expand scope beyond the delegation.');
|
|
573
|
+
lines.push('');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Delegation review context (when this turn reviews completed delegations)
|
|
577
|
+
if (turn.delegation_review) {
|
|
578
|
+
const dr = turn.delegation_review;
|
|
579
|
+
lines.push('## Delegation Review');
|
|
580
|
+
lines.push('');
|
|
581
|
+
lines.push('Your delegated sub-tasks have been completed. Review the results below.');
|
|
582
|
+
lines.push('');
|
|
583
|
+
for (const result of dr.results || []) {
|
|
584
|
+
lines.push(`### ${result.delegation_id} → ${result.to_role}`);
|
|
585
|
+
lines.push('');
|
|
586
|
+
lines.push(`- **Charter:** ${result.charter}`);
|
|
587
|
+
lines.push(`- **Status:** ${result.status}`);
|
|
588
|
+
lines.push(`- **Summary:** ${result.summary}`);
|
|
589
|
+
if (result.files_changed?.length > 0) {
|
|
590
|
+
lines.push(`- **Files changed:** ${result.files_changed.join(', ')}`);
|
|
591
|
+
}
|
|
592
|
+
if (result.verification?.status) {
|
|
593
|
+
lines.push(`- **Verification:** ${result.verification.status}`);
|
|
594
|
+
}
|
|
595
|
+
lines.push('');
|
|
596
|
+
}
|
|
597
|
+
lines.push('Evaluate whether each delegation met its acceptance contract.');
|
|
598
|
+
lines.push('Your turn result should assess the delegation outcomes and decide next steps.');
|
|
599
|
+
lines.push('');
|
|
600
|
+
}
|
|
601
|
+
|
|
550
602
|
// Inherited context from parent run (when --inherit-context was used)
|
|
551
603
|
if (state.inherited_context) {
|
|
552
604
|
// First turn gets the full rendering; subsequent turns get compact
|
|
@@ -2065,24 +2065,53 @@ export function assignGovernedTurn(root, config, roleId) {
|
|
|
2065
2065
|
// Record which turns are concurrent siblings (for conflict detection context)
|
|
2066
2066
|
const concurrentWith = Object.keys(activeTurns);
|
|
2067
2067
|
|
|
2068
|
+
// Build the new turn object
|
|
2069
|
+
const newTurn = {
|
|
2070
|
+
turn_id: turnId,
|
|
2071
|
+
assigned_role: roleId,
|
|
2072
|
+
status: 'running',
|
|
2073
|
+
attempt: 1,
|
|
2074
|
+
started_at: now,
|
|
2075
|
+
deadline_at: new Date(Date.now() + timeoutMinutes * 60 * 1000).toISOString(),
|
|
2076
|
+
runtime_id: runtimeId,
|
|
2077
|
+
baseline,
|
|
2078
|
+
assigned_sequence: nextSequence,
|
|
2079
|
+
concurrent_with: concurrentWith,
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
// Attach delegation context if this turn fulfills a pending delegation
|
|
2083
|
+
const delegationQueue = state.delegation_queue || [];
|
|
2084
|
+
const pendingDelegation = delegationQueue.find(d => d.status === 'pending' && d.to_role === roleId);
|
|
2085
|
+
if (pendingDelegation) {
|
|
2086
|
+
newTurn.delegation_context = {
|
|
2087
|
+
delegation_id: pendingDelegation.delegation_id,
|
|
2088
|
+
parent_turn_id: pendingDelegation.parent_turn_id,
|
|
2089
|
+
parent_role: pendingDelegation.parent_role,
|
|
2090
|
+
charter: pendingDelegation.charter,
|
|
2091
|
+
acceptance_contract: pendingDelegation.acceptance_contract,
|
|
2092
|
+
};
|
|
2093
|
+
// Mark the delegation as active
|
|
2094
|
+
pendingDelegation.status = 'active';
|
|
2095
|
+
pendingDelegation.child_turn_id = turnId;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// Attach delegation review context if a review is pending for this role
|
|
2099
|
+
const pendingReview = state.pending_delegation_review;
|
|
2100
|
+
if (pendingReview && pendingReview.parent_role === roleId && !pendingDelegation) {
|
|
2101
|
+
newTurn.delegation_review = {
|
|
2102
|
+
parent_turn_id: pendingReview.parent_turn_id,
|
|
2103
|
+
results: pendingReview.delegation_results,
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2068
2107
|
const updatedState = {
|
|
2069
2108
|
...state,
|
|
2070
2109
|
turn_sequence: nextSequence,
|
|
2071
2110
|
budget_reservations: reservations,
|
|
2111
|
+
delegation_queue: delegationQueue,
|
|
2072
2112
|
active_turns: {
|
|
2073
2113
|
...activeTurns,
|
|
2074
|
-
[turnId]:
|
|
2075
|
-
turn_id: turnId,
|
|
2076
|
-
assigned_role: roleId,
|
|
2077
|
-
status: 'running',
|
|
2078
|
-
attempt: 1,
|
|
2079
|
-
started_at: now,
|
|
2080
|
-
deadline_at: new Date(Date.now() + timeoutMinutes * 60 * 1000).toISOString(),
|
|
2081
|
-
runtime_id: runtimeId,
|
|
2082
|
-
baseline,
|
|
2083
|
-
assigned_sequence: nextSequence,
|
|
2084
|
-
concurrent_with: concurrentWith,
|
|
2085
|
-
},
|
|
2114
|
+
[turnId]: newTurn,
|
|
2086
2115
|
},
|
|
2087
2116
|
};
|
|
2088
2117
|
|
|
@@ -2649,6 +2678,97 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2649
2678
|
},
|
|
2650
2679
|
};
|
|
2651
2680
|
|
|
2681
|
+
// ── Delegation queue management ──────────────────────────────────────────
|
|
2682
|
+
// Initialize delegation queue if not present
|
|
2683
|
+
if (!updatedState.delegation_queue) {
|
|
2684
|
+
updatedState.delegation_queue = [];
|
|
2685
|
+
}
|
|
2686
|
+
if (!updatedState.pending_delegation_review) {
|
|
2687
|
+
updatedState.pending_delegation_review = null;
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// If this turn has delegations, enqueue them
|
|
2691
|
+
if (Array.isArray(turnResult.delegations) && turnResult.delegations.length > 0) {
|
|
2692
|
+
for (const del of turnResult.delegations) {
|
|
2693
|
+
updatedState.delegation_queue.push({
|
|
2694
|
+
delegation_id: del.id,
|
|
2695
|
+
parent_turn_id: turnResult.turn_id,
|
|
2696
|
+
parent_role: turnResult.role,
|
|
2697
|
+
to_role: del.to_role,
|
|
2698
|
+
charter: del.charter,
|
|
2699
|
+
acceptance_contract: del.acceptance_contract,
|
|
2700
|
+
status: 'pending',
|
|
2701
|
+
child_turn_id: null,
|
|
2702
|
+
created_at: now,
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
// Override next_recommended_role to first pending delegation
|
|
2706
|
+
updatedState.next_recommended_role = turnResult.delegations[0].to_role;
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
// If this turn was a delegated sub-task, update the delegation queue entry
|
|
2710
|
+
if (currentTurn.delegation_context) {
|
|
2711
|
+
const delId = currentTurn.delegation_context.delegation_id;
|
|
2712
|
+
const queueEntry = updatedState.delegation_queue.find(d => d.delegation_id === delId);
|
|
2713
|
+
if (queueEntry) {
|
|
2714
|
+
queueEntry.status = turnResult.status === 'completed' ? 'completed' : 'failed';
|
|
2715
|
+
queueEntry.child_turn_id = currentTurn.turn_id;
|
|
2716
|
+
|
|
2717
|
+
// Check if all delegations from the same parent are now complete
|
|
2718
|
+
const parentTurnId = queueEntry.parent_turn_id;
|
|
2719
|
+
const parentDelegations = updatedState.delegation_queue.filter(d => d.parent_turn_id === parentTurnId);
|
|
2720
|
+
const allDone = parentDelegations.every(d => d.status === 'completed' || d.status === 'failed');
|
|
2721
|
+
|
|
2722
|
+
if (allDone) {
|
|
2723
|
+
// Build delegation review context
|
|
2724
|
+
const delegationResults = parentDelegations.map(d => {
|
|
2725
|
+
const childHistory = nextHistoryEntries.find(h => h.turn_id === d.child_turn_id);
|
|
2726
|
+
return {
|
|
2727
|
+
delegation_id: d.delegation_id,
|
|
2728
|
+
child_turn_id: d.child_turn_id,
|
|
2729
|
+
to_role: d.to_role,
|
|
2730
|
+
charter: d.charter,
|
|
2731
|
+
acceptance_contract: d.acceptance_contract,
|
|
2732
|
+
summary: childHistory?.summary || '(no summary)',
|
|
2733
|
+
status: d.status,
|
|
2734
|
+
files_changed: childHistory?.files_changed || [],
|
|
2735
|
+
verification: childHistory?.verification || { status: 'skipped' },
|
|
2736
|
+
};
|
|
2737
|
+
});
|
|
2738
|
+
|
|
2739
|
+
updatedState.pending_delegation_review = {
|
|
2740
|
+
parent_turn_id: parentTurnId,
|
|
2741
|
+
parent_role: queueEntry.parent_role,
|
|
2742
|
+
delegation_results: delegationResults,
|
|
2743
|
+
};
|
|
2744
|
+
|
|
2745
|
+
// Recommend the parent role for the review turn
|
|
2746
|
+
updatedState.next_recommended_role = queueEntry.parent_role;
|
|
2747
|
+
|
|
2748
|
+
// Clear completed delegation queue entries for this parent
|
|
2749
|
+
updatedState.delegation_queue = updatedState.delegation_queue.filter(
|
|
2750
|
+
d => d.parent_turn_id !== parentTurnId
|
|
2751
|
+
);
|
|
2752
|
+
} else {
|
|
2753
|
+
// More pending delegations — route to the next one
|
|
2754
|
+
const nextPending = updatedState.delegation_queue.find(
|
|
2755
|
+
d => d.parent_turn_id === parentTurnId && d.status === 'pending'
|
|
2756
|
+
);
|
|
2757
|
+
if (nextPending) {
|
|
2758
|
+
updatedState.next_recommended_role = nextPending.to_role;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
// If a delegation review was pending and this turn was the review, clear it
|
|
2765
|
+
if (updatedState.pending_delegation_review &&
|
|
2766
|
+
turnResult.role === updatedState.pending_delegation_review.parent_role &&
|
|
2767
|
+
!currentTurn.delegation_context) {
|
|
2768
|
+
// The parent role just completed their review turn
|
|
2769
|
+
updatedState.pending_delegation_review = null;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2652
2772
|
if (updatedState.status === 'blocked' && !hasBlockingActiveTurn(remainingTurns)) {
|
|
2653
2773
|
updatedState.status = 'active';
|
|
2654
2774
|
updatedState.blocked_on = null;
|
|
@@ -30,6 +30,15 @@ export function resolveGovernedRole({ override = null, state = null, config }) {
|
|
|
30
30
|
const routing = phase ? config?.routing?.[phase] : null;
|
|
31
31
|
const warnings = [];
|
|
32
32
|
|
|
33
|
+
// ── Delegation queue priority ───────────────────────────────────────────
|
|
34
|
+
// If there are pending delegations, the next role is the delegation target
|
|
35
|
+
const pendingDelegation = Array.isArray(state?.delegation_queue)
|
|
36
|
+
? state.delegation_queue.find(d => d.status === 'pending')
|
|
37
|
+
: null;
|
|
38
|
+
|
|
39
|
+
// If a delegation review is pending, the parent role takes priority
|
|
40
|
+
const pendingReview = state?.pending_delegation_review || null;
|
|
41
|
+
|
|
33
42
|
if (override) {
|
|
34
43
|
if (!config?.roles?.[override]) {
|
|
35
44
|
return {
|
|
@@ -45,6 +54,14 @@ export function resolveGovernedRole({ override = null, state = null, config }) {
|
|
|
45
54
|
warnings.push(`role "${override}" is not in allowed_next_roles for phase "${phase}"`);
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
// Warn if override skips a pending delegation
|
|
58
|
+
if (pendingDelegation && override !== pendingDelegation.to_role) {
|
|
59
|
+
warnings.push(`Override skips pending delegation ${pendingDelegation.delegation_id} to role "${pendingDelegation.to_role}"`);
|
|
60
|
+
}
|
|
61
|
+
if (pendingReview && override !== pendingReview.parent_role) {
|
|
62
|
+
warnings.push(`Override skips pending delegation review for role "${pendingReview.parent_role}"`);
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
return {
|
|
49
66
|
roleId: override,
|
|
50
67
|
warnings,
|
|
@@ -54,6 +71,30 @@ export function resolveGovernedRole({ override = null, state = null, config }) {
|
|
|
54
71
|
};
|
|
55
72
|
}
|
|
56
73
|
|
|
74
|
+
// Delegation review takes priority over pending delegations
|
|
75
|
+
if (pendingReview && config?.roles?.[pendingReview.parent_role]) {
|
|
76
|
+
return {
|
|
77
|
+
roleId: pendingReview.parent_role,
|
|
78
|
+
warnings,
|
|
79
|
+
error: null,
|
|
80
|
+
availableRoles: roles,
|
|
81
|
+
phase,
|
|
82
|
+
delegation_review: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Pending delegation takes priority over normal resolution
|
|
87
|
+
if (pendingDelegation && config?.roles?.[pendingDelegation.to_role]) {
|
|
88
|
+
return {
|
|
89
|
+
roleId: pendingDelegation.to_role,
|
|
90
|
+
warnings,
|
|
91
|
+
error: null,
|
|
92
|
+
availableRoles: roles,
|
|
93
|
+
phase,
|
|
94
|
+
delegation: pendingDelegation,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
57
98
|
if (state?.next_recommended_role && config?.roles?.[state.next_recommended_role]) {
|
|
58
99
|
const recommended = state.next_recommended_role;
|
|
59
100
|
const isLegal = !routing?.allowed_next_roles || routing.allowed_next_roles.includes(recommended);
|
|
@@ -228,6 +228,39 @@
|
|
|
228
228
|
"output_tokens": { "type": "integer", "minimum": 0 },
|
|
229
229
|
"usd": { "type": "number", "minimum": 0 }
|
|
230
230
|
}
|
|
231
|
+
},
|
|
232
|
+
"delegations": {
|
|
233
|
+
"type": "array",
|
|
234
|
+
"description": "Sub-tasks delegated to other roles. The delegating role defines the charter; delegates execute it; the delegating role reviews results.",
|
|
235
|
+
"maxItems": 5,
|
|
236
|
+
"items": {
|
|
237
|
+
"type": "object",
|
|
238
|
+
"required": ["id", "to_role", "charter", "acceptance_contract"],
|
|
239
|
+
"additionalProperties": false,
|
|
240
|
+
"properties": {
|
|
241
|
+
"id": {
|
|
242
|
+
"type": "string",
|
|
243
|
+
"pattern": "^del-\\d{3}$",
|
|
244
|
+
"description": "Delegation ID, e.g. del-001"
|
|
245
|
+
},
|
|
246
|
+
"to_role": {
|
|
247
|
+
"type": "string",
|
|
248
|
+
"pattern": "^[a-z0-9_-]+$",
|
|
249
|
+
"description": "Role that should execute this sub-task. Must be a defined role and routing-legal."
|
|
250
|
+
},
|
|
251
|
+
"charter": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"minLength": 1,
|
|
254
|
+
"description": "Scope of the delegated work."
|
|
255
|
+
},
|
|
256
|
+
"acceptance_contract": {
|
|
257
|
+
"type": "array",
|
|
258
|
+
"items": { "type": "string", "minLength": 1 },
|
|
259
|
+
"minItems": 1,
|
|
260
|
+
"description": "What the delegate must achieve for this delegation to be considered complete."
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
231
264
|
}
|
|
232
265
|
}
|
|
233
266
|
}
|
|
@@ -602,6 +602,54 @@ function validateProtocol(tr, state, config) {
|
|
|
602
602
|
warnings.push('status is "needs_human" but needs_human_reason is empty.');
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
+
// ── Delegation validation ───────────────────────────────────────────────
|
|
606
|
+
if (Array.isArray(tr.delegations) && tr.delegations.length > 0) {
|
|
607
|
+
// Maximum 5 delegations per turn
|
|
608
|
+
if (tr.delegations.length > 5) {
|
|
609
|
+
errors.push(`Maximum 5 delegations per turn (got ${tr.delegations.length}).`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Delegations are mutually exclusive with run_completion_request
|
|
613
|
+
if (tr.run_completion_request) {
|
|
614
|
+
errors.push('delegations are mutually exclusive with run_completion_request — a turn cannot delegate and complete the run.');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// No recursive delegation: if this turn is a delegation review, it cannot delegate further
|
|
618
|
+
const activeTurn = state?.active_turns ? Object.values(state.active_turns)[0] : null;
|
|
619
|
+
if (activeTurn?.delegation_context) {
|
|
620
|
+
errors.push('Delegation review turns cannot contain further delegations.');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const seenIds = new Set();
|
|
624
|
+
for (const del of tr.delegations) {
|
|
625
|
+
// Duplicate IDs
|
|
626
|
+
if (seenIds.has(del.id)) {
|
|
627
|
+
errors.push(`Duplicate delegation id "${del.id}".`);
|
|
628
|
+
}
|
|
629
|
+
seenIds.add(del.id);
|
|
630
|
+
|
|
631
|
+
// Self-delegation
|
|
632
|
+
if (del.to_role === tr.role) {
|
|
633
|
+
errors.push(`Role "${tr.role}" cannot delegate to itself (delegation ${del.id}).`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// to_role must be a defined role
|
|
637
|
+
if (!config.roles?.[del.to_role]) {
|
|
638
|
+
errors.push(`Delegation to_role "${del.to_role}" is not a defined role (delegation ${del.id}).`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// to_role must be routing-legal for current phase
|
|
642
|
+
if (routing && del.to_role) {
|
|
643
|
+
const allowed = routing.allowed_next_roles || [];
|
|
644
|
+
if (allowed.length > 0 && !allowed.includes(del.to_role)) {
|
|
645
|
+
errors.push(
|
|
646
|
+
`Delegation to_role "${del.to_role}" is not in allowed_next_roles for phase "${phase}": [${allowed.join(', ')}] (delegation ${del.id}).`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
605
653
|
return { errors, warnings };
|
|
606
654
|
}
|
|
607
655
|
|