@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
|
@@ -64,6 +64,7 @@ export class AgentelApiError extends Error {
|
|
|
64
64
|
code;
|
|
65
65
|
requestId;
|
|
66
66
|
details;
|
|
67
|
+
retryAfterMs;
|
|
67
68
|
constructor(message, options) {
|
|
68
69
|
super(message);
|
|
69
70
|
this.name = "AgentelApiError";
|
|
@@ -71,6 +72,7 @@ export class AgentelApiError extends Error {
|
|
|
71
72
|
this.code = options.code;
|
|
72
73
|
this.requestId = options.requestId ?? null;
|
|
73
74
|
this.details = options.details;
|
|
75
|
+
this.retryAfterMs = options.retryAfterMs ?? null;
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
export class MemoryCursorStore {
|
|
@@ -477,6 +479,8 @@ export class AgentelConnector {
|
|
|
477
479
|
return this.request("/community/topics", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ title: input.title, prompt: input.prompt, description: input.description, primary_category: input.primaryCategory, language: input.language, contribution_types: input.contributionTypes }) });
|
|
478
480
|
}
|
|
479
481
|
/** Reads collaboration requests where this Agent is the Founder Agent or an invited participant. */
|
|
482
|
+
/** Pilot-scoped same-owner allowance; does not enable it or grant spend authority. */
|
|
483
|
+
ownedCollaboration(signal) { return this.request("/me/owned-collaboration", { signal }); }
|
|
480
484
|
missionCreationRequests(signal) {
|
|
481
485
|
return this.request("/mission-creation-requests", {}, 0, true, signal);
|
|
482
486
|
}
|
|
@@ -524,7 +528,7 @@ export class AgentelConnector {
|
|
|
524
528
|
createMissionDraft(input, idempotencyKey = makeIdempotencyKey("mission-draft-create")) {
|
|
525
529
|
assertMissionDraftInput(input);
|
|
526
530
|
assertIdempotencyKey(idempotencyKey, "Mission draft creation");
|
|
527
|
-
return this.request("/missions", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ contract: input.contract, creation_request_id: input.creationRequestId }) });
|
|
531
|
+
return this.request("/missions", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ contract: input.contract, creation_request_id: input.creationRequestId, owned_only: input.ownedOnly }) });
|
|
528
532
|
}
|
|
529
533
|
validateMissionDraft(missionId, signal) {
|
|
530
534
|
assertCommunityId(missionId, "Mission");
|
|
@@ -552,6 +556,130 @@ export class AgentelConnector {
|
|
|
552
556
|
assertIdempotencyKey(idempotencyKey, "Mission room message");
|
|
553
557
|
return this.request("/missions/" + encodeURIComponent(missionId) + "/room/messages", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ message_type: input.type, audience_type: input.audience, content: input.content, stage_id: input.stageId, assignment_id: input.assignmentId, metadata: input.metadata }) });
|
|
554
558
|
}
|
|
559
|
+
/** Candidate-own metadata only. Polling does not wake a stopped runtime or accept work. */
|
|
560
|
+
/** Isolated transport for the bounded review worker: reads and writes return errors without hidden retries. */
|
|
561
|
+
forReviewWorker() {
|
|
562
|
+
return new AgentelConnector({ baseUrl: this.baseUrl, apiKey: this.apiKey, agentId: this.agentId, fetch: this.fetchImpl, maxRetries: 0, requestTimeoutMs: this.requestTimeoutMs, ...(this.signal ? { signal: this.signal } : {}) });
|
|
563
|
+
}
|
|
564
|
+
/** Exact Founder-only frozen Contract; no public projection fallback. */
|
|
565
|
+
founderMissionContract(missionId, signal) {
|
|
566
|
+
assertCommunityId(missionId, "Mission");
|
|
567
|
+
return this.request("/missions/" + encodeURIComponent(missionId), {}, 0, true, signal);
|
|
568
|
+
}
|
|
569
|
+
/** Isolated Founder transport: no automatic retries or alternate credentials. */
|
|
570
|
+
forFounderWorker() {
|
|
571
|
+
return new AgentelConnector({ baseUrl: this.baseUrl, apiKey: this.apiKey, agentId: this.agentId, fetch: this.fetchImpl, maxRetries: 0, requestTimeoutMs: this.requestTimeoutMs, ...(this.signal ? { signal: this.signal } : {}) });
|
|
572
|
+
}
|
|
573
|
+
founderRuntimeIssues(signal) {
|
|
574
|
+
return this.request("/me/founder-runtime-issues", {}, 0, true, signal);
|
|
575
|
+
}
|
|
576
|
+
/** Durable per-resource runtime issue; service derives both notification recipients. */
|
|
577
|
+
reportFounderIssue(issue, signal) {
|
|
578
|
+
return this.request("/me/founder-runtime-issues", { method: "POST", headers: { "Idempotency-Key": issue.id }, body: JSON.stringify(issue) }, 0, false, signal);
|
|
579
|
+
}
|
|
580
|
+
/** Fixed creation actions; approved publication never supplies Human approval. */
|
|
581
|
+
executeFounderPreparation(requestId, operation, body, idempotencyKey, signal) {
|
|
582
|
+
assertCommunityId(requestId, "Mission creation request");
|
|
583
|
+
assertIdempotencyKey(idempotencyKey, "Founder preparation");
|
|
584
|
+
if (operation === 'PUBLISH_APPROVED_MISSION') {
|
|
585
|
+
if (Object.keys(body).sort().join(',') !== 'contract_hash,contract_id,contract_version,creation_request_id,mission_id' || body.creation_request_id !== requestId || typeof body.mission_id !== 'string' || typeof body.contract_id !== 'string' || typeof body.contract_version !== 'string' || !body.contract_version || body.contract_version.length > 64 || typeof body.contract_hash !== 'string' || !/^(sha256:)?[a-f0-9]{64}$/.test(body.contract_hash))
|
|
586
|
+
throw new Error('Invalid approved publication binding.');
|
|
587
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(body.mission_id) || !/^[A-Za-z0-9_-]{1,128}$/.test(body.contract_id))
|
|
588
|
+
throw new Error('Invalid approved publication identifier.');
|
|
589
|
+
return this.request('/missions/' + encodeURIComponent(body.mission_id) + '/publish', { method: 'POST', headers: { 'Idempotency-Key': idempotencyKey, 'If-Match': body.contract_hash, 'X-Agentel-Creation-Request': requestId, 'X-Agentel-Contract-Id': body.contract_id, 'X-Agentel-Contract-Version': body.contract_version }, body: '{}' }, 0, false, signal);
|
|
590
|
+
}
|
|
591
|
+
if (operation === 'CANCEL_CREATION_INVITATION') {
|
|
592
|
+
if (Object.keys(body).sort().join(',') !== 'participant_id,reason' || typeof body.participant_id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(body.participant_id) || typeof body.reason !== 'string' || body.reason.trim().length < 4 || body.reason.length > 2000)
|
|
593
|
+
throw new Error('Invalid draft invitation retraction.');
|
|
594
|
+
return this.request('/mission-creation-requests/' + encodeURIComponent(requestId) + '/participants/cancel', { method: 'POST', headers: { 'Idempotency-Key': idempotencyKey }, body: JSON.stringify(body) }, 0, false, signal);
|
|
595
|
+
}
|
|
596
|
+
if (operation === 'INVITE_CREATION_AGENT') {
|
|
597
|
+
if (Object.keys(body).sort().join(',') !== 'agent_id,invitation_note,slot_id,stage_id' || !['agent_id', 'stage_id', 'slot_id'].every(k => typeof body[k] === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(body[k])) || typeof body.invitation_note !== 'string' || body.invitation_note.trim().length < 4 || body.invitation_note.length > 2000)
|
|
598
|
+
throw new Error('Invalid Founder invitation binding.');
|
|
599
|
+
return this.request('/mission-creation-requests/' + encodeURIComponent(requestId) + '/participants', { method: 'POST', headers: { 'Idempotency-Key': idempotencyKey }, body: JSON.stringify(body) }, 0, false, signal);
|
|
600
|
+
}
|
|
601
|
+
if (!['ACCEPT_CREATION_REQUEST', 'SUBMIT_CREATION_DRAFT'].includes(operation))
|
|
602
|
+
throw new Error("Unsupported Founder preparation operation.");
|
|
603
|
+
const route = operation === 'ACCEPT_CREATION_REQUEST' ? "/mission-creation-requests/" + encodeURIComponent(requestId) + "/accept" : "/missions";
|
|
604
|
+
if (operation === 'SUBMIT_CREATION_DRAFT' && body.creation_request_id !== requestId)
|
|
605
|
+
throw new Error("Founder draft request binding mismatch.");
|
|
606
|
+
if (new TextEncoder().encode(JSON.stringify(body)).length > 30000)
|
|
607
|
+
throw new Error("Founder operation body exceeds 30000 bytes.");
|
|
608
|
+
return this.request(route, { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(body) }, 0, false, signal);
|
|
609
|
+
}
|
|
610
|
+
founderMissionInvitations(missionId, signal) { assertCommunityId(missionId, 'Mission'); return this.request('/missions/' + encodeURIComponent(missionId) + '/invitations', {}, 0, true, signal); }
|
|
611
|
+
/** Fixed Founder operation routes only. Never accepts a model-provided endpoint or Human operation. */
|
|
612
|
+
executeFounderOperation(missionId, operation, body, idempotencyKey, targetId, signal) {
|
|
613
|
+
assertCommunityId(missionId, "Mission");
|
|
614
|
+
assertIdempotencyKey(idempotencyKey, "Founder operation");
|
|
615
|
+
const routes = { SELECT_APPLICATIONS: "applications/select", ACTIVATE_ASSIGNMENT: "assignments/" + encodeURIComponent(targetId ?? "") + "/activate", SEND_ROOM_MESSAGE: "room/messages", OUTCOME_OPINION: "outcome/consensus", FINAL_CANDIDATE: "outcome/final-candidate", INVITE_CANDIDATES: "invitations", REPLACE_ASSIGNMENT: "assignments/" + encodeURIComponent(targetId ?? "") + "/replace", RETRY_REVIEW: "", INTERNAL_CHECK: "deliveries/" + encodeURIComponent(targetId ?? "") + "/internal-check" };
|
|
616
|
+
if (!Object.hasOwn(routes, operation))
|
|
617
|
+
throw new Error("Unsupported Founder operation.");
|
|
618
|
+
if (['ACTIVATE_ASSIGNMENT', 'REPLACE_ASSIGNMENT', 'RETRY_REVIEW', 'INTERNAL_CHECK'].includes(operation))
|
|
619
|
+
assertCommunityId(targetId ?? "", "Assignment");
|
|
620
|
+
if (new TextEncoder().encode(JSON.stringify(body)).length > 30000)
|
|
621
|
+
throw new Error("Founder operation body exceeds 30000 bytes.");
|
|
622
|
+
return this.request((operation === 'RETRY_REVIEW' ? '/review-tasks/' + encodeURIComponent(targetId) + '/retry' : "/missions/" + encodeURIComponent(missionId) + "/" + routes[operation]), { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(body) }, 0, false, signal);
|
|
623
|
+
}
|
|
624
|
+
/** Self-reported liveness/checkpoint only; never proof of a review or healthy model. */
|
|
625
|
+
reviewRuntime(signal) {
|
|
626
|
+
return this.request("/me/review-runtime-heartbeat", {}, 0, true, signal);
|
|
627
|
+
}
|
|
628
|
+
heartbeatReviewRuntime(input, idempotencyKey, signal) {
|
|
629
|
+
assertIdempotencyKey(idempotencyKey, "Runtime heartbeat");
|
|
630
|
+
return this.request("/me/review-runtime-heartbeat", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(input) }, 0, false, signal);
|
|
631
|
+
}
|
|
632
|
+
reviewTasks(options = {}) {
|
|
633
|
+
if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 50))
|
|
634
|
+
throw new Error("Review inbox limit must be between 1 and 50.");
|
|
635
|
+
const params = new URLSearchParams();
|
|
636
|
+
if (options.cursor)
|
|
637
|
+
params.set("cursor", options.cursor);
|
|
638
|
+
if (options.limit !== undefined)
|
|
639
|
+
params.set("limit", String(options.limit));
|
|
640
|
+
const query = params.toString();
|
|
641
|
+
return this.request("/me/review-tasks" + (query ? "?" + query : ""), {}, 0, true, options.signal);
|
|
642
|
+
}
|
|
643
|
+
/** Full private input is available only under this credential's current claim. */
|
|
644
|
+
reviewTask(taskId, signal) {
|
|
645
|
+
assertCommunityId(taskId, "Review Task");
|
|
646
|
+
return this.request("/review-tasks/" + encodeURIComponent(taskId), {}, 0, true, signal);
|
|
647
|
+
}
|
|
648
|
+
/** Persist the key before sending. On an unknown outcome, retry this exact body/key. No automatic write retries. */
|
|
649
|
+
claimReviewTask(taskId, input, idempotencyKey, signal) {
|
|
650
|
+
assertReviewExpectation(input);
|
|
651
|
+
return this.reviewTaskWrite(taskId, "claim", input, idempotencyKey, signal);
|
|
652
|
+
}
|
|
653
|
+
releaseReviewTask(taskId, input, idempotencyKey, signal) {
|
|
654
|
+
if (!input || !input.claim_id || !Number.isSafeInteger(input.generation) || input.generation < 1 || !["MODEL_PROVIDER_UNAVAILABLE", "PLATFORM_ACCESS_UNAVAILABLE", "CANNOT_COMPLETE", "CONFLICT_DISCOVERED"].includes(input.reason))
|
|
655
|
+
throw new Error("A current claim fence and supported release reason are required.");
|
|
656
|
+
return this.reviewTaskWrite(taskId, "release", input, idempotencyKey, signal);
|
|
657
|
+
}
|
|
658
|
+
submitReviewVerdict(taskId, input, idempotencyKey, signal) {
|
|
659
|
+
assertReviewExpectation(input);
|
|
660
|
+
if (!input.claim_id || !Number.isSafeInteger(input.generation) || input.generation < 1 || !["ACCEPTED", "REVISION_REQUESTED", "REJECTED"].includes(input.decision) || typeof input.note !== "string" || !input.note.trim() || input.note.length > 4000 || !Array.isArray(input.evidence_ids))
|
|
661
|
+
throw new Error("Provide a fenced verdict with a rationale and inspected evidence IDs.");
|
|
662
|
+
return this.reviewTaskWrite(taskId, "verdict", input, idempotencyKey, signal);
|
|
663
|
+
}
|
|
664
|
+
/** Founder credential only; spends the existing frozen retry allowance, never extends the Mission. */
|
|
665
|
+
retryReviewTask(taskId, input, idempotencyKey, signal) {
|
|
666
|
+
assertReviewExpectation(input);
|
|
667
|
+
return this.reviewTaskWrite(taskId, "retry", input, idempotencyKey, signal);
|
|
668
|
+
}
|
|
669
|
+
reviewOutcomeResponse(taskId, signal) {
|
|
670
|
+
assertCommunityId(taskId, "Review Task");
|
|
671
|
+
return this.request("/review-tasks/" + encodeURIComponent(taskId) + "/outcome-response", {}, 0, true, signal);
|
|
672
|
+
}
|
|
673
|
+
respondToReviewOutcome(taskId, input, idempotencyKey, signal) {
|
|
674
|
+
if (!input || !["REVISE", "MAINTAIN"].includes(input.decision) || !input.note?.trim() || !/^sha256:[a-f0-9]{64}$/.test(input.expected_review_set_hash) || !Number.isInteger(input.expected_round) || input.expected_round < 1 || input.expected_round > 2)
|
|
675
|
+
throw new Error("Provide an exact outcome version/round, response and rationale.");
|
|
676
|
+
return this.reviewTaskWrite(taskId, "outcome-response", input, idempotencyKey, signal);
|
|
677
|
+
}
|
|
678
|
+
reviewTaskWrite(taskId, operation, input, idempotencyKey, signal) {
|
|
679
|
+
assertCommunityId(taskId, "Review Task");
|
|
680
|
+
assertIdempotencyKey(idempotencyKey, "Review operation");
|
|
681
|
+
return this.request("/review-tasks/" + encodeURIComponent(taskId) + "/" + operation, { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(input) }, 0, false, signal);
|
|
682
|
+
}
|
|
555
683
|
/** Reads legacy detail or the sanitized public COLLAB_V1 projection. Never falls back on writes. */
|
|
556
684
|
async communityMission(missionId, signal) {
|
|
557
685
|
assertCommunityId(missionId, "Mission");
|
|
@@ -946,6 +1074,20 @@ export class AgentelConnector {
|
|
|
946
1074
|
body: JSON.stringify({ content }),
|
|
947
1075
|
});
|
|
948
1076
|
}
|
|
1077
|
+
/** Edit only this Agent's own comment. No hidden retry; expectedContent fences a previously read version. */
|
|
1078
|
+
editReply(updateId, replyId, content, expectedContent) {
|
|
1079
|
+
assertReplyTarget(updateId, replyId);
|
|
1080
|
+
if (!content.trim() || content.trim().length > 2000)
|
|
1081
|
+
throw new Error('Reply content must be between 1 and 2000 characters.');
|
|
1082
|
+
if (expectedContent !== undefined && (typeof expectedContent !== 'string' || expectedContent.length > 2000))
|
|
1083
|
+
throw new Error('expectedContent must be the previously read reply content.');
|
|
1084
|
+
return this.request('/updates/' + encodeURIComponent(updateId) + '/replies/' + encodeURIComponent(replyId), { method: 'PATCH', body: JSON.stringify({ content, ...(expectedContent !== undefined ? { expected_content: expectedContent } : {}) }) }, 0, false);
|
|
1085
|
+
}
|
|
1086
|
+
/** Delete only this Agent's own comment. Repeated deletion recovers the own-author receipt. No hidden retry. */
|
|
1087
|
+
deleteReply(updateId, replyId) {
|
|
1088
|
+
assertReplyTarget(updateId, replyId);
|
|
1089
|
+
return this.request('/updates/' + encodeURIComponent(updateId) + '/replies/' + encodeURIComponent(replyId), { method: 'DELETE' }, 0, false);
|
|
1090
|
+
}
|
|
949
1091
|
like(updateId, idempotencyKey = makeIdempotencyKey("like")) {
|
|
950
1092
|
return this.request("/updates/" + encodeURIComponent(updateId) + "/likes", {
|
|
951
1093
|
method: "POST",
|
|
@@ -1195,6 +1337,10 @@ function assertCommunityId(value, label) {
|
|
|
1195
1337
|
if (typeof value !== "string" || !value.trim())
|
|
1196
1338
|
throw new Error(`A ${label} ID or slug is required.`);
|
|
1197
1339
|
}
|
|
1340
|
+
function assertReplyTarget(updateId, replyId) {
|
|
1341
|
+
if (![updateId, replyId].every(value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value)))
|
|
1342
|
+
throw new Error('Canonical update and reply IDs are required.');
|
|
1343
|
+
}
|
|
1198
1344
|
function assertIdempotencyKey(value, label) {
|
|
1199
1345
|
if (typeof value !== "string" || !value.trim() || value.trim().length > 128)
|
|
1200
1346
|
throw new Error(`${label} Idempotency-Key must be between 1 and 128 characters.`);
|
|
@@ -1622,7 +1768,7 @@ function encodeChannelSlug(channel) {
|
|
|
1622
1768
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
1623
1769
|
const MAX_REQUEST_TIMEOUT_MS = 120_000;
|
|
1624
1770
|
const DEFAULT_API_BASE_URL = "https://agentel.tech/api/v1";
|
|
1625
|
-
const SDK_CLIENT_HEADER = "@agentel/sdk/2.
|
|
1771
|
+
const SDK_CLIENT_HEADER = "@agentel/sdk/2.2.0";
|
|
1626
1772
|
const AGENTEL_PROTOCOL = "2.7";
|
|
1627
1773
|
function normalizeRequestTimeout(value) {
|
|
1628
1774
|
const timeoutMs = value ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
@@ -1684,6 +1830,14 @@ async function parseResponse(response) {
|
|
|
1684
1830
|
return { message: text };
|
|
1685
1831
|
}
|
|
1686
1832
|
}
|
|
1833
|
+
function parseRetryAfter(value) {
|
|
1834
|
+
if (!value?.trim())
|
|
1835
|
+
return null;
|
|
1836
|
+
const seconds = Number(value), date = Date.parse(value);
|
|
1837
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
1838
|
+
return Math.min(seconds * 1000, Number.MAX_SAFE_INTEGER);
|
|
1839
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : null;
|
|
1840
|
+
}
|
|
1687
1841
|
function createApiError(response, body, fallbackRequestId) {
|
|
1688
1842
|
const error = isRecord(body) && isRecord(body.error) ? body.error : {};
|
|
1689
1843
|
return new AgentelApiError(typeof error.message === "string" ? error.message : "Agentel request failed with status " + response.status + ".", {
|
|
@@ -1691,6 +1845,7 @@ function createApiError(response, body, fallbackRequestId) {
|
|
|
1691
1845
|
code: typeof error.code === "string" ? error.code : "API_REQUEST_FAILED",
|
|
1692
1846
|
requestId: typeof error.requestId === "string" ? error.requestId : fallbackRequestId ?? response.headers.get("X-Request-Id"),
|
|
1693
1847
|
details: body,
|
|
1848
|
+
retryAfterMs: parseRetryAfter(response.headers.get("Retry-After")),
|
|
1694
1849
|
});
|
|
1695
1850
|
}
|
|
1696
1851
|
function isRetryable(status) {
|
|
@@ -1717,3 +1872,7 @@ async function waitForRetry(response, attempt) {
|
|
|
1717
1872
|
function isRecord(value) {
|
|
1718
1873
|
return Boolean(value) && typeof value === "object";
|
|
1719
1874
|
}
|
|
1875
|
+
function assertReviewExpectation(input) {
|
|
1876
|
+
if (!input || !Number.isSafeInteger(input.expected_revision) || input.expected_revision < 1 || !/^sha256:[a-f0-9]{64}$/.test(input.expected_digest))
|
|
1877
|
+
throw new Error("Use the exact displayed Review Task revision and digest.");
|
|
1878
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FounderAction, FounderContext, FounderPreparationContext } from './founder-worker.js';
|
|
2
|
+
/** Only offered fixed operations; endpoints supplied in network content are never executed. */
|
|
3
|
+
export declare function extendedFounderActions(context: FounderContext, candidates: string[], founderId: string): FounderAction[];
|
|
4
|
+
export declare function creationInvitationActions(snapshot: Record<string, unknown>, candidates: string[], founderId: string, now: number): FounderPreparationContext['actions'];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const obj = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
2
|
+
const rows = (v) => Array.isArray(v) ? v.map(obj) : [];
|
|
3
|
+
const valid = (v) => typeof v === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(v);
|
|
4
|
+
const live = new Set(['RESERVED', 'ACCEPTED', 'ACTIVE', 'BLOCKED']);
|
|
5
|
+
/** Only offered fixed operations; endpoints supplied in network content are never executed. */
|
|
6
|
+
export function extendedFounderActions(context, candidates, founderId) {
|
|
7
|
+
const w = context.workspace, c = obj(w.founderControl), raw = obj(context.contract.contract), out = [];
|
|
8
|
+
if (['CANCELLED', 'COMPLETED', 'FAILED'].includes(w.mission.status) || ['RESULT_PUBLISHED', 'OUTCOME_ACCEPTED'].includes(w.state) || obj(c.outcomeNextAction).action === 'WAIT_FOR_HUMAN_FINAL_DECISION')
|
|
9
|
+
return out;
|
|
10
|
+
for (const a of rows(c.assignments))
|
|
11
|
+
if (valid(a.id))
|
|
12
|
+
for (const n of rows(a.nextActions)) {
|
|
13
|
+
const b = obj(n.body);
|
|
14
|
+
if (n.type === 'REVIEW_REPLACEMENT' && valid(b.application_id) && rows(a.replacementCandidates).some(x => x.applicationId === b.application_id))
|
|
15
|
+
out.push({ id: 'replace:' + a.id + ':' + b.application_id, operation: 'REPLACE_ASSIGNMENT', targetId: a.id, binding: b });
|
|
16
|
+
}
|
|
17
|
+
for (const t of rows(obj(c.poolReview).tasks))
|
|
18
|
+
if (valid(t.id) && t.canRetry === true && Number.isInteger(t.revision) && typeof t.digest === 'string')
|
|
19
|
+
out.push({ id: 'retry:' + t.id, operation: 'RETRY_REVIEW', targetId: t.id, binding: { expected_revision: t.revision, expected_digest: t.digest } });
|
|
20
|
+
if (context.deliveries.internalCheck && context.deliveries.reviewMode === 'INTERNAL_CHECK') {
|
|
21
|
+
const latest = new Set();
|
|
22
|
+
for (const d of rows(context.deliveries.deliveries)) {
|
|
23
|
+
if (!valid(d.id) || !valid(d.assignmentId) || latest.has(d.assignmentId))
|
|
24
|
+
continue;
|
|
25
|
+
latest.add(d.assignmentId);
|
|
26
|
+
if (d.status === 'SUBMITTED' && d.agentId !== founderId && d.contractHash === w.mission.contractHash && d.contractVersion === w.mission.contractVersion)
|
|
27
|
+
out.push({ id: 'check:' + d.id, operation: 'INTERNAL_CHECK', targetId: d.id });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (raw.visibility === 'PUBLIC' && raw.participation_mode === 'INVITE_ONLY' && candidates.length) {
|
|
31
|
+
const invitations = rows(context.invitations?.invitations), assignments = rows(c.assignments);
|
|
32
|
+
for (const stage of rows(raw.stages))
|
|
33
|
+
for (const slot of rows(stage.role_slots)) {
|
|
34
|
+
if (!valid(stage.stage_id) || !valid(slot.slot_id) || !Number.isInteger(slot.quantity) || Number(slot.quantity) < 1 || Number(slot.quantity) > 10)
|
|
35
|
+
continue;
|
|
36
|
+
if (obj(raw.review_policy).protocol === 'POOL_REVIEW_V1' && stage.stage_kind === 'REVIEW' && obj(raw.review_policy).mode !== 'INTERNAL_CHECK')
|
|
37
|
+
continue;
|
|
38
|
+
for (let seat = 1; seat <= Number(slot.quantity); seat++) {
|
|
39
|
+
if (invitations.some(i => i.stageId === stage.stage_id && i.slotId === slot.slot_id && i.slotIndex === seat && ['OFFERED', 'WAITING'].includes(String(i.status))))
|
|
40
|
+
continue;
|
|
41
|
+
const current = assignments.find(a => a.stageId === stage.stage_id && a.slotId === slot.slot_id && a.slotIndex === seat && live.has(String(a.status)));
|
|
42
|
+
if (current && !['ACK_OVERDUE', 'DELIVERY_OVERDUE', 'REPLACEMENT_REVIEW_REQUIRED'].includes(String(obj(current.sla).state)))
|
|
43
|
+
continue;
|
|
44
|
+
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) } });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export function creationInvitationActions(snapshot, candidates, founderId, now) {
|
|
51
|
+
const raw = obj(snapshot.contract), request = obj(snapshot.request), participants = rows(snapshot.participants), out = [];
|
|
52
|
+
if (raw.visibility !== 'PRIVATE' || !request.missionId)
|
|
53
|
+
return out;
|
|
54
|
+
for (const p of participants)
|
|
55
|
+
if (valid(p.id) && ['INVITED', 'ACCEPTED'].includes(String(p.status))) {
|
|
56
|
+
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;
|
|
57
|
+
if (p.checkerOwnerValid === false || p.status === 'INVITED' && Number.isFinite(due) && due <= now)
|
|
58
|
+
out.push({ id: 'cancel-invite:' + p.id, operation: 'CANCEL_CREATION_INVITATION', binding: { participant_id: p.id } });
|
|
59
|
+
}
|
|
60
|
+
for (const stage of rows(raw.stages))
|
|
61
|
+
for (const slot of rows(stage.role_slots)) {
|
|
62
|
+
if (!valid(stage.stage_id) || !valid(slot.slot_id) || !Number.isInteger(slot.quantity) || Number(slot.quantity) < 1 || Number(slot.quantity) > 10)
|
|
63
|
+
continue;
|
|
64
|
+
if (obj(raw.review_policy).protocol === 'POOL_REVIEW_V1' && stage.stage_kind === 'REVIEW' && obj(raw.review_policy).mode !== 'INTERNAL_CHECK')
|
|
65
|
+
continue;
|
|
66
|
+
const occupied = participants.filter(p => p.stageId === stage.stage_id && p.slotId === slot.slot_id && ['INVITED', 'ACCEPTED', 'ASSIGNED'].includes(String(p.status)));
|
|
67
|
+
if (occupied.length >= Number(slot.quantity))
|
|
68
|
+
continue;
|
|
69
|
+
for (const candidate of candidates)
|
|
70
|
+
if (candidate !== founderId && !participants.some(p => p.stageId === stage.stage_id && p.slotId === slot.slot_id && obj(p.agent).id === candidate))
|
|
71
|
+
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 } });
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { type AgentelConnector, type AgentelMissionWorkspace } from './agentel-connector.js';
|
|
2
|
+
export type FounderPreparationOperation = 'ACCEPT_CREATION_REQUEST' | 'SUBMIT_CREATION_DRAFT' | 'PUBLISH_APPROVED_MISSION' | 'INVITE_CREATION_AGENT' | 'CANCEL_CREATION_INVITATION';
|
|
3
|
+
export type FounderPreparationContext = {
|
|
4
|
+
inputTrust: 'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS';
|
|
5
|
+
request: Record<string, unknown>;
|
|
6
|
+
discussion: Record<string, unknown>;
|
|
7
|
+
rejections: FounderRejection[];
|
|
8
|
+
actions: Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
operation: FounderPreparationOperation;
|
|
11
|
+
binding?: Record<string, unknown>;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
export type FounderOperation = 'SELECT_APPLICATIONS' | 'ACTIVATE_ASSIGNMENT' | 'SEND_ROOM_MESSAGE' | 'OUTCOME_OPINION' | 'FINAL_CANDIDATE' | 'INVITE_CANDIDATES' | 'REPLACE_ASSIGNMENT' | 'RETRY_REVIEW' | 'INTERNAL_CHECK';
|
|
15
|
+
export type FounderAction = {
|
|
16
|
+
id: string;
|
|
17
|
+
operation: FounderOperation;
|
|
18
|
+
targetId?: string;
|
|
19
|
+
binding?: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
export type FounderContext = {
|
|
22
|
+
inputTrust: 'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS';
|
|
23
|
+
workspace: AgentelMissionWorkspace;
|
|
24
|
+
contract: Record<string, unknown>;
|
|
25
|
+
applications: Record<string, unknown>;
|
|
26
|
+
deliveries: Record<string, unknown>;
|
|
27
|
+
room: Record<string, unknown>;
|
|
28
|
+
actions: FounderAction[];
|
|
29
|
+
invitations?: Record<string, unknown>;
|
|
30
|
+
rejections?: FounderRejection[];
|
|
31
|
+
};
|
|
32
|
+
export type FounderRejection = {
|
|
33
|
+
missionId: string;
|
|
34
|
+
operation: FounderOperation | FounderPreparationOperation;
|
|
35
|
+
resourceKind?: 'CREATION_REQUEST';
|
|
36
|
+
targetId?: string;
|
|
37
|
+
issues?: Array<{
|
|
38
|
+
path: string;
|
|
39
|
+
code: string;
|
|
40
|
+
suggestedValue?: number;
|
|
41
|
+
}>;
|
|
42
|
+
key: string;
|
|
43
|
+
bodyHash: string;
|
|
44
|
+
basisHash: string | null;
|
|
45
|
+
code: string;
|
|
46
|
+
status: number;
|
|
47
|
+
rejectedAt: string;
|
|
48
|
+
};
|
|
49
|
+
export type FounderIssue = {
|
|
50
|
+
id: string;
|
|
51
|
+
resourceId: string;
|
|
52
|
+
resourceKind: 'MISSION' | 'CREATION_REQUEST';
|
|
53
|
+
code: string;
|
|
54
|
+
status: number;
|
|
55
|
+
operationKeyHash: string;
|
|
56
|
+
};
|
|
57
|
+
type Pending = {
|
|
58
|
+
failure?: {
|
|
59
|
+
code: string;
|
|
60
|
+
status: number;
|
|
61
|
+
runs: number;
|
|
62
|
+
lastRun: string;
|
|
63
|
+
};
|
|
64
|
+
quarantined?: boolean;
|
|
65
|
+
quarantineReason?: 'UNRESOLVED_REJECTION';
|
|
66
|
+
issue?: FounderIssue;
|
|
67
|
+
noticeRecorded?: boolean;
|
|
68
|
+
retryAt?: number;
|
|
69
|
+
basisHash?: string;
|
|
70
|
+
resourceKind?: 'CREATION_REQUEST';
|
|
71
|
+
missionId: string;
|
|
72
|
+
operation: FounderOperation | FounderPreparationOperation;
|
|
73
|
+
targetId?: string;
|
|
74
|
+
body: Record<string, unknown>;
|
|
75
|
+
key: string;
|
|
76
|
+
};
|
|
77
|
+
export type FounderWorkerState = {
|
|
78
|
+
schema: 1;
|
|
79
|
+
role: 'FOUNDER';
|
|
80
|
+
agentId: string;
|
|
81
|
+
notBefore: number;
|
|
82
|
+
cursor: number;
|
|
83
|
+
pending: Pending | null;
|
|
84
|
+
rejections?: FounderRejection[];
|
|
85
|
+
deferred?: Pending[];
|
|
86
|
+
followedMissions?: Array<{
|
|
87
|
+
requestId: string;
|
|
88
|
+
missionId: string;
|
|
89
|
+
}>;
|
|
90
|
+
};
|
|
91
|
+
export type FounderWorkerSession = {
|
|
92
|
+
load(): Promise<FounderWorkerState | null>;
|
|
93
|
+
save(state: FounderWorkerState): Promise<void>;
|
|
94
|
+
};
|
|
95
|
+
export type FounderWorkerStore = {
|
|
96
|
+
exclusive<T>(work: (session: FounderWorkerSession) => Promise<T>): Promise<T | null>;
|
|
97
|
+
};
|
|
98
|
+
export type FounderDecision = {
|
|
99
|
+
actionId: string;
|
|
100
|
+
body: Record<string, unknown>;
|
|
101
|
+
} | {
|
|
102
|
+
wait: true;
|
|
103
|
+
};
|
|
104
|
+
export type FounderWorkerOptions = {
|
|
105
|
+
agentId: string;
|
|
106
|
+
client: AgentelConnector;
|
|
107
|
+
store: FounderWorkerStore;
|
|
108
|
+
judge: {
|
|
109
|
+
ready(signal: AbortSignal): Promise<boolean>;
|
|
110
|
+
decide(context: Readonly<FounderContext>, signal: AbortSignal): Promise<FounderDecision>;
|
|
111
|
+
prepare?(context: Readonly<FounderPreparationContext>, signal: AbortSignal): Promise<FounderDecision>;
|
|
112
|
+
};
|
|
113
|
+
/** Operator role-specific liveness port, not the reviewer-only heartbeat route. No business authority. */
|
|
114
|
+
contact(metadata: {
|
|
115
|
+
role: 'FOUNDER';
|
|
116
|
+
state: 'START' | 'ACTIVE' | 'END';
|
|
117
|
+
missionId: string | null;
|
|
118
|
+
}, signal: AbortSignal): Promise<void>;
|
|
119
|
+
/** Ordinary authenticated notification API by default; injected ports must durably alert both audiences. */
|
|
120
|
+
reportIssue?(issue: FounderIssue, signal: AbortSignal): Promise<void>;
|
|
121
|
+
now?: () => number;
|
|
122
|
+
random?: () => number;
|
|
123
|
+
};
|
|
124
|
+
export type FounderWorkerRun = {
|
|
125
|
+
missionIds: string[];
|
|
126
|
+
creationRequestIds?: string[];
|
|
127
|
+
candidateAgentIds?: string[];
|
|
128
|
+
maxMissions: number;
|
|
129
|
+
maxSteps: number;
|
|
130
|
+
maxRunMs: number;
|
|
131
|
+
signal?: AbortSignal;
|
|
132
|
+
};
|
|
133
|
+
/** Operator supplies the explicit owned Mission inventory. No arbitrary model tools or Human decisions. */
|
|
134
|
+
export declare function runFounderWorker(o: FounderWorkerOptions, run: FounderWorkerRun): Promise<{
|
|
135
|
+
status: string;
|
|
136
|
+
completed: number;
|
|
137
|
+
rejected: number;
|
|
138
|
+
missionIssues: {
|
|
139
|
+
missionId: string;
|
|
140
|
+
code: string;
|
|
141
|
+
}[];
|
|
142
|
+
code: string | null;
|
|
143
|
+
nextCheckAt: number;
|
|
144
|
+
} | {
|
|
145
|
+
status: string;
|
|
146
|
+
completed: number;
|
|
147
|
+
rejected: number;
|
|
148
|
+
missionIssues: never[];
|
|
149
|
+
code: string;
|
|
150
|
+
nextCheckAt: null;
|
|
151
|
+
}>;
|
|
152
|
+
export {};
|