agentxchain 2.87.0 → 2.89.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/dashboard/app.js +2 -0
- package/dashboard/components/delegations.js +237 -0
- package/dashboard/components/timeline.js +23 -0
- package/dashboard/index.html +1 -0
- package/package.json +1 -1
- package/src/lib/dispatch-bundle.js +72 -2
- package/src/lib/export.js +88 -0
- package/src/lib/governed-state.js +161 -12
- package/src/lib/report.js +85 -0
- package/src/lib/role-resolution.js +41 -0
- package/src/lib/schemas/turn-result.schema.json +33 -0
- package/src/lib/token-budget.js +1 -0
- package/src/lib/turn-result-validator.js +48 -0
package/dashboard/app.js
CHANGED
|
@@ -12,6 +12,7 @@ import { render as renderBlocked } from './components/blocked.js';
|
|
|
12
12
|
import { render as renderGate } from './components/gate.js';
|
|
13
13
|
import { render as renderInitiative } from './components/initiative.js';
|
|
14
14
|
import { render as renderCrossRepo } from './components/cross-repo.js';
|
|
15
|
+
import { render as renderDelegations } from './components/delegations.js';
|
|
15
16
|
import { render as renderBlockers } from './components/blockers.js';
|
|
16
17
|
import { render as renderArtifacts } from './components/artifacts.js';
|
|
17
18
|
import { render as renderRunHistory } from './components/run-history.js';
|
|
@@ -20,6 +21,7 @@ import { render as renderCoordinatorTimeouts } from './components/coordinator-ti
|
|
|
20
21
|
|
|
21
22
|
const VIEWS = {
|
|
22
23
|
timeline: { fetch: ['state', 'continuity', 'history', 'audit', 'annotations', 'connectors', 'coordinatorAudit', 'coordinatorAnnotations'], render: renderTimeline },
|
|
24
|
+
delegations: { fetch: ['state', 'history'], render: renderDelegations },
|
|
23
25
|
ledger: { fetch: ['state', 'ledger', 'coordinatorState', 'coordinatorLedger'], render: renderLedger },
|
|
24
26
|
hooks: { fetch: ['audit', 'annotations', 'coordinatorAudit', 'coordinatorAnnotations'], render: renderHooks },
|
|
25
27
|
blocked: { fetch: ['state', 'audit', 'coordinatorState', 'coordinatorAudit'], render: renderBlocked },
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
function esc(str) {
|
|
2
|
+
if (str == null) return '';
|
|
3
|
+
return String(str)
|
|
4
|
+
.replace(/&/g, '&')
|
|
5
|
+
.replace(/</g, '<')
|
|
6
|
+
.replace(/>/g, '>')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/'/g, ''');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function badge(label, color = 'var(--text-dim)') {
|
|
12
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function statusColor(status) {
|
|
16
|
+
const colors = {
|
|
17
|
+
pending: 'var(--text-dim)',
|
|
18
|
+
active: 'var(--yellow)',
|
|
19
|
+
completed: 'var(--green)',
|
|
20
|
+
failed: 'var(--red)',
|
|
21
|
+
review_pending: 'var(--yellow)',
|
|
22
|
+
reviewed: 'var(--accent)',
|
|
23
|
+
unknown: 'var(--text-dim)',
|
|
24
|
+
};
|
|
25
|
+
return colors[status] || 'var(--text-dim)';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function summarizeReview(results) {
|
|
29
|
+
const counts = { completed: 0, failed: 0, other: 0 };
|
|
30
|
+
for (const result of results || []) {
|
|
31
|
+
if (result?.status === 'completed') counts.completed += 1;
|
|
32
|
+
else if (result?.status === 'failed') counts.failed += 1;
|
|
33
|
+
else counts.other += 1;
|
|
34
|
+
}
|
|
35
|
+
return counts;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeDelegationChain(parentEntry, state, history) {
|
|
39
|
+
const issued = Array.isArray(parentEntry?.delegations_issued) ? parentEntry.delegations_issued : [];
|
|
40
|
+
const pendingQueue = Array.isArray(state?.delegation_queue)
|
|
41
|
+
? state.delegation_queue.filter((entry) => entry.parent_turn_id === parentEntry.turn_id)
|
|
42
|
+
: [];
|
|
43
|
+
const pendingReview = state?.pending_delegation_review?.parent_turn_id === parentEntry.turn_id
|
|
44
|
+
? state.pending_delegation_review
|
|
45
|
+
: null;
|
|
46
|
+
const reviewEntry = Array.isArray(history)
|
|
47
|
+
? history.find((entry) => entry?.delegation_review?.parent_turn_id === parentEntry.turn_id)
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
const delegations = issued.map((item) => {
|
|
51
|
+
const queueEntry = pendingQueue.find((entry) => entry.delegation_id === item.id) || null;
|
|
52
|
+
const reviewResult = Array.isArray(pendingReview?.delegation_results)
|
|
53
|
+
? pendingReview.delegation_results.find((entry) => entry.delegation_id === item.id)
|
|
54
|
+
: null;
|
|
55
|
+
const reviewHistoryResult = Array.isArray(reviewEntry?.delegation_review?.results)
|
|
56
|
+
? reviewEntry.delegation_review.results.find((entry) => entry.delegation_id === item.id)
|
|
57
|
+
: null;
|
|
58
|
+
const childTurn = Array.isArray(history)
|
|
59
|
+
? history.find((entry) => entry?.delegation_context?.delegation_id === item.id)
|
|
60
|
+
: null;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
delegation_id: item.id,
|
|
64
|
+
to_role: item.to_role,
|
|
65
|
+
charter: item.charter,
|
|
66
|
+
acceptance_contract: Array.isArray(item.acceptance_contract) ? item.acceptance_contract : [],
|
|
67
|
+
status: queueEntry?.status
|
|
68
|
+
|| reviewResult?.status
|
|
69
|
+
|| reviewHistoryResult?.status
|
|
70
|
+
|| childTurn?.status
|
|
71
|
+
|| 'unknown',
|
|
72
|
+
child_turn_id: queueEntry?.child_turn_id
|
|
73
|
+
|| reviewResult?.child_turn_id
|
|
74
|
+
|| reviewHistoryResult?.child_turn_id
|
|
75
|
+
|| childTurn?.turn_id
|
|
76
|
+
|| null,
|
|
77
|
+
summary: reviewResult?.summary
|
|
78
|
+
|| reviewHistoryResult?.summary
|
|
79
|
+
|| childTurn?.summary
|
|
80
|
+
|| null,
|
|
81
|
+
files_changed: reviewResult?.files_changed
|
|
82
|
+
|| reviewHistoryResult?.files_changed
|
|
83
|
+
|| childTurn?.files_changed
|
|
84
|
+
|| [],
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let chainStatus = 'unknown';
|
|
89
|
+
if (pendingReview) chainStatus = 'review_pending';
|
|
90
|
+
else if (pendingQueue.length > 0) chainStatus = delegations.some((entry) => entry.status === 'active') ? 'active' : 'pending';
|
|
91
|
+
else if (reviewEntry) chainStatus = 'reviewed';
|
|
92
|
+
else if (delegations.length > 0) chainStatus = delegations.every((entry) => entry.status === 'completed' || entry.status === 'failed') ? 'reviewed' : 'unknown';
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
parent_turn_id: parentEntry.turn_id,
|
|
96
|
+
parent_role: parentEntry.role,
|
|
97
|
+
parent_summary: parentEntry.summary || '(no summary)',
|
|
98
|
+
accepted_at: parentEntry.accepted_at || null,
|
|
99
|
+
status: chainStatus,
|
|
100
|
+
delegations,
|
|
101
|
+
pending_review: pendingReview,
|
|
102
|
+
review_entry: reviewEntry,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderAcceptanceContract(items) {
|
|
107
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
108
|
+
return '<span class="turn-detail">No acceptance contract recorded.</span>';
|
|
109
|
+
}
|
|
110
|
+
return `<ul>${items.map((item) => `<li>${esc(item)}</li>`).join('')}</ul>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderChainCard(chain) {
|
|
114
|
+
const reviewResults = chain.pending_review?.delegation_results
|
|
115
|
+
|| chain.review_entry?.delegation_review?.results
|
|
116
|
+
|| [];
|
|
117
|
+
const reviewCounts = summarizeReview(reviewResults);
|
|
118
|
+
|
|
119
|
+
let html = `<div class="turn-card">
|
|
120
|
+
<div class="turn-header">
|
|
121
|
+
${badge(chain.parent_role || 'unknown', '#fb923c')}
|
|
122
|
+
<span class="mono">${esc(chain.parent_turn_id)}</span>
|
|
123
|
+
${badge(chain.status.replace(/_/g, ' '), statusColor(chain.status))}
|
|
124
|
+
</div>
|
|
125
|
+
<div class="turn-summary">${esc(chain.parent_summary)}</div>`;
|
|
126
|
+
|
|
127
|
+
if (chain.accepted_at) {
|
|
128
|
+
html += `<div class="turn-detail"><span class="detail-label">Accepted:</span> ${esc(chain.accepted_at)}</div>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
html += `<div class="turn-detail"><span class="detail-label">Delegations:</span> ${esc(chain.delegations.length)}</div>`;
|
|
132
|
+
|
|
133
|
+
if (chain.delegations.length > 0) {
|
|
134
|
+
html += `<div class="turn-list">`;
|
|
135
|
+
for (const delegation of chain.delegations) {
|
|
136
|
+
html += `<div class="turn-card">
|
|
137
|
+
<div class="turn-header">
|
|
138
|
+
<span class="mono">${esc(delegation.delegation_id)}</span>
|
|
139
|
+
${badge(delegation.to_role || 'unknown', '#38bdf8')}
|
|
140
|
+
${badge(delegation.status, statusColor(delegation.status))}
|
|
141
|
+
</div>
|
|
142
|
+
<div class="turn-detail"><span class="detail-label">Charter:</span> ${esc(delegation.charter || '(none)')}</div>
|
|
143
|
+
<div class="turn-detail"><span class="detail-label">Acceptance Contract:</span>${renderAcceptanceContract(delegation.acceptance_contract)}</div>`;
|
|
144
|
+
if (delegation.child_turn_id) {
|
|
145
|
+
html += `<div class="turn-detail"><span class="detail-label">Child Turn:</span> <span class="mono">${esc(delegation.child_turn_id)}</span></div>`;
|
|
146
|
+
}
|
|
147
|
+
if (delegation.summary) {
|
|
148
|
+
html += `<div class="turn-detail"><span class="detail-label">Outcome:</span> ${esc(delegation.summary)}</div>`;
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(delegation.files_changed) && delegation.files_changed.length > 0) {
|
|
151
|
+
html += `<div class="turn-detail"><span class="detail-label">Files:</span> <span class="mono">${delegation.files_changed.map((item) => esc(item)).join(', ')}</span></div>`;
|
|
152
|
+
}
|
|
153
|
+
html += `</div>`;
|
|
154
|
+
}
|
|
155
|
+
html += `</div>`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (chain.pending_review) {
|
|
159
|
+
html += `<div class="turn-detail"><span class="detail-label">Review Pending:</span> ${esc(chain.pending_review.parent_role)} is reviewing ${reviewResults.length} delegated result${reviewResults.length === 1 ? '' : 's'}.</div>`;
|
|
160
|
+
} else if (chain.review_entry) {
|
|
161
|
+
html += `<div class="turn-detail"><span class="detail-label">Review Turn:</span> <span class="mono">${esc(chain.review_entry.turn_id)}</span></div>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (reviewResults.length > 0) {
|
|
165
|
+
html += `<div class="turn-detail"><span class="detail-label">Review Summary:</span> ${reviewCounts.completed} completed, ${reviewCounts.failed} failed</div>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
html += `</div>`;
|
|
169
|
+
return html;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function render({ state, history }) {
|
|
173
|
+
if (!state) {
|
|
174
|
+
return `<div class="placeholder"><h2>No Run</h2><p>No governed run found. Start one with <code class="mono">agentxchain init --governed</code></p></div>`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const historyEntries = Array.isArray(history) ? history : [];
|
|
178
|
+
const parentTurns = historyEntries
|
|
179
|
+
.filter((entry) => Array.isArray(entry?.delegations_issued) && entry.delegations_issued.length > 0)
|
|
180
|
+
.reverse();
|
|
181
|
+
const chains = parentTurns.map((entry) => normalizeDelegationChain(entry, state, historyEntries));
|
|
182
|
+
const queue = Array.isArray(state.delegation_queue) ? state.delegation_queue : [];
|
|
183
|
+
const pendingReview = state.pending_delegation_review || null;
|
|
184
|
+
|
|
185
|
+
if (chains.length === 0 && queue.length === 0 && !pendingReview) {
|
|
186
|
+
return `<div class="placeholder"><h2>No Delegations</h2><p>This run has no delegation chains yet. Delegation state will appear here once a turn emits a <code class="mono">delegations</code> array.</p></div>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let html = `<div class="delegations-view">`;
|
|
190
|
+
|
|
191
|
+
html += `<div class="run-header">
|
|
192
|
+
<div class="run-meta">
|
|
193
|
+
<span class="mono run-id">${esc(state.run_id || 'unknown')}</span>
|
|
194
|
+
${badge(state.status || 'unknown', statusColor(state.status || 'unknown'))}
|
|
195
|
+
<span class="phase-label">Phase: <strong>${esc(state.phase || 'unknown')}</strong></span>
|
|
196
|
+
<span class="turn-count">${chains.length} chain${chains.length === 1 ? '' : 's'} recorded</span>
|
|
197
|
+
</div>
|
|
198
|
+
</div>`;
|
|
199
|
+
|
|
200
|
+
if (queue.length > 0 || pendingReview) {
|
|
201
|
+
html += `<div class="section"><h3>Live Delegation State</h3><div class="turn-list">`;
|
|
202
|
+
if (queue.length > 0) {
|
|
203
|
+
html += `<div class="turn-card">
|
|
204
|
+
<div class="turn-header">
|
|
205
|
+
<span class="mono">delegation_queue</span>
|
|
206
|
+
${badge(`${queue.length} item${queue.length === 1 ? '' : 's'}`, statusColor('pending'))}
|
|
207
|
+
</div>`;
|
|
208
|
+
for (const entry of queue) {
|
|
209
|
+
html += `<div class="turn-detail"><span class="detail-label">${esc(entry.delegation_id)}:</span> ${esc(entry.parent_role || 'unknown')} → ${esc(entry.to_role || 'unknown')} (${esc(entry.status || 'unknown')})</div>`;
|
|
210
|
+
}
|
|
211
|
+
html += `</div>`;
|
|
212
|
+
}
|
|
213
|
+
if (pendingReview) {
|
|
214
|
+
const counts = summarizeReview(pendingReview.delegation_results);
|
|
215
|
+
html += `<div class="turn-card">
|
|
216
|
+
<div class="turn-header">
|
|
217
|
+
<span class="mono">${esc(pendingReview.parent_turn_id)}</span>
|
|
218
|
+
${badge('review pending', statusColor('review_pending'))}
|
|
219
|
+
</div>
|
|
220
|
+
<div class="turn-detail"><span class="detail-label">Parent Role:</span> ${esc(pendingReview.parent_role || 'unknown')}</div>
|
|
221
|
+
<div class="turn-detail"><span class="detail-label">Results:</span> ${counts.completed} completed, ${counts.failed} failed</div>
|
|
222
|
+
</div>`;
|
|
223
|
+
}
|
|
224
|
+
html += `</div></div>`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
html += `<div class="section"><h3>Delegation Chains</h3>`;
|
|
228
|
+
if (chains.length === 0) {
|
|
229
|
+
html += `<div class="placeholder compact"><p>No completed delegation chains recorded yet.</p></div>`;
|
|
230
|
+
} else {
|
|
231
|
+
html += `<div class="turn-list">${chains.map((chain) => renderChainCard(chain)).join('')}</div>`;
|
|
232
|
+
}
|
|
233
|
+
html += `</div>`;
|
|
234
|
+
|
|
235
|
+
html += `</div>`;
|
|
236
|
+
return html;
|
|
237
|
+
}
|
|
@@ -187,6 +187,23 @@ function renderTurnDetailPanel(turnId, annotations, audit, coordinatorAnnotation
|
|
|
187
187
|
return html;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
function renderDelegationIssued(entry) {
|
|
191
|
+
const issued = Array.isArray(entry?.delegations_issued) ? entry.delegations_issued : [];
|
|
192
|
+
if (issued.length === 0) return '';
|
|
193
|
+
return `<div class="turn-detail"><span class="detail-label">Delegated:</span> ${issued.map((item) => `${esc(item.id)} → ${esc(item.to_role)}`).join(', ')}</div>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderDelegationContext(context) {
|
|
197
|
+
if (!context) return '';
|
|
198
|
+
return `<div class="turn-detail"><span class="detail-label">Delegation:</span> <span class="mono">${esc(context.delegation_id)}</span> from ${esc(context.parent_role || 'unknown')} — ${esc(context.charter || '(no charter)')}</div>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderDelegationReview(review) {
|
|
202
|
+
if (!review) return '';
|
|
203
|
+
const resultCount = Array.isArray(review.results) ? review.results.length : 0;
|
|
204
|
+
return `<div class="turn-detail"><span class="detail-label">Delegation Review:</span> <span class="mono">${esc(review.parent_turn_id || 'unknown')}</span> with ${esc(resultCount)} result${resultCount === 1 ? '' : 's'}</div>`;
|
|
205
|
+
}
|
|
206
|
+
|
|
190
207
|
function renderContinuityPanel(continuity) {
|
|
191
208
|
if (!continuity) return '';
|
|
192
209
|
|
|
@@ -323,6 +340,8 @@ export function render({ state, continuity, history, annotations, audit, connect
|
|
|
323
340
|
<span class="turn-status">${esc(turn.status || 'assigned')}</span>
|
|
324
341
|
${elapsedStr ? `<span class="turn-timing">Elapsed: ${esc(elapsedStr)}</span>` : ''}
|
|
325
342
|
</div>
|
|
343
|
+
${renderDelegationContext(turn.delegation_context)}
|
|
344
|
+
${renderDelegationReview(turn.delegation_review)}
|
|
326
345
|
</div>`;
|
|
327
346
|
}
|
|
328
347
|
html += `</div></div>`;
|
|
@@ -355,6 +374,10 @@ export function render({ state, continuity, history, annotations, audit, connect
|
|
|
355
374
|
html += `<div class="turn-summary">${esc(entry.summary)}</div>`;
|
|
356
375
|
}
|
|
357
376
|
|
|
377
|
+
html += renderDelegationIssued(entry);
|
|
378
|
+
html += renderDelegationContext(entry.delegation_context);
|
|
379
|
+
html += renderDelegationReview(entry.delegation_review);
|
|
380
|
+
|
|
358
381
|
if (files.length > 0) {
|
|
359
382
|
html += `<div class="turn-detail"><span class="detail-label">Files:</span> <span class="mono">${files.map(f => esc(f)).join(', ')}</span></div>`;
|
|
360
383
|
}
|
package/dashboard/index.html
CHANGED
|
@@ -377,6 +377,7 @@
|
|
|
377
377
|
<a href="#initiative">Initiative</a>
|
|
378
378
|
<a href="#cross-repo">Cross-Repo</a>
|
|
379
379
|
<a href="#timeline" class="active">Timeline</a>
|
|
380
|
+
<a href="#delegations">Delegations</a>
|
|
380
381
|
<a href="#ledger">Decisions</a>
|
|
381
382
|
<a href="#hooks">Hooks</a>
|
|
382
383
|
<a href="#blocked">Blocked</a>
|
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
|
}
|
|
@@ -238,6 +244,8 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
238
244
|
lines.push('- Your artifact type should be `patch`.');
|
|
239
245
|
if (runtimeType === 'api_proxy' || runtimeType === 'remote_agent') {
|
|
240
246
|
lines.push('- **This runtime cannot write repo files directly.** When doing work, you MUST return proposed changes as structured JSON.');
|
|
247
|
+
lines.push('- For non-completion turns, set `artifact.type` to `patch`. Do NOT use `workspace` or `commit`.');
|
|
248
|
+
lines.push('- Use `artifact.type: "review"` only for completion-only final-phase turns that propose no file changes.');
|
|
241
249
|
lines.push('- Include a `proposed_changes` array in your turn result with each file change (omit or set to `[]` on completion-only turns):');
|
|
242
250
|
lines.push(' ```json');
|
|
243
251
|
lines.push(' "proposed_changes": [');
|
|
@@ -390,6 +398,10 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
390
398
|
if (role.write_authority === 'review_only') {
|
|
391
399
|
lines.push('- `objections`: **must be non-empty** (challenge requirement for review_only roles)');
|
|
392
400
|
}
|
|
401
|
+
if (role.write_authority === 'proposed' && (runtimeType === 'api_proxy' || runtimeType === 'remote_agent')) {
|
|
402
|
+
lines.push('- For `proposed` `api_proxy`/`remote_agent` turns with file changes, `artifact.type` must be `patch` and `proposed_changes` must be non-empty.');
|
|
403
|
+
lines.push('- Do NOT use `artifact.type: "workspace"` or `artifact.type: "commit"` on a `proposed` `api_proxy`/`remote_agent` turn.');
|
|
404
|
+
}
|
|
393
405
|
// List valid phase names explicitly to prevent gate-name confusion
|
|
394
406
|
const phaseNames = config.routing ? Object.keys(config.routing) : [];
|
|
395
407
|
if (phaseNames.length > 0) {
|
|
@@ -547,6 +559,52 @@ function renderContext(state, config, root, turn, role) {
|
|
|
547
559
|
lines.push('');
|
|
548
560
|
}
|
|
549
561
|
|
|
562
|
+
// Delegation context (when this turn is a delegated sub-task)
|
|
563
|
+
if (turn.delegation_context) {
|
|
564
|
+
const dc = turn.delegation_context;
|
|
565
|
+
lines.push('## Delegation Context');
|
|
566
|
+
lines.push('');
|
|
567
|
+
lines.push('You are executing a delegated sub-task.');
|
|
568
|
+
lines.push('');
|
|
569
|
+
lines.push(`- **Delegated by:** ${dc.parent_role} (turn ${dc.parent_turn_id})`);
|
|
570
|
+
lines.push(`- **Charter:** ${dc.charter}`);
|
|
571
|
+
if (Array.isArray(dc.acceptance_contract) && dc.acceptance_contract.length > 0) {
|
|
572
|
+
lines.push('- **Acceptance contract:**');
|
|
573
|
+
for (const req of dc.acceptance_contract) {
|
|
574
|
+
lines.push(` - ${req}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
lines.push('');
|
|
578
|
+
lines.push('Focus exclusively on the charter above. Do not expand scope beyond the delegation.');
|
|
579
|
+
lines.push('');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Delegation review context (when this turn reviews completed delegations)
|
|
583
|
+
if (turn.delegation_review) {
|
|
584
|
+
const dr = turn.delegation_review;
|
|
585
|
+
lines.push('## Delegation Review');
|
|
586
|
+
lines.push('');
|
|
587
|
+
lines.push('Your delegated sub-tasks have been completed. Review the results below.');
|
|
588
|
+
lines.push('');
|
|
589
|
+
for (const result of dr.results || []) {
|
|
590
|
+
lines.push(`### ${result.delegation_id} → ${result.to_role}`);
|
|
591
|
+
lines.push('');
|
|
592
|
+
lines.push(`- **Charter:** ${result.charter}`);
|
|
593
|
+
lines.push(`- **Status:** ${result.status}`);
|
|
594
|
+
lines.push(`- **Summary:** ${result.summary}`);
|
|
595
|
+
if (result.files_changed?.length > 0) {
|
|
596
|
+
lines.push(`- **Files changed:** ${result.files_changed.join(', ')}`);
|
|
597
|
+
}
|
|
598
|
+
if (result.verification?.status) {
|
|
599
|
+
lines.push(`- **Verification:** ${result.verification.status}`);
|
|
600
|
+
}
|
|
601
|
+
lines.push('');
|
|
602
|
+
}
|
|
603
|
+
lines.push('Evaluate whether each delegation met its acceptance contract.');
|
|
604
|
+
lines.push('Your turn result should assess the delegation outcomes and decide next steps.');
|
|
605
|
+
lines.push('');
|
|
606
|
+
}
|
|
607
|
+
|
|
550
608
|
// Inherited context from parent run (when --inherit-context was used)
|
|
551
609
|
if (state.inherited_context) {
|
|
552
610
|
// First turn gets the full rendering; subsequent turns get compact
|
|
@@ -1214,6 +1272,7 @@ function readLastHistoryEntry(root, warnings = []) {
|
|
|
1214
1272
|
|
|
1215
1273
|
function buildTurnResultTemplate(state, turn, roleId, role) {
|
|
1216
1274
|
const isReviewOnly = role.write_authority === 'review_only';
|
|
1275
|
+
const isProposed = role.write_authority === 'proposed';
|
|
1217
1276
|
return {
|
|
1218
1277
|
schema_version: '1.0',
|
|
1219
1278
|
run_id: state.run_id,
|
|
@@ -1254,13 +1313,24 @@ function buildTurnResultTemplate(state, turn, roleId, role) {
|
|
|
1254
1313
|
: [{ command: '<exact command that was run>', exit_code: 0 }],
|
|
1255
1314
|
},
|
|
1256
1315
|
artifact: {
|
|
1257
|
-
type: isReviewOnly ? 'review' : 'workspace',
|
|
1258
|
-
ref: isReviewOnly ? null : 'git:dirty',
|
|
1316
|
+
type: isReviewOnly ? 'review' : (isProposed ? 'patch' : 'workspace'),
|
|
1317
|
+
ref: isReviewOnly ? null : (isProposed ? null : 'git:dirty'),
|
|
1259
1318
|
},
|
|
1260
1319
|
proposed_next_role: '<role_id that should act next>',
|
|
1261
1320
|
phase_transition_request: null,
|
|
1262
1321
|
run_completion_request: null,
|
|
1263
1322
|
needs_human_reason: null,
|
|
1323
|
+
...(isProposed
|
|
1324
|
+
? {
|
|
1325
|
+
proposed_changes: [
|
|
1326
|
+
{
|
|
1327
|
+
path: '<path/to/modified/file>',
|
|
1328
|
+
action: 'create',
|
|
1329
|
+
content: '<full file content>',
|
|
1330
|
+
},
|
|
1331
|
+
],
|
|
1332
|
+
}
|
|
1333
|
+
: {}),
|
|
1264
1334
|
cost: {
|
|
1265
1335
|
input_tokens: 0,
|
|
1266
1336
|
output_tokens: 0,
|
package/src/lib/export.js
CHANGED
|
@@ -166,6 +166,93 @@ function countDirectoryFiles(files, prefix) {
|
|
|
166
166
|
return Object.keys(files).filter((path) => path.startsWith(`${prefix}/`)).length;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
export function buildDelegationSummary(files) {
|
|
170
|
+
const historyData = files['.agentxchain/history.jsonl']?.data;
|
|
171
|
+
if (!Array.isArray(historyData)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Index history entries by delegation-related fields
|
|
176
|
+
const parentTurns = new Map(); // parent_turn_id -> { role, delegations_issued }
|
|
177
|
+
const childTurns = new Map(); // delegation_id -> { child entry }
|
|
178
|
+
const reviewTurns = new Map(); // parent_turn_id -> { review entry }
|
|
179
|
+
|
|
180
|
+
for (const entry of historyData) {
|
|
181
|
+
if (entry.delegations_issued && Array.isArray(entry.delegations_issued)) {
|
|
182
|
+
parentTurns.set(entry.turn_id, {
|
|
183
|
+
role: entry.role,
|
|
184
|
+
delegations_issued: entry.delegations_issued,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (entry.delegation_context) {
|
|
188
|
+
childTurns.set(entry.delegation_context.delegation_id, {
|
|
189
|
+
turn_id: entry.turn_id,
|
|
190
|
+
status: entry.status || 'completed',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (entry.delegation_review) {
|
|
194
|
+
reviewTurns.set(entry.delegation_review.parent_turn_id, {
|
|
195
|
+
turn_id: entry.turn_id,
|
|
196
|
+
results: entry.delegation_review.results || [],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let totalDelegationsIssued = 0;
|
|
202
|
+
const delegationChains = [];
|
|
203
|
+
|
|
204
|
+
for (const [parentTurnId, parent] of parentTurns) {
|
|
205
|
+
totalDelegationsIssued += parent.delegations_issued.length;
|
|
206
|
+
|
|
207
|
+
const review = reviewTurns.get(parentTurnId);
|
|
208
|
+
const reviewResultsByDelegation = new Map();
|
|
209
|
+
if (review) {
|
|
210
|
+
for (const r of review.results) {
|
|
211
|
+
if (r.delegation_id) {
|
|
212
|
+
reviewResultsByDelegation.set(r.delegation_id, r);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const delegations = parent.delegations_issued.map((del) => {
|
|
218
|
+
const child = childTurns.get(del.id);
|
|
219
|
+
const reviewResult = reviewResultsByDelegation.get(del.id);
|
|
220
|
+
return {
|
|
221
|
+
delegation_id: del.id,
|
|
222
|
+
to_role: del.to_role,
|
|
223
|
+
charter: del.charter,
|
|
224
|
+
status: reviewResult?.status || child?.status || 'pending',
|
|
225
|
+
child_turn_id: child?.turn_id || null,
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
let outcome;
|
|
230
|
+
if (!review) {
|
|
231
|
+
outcome = 'pending';
|
|
232
|
+
} else {
|
|
233
|
+
const statuses = delegations.map((d) => d.status);
|
|
234
|
+
const allCompleted = statuses.every((s) => s === 'completed');
|
|
235
|
+
const allFailed = statuses.every((s) => s === 'failed');
|
|
236
|
+
if (allCompleted) outcome = 'completed';
|
|
237
|
+
else if (allFailed) outcome = 'failed';
|
|
238
|
+
else outcome = 'mixed';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
delegationChains.push({
|
|
242
|
+
parent_turn_id: parentTurnId,
|
|
243
|
+
parent_role: parent.role,
|
|
244
|
+
delegations,
|
|
245
|
+
review_turn_id: review?.turn_id || null,
|
|
246
|
+
outcome,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
total_delegations_issued: totalDelegationsIssued,
|
|
252
|
+
delegation_chains: delegationChains,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
169
256
|
function isGitRepo(root) {
|
|
170
257
|
try {
|
|
171
258
|
execSync('git rev-parse --is-inside-work-tree', {
|
|
@@ -317,6 +404,7 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
317
404
|
staging_artifact_files: countDirectoryFiles(files, '.agentxchain/staging'),
|
|
318
405
|
intake_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/intake/')),
|
|
319
406
|
coordinator_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/multirepo/')),
|
|
407
|
+
delegation_summary: buildDelegationSummary(files),
|
|
320
408
|
},
|
|
321
409
|
workspace: buildRunWorkspaceMetadata(root),
|
|
322
410
|
files,
|
|
@@ -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
|
|
|
@@ -2599,6 +2628,35 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2599
2628
|
...(currentTurn.started_at ? { started_at: currentTurn.started_at } : {}),
|
|
2600
2629
|
accepted_at: now,
|
|
2601
2630
|
...(currentTurn.started_at ? { duration_ms: Math.max(0, new Date(now).getTime() - new Date(currentTurn.started_at).getTime()) } : {}),
|
|
2631
|
+
...(Array.isArray(turnResult.delegations) && turnResult.delegations.length > 0
|
|
2632
|
+
? {
|
|
2633
|
+
delegations_issued: turnResult.delegations.map((delegation) => ({
|
|
2634
|
+
id: delegation.id,
|
|
2635
|
+
to_role: delegation.to_role,
|
|
2636
|
+
charter: delegation.charter,
|
|
2637
|
+
acceptance_contract: delegation.acceptance_contract,
|
|
2638
|
+
})),
|
|
2639
|
+
}
|
|
2640
|
+
: {}),
|
|
2641
|
+
...(currentTurn.delegation_context
|
|
2642
|
+
? {
|
|
2643
|
+
delegation_context: {
|
|
2644
|
+
delegation_id: currentTurn.delegation_context.delegation_id,
|
|
2645
|
+
parent_turn_id: currentTurn.delegation_context.parent_turn_id,
|
|
2646
|
+
parent_role: currentTurn.delegation_context.parent_role,
|
|
2647
|
+
charter: currentTurn.delegation_context.charter,
|
|
2648
|
+
acceptance_contract: currentTurn.delegation_context.acceptance_contract,
|
|
2649
|
+
},
|
|
2650
|
+
}
|
|
2651
|
+
: {}),
|
|
2652
|
+
...(currentTurn.delegation_review
|
|
2653
|
+
? {
|
|
2654
|
+
delegation_review: {
|
|
2655
|
+
parent_turn_id: currentTurn.delegation_review.parent_turn_id,
|
|
2656
|
+
results: currentTurn.delegation_review.results,
|
|
2657
|
+
},
|
|
2658
|
+
}
|
|
2659
|
+
: {}),
|
|
2602
2660
|
};
|
|
2603
2661
|
const nextHistoryEntries = [...historyEntries, historyEntry];
|
|
2604
2662
|
// Build ledger entries for the journal
|
|
@@ -2649,6 +2707,97 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2649
2707
|
},
|
|
2650
2708
|
};
|
|
2651
2709
|
|
|
2710
|
+
// ── Delegation queue management ──────────────────────────────────────────
|
|
2711
|
+
// Initialize delegation queue if not present
|
|
2712
|
+
if (!updatedState.delegation_queue) {
|
|
2713
|
+
updatedState.delegation_queue = [];
|
|
2714
|
+
}
|
|
2715
|
+
if (!updatedState.pending_delegation_review) {
|
|
2716
|
+
updatedState.pending_delegation_review = null;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
// If this turn has delegations, enqueue them
|
|
2720
|
+
if (Array.isArray(turnResult.delegations) && turnResult.delegations.length > 0) {
|
|
2721
|
+
for (const del of turnResult.delegations) {
|
|
2722
|
+
updatedState.delegation_queue.push({
|
|
2723
|
+
delegation_id: del.id,
|
|
2724
|
+
parent_turn_id: turnResult.turn_id,
|
|
2725
|
+
parent_role: turnResult.role,
|
|
2726
|
+
to_role: del.to_role,
|
|
2727
|
+
charter: del.charter,
|
|
2728
|
+
acceptance_contract: del.acceptance_contract,
|
|
2729
|
+
status: 'pending',
|
|
2730
|
+
child_turn_id: null,
|
|
2731
|
+
created_at: now,
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
// Override next_recommended_role to first pending delegation
|
|
2735
|
+
updatedState.next_recommended_role = turnResult.delegations[0].to_role;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
// If this turn was a delegated sub-task, update the delegation queue entry
|
|
2739
|
+
if (currentTurn.delegation_context) {
|
|
2740
|
+
const delId = currentTurn.delegation_context.delegation_id;
|
|
2741
|
+
const queueEntry = updatedState.delegation_queue.find(d => d.delegation_id === delId);
|
|
2742
|
+
if (queueEntry) {
|
|
2743
|
+
queueEntry.status = turnResult.status === 'completed' ? 'completed' : 'failed';
|
|
2744
|
+
queueEntry.child_turn_id = currentTurn.turn_id;
|
|
2745
|
+
|
|
2746
|
+
// Check if all delegations from the same parent are now complete
|
|
2747
|
+
const parentTurnId = queueEntry.parent_turn_id;
|
|
2748
|
+
const parentDelegations = updatedState.delegation_queue.filter(d => d.parent_turn_id === parentTurnId);
|
|
2749
|
+
const allDone = parentDelegations.every(d => d.status === 'completed' || d.status === 'failed');
|
|
2750
|
+
|
|
2751
|
+
if (allDone) {
|
|
2752
|
+
// Build delegation review context
|
|
2753
|
+
const delegationResults = parentDelegations.map(d => {
|
|
2754
|
+
const childHistory = nextHistoryEntries.find(h => h.turn_id === d.child_turn_id);
|
|
2755
|
+
return {
|
|
2756
|
+
delegation_id: d.delegation_id,
|
|
2757
|
+
child_turn_id: d.child_turn_id,
|
|
2758
|
+
to_role: d.to_role,
|
|
2759
|
+
charter: d.charter,
|
|
2760
|
+
acceptance_contract: d.acceptance_contract,
|
|
2761
|
+
summary: childHistory?.summary || '(no summary)',
|
|
2762
|
+
status: d.status,
|
|
2763
|
+
files_changed: childHistory?.files_changed || [],
|
|
2764
|
+
verification: childHistory?.verification || { status: 'skipped' },
|
|
2765
|
+
};
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
updatedState.pending_delegation_review = {
|
|
2769
|
+
parent_turn_id: parentTurnId,
|
|
2770
|
+
parent_role: queueEntry.parent_role,
|
|
2771
|
+
delegation_results: delegationResults,
|
|
2772
|
+
};
|
|
2773
|
+
|
|
2774
|
+
// Recommend the parent role for the review turn
|
|
2775
|
+
updatedState.next_recommended_role = queueEntry.parent_role;
|
|
2776
|
+
|
|
2777
|
+
// Clear completed delegation queue entries for this parent
|
|
2778
|
+
updatedState.delegation_queue = updatedState.delegation_queue.filter(
|
|
2779
|
+
d => d.parent_turn_id !== parentTurnId
|
|
2780
|
+
);
|
|
2781
|
+
} else {
|
|
2782
|
+
// More pending delegations — route to the next one
|
|
2783
|
+
const nextPending = updatedState.delegation_queue.find(
|
|
2784
|
+
d => d.parent_turn_id === parentTurnId && d.status === 'pending'
|
|
2785
|
+
);
|
|
2786
|
+
if (nextPending) {
|
|
2787
|
+
updatedState.next_recommended_role = nextPending.to_role;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
// If a delegation review was pending and this turn was the review, clear it
|
|
2794
|
+
if (updatedState.pending_delegation_review &&
|
|
2795
|
+
turnResult.role === updatedState.pending_delegation_review.parent_role &&
|
|
2796
|
+
!currentTurn.delegation_context) {
|
|
2797
|
+
// The parent role just completed their review turn
|
|
2798
|
+
updatedState.pending_delegation_review = null;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2652
2801
|
if (updatedState.status === 'blocked' && !hasBlockingActiveTurn(remainingTurns)) {
|
|
2653
2802
|
updatedState.status = 'active';
|
|
2654
2803
|
updatedState.blocked_on = null;
|
package/src/lib/report.js
CHANGED
|
@@ -1,8 +1,63 @@
|
|
|
1
1
|
import { verifyExportArtifact } from './export-verifier.js';
|
|
2
|
+
import { buildDelegationSummary } from './export.js';
|
|
2
3
|
import { normalizeRunProvenance, summarizeRunProvenance } from './run-provenance.js';
|
|
3
4
|
|
|
4
5
|
export const GOVERNANCE_REPORT_VERSION = '0.1';
|
|
5
6
|
|
|
7
|
+
const VALID_DELEGATION_OUTCOMES = new Set(['completed', 'failed', 'mixed', 'pending']);
|
|
8
|
+
|
|
9
|
+
function normalizeDelegationSummary(summary) {
|
|
10
|
+
if (!summary || typeof summary !== 'object' || Array.isArray(summary)) return null;
|
|
11
|
+
if (!Number.isInteger(summary.total_delegations_issued) || summary.total_delegations_issued < 0) return null;
|
|
12
|
+
if (!Array.isArray(summary.delegation_chains)) return null;
|
|
13
|
+
|
|
14
|
+
const chains = [];
|
|
15
|
+
for (const chain of summary.delegation_chains) {
|
|
16
|
+
if (!chain || typeof chain !== 'object' || Array.isArray(chain)) return null;
|
|
17
|
+
if (typeof chain.parent_turn_id !== 'string' || chain.parent_turn_id.length === 0) return null;
|
|
18
|
+
if (typeof chain.parent_role !== 'string' || chain.parent_role.length === 0) return null;
|
|
19
|
+
if (!Array.isArray(chain.delegations)) return null;
|
|
20
|
+
if (chain.review_turn_id !== null && (typeof chain.review_turn_id !== 'string' || chain.review_turn_id.length === 0)) return null;
|
|
21
|
+
if (!VALID_DELEGATION_OUTCOMES.has(chain.outcome)) return null;
|
|
22
|
+
|
|
23
|
+
const delegations = [];
|
|
24
|
+
for (const delegation of chain.delegations) {
|
|
25
|
+
if (!delegation || typeof delegation !== 'object' || Array.isArray(delegation)) return null;
|
|
26
|
+
if (typeof delegation.delegation_id !== 'string' || delegation.delegation_id.length === 0) return null;
|
|
27
|
+
if (typeof delegation.to_role !== 'string' || delegation.to_role.length === 0) return null;
|
|
28
|
+
if (typeof delegation.charter !== 'string') return null;
|
|
29
|
+
if (!['completed', 'failed', 'pending'].includes(delegation.status)) return null;
|
|
30
|
+
if (delegation.child_turn_id !== null && (typeof delegation.child_turn_id !== 'string' || delegation.child_turn_id.length === 0)) return null;
|
|
31
|
+
delegations.push({
|
|
32
|
+
delegation_id: delegation.delegation_id,
|
|
33
|
+
to_role: delegation.to_role,
|
|
34
|
+
charter: delegation.charter,
|
|
35
|
+
status: delegation.status,
|
|
36
|
+
child_turn_id: delegation.child_turn_id,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
chains.push({
|
|
41
|
+
parent_turn_id: chain.parent_turn_id,
|
|
42
|
+
parent_role: chain.parent_role,
|
|
43
|
+
delegations,
|
|
44
|
+
review_turn_id: chain.review_turn_id,
|
|
45
|
+
outcome: chain.outcome,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
total_delegations_issued: summary.total_delegations_issued,
|
|
51
|
+
delegation_chains: chains,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractDelegationSummary(artifact) {
|
|
56
|
+
const fromSummary = normalizeDelegationSummary(artifact.summary?.delegation_summary);
|
|
57
|
+
if (fromSummary) return fromSummary;
|
|
58
|
+
return normalizeDelegationSummary(buildDelegationSummary(artifact.files || {}));
|
|
59
|
+
}
|
|
60
|
+
|
|
6
61
|
function yesNo(value) {
|
|
7
62
|
return value ? 'yes' : 'no';
|
|
8
63
|
}
|
|
@@ -846,6 +901,7 @@ function buildRunSubject(artifact) {
|
|
|
846
901
|
const recoverySummary = extractRecoverySummary(artifact);
|
|
847
902
|
const continuity = extractContinuityMetadata(artifact);
|
|
848
903
|
const governanceEvents = extractGovernanceEventDigest(artifact);
|
|
904
|
+
const delegationSummary = extractDelegationSummary(artifact);
|
|
849
905
|
|
|
850
906
|
return {
|
|
851
907
|
kind: 'governed_run',
|
|
@@ -881,6 +937,7 @@ function buildRunSubject(artifact) {
|
|
|
881
937
|
governance_events: governanceEvents,
|
|
882
938
|
gate_failures: gateFailures,
|
|
883
939
|
timeout_events: timeoutEvents,
|
|
940
|
+
delegation_summary: delegationSummary,
|
|
884
941
|
hook_summary: hookSummary,
|
|
885
942
|
gate_summary: gateSummary,
|
|
886
943
|
intake_links: intakeLinks,
|
|
@@ -1174,6 +1231,17 @@ export function formatGovernanceReportText(report) {
|
|
|
1174
1231
|
}
|
|
1175
1232
|
}
|
|
1176
1233
|
|
|
1234
|
+
if (run.delegation_summary?.delegation_chains?.length > 0) {
|
|
1235
|
+
lines.push('', 'Delegation Summary:');
|
|
1236
|
+
lines.push(` Total delegations issued: ${run.delegation_summary.total_delegations_issued}`);
|
|
1237
|
+
for (const chain of run.delegation_summary.delegation_chains) {
|
|
1238
|
+
lines.push(` - ${chain.parent_role} (${chain.parent_turn_id}) | outcome: ${chain.outcome} | review: ${chain.review_turn_id || 'pending'}`);
|
|
1239
|
+
for (const delegation of chain.delegations) {
|
|
1240
|
+
lines.push(` ${delegation.delegation_id} -> ${delegation.to_role} | ${delegation.status} | child: ${delegation.child_turn_id || 'pending'} | ${delegation.charter}`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1177
1245
|
if (run.turns && run.turns.length > 0) {
|
|
1178
1246
|
lines.push('', 'Turn Timeline:');
|
|
1179
1247
|
for (let i = 0; i < run.turns.length; i++) {
|
|
@@ -1628,6 +1696,23 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1628
1696
|
}
|
|
1629
1697
|
}
|
|
1630
1698
|
|
|
1699
|
+
if (run.delegation_summary?.delegation_chains?.length > 0) {
|
|
1700
|
+
lines.push('', '## Delegation Summary', '');
|
|
1701
|
+
lines.push(`- Total delegations issued: ${run.delegation_summary.total_delegations_issued}`, '');
|
|
1702
|
+
lines.push('| Parent Role | Parent Turn | Outcome | Review Turn | Delegation | Child Turn | Status | Charter |', '|-------------|-------------|---------|-------------|------------|------------|--------|---------|');
|
|
1703
|
+
for (const chain of run.delegation_summary.delegation_chains) {
|
|
1704
|
+
for (let i = 0; i < chain.delegations.length; i++) {
|
|
1705
|
+
const delegation = chain.delegations[i];
|
|
1706
|
+
const parentRole = i === 0 ? chain.parent_role : '';
|
|
1707
|
+
const parentTurn = i === 0 ? `\`${chain.parent_turn_id}\`` : '';
|
|
1708
|
+
const outcome = i === 0 ? `\`${chain.outcome}\`` : '';
|
|
1709
|
+
const reviewTurn = i === 0 ? `\`${chain.review_turn_id || 'pending'}\`` : '';
|
|
1710
|
+
const charter = delegation.charter.replace(/\|/g, '\\|');
|
|
1711
|
+
lines.push(`| ${parentRole} | ${parentTurn} | ${outcome} | ${reviewTurn} | \`${delegation.delegation_id}\` → \`${delegation.to_role}\` | \`${delegation.child_turn_id || 'pending'}\` | \`${delegation.status}\` | ${charter} |`);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1631
1716
|
if (run.turns && run.turns.length > 0) {
|
|
1632
1717
|
lines.push('', '## Turn Timeline', '', '| # | Role | Phase | Summary | Files | Cost | Time |', '|---|------|-------|---------|-------|------|------|');
|
|
1633
1718
|
for (let i = 0; i < run.turns.length; i++) {
|
|
@@ -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
|
}
|
package/src/lib/token-budget.js
CHANGED
|
@@ -18,6 +18,7 @@ const SEPARATOR = '\n\n---\n\n';
|
|
|
18
18
|
const SYSTEM_PROMPT = [
|
|
19
19
|
'You are acting as a governed agent in an AgentXchain protocol run.',
|
|
20
20
|
'Your task and rules are described in the user message.',
|
|
21
|
+
'You MUST obey the write-authority-specific rules in the prompt exactly.',
|
|
21
22
|
'You MUST respond with a valid JSON object matching the turn result schema provided in the prompt.',
|
|
22
23
|
'Do NOT wrap the JSON in markdown code fences. Respond with raw JSON only.',
|
|
23
24
|
].join('\n');
|
|
@@ -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
|
|