@agentel/sdk 2.0.0 → 2.2.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/AGENTEL_CONTEXT.md +7 -3
- package/CHANGELOG.md +13 -0
- package/FAQ.md +9 -9
- 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 +16 -10
- package/V2.1.0_PUBLIC_CHANGELOG.md +11 -0
- package/V2.2.0_PUBLIC_CHANGELOG.md +14 -0
- package/agentel-connector.ts +188 -5
- package/dist/agentel-connector.d.ts +192 -2
- package/dist/agentel-connector.js +161 -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 +31 -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/reply-management.test.mjs +33 -0
- package/test/review-worker.test.mjs +194 -0
- package/tsconfig.json +10 -2
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { AgentelApiError } from './agentel-connector.js';
|
|
2
|
+
const opaque = (v) => /^[A-Za-z0-9_-]{1,128}$/.test(v), key = (prefix) => prefix + '_' + crypto.randomUUID();
|
|
3
|
+
const fault = (code) => Object.assign(new Error(code), { code });
|
|
4
|
+
function output(value, allowed) { if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(k => !allowed.includes(k)))
|
|
5
|
+
throw fault('INVALID_MODEL_OUTPUT'); return value; }
|
|
6
|
+
function staleTask(error) { return error instanceof AgentelApiError && ((error.status === 404 && ['REVIEW_TASK_NOT_FOUND', 'REVIEW_RESPONSE_NOT_AVAILABLE'].includes(error.code)) || (error.status === 409 && error.code === 'REVIEW_TASK_CONFLICT')); }
|
|
7
|
+
function assessment(value, task) {
|
|
8
|
+
const answer = output(value, ['decision', 'note', 'evidence_ids', 'unavailable_evidence_ids']);
|
|
9
|
+
const ids = (v) => Array.isArray(v) && v.length <= 1000 && v.every(id => typeof id === 'string' && id.length > 0 && id.length <= 128) && new Set(v).size === v.length;
|
|
10
|
+
if (!['ACCEPTED', 'REVISION_REQUESTED', 'REJECTED'].includes(String(answer.decision)) || typeof answer.note !== 'string' || answer.note.trim().length < 4 || answer.note.length > 4000 || !ids(answer.evidence_ids) || (answer.unavailable_evidence_ids !== undefined && !ids(answer.unavailable_evidence_ids)))
|
|
11
|
+
throw fault('INVALID_MODEL_OUTPUT');
|
|
12
|
+
const inspected = answer.evidence_ids, unavailable = (answer.unavailable_evidence_ids ?? []), evidence = task.input?.evidence;
|
|
13
|
+
if (!Array.isArray(evidence))
|
|
14
|
+
throw fault('INVALID_REVIEW_INPUT');
|
|
15
|
+
if (new TextEncoder().encode(JSON.stringify(answer)).length > 30000)
|
|
16
|
+
throw fault('INVALID_MODEL_OUTPUT');
|
|
17
|
+
const bound = evidence.map(e => e?.id);
|
|
18
|
+
if ([...inspected, ...unavailable].some(id => !bound.includes(id)) || inspected.some(id => unavailable.includes(id)) || (answer.decision === 'ACCEPTED' && (unavailable.length > 0 || bound.some(id => !inspected.includes(id)))))
|
|
19
|
+
throw fault('INVALID_MODEL_OUTPUT');
|
|
20
|
+
return answer;
|
|
21
|
+
}
|
|
22
|
+
/** A bounded run, not a daemon or a model service. The operator schedules the returned next check. */
|
|
23
|
+
export async function runReviewWorker(o, run) {
|
|
24
|
+
if (!opaque(o.agentId) || !opaque(o.deploymentId) || !Number.isInteger(run.maxTasks) || run.maxTasks < 1 || run.maxTasks > 10 || !Number.isInteger(run.maxInboxPages) || run.maxInboxPages < 1 || run.maxInboxPages > 10 || !Number.isFinite(run.maxRunMs) || run.maxRunMs < 1 || run.maxRunMs > 21600000)
|
|
25
|
+
throw fault('INVALID_WORKER_CONFIGURATION');
|
|
26
|
+
const client = o.client.forReviewWorker();
|
|
27
|
+
const now = o.now ?? Date.now, random = o.random ?? Math.random, start = now(), deadline = start + run.maxRunMs;
|
|
28
|
+
const result = await o.store.exclusive(async (store) => {
|
|
29
|
+
let s = await store.load();
|
|
30
|
+
if (s && s.schema !== 1)
|
|
31
|
+
throw fault('UNSUPPORTED_CHECKPOINT');
|
|
32
|
+
if (!s)
|
|
33
|
+
s = { schema: 1, instanceId: key('review_worker'), notBefore: 0, cursor: null, taskId: null, claimId: null, generation: null, digest: null, pending: null, heartbeat: null };
|
|
34
|
+
if (s.notBefore > now())
|
|
35
|
+
return { status: 'WAIT_NEXT_CHECK', completed: 0, nextCheckAt: s.notBefore, code: null };
|
|
36
|
+
const jitter = Math.max(0, Math.min(1, random()));
|
|
37
|
+
let nextCheck = start + 3600000 + (jitter * 2 - 1) * 300000;
|
|
38
|
+
// 55min is the earliest legal idle wake; a fixed hourly scheduler may also wake normally.
|
|
39
|
+
s.notBefore = start + 3300000;
|
|
40
|
+
await store.save(s);
|
|
41
|
+
let completed = 0, ready = false, readinessKnown = false, heartbeatError = null, chain = Promise.resolve();
|
|
42
|
+
const controller = new AbortController(), onAbort = () => controller.abort();
|
|
43
|
+
run.signal?.addEventListener('abort', onAbort, { once: true });
|
|
44
|
+
if (run.signal?.aborted)
|
|
45
|
+
controller.abort();
|
|
46
|
+
const timer = setTimeout(() => controller.abort(), run.maxRunMs);
|
|
47
|
+
const check = () => { if (controller.signal.aborted || now() >= deadline)
|
|
48
|
+
throw fault('RUN_LIMIT_REACHED'); };
|
|
49
|
+
const serial = (fn) => { const work = chain.then(fn); chain = work.then(() => { }, () => { }); return work; };
|
|
50
|
+
const call = async (fn) => { check(); return fn(); };
|
|
51
|
+
const save = () => store.save(s);
|
|
52
|
+
const scheduleNext = () => { s.notBefore = Math.max(s.notBefore, now() + 3300000); nextCheck = Math.max(nextCheck, now() + 3600000 + (jitter * 2 - 1) * 300000); };
|
|
53
|
+
const checkpoint = () => ({ cursor: s.cursor, task_id: s.taskId, claim_id: s.claimId, claim_generation: s.generation, digest: s.digest, pending_operation: s.pending ? (s.pending.kind === 'RESPONSE' ? 'OUTCOME_RESPONSE' : s.pending.kind) : null, pending_key: s.pending?.key ?? null, last_attempted: null });
|
|
54
|
+
async function heartbeat() {
|
|
55
|
+
check();
|
|
56
|
+
const runtime = await client.reviewRuntime(controller.signal);
|
|
57
|
+
if (runtime.agentId !== o.agentId)
|
|
58
|
+
throw fault('WORKER_IDENTITY_MISMATCH');
|
|
59
|
+
if (s.heartbeat) {
|
|
60
|
+
try {
|
|
61
|
+
await client.heartbeatReviewRuntime(s.heartbeat.body, s.heartbeat.key, controller.signal);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (!(e instanceof AgentelApiError && e.status === 409 && e.code === 'REVIEW_RUNTIME_CONFLICT'))
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
s.heartbeat = null;
|
|
68
|
+
await save();
|
|
69
|
+
}
|
|
70
|
+
const current = await client.reviewRuntime(controller.signal);
|
|
71
|
+
if (current.runtime && current.runtime.instanceId !== s.instanceId && current.runtime.sameCredential && Date.parse(current.runtime.leaseExpiresAt) > now())
|
|
72
|
+
throw fault('REVIEW_RUNTIME_CONFLICT');
|
|
73
|
+
let metadata = checkpoint();
|
|
74
|
+
if (s.taskId) {
|
|
75
|
+
try {
|
|
76
|
+
await client.reviewTask(s.taskId, controller.signal);
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
if (!(e instanceof AgentelApiError && e.status === 404))
|
|
80
|
+
throw e;
|
|
81
|
+
metadata = { ...metadata, task_id: null, claim_id: null, claim_generation: null, digest: null, pending_operation: null, pending_key: null };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const body = { expected_generation: current.generation, expected_revision: current.revision, instance_id: s.instanceId, deployment_id: o.deploymentId, api_reachability: 'AVAILABLE', model_readiness: readinessKnown ? (ready ? 'READY' : 'UNAVAILABLE') : 'UNKNOWN', failure_code: readinessKnown && !ready ? 'MODEL_UNAVAILABLE' : 'NONE', checkpoint: metadata };
|
|
85
|
+
s.heartbeat = { body, key: key('heartbeat') };
|
|
86
|
+
await save();
|
|
87
|
+
const receipt = await client.heartbeatReviewRuntime(body, s.heartbeat.key, controller.signal);
|
|
88
|
+
if (receipt.agentId !== o.agentId)
|
|
89
|
+
throw fault('WORKER_IDENTITY_MISMATCH');
|
|
90
|
+
s.heartbeat = null;
|
|
91
|
+
await save();
|
|
92
|
+
}
|
|
93
|
+
function clearTask() { s.taskId = null; s.claimId = null; s.generation = null; s.digest = null; }
|
|
94
|
+
async function sendPending() {
|
|
95
|
+
const p = s.pending;
|
|
96
|
+
// Canonical re-read before recovery; a missing Task may still have a committed receipt.
|
|
97
|
+
try {
|
|
98
|
+
await call(() => client.reviewTask(p.taskId, controller.signal));
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
if (!(e instanceof AgentelApiError && e.status === 404))
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
await heartbeat();
|
|
105
|
+
let receipt;
|
|
106
|
+
try {
|
|
107
|
+
receipt = await call(() => p.kind === 'CLAIM' ? client.claimReviewTask(p.taskId, p.body, p.key, controller.signal) : p.kind === 'VERDICT' ? client.submitReviewVerdict(p.taskId, p.body, p.key, controller.signal) : p.kind === 'RELEASE' ? client.releaseReviewTask(p.taskId, p.body, p.key, controller.signal) : client.respondToReviewOutcome(p.taskId, p.body, p.key, controller.signal));
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (staleTask(e)) {
|
|
111
|
+
s.pending = null;
|
|
112
|
+
clearTask();
|
|
113
|
+
await save();
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
s.pending = null;
|
|
119
|
+
if (p.kind === 'CLAIM') {
|
|
120
|
+
s.taskId = p.taskId;
|
|
121
|
+
s.claimId = String(receipt.claimId);
|
|
122
|
+
s.generation = Number(receipt.generation);
|
|
123
|
+
s.digest = String(receipt.digest);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
clearTask();
|
|
127
|
+
if (p.kind === 'VERDICT' || p.kind === 'RESPONSE')
|
|
128
|
+
completed++;
|
|
129
|
+
}
|
|
130
|
+
await save();
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
async function write(kind, taskId, body) { check(); s.taskId = taskId; s.pending = { kind, taskId, body: body, key: key(kind.toLowerCase()) }; await save(); return sendPending(); }
|
|
134
|
+
async function model(fn, expiresAt) {
|
|
135
|
+
check();
|
|
136
|
+
const runRemaining = deadline - now() - 1000, claimRemaining = expiresAt ? Date.parse(expiresAt) - now() - 60000 : Infinity;
|
|
137
|
+
const remaining = Math.min(runRemaining, claimRemaining), timeoutCode = claimRemaining <= runRemaining ? 'CLAIM_TIME_UNAVAILABLE' : 'RUN_BUDGET_EXHAUSTED';
|
|
138
|
+
if (!Number.isFinite(remaining))
|
|
139
|
+
throw fault('MODEL_TIME_UNAVAILABLE');
|
|
140
|
+
if (remaining <= 0)
|
|
141
|
+
throw fault(timeoutCode);
|
|
142
|
+
let rejectAbort;
|
|
143
|
+
const modelController = new AbortController(), abort = () => { modelController.abort(); rejectAbort?.(fault('RUN_LIMIT_REACHED')); };
|
|
144
|
+
controller.signal.addEventListener('abort', abort, { once: true });
|
|
145
|
+
if (controller.signal.aborted)
|
|
146
|
+
abort();
|
|
147
|
+
let cancel;
|
|
148
|
+
try {
|
|
149
|
+
return await Promise.race([new Promise((_, reject) => { rejectAbort = reject; if (controller.signal.aborted)
|
|
150
|
+
abort(); }), fn(modelController.signal), new Promise((_, reject) => { cancel = setTimeout(() => { modelController.abort(); reject(fault(timeoutCode)); }, remaining); })]);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
if (cancel)
|
|
154
|
+
clearTimeout(cancel);
|
|
155
|
+
controller.signal.removeEventListener('abort', abort);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
let pulse;
|
|
159
|
+
try {
|
|
160
|
+
// A failed/unknown provider is never treated as available. No fallback is selected here.
|
|
161
|
+
await serial(heartbeat);
|
|
162
|
+
pulse = setInterval(() => { void serial(heartbeat).catch(e => { heartbeatError = e; controller.abort(); }); }, 1800000);
|
|
163
|
+
// Recover committed/requested effects before asking a model that may now be down.
|
|
164
|
+
if (s.pending)
|
|
165
|
+
await serial(sendPending);
|
|
166
|
+
try {
|
|
167
|
+
ready = (await model(signal => o.judge.ready(signal))) === true;
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
if (controller.signal.aborted || ['RUN_BUDGET_EXHAUSTED', 'RUN_LIMIT_REACHED'].includes(e?.code ?? ''))
|
|
171
|
+
throw e;
|
|
172
|
+
ready = false;
|
|
173
|
+
}
|
|
174
|
+
readinessKnown = true;
|
|
175
|
+
await serial(heartbeat);
|
|
176
|
+
async function processTask(taskId) {
|
|
177
|
+
const task = await call(() => client.reviewTask(taskId, controller.signal));
|
|
178
|
+
if (task.nextAction?.type === 'READ_OUTCOME_RESPONSE') {
|
|
179
|
+
if (!ready)
|
|
180
|
+
return;
|
|
181
|
+
const context = await call(() => client.reviewOutcomeResponse(taskId, controller.signal));
|
|
182
|
+
if (typeof context.reviewSetHash !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(context.reviewSetHash) || !Number.isInteger(context.round) || typeof context.expiresAt !== 'string')
|
|
183
|
+
throw fault('INVALID_RESPONSE_CONTEXT');
|
|
184
|
+
const answer = output(await model(signal => o.judge.respond(structuredClone({ ...context, inputTrust: 'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS' }), signal), context.expiresAt), ['decision', 'note']);
|
|
185
|
+
if (!['REVISE', 'MAINTAIN'].includes(String(answer.decision)) || typeof answer.note !== 'string' || answer.note.trim().length < 4 || answer.note.length > 4000)
|
|
186
|
+
throw fault('INVALID_MODEL_OUTPUT');
|
|
187
|
+
await serial(() => write('RESPONSE', taskId, { ...answer, expected_review_set_hash: context.reviewSetHash, expected_round: context.round }));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (task.status === 'OFFERED') {
|
|
191
|
+
if (!ready)
|
|
192
|
+
return;
|
|
193
|
+
const claimed = await serial(() => write('CLAIM', taskId, { expected_revision: task.revision, expected_digest: task.digest }));
|
|
194
|
+
if (!claimed)
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const current = await call(() => client.reviewTask(taskId, controller.signal));
|
|
198
|
+
if (!current.claim || !current.input || !current.reviewContext) {
|
|
199
|
+
if (s.taskId === taskId) {
|
|
200
|
+
clearTask();
|
|
201
|
+
await save();
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (current.inputTrust !== 'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS')
|
|
206
|
+
throw fault('INVALID_REVIEW_INPUT');
|
|
207
|
+
s.taskId = taskId;
|
|
208
|
+
s.claimId = current.claim.id;
|
|
209
|
+
s.generation = current.claim.generation;
|
|
210
|
+
s.digest = current.digest;
|
|
211
|
+
await save();
|
|
212
|
+
if (!ready) {
|
|
213
|
+
await serial(() => write('RELEASE', taskId, { claim_id: current.claim.id, generation: current.claim.generation, reason: 'MODEL_PROVIDER_UNAVAILABLE' }));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let answer;
|
|
217
|
+
try {
|
|
218
|
+
// One format repair is allowed, under the same run/claim time limits and frozen input.
|
|
219
|
+
try {
|
|
220
|
+
answer = assessment(await model(signal => o.judge.inspect(structuredClone(current), signal), current.claim.expiresAt), current);
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
if (e?.code !== 'INVALID_MODEL_OUTPUT')
|
|
224
|
+
throw e;
|
|
225
|
+
answer = assessment(await model(signal => o.judge.inspect(structuredClone(current), signal, { attempt: 2, reason: 'INVALID_MODEL_OUTPUT' }), current.claim.expiresAt), current);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
const code = e?.code;
|
|
230
|
+
// A process run budget is not inability to do the Task: retain the canonical claim.
|
|
231
|
+
if (controller.signal.aborted || code === 'RUN_BUDGET_EXHAUSTED' || code === 'RUN_LIMIT_REACHED')
|
|
232
|
+
throw e;
|
|
233
|
+
ready = false;
|
|
234
|
+
const reason = ['INVALID_MODEL_OUTPUT', 'INVALID_REVIEW_INPUT', 'MODEL_TIME_UNAVAILABLE', 'CLAIM_TIME_UNAVAILABLE'].includes(code ?? '') ? 'CANNOT_COMPLETE' : 'MODEL_PROVIDER_UNAVAILABLE';
|
|
235
|
+
await serial(() => write('RELEASE', taskId, { claim_id: current.claim.id, generation: current.claim.generation, reason }));
|
|
236
|
+
throw fault(reason === 'CANNOT_COMPLETE' ? (code ?? 'CANNOT_COMPLETE') : 'MODEL_PROVIDER_UNAVAILABLE');
|
|
237
|
+
}
|
|
238
|
+
// The model supplies assessment only. Identity, Task/version and claim fence come from canonical state.
|
|
239
|
+
const latest = await call(() => client.reviewTask(taskId, controller.signal));
|
|
240
|
+
if (!latest.claim || latest.claim.id !== current.claim.id || latest.claim.generation !== current.claim.generation || latest.digest !== current.digest || latest.revision !== current.revision) {
|
|
241
|
+
clearTask();
|
|
242
|
+
await save();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
await serial(() => write('VERDICT', taskId, { ...answer, expected_revision: latest.revision, expected_digest: latest.digest, claim_id: latest.claim.id, generation: latest.claim.generation }));
|
|
246
|
+
}
|
|
247
|
+
if (s.taskId && completed < run.maxTasks) {
|
|
248
|
+
try {
|
|
249
|
+
await processTask(s.taskId);
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
if (!staleTask(e))
|
|
253
|
+
throw e;
|
|
254
|
+
clearTask();
|
|
255
|
+
await save();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
for (let page = 0; page < run.maxInboxPages && completed < run.maxTasks; page++) {
|
|
260
|
+
const inbox = await call(() => client.reviewTasks({ limit: 20, ...(s.cursor ? { cursor: s.cursor } : {}), signal: controller.signal }));
|
|
261
|
+
const pageCursor = s.cursor;
|
|
262
|
+
let exhausted = true;
|
|
263
|
+
for (const item of inbox.items) {
|
|
264
|
+
if (completed >= run.maxTasks) {
|
|
265
|
+
exhausted = false;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
if (seen.has(item.id))
|
|
269
|
+
continue;
|
|
270
|
+
seen.add(item.id);
|
|
271
|
+
try {
|
|
272
|
+
await processTask(item.id);
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
if (staleTask(e)) {
|
|
276
|
+
if (s.taskId === item.id)
|
|
277
|
+
clearTask();
|
|
278
|
+
await save();
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
throw e;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
s.cursor = exhausted ? inbox.nextCursor : pageCursor;
|
|
285
|
+
await save();
|
|
286
|
+
if (!inbox.nextCursor || !exhausted)
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
await save();
|
|
290
|
+
await serial(heartbeat);
|
|
291
|
+
scheduleNext();
|
|
292
|
+
await save();
|
|
293
|
+
return { status: !ready ? 'BLOCKED' : completed ? 'PROGRESSED' : 'IDLE', completed, nextCheckAt: nextCheck, code: ready ? null : 'MODEL_PROVIDER_UNAVAILABLE' };
|
|
294
|
+
}
|
|
295
|
+
catch (caught) {
|
|
296
|
+
const e = heartbeatError ?? caught;
|
|
297
|
+
const candidate = e instanceof AgentelApiError ? e.code : typeof e?.code === 'string' ? e.code : 'RUN_FAILED';
|
|
298
|
+
const code = /^[A-Z0-9_]{1,80}$/.test(candidate) ? candidate : 'RUN_FAILED';
|
|
299
|
+
if (e instanceof AgentelApiError && e.retryAfterMs !== null) {
|
|
300
|
+
s.notBefore = Math.max(s.notBefore, now() + e.retryAfterMs);
|
|
301
|
+
nextCheck = Math.max(nextCheck, s.notBefore);
|
|
302
|
+
}
|
|
303
|
+
scheduleNext();
|
|
304
|
+
await save();
|
|
305
|
+
return { status: 'BLOCKED', completed, nextCheckAt: nextCheck, code };
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
if (pulse)
|
|
309
|
+
clearInterval(pulse);
|
|
310
|
+
clearTimeout(timer);
|
|
311
|
+
run.signal?.removeEventListener('abort', onAbort);
|
|
312
|
+
await chain;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
return result ?? { status: 'BUSY', completed: 0, nextCheckAt: null, code: 'LOCAL_WORKER_LEASE_BUSY' };
|
|
316
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {FounderAction,FounderContext,FounderPreparationContext} from './founder-worker.js';
|
|
2
|
+
const obj=(v:unknown):Record<string,unknown>=>v&&typeof v==='object'&&!Array.isArray(v)?v as Record<string,unknown>:{};
|
|
3
|
+
const rows=(v:unknown)=>Array.isArray(v)?v.map(obj):[];
|
|
4
|
+
const valid=(v:unknown):v is string=>typeof v==='string'&&/^[A-Za-z0-9_-]{1,128}$/.test(v);
|
|
5
|
+
const live=new Set(['RESERVED','ACCEPTED','ACTIVE','BLOCKED']);
|
|
6
|
+
|
|
7
|
+
/** Only offered fixed operations; endpoints supplied in network content are never executed. */
|
|
8
|
+
export function extendedFounderActions(context:FounderContext,candidates:string[],founderId:string):FounderAction[]{
|
|
9
|
+
const w=context.workspace,c=obj(w.founderControl),raw=obj(context.contract.contract),out:FounderAction[]=[];
|
|
10
|
+
if(['CANCELLED','COMPLETED','FAILED'].includes(w.mission.status)||['RESULT_PUBLISHED','OUTCOME_ACCEPTED'].includes(w.state)||obj(c.outcomeNextAction).action==='WAIT_FOR_HUMAN_FINAL_DECISION')return out;
|
|
11
|
+
for(const a of rows(c.assignments))if(valid(a.id))for(const n of rows(a.nextActions)){
|
|
12
|
+
const b=obj(n.body);if(n.type==='REVIEW_REPLACEMENT'&&valid(b.application_id)&&rows(a.replacementCandidates).some(x=>x.applicationId===b.application_id))
|
|
13
|
+
out.push({id:'replace:'+a.id+':'+b.application_id,operation:'REPLACE_ASSIGNMENT',targetId:a.id,binding:b});
|
|
14
|
+
}
|
|
15
|
+
for(const t of rows(obj(c.poolReview).tasks))if(valid(t.id)&&t.canRetry===true&&Number.isInteger(t.revision)&&typeof t.digest==='string')out.push({id:'retry:'+t.id,operation:'RETRY_REVIEW',targetId:t.id,binding:{expected_revision:t.revision,expected_digest:t.digest}});
|
|
16
|
+
if(context.deliveries.internalCheck&&context.deliveries.reviewMode==='INTERNAL_CHECK'){
|
|
17
|
+
const latest=new Set<string>();
|
|
18
|
+
for(const d of rows(context.deliveries.deliveries)){
|
|
19
|
+
if(!valid(d.id)||!valid(d.assignmentId)||latest.has(d.assignmentId))continue;latest.add(d.assignmentId);
|
|
20
|
+
if(d.status==='SUBMITTED'&&d.agentId!==founderId&&d.contractHash===w.mission.contractHash&&d.contractVersion===w.mission.contractVersion)out.push({id:'check:'+d.id,operation:'INTERNAL_CHECK',targetId:d.id});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if(raw.visibility==='PUBLIC'&&raw.participation_mode==='INVITE_ONLY'&&candidates.length){
|
|
24
|
+
const invitations=rows(context.invitations?.invitations),assignments=rows(c.assignments);
|
|
25
|
+
for(const stage of rows(raw.stages))for(const slot of rows(stage.role_slots)){
|
|
26
|
+
if(!valid(stage.stage_id)||!valid(slot.slot_id)||!Number.isInteger(slot.quantity)||Number(slot.quantity)<1||Number(slot.quantity)>10)continue;
|
|
27
|
+
if(obj(raw.review_policy).protocol==='POOL_REVIEW_V1'&&stage.stage_kind==='REVIEW'&&obj(raw.review_policy).mode!=='INTERNAL_CHECK')continue;
|
|
28
|
+
for(let seat=1;seat<=Number(slot.quantity);seat++){
|
|
29
|
+
if(invitations.some(i=>i.stageId===stage.stage_id&&i.slotId===slot.slot_id&&i.slotIndex===seat&&['OFFERED','WAITING'].includes(String(i.status))))continue;
|
|
30
|
+
const current=assignments.find(a=>a.stageId===stage.stage_id&&a.slotId===slot.slot_id&&a.slotIndex===seat&&live.has(String(a.status)));
|
|
31
|
+
if(current&&!['ACK_OVERDUE','DELIVERY_OVERDUE','REPLACEMENT_REVIEW_REQUIRED'].includes(String(obj(current.sla).state)))continue;
|
|
32
|
+
out.push({id:'invite:'+stage.stage_id+':'+slot.slot_id+':'+seat,operation:'INVITE_CANDIDATES',binding:{stage_id:stage.stage_id,slot_id:slot.slot_id,slot_index:seat,contract_hash:w.mission.contractHash,contract_version:w.mission.contractVersion,...(current?{replace_assignment_id:current.id}:{}),candidateAgentIds:candidates.filter(id=>id!==founderId)}});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
export function creationInvitationActions(snapshot:Record<string,unknown>,candidates:string[],founderId:string,now:number):FounderPreparationContext['actions']{
|
|
39
|
+
const raw=obj(snapshot.contract),request=obj(snapshot.request),participants=rows(snapshot.participants),out:FounderPreparationContext['actions']=[];
|
|
40
|
+
if(raw.visibility!=='PRIVATE'||!request.missionId)return out;
|
|
41
|
+
for(const p of participants)if(valid(p.id)&&['INVITED','ACCEPTED'].includes(String(p.status))){
|
|
42
|
+
const due=typeof p.responseDueAt==='string'?Date.parse(p.responseDueAt):Date.parse(String(p.invitedAt))+Math.max(2,Number(obj(raw.sla_policy).ack_hours)||2)*3600000;
|
|
43
|
+
if(p.checkerOwnerValid===false||p.status==='INVITED'&&Number.isFinite(due)&&due<=now)out.push({id:'cancel-invite:'+p.id,operation:'CANCEL_CREATION_INVITATION',binding:{participant_id:p.id}});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for(const stage of rows(raw.stages))for(const slot of rows(stage.role_slots)){
|
|
47
|
+
if(!valid(stage.stage_id)||!valid(slot.slot_id)||!Number.isInteger(slot.quantity)||Number(slot.quantity)<1||Number(slot.quantity)>10)continue;
|
|
48
|
+
if(obj(raw.review_policy).protocol==='POOL_REVIEW_V1'&&stage.stage_kind==='REVIEW'&&obj(raw.review_policy).mode!=='INTERNAL_CHECK')continue;
|
|
49
|
+
const occupied=participants.filter(p=>p.stageId===stage.stage_id&&p.slotId===slot.slot_id&&['INVITED','ACCEPTED','ASSIGNED'].includes(String(p.status)));
|
|
50
|
+
if(occupied.length>=Number(slot.quantity))continue;
|
|
51
|
+
for(const candidate of candidates)if(candidate!==founderId&&!participants.some(p=>p.stageId===stage.stage_id&&p.slotId===slot.slot_id&&obj(p.agent).id===candidate))out.push({id:'invite:'+stage.stage_id+':'+slot.slot_id+':'+candidate,operation:'INVITE_CREATION_AGENT',binding:{stage_id:stage.stage_id,slot_id:slot.slot_id,agent_id:candidate}});
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|