@agentel/sdk 2.0.0 → 2.1.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/CHANGELOG.md +7 -0
- package/FOUNDER_WORKER_V1.md +112 -0
- package/POOL_REVIEW_RUNTIME_V1.md +55 -0
- package/POOL_REVIEW_V1.md +86 -0
- package/POOL_REVIEW_WORKER_V1.md +69 -0
- package/README.md +12 -6
- package/V2.1.0_PUBLIC_CHANGELOG.md +11 -0
- package/agentel-connector.ts +170 -5
- package/dist/agentel-connector.d.ts +188 -2
- package/dist/agentel-connector.js +143 -2
- package/dist/founder-actions.d.ts +4 -0
- package/dist/founder-actions.js +74 -0
- package/dist/founder-worker.d.ts +152 -0
- package/dist/founder-worker.js +551 -0
- package/dist/review-worker.d.ts +62 -0
- package/dist/review-worker.js +316 -0
- package/founder-actions.ts +54 -0
- package/founder-worker.ts +289 -0
- package/node-founder-store.d.ts +5 -0
- package/node-founder-store.mjs +8 -0
- package/node-review-store.d.ts +5 -0
- package/node-review-store.mjs +33 -0
- package/package.json +30 -2
- package/review-worker.ts +157 -0
- package/test/founder-worker.test.mjs +308 -0
- package/test/pool-review.test.mjs +29 -0
- package/test/review-worker.test.mjs +194 -0
- package/tsconfig.json +10 -2
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { extendedFounderActions, creationInvitationActions } from './founder-actions.js';
|
|
2
|
+
import { AgentelApiError } from './agentel-connector.js';
|
|
3
|
+
const fail = (code) => Object.assign(new Error(code), { code });
|
|
4
|
+
const record = (v) => { if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
5
|
+
throw fail('INVALID_FOUNDER_INPUT'); return v; };
|
|
6
|
+
const rows = (v) => Array.isArray(v) ? v.map(record) : [];
|
|
7
|
+
const exact = (o, keys) => { if (Object.keys(o).some(k => !keys.includes(k)))
|
|
8
|
+
throw fail('INVALID_FOUNDER_DECISION'); };
|
|
9
|
+
const id = (v) => typeof v === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(v);
|
|
10
|
+
const note = (v, max = 4000) => typeof v === 'string' && v.trim().length >= 4 && v.length <= max;
|
|
11
|
+
const hash = async (v) => Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(v))))).map(n => n.toString(16).padStart(2, '0')).join('');
|
|
12
|
+
// Only known business denials are terminal. Auth, key collision, generic HTTP errors,
|
|
13
|
+
// timeouts/rate limits and server failures may hide a prior commit and must retain the key.
|
|
14
|
+
const rejectionCodes = new Set([
|
|
15
|
+
'MISSION_CREATION_LIMIT', 'MISSION_DRAFT_NOT_EDITABLE', 'MISSION_CREATION_REQUEST_NOT_FOUND', 'OFFICIAL_MISSION_BUDGET_NOT_FOUND', 'OFFICIAL_MISSION_BUDGET_NOT_PROVISIONED', 'OFFICIAL_MISSION_BUDGET_DISABLED', 'OFFICIAL_MISSION_PLAN_EXCEEDED', 'OFFICIAL_MISSION_SCOPE_EXCEEDED', 'OFFICIAL_MISSION_REQUEST_REQUIRED', 'OWNED_COLLABORATION_NOT_ENABLED', 'OWNED_COLLABORATION_SCOPE_REQUIRED', 'OWNED_COLLABORATION_OWNER_CHANGED', 'OWNED_COLLABORATION_CLOSED', 'OWNED_COLLABORATION_AUTHORITY_CHANGED', 'OWNED_COLLABORATION_ACCESS_REQUIRED', 'OWNED_COLLABORATION_CONTRACT_REQUIRED', 'INVALID_OWNED_MODE', 'CUSTOMER_FOUNDER_REQUIRED', 'MISSION_REQUEST_ACCESS_INVALID', 'MISSION_ACCESS_CONTRACT_MISMATCH', 'MISSION_AUTHORITY_MISMATCH', 'MISSION_AUTHORITY_CHANGED', 'MISSION_CREATION_CONTRACT_MISSING', 'FOUNDER_ENTITLEMENT_REQUIRED', 'FOUNDER_ACCOUNT_REQUIRED',
|
|
16
|
+
'INVALID_MISSION_CONTRACT', 'MISSION_POOL_POLICY_REPAIR_REQUIRED', 'MISSION_PHASE_TIMING_REQUIRED', 'MISSION_CREATION_REQUEST_NOT_READY', 'MISSION_CREATION_REQUEST_NOT_ACCEPTABLE', 'MISSION_DRAFT_CHANGED', 'MISSION_CREATION_DRAFT_EXISTS', 'MISSION_SLUG_TAKEN',
|
|
17
|
+
'INVALID_SELECTIONS', 'DUPLICATE_APPLICATION_SELECTION', 'APPLICATION_NOT_FOUND', 'APPLICATION_NOT_ELIGIBLE_FOR_SELECTION',
|
|
18
|
+
'INTERNAL_CHECKER_OWNER_REQUIRED', 'MISSION_SLOT_NOT_FOUND', 'SELECTION_RATIONALE_TOO_LONG', 'MISSION_SLOT_CAPACITY_CONFLICT', 'MISSION_STAFFING_CLOSED', 'REVIEW_IS_POOL_ROUTED',
|
|
19
|
+
'ASSIGNMENT_NOT_FOUND', 'ASSIGNMENT_NOT_READY', 'MISSION_STAGE_NOT_FOUND', 'MISSION_STAGE_DEPENDENCY_MANAGED', 'MISSION_REVIEW_COVERAGE_REQUIRED', 'ASSIGNMENT_ACTIVATION_CONFLICT',
|
|
20
|
+
'INVALID_ROOM_MESSAGE', 'INVALID_ROOM_MESSAGE_TYPE', 'INVALID_ROOM_AUDIENCE', 'ROOM_MESSAGE_METADATA_TOO_LARGE', 'ROOM_STAGE_REQUIRED', 'ROOM_ASSIGNMENT_REQUIRED',
|
|
21
|
+
'ROOM_AUDIENCE_SHAPE_INVALID', 'MISSION_ROOM_NOT_READY', 'MISSION_ROOM_MESSAGE_CONFLICT', 'INVALID_OUTCOME_OPINION', 'POOL_OUTCOME_VERSION_REQUIRED',
|
|
22
|
+
'POOL_OUTCOME_VERSION_CHANGED', 'MISSION_OUTCOME_NOT_READY', 'MISSION_OUTCOME_NOT_YOUR_TURN', 'POOL_OUTCOME_POLICY_REPAIR_REQUIRED', 'POOL_OUTCOME_CHANGED',
|
|
23
|
+
'MISSION_OUTCOME_OPINION_CONFLICT', 'INVALID_CANDIDATE_PREDECESSOR', 'INVALID_FINAL_MATERIAL', 'FINAL_MATERIAL_PLACEHOLDER', 'FINAL_MATERIAL_CHANGED',
|
|
24
|
+
'FINAL_MATERIAL_HISTORY_UNAVAILABLE', 'FINAL_MATERIAL_PUBLIC_ONLY', 'FINAL_MATERIAL_SOURCES_REQUIRED', 'FINAL_MATERIAL_SOURCE_UNAVAILABLE', 'FINAL_MATERIAL_TOO_LARGE',
|
|
25
|
+
'FINAL_CANDIDATE_CHANGED', 'FINAL_CANDIDATE_SCOPE_REQUIRED', 'FINAL_CANDIDATE_AGREEMENT_REQUIRED', 'FINAL_CANDIDATE_REQUIRED', 'MISSION_OUTCOME_ALREADY_PUBLISHED',
|
|
26
|
+
]);
|
|
27
|
+
const terminalRejection = (e) => e instanceof AgentelApiError && [400, 402, 403, 404, 409, 410, 422].includes(e.status) && e.retryAfterMs === null && rejectionCodes.has(e.code);
|
|
28
|
+
function structuredDenial(e) {
|
|
29
|
+
if (!(e instanceof AgentelApiError) || e.status < 400 || e.status >= 500 || [408, 429].includes(e.status) || e.retryAfterMs !== null)
|
|
30
|
+
return false;
|
|
31
|
+
const envelope = e.details && typeof e.details === 'object' ? e.details : null;
|
|
32
|
+
const error = envelope?.error && typeof envelope.error === 'object' ? envelope.error : null;
|
|
33
|
+
return typeof error?.code === 'string' && error.code === e.code && /^[A-Z0-9_]{1,100}$/.test(error.code);
|
|
34
|
+
}
|
|
35
|
+
const basis = (context) => hash({ ...context, rejections: undefined });
|
|
36
|
+
function rejectionIssues(details) {
|
|
37
|
+
const envelope = details && typeof details === 'object' ? details : null;
|
|
38
|
+
const error = envelope?.error && typeof envelope.error === 'object' ? envelope.error : null;
|
|
39
|
+
const data = error?.details ?? details, d = data && typeof data === 'object' ? data : null;
|
|
40
|
+
const items = Array.isArray(data) ? data : Array.isArray(d?.issues) ? d.issues : [];
|
|
41
|
+
return items.slice(0, 8).flatMap(value => {
|
|
42
|
+
if (!value || typeof value !== 'object')
|
|
43
|
+
return [];
|
|
44
|
+
const issue = value;
|
|
45
|
+
if (typeof issue.path !== 'string' || !/^[A-Za-z0-9_$.[\]-]{1,180}$/.test(issue.path))
|
|
46
|
+
return [];
|
|
47
|
+
const code = typeof issue.code === 'string' && /^[A-Z0-9_]{1,100}$/.test(issue.code) ? issue.code : 'DRAFT_FIELD_INVALID';
|
|
48
|
+
return [{ path: issue.path, code, ...(typeof issue.suggestedValue === 'number' && Number.isFinite(issue.suggestedValue) ? { suggestedValue: issue.suggestedValue } : {}) }];
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function control(workspace) { if (!workspace.founderControl)
|
|
52
|
+
throw fail('FOUNDER_AUTHORITY_REQUIRED'); return record(workspace.founderControl); }
|
|
53
|
+
function actions(workspace) {
|
|
54
|
+
const c = control(workspace), a = [];
|
|
55
|
+
if (['RESULT_PUBLISHED', 'OUTCOME_ACCEPTED'].includes(workspace.state) || ['CANCELLED', 'COMPLETED', 'FAILED'].includes(workspace.mission.status))
|
|
56
|
+
return a;
|
|
57
|
+
const outcome = c.outcomeNextAction ? record(c.outcomeNextAction) : null;
|
|
58
|
+
if (outcome?.action === 'WAIT_FOR_HUMAN_FINAL_DECISION')
|
|
59
|
+
return a;
|
|
60
|
+
if (outcome?.actor === 'FOUNDER_AGENT' && outcome.method === 'POST') {
|
|
61
|
+
if (Array.isArray(outcome.allowedDecisions) && outcome.allowedDecisions.includes('AGREE'))
|
|
62
|
+
a.push({ id: 'opinion', operation: 'OUTCOME_OPINION' });
|
|
63
|
+
else if (['PREPARE_FIXED_FINAL_CANDIDATE', 'CONFIRM_EDITED_FINAL_CANDIDATE'].includes(String(outcome.action)))
|
|
64
|
+
a.push({ id: 'final', operation: 'FINAL_CANDIDATE' });
|
|
65
|
+
}
|
|
66
|
+
if (workspace.mission.status === 'OPEN')
|
|
67
|
+
for (const app of rows(c.applications))
|
|
68
|
+
if (app.status === 'ELIGIBILITY_PASSED' && app.contractHash === workspace.mission.contractHash && app.contractVersion === workspace.mission.contractVersion && id(app.id))
|
|
69
|
+
a.push({ id: 'select:' + app.id, operation: 'SELECT_APPLICATIONS', targetId: app.id });
|
|
70
|
+
for (const assignment of rows(c.assignments))
|
|
71
|
+
if (id(assignment.id) && rows(assignment.nextActions).some(n => n.type === 'ACTIVATE_ASSIGNMENT'))
|
|
72
|
+
a.push({ id: 'activate:' + assignment.id, operation: 'ACTIVATE_ASSIGNMENT', targetId: assignment.id });
|
|
73
|
+
// Coordination uses only the normal Mission room. No arbitrary endpoint or direct-message authority.
|
|
74
|
+
if (Number(record(c.summary ?? {}).blocked ?? 0) > 0 || rows(c.assignments).some(a => record(a.sla ?? {}).state === 'INPUT_PAUSED'))
|
|
75
|
+
a.push({ id: 'coordinate', operation: 'SEND_ROOM_MESSAGE' });
|
|
76
|
+
return a;
|
|
77
|
+
}
|
|
78
|
+
function bodyFor(action, value, context) {
|
|
79
|
+
const b = record(value), c = control(context.workspace), outcome = c.outcomeNextAction ? record(c.outcomeNextAction) : {}, internal = context.deliveries.reviewMode === 'INTERNAL_CHECK' && !!context.deliveries.internalCheck;
|
|
80
|
+
switch (action.operation) {
|
|
81
|
+
case 'INVITE_CANDIDATES': {
|
|
82
|
+
exact(b, ['candidateAgentIds', 'note']);
|
|
83
|
+
const permitted = action.binding?.candidateAgentIds;
|
|
84
|
+
if (!Array.isArray(b.candidateAgentIds) || b.candidateAgentIds.length < 1 || b.candidateAgentIds.length > 6 || new Set(b.candidateAgentIds).size !== b.candidateAgentIds.length || !Array.isArray(permitted) || !b.candidateAgentIds.every(v => id(v) && permitted.includes(v)) || !note(b.note, 2000))
|
|
85
|
+
throw fail('INVALID_FOUNDER_DECISION');
|
|
86
|
+
const binding = { ...action.binding };
|
|
87
|
+
delete binding.candidateAgentIds;
|
|
88
|
+
return { ...binding, candidate_agent_ids: b.candidateAgentIds, reason: b.note };
|
|
89
|
+
}
|
|
90
|
+
case 'REPLACE_ASSIGNMENT':
|
|
91
|
+
exact(b, ['note']);
|
|
92
|
+
if (!note(b.note, 2000))
|
|
93
|
+
throw fail('INVALID_FOUNDER_DECISION');
|
|
94
|
+
return { ...action.binding, evidence: { ...record(action.binding?.evidence), founderReviewNote: b.note } };
|
|
95
|
+
case 'RETRY_REVIEW':
|
|
96
|
+
exact(b, []);
|
|
97
|
+
return { ...action.binding };
|
|
98
|
+
case 'INTERNAL_CHECK':
|
|
99
|
+
exact(b, ['decision', 'note']);
|
|
100
|
+
if (!['ACCEPTED', 'REVISION_REQUESTED', 'REJECTED'].includes(String(b.decision)) || !note(b.note))
|
|
101
|
+
throw fail('INVALID_FOUNDER_DECISION');
|
|
102
|
+
return b;
|
|
103
|
+
case 'SELECT_APPLICATIONS':
|
|
104
|
+
exact(b, ['decision', 'rationale']);
|
|
105
|
+
if (!['SELECTED', 'WAITLISTED', 'NOT_SELECTED'].includes(String(b.decision)) || !note(b.rationale, 2000))
|
|
106
|
+
throw fail('INVALID_FOUNDER_DECISION');
|
|
107
|
+
return { selections: [{ application_id: action.targetId, decision: b.decision, rationale: b.rationale }] };
|
|
108
|
+
case 'ACTIVATE_ASSIGNMENT':
|
|
109
|
+
exact(b, []);
|
|
110
|
+
return {};
|
|
111
|
+
case 'OUTCOME_OPINION':
|
|
112
|
+
exact(b, ['decision', 'note']);
|
|
113
|
+
if (internal && ['AGREE', 'DISAGREE'].includes(String(b.decision)) && note(b.note))
|
|
114
|
+
return b;
|
|
115
|
+
if (!['AGREE', 'DISAGREE'].includes(String(b.decision)) || !note(b.note) || typeof outcome.expected_review_set_hash !== 'string' || !Number.isInteger(outcome.expected_round))
|
|
116
|
+
throw fail('INVALID_FOUNDER_DECISION');
|
|
117
|
+
return { ...b, expected_review_set_hash: outcome.expected_review_set_hash, expected_round: outcome.expected_round };
|
|
118
|
+
case 'FINAL_CANDIDATE':
|
|
119
|
+
exact(b, ['finalMaterial']);
|
|
120
|
+
record(b.finalMaterial);
|
|
121
|
+
if (internal)
|
|
122
|
+
return { ...b, expectedCandidateId: outcome.expectedCandidateId ?? null };
|
|
123
|
+
if (typeof outcome.expected_review_set_hash !== 'string')
|
|
124
|
+
throw fail('INVALID_FOUNDER_DECISION');
|
|
125
|
+
return { ...b, expectedCandidateId: outcome.expectedCandidateId ?? null, expected_review_set_hash: outcome.expected_review_set_hash };
|
|
126
|
+
case 'SEND_ROOM_MESSAGE':
|
|
127
|
+
exact(b, ['message_type', 'audience_type', 'content', 'stage_id', 'assignment_id']);
|
|
128
|
+
if (!['CHAT', 'HANDOFF_PROPOSAL', 'QUESTION', 'DECISION_REQUEST'].includes(String(b.message_type)) || !['ROOM', 'STAGE', 'ASSIGNMENT'].includes(String(b.audience_type)) || !note(b.content, 12000))
|
|
129
|
+
throw fail('INVALID_FOUNDER_DECISION');
|
|
130
|
+
return b;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Operator supplies the explicit owned Mission inventory. No arbitrary model tools or Human decisions. */
|
|
134
|
+
export async function runFounderWorker(o, run) {
|
|
135
|
+
if (!id(o.agentId) || o.client.currentAgentId !== o.agentId || !Array.isArray(run.missionIds) || run.missionIds.length > 50 || new Set(run.missionIds).size !== run.missionIds.length || !run.missionIds.every(id) || !Number.isInteger(run.maxMissions) || run.maxMissions < 1 || run.maxMissions > 10 || !Number.isInteger(run.maxSteps) || run.maxSteps < 1 || run.maxSteps > 20 || !Number.isFinite(run.maxRunMs) || run.maxRunMs < 1 || run.maxRunMs > 21600000)
|
|
136
|
+
throw fail('INVALID_FOUNDER_CONFIGURATION');
|
|
137
|
+
const requestIds = run.creationRequestIds ?? [], missionIds = [...run.missionIds], candidateIds = run.candidateAgentIds ?? [];
|
|
138
|
+
if (!Array.isArray(candidateIds) || candidateIds.length > 50 || !candidateIds.every(id) || new Set(candidateIds).size !== candidateIds.length)
|
|
139
|
+
throw fail('INVALID_FOUNDER_CONFIGURATION');
|
|
140
|
+
if (!Array.isArray(requestIds) || requestIds.length > 50 || !requestIds.every(id) || new Set(requestIds).size !== requestIds.length)
|
|
141
|
+
throw fail('INVALID_FOUNDER_CONFIGURATION');
|
|
142
|
+
const inventory = [...requestIds.map(id => ({ id, kind: 'CREATION_REQUEST' })), ...run.missionIds.map(id => ({ id, kind: 'MISSION' }))];
|
|
143
|
+
if (inventory.length > 50)
|
|
144
|
+
throw fail('INVALID_FOUNDER_CONFIGURATION');
|
|
145
|
+
const now = o.now ?? Date.now, client = o.client.forFounderWorker();
|
|
146
|
+
return await o.store.exclusive(async (store) => {
|
|
147
|
+
let s = await store.load();
|
|
148
|
+
if (s && (s.schema !== 1 || s.role !== 'FOUNDER' || s.agentId !== o.agentId))
|
|
149
|
+
throw fail('FOUNDER_CHECKPOINT_MISMATCH');
|
|
150
|
+
s ??= { schema: 1, role: 'FOUNDER', agentId: o.agentId, notBefore: 0, cursor: 0, pending: null };
|
|
151
|
+
if (s.notBefore > now())
|
|
152
|
+
return { status: 'WAIT_NEXT_CHECK', completed: 0, rejected: 0, missionIssues: [], code: null, nextCheckAt: s.notBefore };
|
|
153
|
+
const controller = new AbortController(), stop = () => controller.abort();
|
|
154
|
+
run.signal?.addEventListener('abort', stop, { once: true });
|
|
155
|
+
if (run.signal?.aborted)
|
|
156
|
+
stop();
|
|
157
|
+
const deadline = now() + run.maxRunMs, timer = setTimeout(stop, run.maxRunMs);
|
|
158
|
+
let completed = 0, rejected = 0, attempts = 0, lastContact = now(), missionId = null, ready = false;
|
|
159
|
+
const runId = crypto.randomUUID();
|
|
160
|
+
s.deferred ??= [];
|
|
161
|
+
// Membership is local routing metadata, not a business receipt. Removed request inventories must not accumulate forever.
|
|
162
|
+
s.followedMissions = (s.followedMissions ?? []).filter(f => requestIds.includes(f.requestId));
|
|
163
|
+
const sameResource = (a, b) => a.missionId === b.missionId && a.resourceKind === b.resourceKind;
|
|
164
|
+
const missionIssues = [];
|
|
165
|
+
const follow = (requestId, mid) => {
|
|
166
|
+
if (!id(mid) || !requestIds.includes(requestId))
|
|
167
|
+
throw fail('FOUNDER_FOLLOW_BINDING_INVALID');
|
|
168
|
+
s.followedMissions ??= [];
|
|
169
|
+
const prior = s.followedMissions.find(x => x.requestId === requestId);
|
|
170
|
+
if (prior && prior.missionId !== mid)
|
|
171
|
+
throw fail('FOUNDER_FOLLOW_BINDING_CHANGED');
|
|
172
|
+
if (!prior) {
|
|
173
|
+
if (s.followedMissions.length >= 50)
|
|
174
|
+
throw fail('FOUNDER_FOLLOW_CAPACITY');
|
|
175
|
+
s.followedMissions.push({ requestId, missionId: mid });
|
|
176
|
+
}
|
|
177
|
+
if (!missionIds.includes(mid)) {
|
|
178
|
+
if (inventory.length >= 50) {
|
|
179
|
+
missionIssues.push({ missionId: mid, code: 'FOUNDER_INVENTORY_CAPACITY' });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
missionIds.push(mid);
|
|
183
|
+
inventory.push({ id: mid, kind: 'MISSION' });
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const check = () => { if (controller.signal.aborted || now() >= deadline)
|
|
187
|
+
throw fail('RUN_BUDGET_EXHAUSTED'); };
|
|
188
|
+
const save = () => store.save(s);
|
|
189
|
+
const contact = async (state) => { check(); await o.contact({ role: 'FOUNDER', state, missionId }, controller.signal); lastContact = now(); };
|
|
190
|
+
const pulse = async () => { if (now() - lastContact >= 1800000)
|
|
191
|
+
await contact('ACTIVE'); };
|
|
192
|
+
const bounded = async (work) => {
|
|
193
|
+
check();
|
|
194
|
+
let abort = () => { };
|
|
195
|
+
try {
|
|
196
|
+
return await Promise.race([new Promise((_, reject) => { abort = () => reject(fail('RUN_BUDGET_EXHAUSTED')); controller.signal.addEventListener('abort', abort, { once: true }); if (controller.signal.aborted)
|
|
197
|
+
abort(); }), work()]);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
controller.signal.removeEventListener('abort', abort);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const replay = async () => {
|
|
204
|
+
const p = s.pending;
|
|
205
|
+
if (!(p.resourceKind === 'CREATION_REQUEST' ? requestIds : missionIds).includes(p.missionId)) {
|
|
206
|
+
s.deferred = [...(s.deferred ?? []).filter(q => !sameResource(q, p)), p];
|
|
207
|
+
s.pending = null;
|
|
208
|
+
await save();
|
|
209
|
+
missionIssues.push({ missionId: p.missionId, code: 'PENDING_MISSION_OUTSIDE_INVENTORY' });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (attempts >= run.maxSteps)
|
|
213
|
+
throw fail('FOUNDER_STEP_LIMIT');
|
|
214
|
+
attempts++;
|
|
215
|
+
try {
|
|
216
|
+
const result = await bounded(() => p.resourceKind === 'CREATION_REQUEST' ? client.executeFounderPreparation(p.missionId, p.operation, p.body, p.key, controller.signal) : client.executeFounderOperation(p.missionId, p.operation, p.body, p.key, p.targetId, controller.signal));
|
|
217
|
+
if (p.operation === 'PUBLISH_APPROVED_MISSION') {
|
|
218
|
+
const r = record(result), m = record(r.mission);
|
|
219
|
+
if (r.published !== true || m.id !== p.body.mission_id || m.contractHash !== p.body.contract_hash || m.contractVersion !== p.body.contract_version)
|
|
220
|
+
throw fail('FOUNDER_PUBLICATION_RECEIPT_UNCONFIRMED');
|
|
221
|
+
follow(p.missionId, String(m.id));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
if (!terminalRejection(e) || e.code === 'MISSION_CREATION_LIMIT') {
|
|
226
|
+
const explicit = structuredDenial(e), prior = p.failure;
|
|
227
|
+
const runs = explicit ? (prior?.runs ?? 0) + (prior?.lastRun === runId ? 0 : 1) : 0;
|
|
228
|
+
p.failure = { code: e instanceof AgentelApiError ? e.code : 'TRANSPORT_OUTCOME_UNKNOWN', status: e instanceof AgentelApiError ? e.status : 0, runs, lastRun: runId };
|
|
229
|
+
if (e instanceof AgentelApiError && e.retryAfterMs !== null)
|
|
230
|
+
p.retryAt = now() + e.retryAfterMs;
|
|
231
|
+
if (explicit && runs >= 2) {
|
|
232
|
+
p.quarantined = true;
|
|
233
|
+
p.quarantineReason = 'UNRESOLVED_REJECTION';
|
|
234
|
+
p.issue = { id: 'founder_issue_' + crypto.randomUUID(), resourceId: p.missionId, resourceKind: p.resourceKind ?? 'MISSION', code: e.code, status: e.status, operationKeyHash: await hash(p.key) };
|
|
235
|
+
}
|
|
236
|
+
s.deferred = [...(s.deferred ?? []).filter(q => !sameResource(q, p)), p];
|
|
237
|
+
s.pending = null;
|
|
238
|
+
await save();
|
|
239
|
+
missionIssues.push({ missionId: p.missionId, code: p.quarantined ? 'UNRESOLVED_REJECTION' : p.failure.code });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const rejection = { missionId: p.missionId, operation: p.operation, ...(p.resourceKind ? { resourceKind: p.resourceKind } : {}), ...((p.operation === 'SUBMIT_CREATION_DRAFT' && rejectionIssues(e.details).length) ? { issues: rejectionIssues(e.details) } : {}), ...(p.targetId ? { targetId: p.targetId } : {}), key: p.key, bodyHash: await hash(p.body), basisHash: p.basisHash ?? null, code: e.code, status: e.status, rejectedAt: new Date(now()).toISOString() };
|
|
243
|
+
// Clear the pending intent only in the same durable checkpoint as its rejection record.
|
|
244
|
+
s.rejections = [...(s.rejections ?? []).map(({ issues: oldIssues, ...metadata }) => { void oldIssues; return metadata; }), rejection].slice(-32);
|
|
245
|
+
s.pending = null;
|
|
246
|
+
await save();
|
|
247
|
+
rejected++;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
s.pending = null;
|
|
251
|
+
completed++;
|
|
252
|
+
await save();
|
|
253
|
+
};
|
|
254
|
+
const read = async (mid) => {
|
|
255
|
+
const workspace = await bounded(() => client.missionWorkspace(mid, controller.signal));
|
|
256
|
+
if (workspace.mission.id !== mid)
|
|
257
|
+
throw fail('MISSION_ID_MISMATCH');
|
|
258
|
+
control(workspace);
|
|
259
|
+
const contract = await bounded(() => client.founderMissionContract(mid, controller.signal));
|
|
260
|
+
if (record(contract.mission).contractHash !== workspace.mission.contractHash)
|
|
261
|
+
throw fail('FOUNDER_CONTEXT_CHANGED');
|
|
262
|
+
const applications = await bounded(() => client.missionApplications(mid, controller.signal));
|
|
263
|
+
const deliveries = await bounded(() => client.missionDeliveries(mid, controller.signal));
|
|
264
|
+
let room;
|
|
265
|
+
try {
|
|
266
|
+
room = await bounded(() => client.missionRoom(mid, { limit: 100, signal: controller.signal }));
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
if (e instanceof AgentelApiError && e.code === 'MISSION_ROOM_NOT_READY' && !rows(control(workspace).assignments).some(a => ['ACCEPTED', 'ACTIVE', 'BLOCKED', 'COMPLETED'].includes(String(a.status))))
|
|
270
|
+
room = { available: false, messages: [], reason: 'MISSION_ROOM_NOT_READY' };
|
|
271
|
+
else
|
|
272
|
+
throw e;
|
|
273
|
+
}
|
|
274
|
+
const invitations = record(contract.contract ?? {}).participation_mode === 'INVITE_ONLY' ? await bounded(() => client.founderMissionInvitations(mid, controller.signal)) : undefined;
|
|
275
|
+
const context = { inputTrust: 'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS', workspace, contract, applications, deliveries, room, ...(invitations ? { invitations } : {}), rejections: (s.rejections ?? []).filter(r => r.missionId === mid && !r.resourceKind), actions: actions(workspace).filter(a => a.operation !== 'SEND_ROOM_MESSAGE' || room.available !== false) };
|
|
276
|
+
context.actions.push(...extendedFounderActions(context, candidateIds.length ? candidateIds : rows(control(workspace).candidateAgents).map(a => a.id).filter(id), o.agentId));
|
|
277
|
+
if (record(control(workspace).poolReview ?? {}).taskOverflow === true)
|
|
278
|
+
throw fail('FOUNDER_CONTEXT_TRUNCATED');
|
|
279
|
+
if (invitations && rows(invitations.invitations).length >= 100)
|
|
280
|
+
throw fail('FOUNDER_CONTEXT_TRUNCATED');
|
|
281
|
+
if (room.hasMore === true || deliveries.deliveries.length >= 100 || applications.applications.length >= 250)
|
|
282
|
+
throw fail('FOUNDER_CONTEXT_TRUNCATED');
|
|
283
|
+
if (new TextEncoder().encode(JSON.stringify(context)).length > 1000000)
|
|
284
|
+
throw fail('FOUNDER_CONTEXT_TOO_LARGE');
|
|
285
|
+
return context;
|
|
286
|
+
};
|
|
287
|
+
let code = null, contactError = false, contactInFlight = null;
|
|
288
|
+
const interval = setInterval(() => { if (contactInFlight)
|
|
289
|
+
return; contactInFlight = bounded(() => contact('ACTIVE')).catch(() => { contactError = true; controller.abort(); }).finally(() => { contactInFlight = null; }); }, 1800000);
|
|
290
|
+
try {
|
|
291
|
+
s.notBefore = now() + 3300000;
|
|
292
|
+
await save();
|
|
293
|
+
await bounded(() => contact('START'));
|
|
294
|
+
for (const f of s.followedMissions ?? [])
|
|
295
|
+
if (requestIds.includes(f.requestId))
|
|
296
|
+
follow(f.requestId, f.missionId);
|
|
297
|
+
// Receipt recovery is independent of model/provider availability.
|
|
298
|
+
if (s.pending)
|
|
299
|
+
await replay();
|
|
300
|
+
// Every retained unknown outcome is resource-scoped, keeps its original key and
|
|
301
|
+
// is attempted at most once per run. Quarantine never asserts non-execution.
|
|
302
|
+
for (const p of s.deferred)
|
|
303
|
+
if (!(p.resourceKind === 'CREATION_REQUEST' ? requestIds : missionIds).includes(p.missionId))
|
|
304
|
+
missionIssues.push({ missionId: p.missionId, code: 'PENDING_MISSION_OUTSIDE_INVENTORY' });
|
|
305
|
+
for (const pending of [...s.deferred].filter(p => !p.quarantined && p.failure?.lastRun !== runId && (p.retryAt ?? 0) <= now() && (p.resourceKind === 'CREATION_REQUEST' ? requestIds : missionIds).includes(p.missionId)).slice(0, run.maxMissions)) {
|
|
306
|
+
if (attempts >= run.maxSteps)
|
|
307
|
+
break;
|
|
308
|
+
if (pending.quarantined || pending.failure?.lastRun === runId || (pending.retryAt ?? 0) > now())
|
|
309
|
+
continue;
|
|
310
|
+
if (!(pending.resourceKind === 'CREATION_REQUEST' ? requestIds : missionIds).includes(pending.missionId)) {
|
|
311
|
+
missionIssues.push({ missionId: pending.missionId, code: 'PENDING_MISSION_OUTSIDE_INVENTORY' });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
s.pending = pending;
|
|
315
|
+
s.deferred = s.deferred.filter(q => q.key !== pending.key);
|
|
316
|
+
await save();
|
|
317
|
+
await replay();
|
|
318
|
+
}
|
|
319
|
+
for (const pending of s.deferred) {
|
|
320
|
+
if (!pending.quarantined || !pending.issue || pending.noticeRecorded)
|
|
321
|
+
continue;
|
|
322
|
+
try {
|
|
323
|
+
await bounded(async () => { if (o.reportIssue)
|
|
324
|
+
await o.reportIssue(pending.issue, controller.signal);
|
|
325
|
+
else {
|
|
326
|
+
const receipt = await client.reportFounderIssue(pending.issue, controller.signal);
|
|
327
|
+
if (receipt.founderInboxRecorded !== true || receipt.humanNotificationRecorded !== true)
|
|
328
|
+
throw fail('FOUNDER_ALERT_UNCONFIRMED');
|
|
329
|
+
} });
|
|
330
|
+
pending.noticeRecorded = true;
|
|
331
|
+
await save();
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
check();
|
|
335
|
+
missionIssues.push({ missionId: pending.missionId, code: 'FOUNDER_ALERT_DELIVERY_PENDING' });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
ready = (await bounded(() => o.judge.ready(controller.signal))) === true;
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
check();
|
|
343
|
+
if (e.code === 'RUN_BUDGET_EXHAUSTED')
|
|
344
|
+
throw e;
|
|
345
|
+
ready = false;
|
|
346
|
+
}
|
|
347
|
+
if (!ready)
|
|
348
|
+
throw fail('MODEL_PROVIDER_UNAVAILABLE');
|
|
349
|
+
let steps = attempts;
|
|
350
|
+
for (let scanned = 0; scanned < Math.min(run.maxMissions, inventory.length) && steps < run.maxSteps; scanned++) {
|
|
351
|
+
const position = s.cursor % inventory.length, item = inventory[position];
|
|
352
|
+
try {
|
|
353
|
+
missionId = item.kind === 'MISSION' ? item.id : null;
|
|
354
|
+
if (s.deferred.length >= 50 && !s.deferred.some(p => sameResource(p, { missionId: item.id, ...(item.kind === 'CREATION_REQUEST' ? { resourceKind: 'CREATION_REQUEST' } : {}) }))) {
|
|
355
|
+
missionIssues.push({ missionId: item.id, code: 'FOUNDER_DEFERRED_CAPACITY' });
|
|
356
|
+
s.cursor = (position + 1) % inventory.length;
|
|
357
|
+
await save();
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const retained = s.deferred.find(p => sameResource(p, { missionId: item.id, ...(item.kind === 'CREATION_REQUEST' ? { resourceKind: 'CREATION_REQUEST' } : {}) }));
|
|
361
|
+
if (retained) {
|
|
362
|
+
missionIssues.push({ missionId: item.id, code: retained.quarantined ? 'UNRESOLVED_REJECTION' : retained.failure?.code ?? 'PENDING_RECEIPT_RECOVERY' });
|
|
363
|
+
s.cursor = (position + 1) % inventory.length;
|
|
364
|
+
await save();
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const seen = new Set();
|
|
368
|
+
if (item.kind === 'CREATION_REQUEST') {
|
|
369
|
+
while (steps < run.maxSteps) {
|
|
370
|
+
await pulse();
|
|
371
|
+
const snapshot = await bounded(() => client.missionCreationRequest(item.id, controller.signal));
|
|
372
|
+
const request = record(snapshot.request), founder = record(request.founderAgent);
|
|
373
|
+
if (request.id !== item.id || founder.id !== o.agentId)
|
|
374
|
+
throw fail('FOUNDER_AUTHORITY_REQUIRED');
|
|
375
|
+
// Publication is deterministic after exact Human approval; no model can approve or select its target.
|
|
376
|
+
if (request.status === 'PUBLISHED') {
|
|
377
|
+
if (!id(request.missionId))
|
|
378
|
+
throw fail('FOUNDER_APPROVED_BINDING_REQUIRED');
|
|
379
|
+
follow(item.id, request.missionId);
|
|
380
|
+
await save();
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
if (request.status === 'APPROVED') {
|
|
384
|
+
if (!id(request.missionId) || !id(request.contractId) || typeof request.contractVersion !== 'string' || !request.contractVersion || request.contractVersion.length > 64 || typeof request.contractHash !== 'string' || !/^(sha256:)?[a-f0-9]{64}$/.test(request.contractHash))
|
|
385
|
+
throw fail('FOUNDER_APPROVED_BINDING_REQUIRED');
|
|
386
|
+
const body = { mission_id: request.missionId, contract_id: request.contractId, contract_version: request.contractVersion, contract_hash: request.contractHash, creation_request_id: item.id };
|
|
387
|
+
const fresh = await bounded(() => client.missionCreationRequest(item.id, controller.signal));
|
|
388
|
+
if (await hash(fresh) !== await hash(snapshot))
|
|
389
|
+
throw fail('FOUNDER_CONTEXT_CHANGED');
|
|
390
|
+
s.pending = { resourceKind: 'CREATION_REQUEST', missionId: item.id, operation: 'PUBLISH_APPROVED_MISSION', body, key: 'founder_' + crypto.randomUUID() };
|
|
391
|
+
await save();
|
|
392
|
+
await replay();
|
|
393
|
+
steps = attempts;
|
|
394
|
+
if (s.deferred.some(p => p.missionId === item.id && p.resourceKind === 'CREATION_REQUEST'))
|
|
395
|
+
break;
|
|
396
|
+
// A successful receipt is authoritative even if a subsequent projection lags; never mint a second key this run.
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
// The request, exact contract and authoritative Human review note come from the service.
|
|
400
|
+
const invitationActions = request.status === 'CHANGES_REQUESTED' && typeof request.reviewNote === 'string' && request.reviewNote.trim() ? [] : creationInvitationActions(snapshot, candidateIds.length ? candidateIds : rows(snapshot.candidateAgents).map(a => a.id).filter(id), o.agentId, now());
|
|
401
|
+
if (!['REQUESTED', 'DRAFTING', 'CHANGES_REQUESTED', 'STAFFING'].includes(String(request.status)) && !(request.status === 'DRAFT_READY' && invitationActions.length))
|
|
402
|
+
break;
|
|
403
|
+
const discussion = await bounded(() => client.missionCreationMessages(item.id, controller.signal));
|
|
404
|
+
if (rows(discussion.messages).length >= 250)
|
|
405
|
+
throw fail('FOUNDER_CONTEXT_TRUNCATED');
|
|
406
|
+
let operation = request.status === 'REQUESTED' ? 'ACCEPT_CREATION_REQUEST' : 'SUBMIT_CREATION_DRAFT';
|
|
407
|
+
const context = { inputTrust: 'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS', request: snapshot, discussion, rejections: (s.rejections ?? []).filter(r => r.missionId === item.id && r.resourceKind === 'CREATION_REQUEST'), actions: [...(['STAFFING', 'DRAFT_READY'].includes(String(request.status)) || invitationActions.length ? [] : [{ id: operation === 'ACCEPT_CREATION_REQUEST' ? 'accept-request' : 'draft', operation }]), ...invitationActions] };
|
|
408
|
+
if (new TextEncoder().encode(JSON.stringify(context)).length > 1000000)
|
|
409
|
+
throw fail('FOUNDER_CONTEXT_TOO_LARGE');
|
|
410
|
+
if (context.actions.length === 0)
|
|
411
|
+
break;
|
|
412
|
+
if (!o.judge.prepare) {
|
|
413
|
+
missionIssues.push({ missionId: item.id, code: 'FOUNDER_PREPARATION_ADAPTER_REQUIRED' });
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
const fingerprint = await basis(context), decision = record(await bounded(() => o.judge.prepare(structuredClone(context), controller.signal)));
|
|
417
|
+
check();
|
|
418
|
+
if (decision.wait === true) {
|
|
419
|
+
exact(decision, ['wait']);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
exact(decision, ['actionId', 'body']);
|
|
423
|
+
const action = context.actions.find(a => a.id === decision.actionId);
|
|
424
|
+
if (!action)
|
|
425
|
+
throw fail('FOUNDER_ACTION_NOT_OFFERED');
|
|
426
|
+
operation = action.operation;
|
|
427
|
+
const draft = record(decision.body);
|
|
428
|
+
let body;
|
|
429
|
+
if (operation === 'ACCEPT_CREATION_REQUEST') {
|
|
430
|
+
exact(draft, []);
|
|
431
|
+
body = {};
|
|
432
|
+
}
|
|
433
|
+
else if (operation === 'CANCEL_CREATION_INVITATION') {
|
|
434
|
+
exact(draft, ['note']);
|
|
435
|
+
if (!note(draft.note, 2000))
|
|
436
|
+
throw fail('INVALID_FOUNDER_DECISION');
|
|
437
|
+
body = { ...action.binding, reason: draft.note };
|
|
438
|
+
}
|
|
439
|
+
else if (operation === 'INVITE_CREATION_AGENT') {
|
|
440
|
+
exact(draft, ['note']);
|
|
441
|
+
if (!note(draft.note, 2000))
|
|
442
|
+
throw fail('INVALID_FOUNDER_DECISION');
|
|
443
|
+
body = { ...action.binding, invitation_note: draft.note };
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
exact(draft, ['contract']);
|
|
447
|
+
record(draft.contract);
|
|
448
|
+
body = { contract: draft.contract, creation_request_id: item.id };
|
|
449
|
+
}
|
|
450
|
+
if (new TextEncoder().encode(JSON.stringify(body)).length > 30000)
|
|
451
|
+
throw fail('FOUNDER_BODY_TOO_LARGE');
|
|
452
|
+
const bodyHash = await hash(body), repeat = await hash({ operation, body });
|
|
453
|
+
if (seen.has(repeat) || (s.rejections ?? []).some(r => r.resourceKind === 'CREATION_REQUEST' && r.missionId === item.id && r.operation === operation && r.bodyHash === bodyHash && r.basisHash === fingerprint)) {
|
|
454
|
+
missionIssues.push({ missionId: item.id, code: 'FOUNDER_NO_PROGRESS' });
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
seen.add(repeat);
|
|
458
|
+
const fresh = await bounded(() => client.missionCreationRequest(item.id, controller.signal)), freshDiscussion = await bounded(() => client.missionCreationMessages(item.id, controller.signal));
|
|
459
|
+
if (await hash({ snapshot: fresh, discussion: freshDiscussion }) !== await hash({ snapshot, discussion }))
|
|
460
|
+
throw fail('FOUNDER_CONTEXT_CHANGED');
|
|
461
|
+
s.pending = { resourceKind: 'CREATION_REQUEST', basisHash: fingerprint, missionId: item.id, operation, body, key: 'founder_' + crypto.randomUUID() };
|
|
462
|
+
await save();
|
|
463
|
+
await replay();
|
|
464
|
+
steps = attempts;
|
|
465
|
+
if (s.deferred.some(p => p.key === s.pending?.key || p.missionId === item.id && p.resourceKind === (item.kind === 'CREATION_REQUEST' ? 'CREATION_REQUEST' : undefined)))
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
s.cursor = (position + 1) % inventory.length;
|
|
469
|
+
await save();
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
missionId = item.id;
|
|
473
|
+
while (steps < run.maxSteps) {
|
|
474
|
+
await pulse();
|
|
475
|
+
const context = await read(missionId);
|
|
476
|
+
if (context.actions.length === 0)
|
|
477
|
+
break;
|
|
478
|
+
const fingerprint = await basis(context), decision = record(await bounded(() => o.judge.decide(structuredClone(context), controller.signal)));
|
|
479
|
+
check();
|
|
480
|
+
if (decision.wait === true) {
|
|
481
|
+
exact(decision, ['wait']);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
exact(decision, ['actionId', 'body']);
|
|
485
|
+
const action = context.actions.find(a => a.id === decision.actionId);
|
|
486
|
+
if (!action)
|
|
487
|
+
throw fail('FOUNDER_ACTION_NOT_OFFERED');
|
|
488
|
+
const body = bodyFor(action, decision.body, context);
|
|
489
|
+
if (new TextEncoder().encode(JSON.stringify(body)).length > 30000)
|
|
490
|
+
throw fail('FOUNDER_BODY_TOO_LARGE');
|
|
491
|
+
const repeat = await hash({ action, body });
|
|
492
|
+
const bodyHash = await hash(body);
|
|
493
|
+
if (seen.has(repeat) || (s.rejections ?? []).some(r => !r.resourceKind && r.missionId === missionId && r.operation === action.operation && r.targetId === action.targetId && r.bodyHash === bodyHash && r.basisHash === fingerprint)) {
|
|
494
|
+
missionIssues.push({ missionId, code: 'FOUNDER_NO_PROGRESS' });
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
seen.add(repeat);
|
|
498
|
+
if (await basis(await read(missionId)) !== fingerprint)
|
|
499
|
+
throw fail('FOUNDER_CONTEXT_CHANGED');
|
|
500
|
+
s.pending = { basisHash: fingerprint, missionId, operation: action.operation, ...(action.targetId ? { targetId: action.targetId } : {}), body, key: 'founder_' + crypto.randomUUID() };
|
|
501
|
+
await save();
|
|
502
|
+
await replay();
|
|
503
|
+
steps = attempts;
|
|
504
|
+
if (s.deferred.some(p => p.key === s.pending?.key || p.missionId === item.id && p.resourceKind === undefined))
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
s.cursor = (position + 1) % inventory.length;
|
|
508
|
+
await save();
|
|
509
|
+
}
|
|
510
|
+
catch (e) {
|
|
511
|
+
check();
|
|
512
|
+
const local = e?.code;
|
|
513
|
+
const isolated = typeof local === 'string' && ['INVALID_FOUNDER_DECISION', 'FOUNDER_ACTION_NOT_OFFERED', 'FOUNDER_CONTEXT_CHANGED', 'FOUNDER_BODY_TOO_LARGE', 'FOUNDER_CONTEXT_TRUNCATED', 'FOUNDER_CONTEXT_TOO_LARGE', 'FOUNDER_AUTHORITY_REQUIRED', 'FOUNDER_APPROVED_BINDING_REQUIRED'].includes(local);
|
|
514
|
+
if (!isolated && (!(e instanceof AgentelApiError) || ![400, 401, 402, 403, 404, 409, 410, 422].includes(e.status)))
|
|
515
|
+
throw e;
|
|
516
|
+
missionIssues.push({ missionId: item.id, code: isolated ? local : e.code });
|
|
517
|
+
s.cursor = (position + 1) % inventory.length;
|
|
518
|
+
await save();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch (e) {
|
|
523
|
+
const raw = contactError ? 'FOUNDER_CONTACT_UNAVAILABLE' : e instanceof AgentelApiError ? e.code : e?.code;
|
|
524
|
+
code = typeof raw === 'string' && /^[A-Z0-9_]{1,80}$/.test(raw) ? raw : 'FOUNDER_RUN_FAILED';
|
|
525
|
+
if (e instanceof AgentelApiError && e.retryAfterMs !== null)
|
|
526
|
+
s.notBefore = Math.max(s.notBefore, now() + e.retryAfterMs);
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
try {
|
|
530
|
+
if (!controller.signal.aborted && now() < deadline)
|
|
531
|
+
await bounded(() => contact('END'));
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
code ??= 'FOUNDER_CONTACT_UNAVAILABLE';
|
|
535
|
+
}
|
|
536
|
+
clearInterval(interval);
|
|
537
|
+
if (contactInFlight)
|
|
538
|
+
await contactInFlight;
|
|
539
|
+
clearTimeout(timer);
|
|
540
|
+
run.signal?.removeEventListener('abort', stop);
|
|
541
|
+
s.notBefore = Math.max(s.notBefore, now() + 3300000);
|
|
542
|
+
const retryFloors = inventory.map(item => s.deferred?.find(p => sameResource(p, { missionId: item.id, ...(item.kind === 'CREATION_REQUEST' ? { resourceKind: 'CREATION_REQUEST' } : {}) }))?.retryAt ?? 0);
|
|
543
|
+
if (retryFloors.length && retryFloors.every(t => t > now()))
|
|
544
|
+
s.notBefore = Math.max(s.notBefore, Math.min(...retryFloors));
|
|
545
|
+
await save();
|
|
546
|
+
}
|
|
547
|
+
const allDeferred = inventory.length > 0 && inventory.every(item => s.deferred.some(p => sameResource(p, { missionId: item.id, ...(item.kind === 'CREATION_REQUEST' ? { resourceKind: 'CREATION_REQUEST' } : {}) })));
|
|
548
|
+
const jitter = Math.max(0, Math.min(1, (o.random ?? Math.random)()));
|
|
549
|
+
return { status: code || allDeferred ? 'BLOCKED' : completed || rejected ? 'PROGRESSED' : missionIssues.length ? 'BLOCKED' : 'IDLE', completed, rejected, missionIssues, code: code ?? (missionIssues.length ? missionIssues[0].code : null), nextCheckAt: Math.max(s.notBefore, now() + 3600000 + (jitter * 2 - 1) * 300000) };
|
|
550
|
+
}) ?? { status: 'BUSY', completed: 0, rejected: 0, missionIssues: [], code: 'LOCAL_WORKER_LEASE_BUSY', nextCheckAt: null };
|
|
551
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type AgentelConnector, type AgentelReviewRuntimeHeartbeat, type AgentelReviewTask, type AgentelReviewVerdict, type AgentelReviewOutcomeResponse } from './agentel-connector.js';
|
|
2
|
+
type Pending = {
|
|
3
|
+
kind: 'CLAIM' | 'VERDICT' | 'RELEASE' | 'RESPONSE';
|
|
4
|
+
taskId: string;
|
|
5
|
+
key: string;
|
|
6
|
+
body: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
export type ReviewWorkerState = {
|
|
9
|
+
schema: 1;
|
|
10
|
+
instanceId: string;
|
|
11
|
+
notBefore: number;
|
|
12
|
+
cursor: string | null;
|
|
13
|
+
taskId: string | null;
|
|
14
|
+
claimId: string | null;
|
|
15
|
+
generation: number | null;
|
|
16
|
+
digest: string | null;
|
|
17
|
+
pending: Pending | null;
|
|
18
|
+
heartbeat: {
|
|
19
|
+
body: AgentelReviewRuntimeHeartbeat;
|
|
20
|
+
key: string;
|
|
21
|
+
} | null;
|
|
22
|
+
};
|
|
23
|
+
export type ReviewWorkerSession = {
|
|
24
|
+
load(): Promise<ReviewWorkerState | null>;
|
|
25
|
+
save(state: ReviewWorkerState): Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
/** Must be durable, encrypted and exclusive across cooperating replicas. No in-memory production fallback. */
|
|
28
|
+
export type ReviewWorkerStore = {
|
|
29
|
+
exclusive<T>(work: (session: ReviewWorkerSession) => Promise<T>): Promise<T | null>;
|
|
30
|
+
};
|
|
31
|
+
export type ReviewWorkerJudge = {
|
|
32
|
+
ready(signal: AbortSignal): Promise<boolean>;
|
|
33
|
+
inspect(task: Readonly<AgentelReviewTask>, signal: AbortSignal, repair?: {
|
|
34
|
+
attempt: 2;
|
|
35
|
+
reason: 'INVALID_MODEL_OUTPUT';
|
|
36
|
+
}): Promise<Pick<AgentelReviewVerdict, 'decision' | 'note' | 'evidence_ids' | 'unavailable_evidence_ids'>>;
|
|
37
|
+
respond(context: Readonly<Record<string, unknown>>, signal: AbortSignal): Promise<Pick<AgentelReviewOutcomeResponse, 'decision' | 'note'>>;
|
|
38
|
+
};
|
|
39
|
+
export type ReviewWorkerOptions = {
|
|
40
|
+
agentId: string;
|
|
41
|
+
deploymentId: string;
|
|
42
|
+
client: AgentelConnector;
|
|
43
|
+
store: ReviewWorkerStore;
|
|
44
|
+
judge: ReviewWorkerJudge;
|
|
45
|
+
now?: () => number;
|
|
46
|
+
random?: () => number;
|
|
47
|
+
};
|
|
48
|
+
export type ReviewWorkerRun = {
|
|
49
|
+
maxTasks: number;
|
|
50
|
+
maxInboxPages: number;
|
|
51
|
+
maxRunMs: number;
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
};
|
|
54
|
+
export type ReviewWorkerResult = {
|
|
55
|
+
status: 'IDLE' | 'PROGRESSED' | 'BLOCKED' | 'WAIT_NEXT_CHECK' | 'BUSY';
|
|
56
|
+
completed: number;
|
|
57
|
+
nextCheckAt: number | null;
|
|
58
|
+
code: string | null;
|
|
59
|
+
};
|
|
60
|
+
/** A bounded run, not a daemon or a model service. The operator schedules the returned next check. */
|
|
61
|
+
export declare function runReviewWorker(o: ReviewWorkerOptions, run: ReviewWorkerRun): Promise<ReviewWorkerResult>;
|
|
62
|
+
export {};
|