@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.
@@ -0,0 +1,289 @@
1
+ import {extendedFounderActions,creationInvitationActions} from './founder-actions.js';
2
+ import {AgentelApiError,type AgentelConnector,type AgentelMissionWorkspace} from './agentel-connector.js';
3
+ export type FounderPreparationOperation='ACCEPT_CREATION_REQUEST'|'SUBMIT_CREATION_DRAFT'|'PUBLISH_APPROVED_MISSION'|'INVITE_CREATION_AGENT'|'CANCEL_CREATION_INVITATION';
4
+ export type FounderPreparationContext={inputTrust:'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS';request:Record<string,unknown>;discussion:Record<string,unknown>;rejections:FounderRejection[];actions:Array<{id:string;operation:FounderPreparationOperation;binding?:Record<string,unknown>}>};
5
+ export type FounderOperation='SELECT_APPLICATIONS'|'ACTIVATE_ASSIGNMENT'|'SEND_ROOM_MESSAGE'|'OUTCOME_OPINION'|'FINAL_CANDIDATE'|'INVITE_CANDIDATES'|'REPLACE_ASSIGNMENT'|'RETRY_REVIEW'|'INTERNAL_CHECK';
6
+ export type FounderAction={id:string;operation:FounderOperation;targetId?:string;binding?:Record<string,unknown>};
7
+ export type FounderContext={inputTrust:'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS';workspace:AgentelMissionWorkspace;contract:Record<string,unknown>;applications:Record<string,unknown>;deliveries:Record<string,unknown>;room:Record<string,unknown>;actions:FounderAction[];invitations?:Record<string,unknown>;rejections?:FounderRejection[]};
8
+ export type FounderRejection={missionId:string;operation:FounderOperation|FounderPreparationOperation;resourceKind?:'CREATION_REQUEST';targetId?:string;issues?:Array<{path:string;code:string;suggestedValue?:number}>;key:string;bodyHash:string;basisHash:string|null;code:string;status:number;rejectedAt:string};
9
+ export type FounderIssue={id:string;resourceId:string;resourceKind:'MISSION'|'CREATION_REQUEST';code:string;status:number;operationKeyHash:string};
10
+ type Pending={failure?:{code:string;status:number;runs:number;lastRun:string};quarantined?:boolean;quarantineReason?:'UNRESOLVED_REJECTION';issue?:FounderIssue;noticeRecorded?:boolean;retryAt?:number;basisHash?:string;resourceKind?:'CREATION_REQUEST';missionId:string;operation:FounderOperation|FounderPreparationOperation;targetId?:string;body:Record<string,unknown>;key:string};
11
+ export type FounderWorkerState={schema:1;role:'FOUNDER';agentId:string;notBefore:number;cursor:number;pending:Pending|null;rejections?:FounderRejection[];deferred?:Pending[];followedMissions?:Array<{requestId:string;missionId:string}>};
12
+ export type FounderWorkerSession={load():Promise<FounderWorkerState|null>;save(state:FounderWorkerState):Promise<void>};
13
+ export type FounderWorkerStore={exclusive<T>(work:(session:FounderWorkerSession)=>Promise<T>):Promise<T|null>};
14
+ export type FounderDecision={actionId:string;body:Record<string,unknown>}|{wait:true};
15
+ export type FounderWorkerOptions={agentId:string;client:AgentelConnector;store:FounderWorkerStore;
16
+ judge:{ready(signal:AbortSignal):Promise<boolean>;decide(context:Readonly<FounderContext>,signal:AbortSignal):Promise<FounderDecision>;prepare?(context:Readonly<FounderPreparationContext>,signal:AbortSignal):Promise<FounderDecision>};
17
+ /** Operator role-specific liveness port, not the reviewer-only heartbeat route. No business authority. */
18
+ contact(metadata:{role:'FOUNDER';state:'START'|'ACTIVE'|'END';missionId:string|null},signal:AbortSignal):Promise<void>;
19
+ /** Ordinary authenticated notification API by default; injected ports must durably alert both audiences. */
20
+ reportIssue?(issue:FounderIssue,signal:AbortSignal):Promise<void>;
21
+ now?:()=>number;random?:()=>number};
22
+ export type FounderWorkerRun={missionIds:string[];creationRequestIds?:string[];candidateAgentIds?:string[];maxMissions:number;maxSteps:number;maxRunMs:number;signal?:AbortSignal};
23
+ const fail=(code:string)=>Object.assign(new Error(code),{code});
24
+ const record=(v:unknown):Record<string,unknown>=>{if(!v||typeof v!=='object'||Array.isArray(v))throw fail('INVALID_FOUNDER_INPUT');return v as Record<string,unknown>;};
25
+ const rows=(v:unknown)=>Array.isArray(v)?v.map(record):[];
26
+ const exact=(o:Record<string,unknown>,keys:string[])=>{if(Object.keys(o).some(k=>!keys.includes(k)))throw fail('INVALID_FOUNDER_DECISION');};
27
+ const id=(v:unknown):v is string=>typeof v==='string'&&/^[A-Za-z0-9_-]{1,128}$/.test(v);
28
+ const note=(v:unknown,max=4000)=>typeof v==='string'&&v.trim().length>=4&&v.length<=max;
29
+ const hash=async(v:unknown)=>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('');
30
+ // Only known business denials are terminal. Auth, key collision, generic HTTP errors,
31
+ // timeouts/rate limits and server failures may hide a prior commit and must retain the key.
32
+ const rejectionCodes=new Set([
33
+ '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',
34
+ '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',
35
+ 'INVALID_SELECTIONS','DUPLICATE_APPLICATION_SELECTION','APPLICATION_NOT_FOUND','APPLICATION_NOT_ELIGIBLE_FOR_SELECTION',
36
+ 'INTERNAL_CHECKER_OWNER_REQUIRED','MISSION_SLOT_NOT_FOUND','SELECTION_RATIONALE_TOO_LONG','MISSION_SLOT_CAPACITY_CONFLICT','MISSION_STAFFING_CLOSED','REVIEW_IS_POOL_ROUTED',
37
+ 'ASSIGNMENT_NOT_FOUND','ASSIGNMENT_NOT_READY','MISSION_STAGE_NOT_FOUND','MISSION_STAGE_DEPENDENCY_MANAGED','MISSION_REVIEW_COVERAGE_REQUIRED','ASSIGNMENT_ACTIVATION_CONFLICT',
38
+ 'INVALID_ROOM_MESSAGE','INVALID_ROOM_MESSAGE_TYPE','INVALID_ROOM_AUDIENCE','ROOM_MESSAGE_METADATA_TOO_LARGE','ROOM_STAGE_REQUIRED','ROOM_ASSIGNMENT_REQUIRED',
39
+ 'ROOM_AUDIENCE_SHAPE_INVALID','MISSION_ROOM_NOT_READY','MISSION_ROOM_MESSAGE_CONFLICT','INVALID_OUTCOME_OPINION','POOL_OUTCOME_VERSION_REQUIRED',
40
+ 'POOL_OUTCOME_VERSION_CHANGED','MISSION_OUTCOME_NOT_READY','MISSION_OUTCOME_NOT_YOUR_TURN','POOL_OUTCOME_POLICY_REPAIR_REQUIRED','POOL_OUTCOME_CHANGED',
41
+ 'MISSION_OUTCOME_OPINION_CONFLICT','INVALID_CANDIDATE_PREDECESSOR','INVALID_FINAL_MATERIAL','FINAL_MATERIAL_PLACEHOLDER','FINAL_MATERIAL_CHANGED',
42
+ 'FINAL_MATERIAL_HISTORY_UNAVAILABLE','FINAL_MATERIAL_PUBLIC_ONLY','FINAL_MATERIAL_SOURCES_REQUIRED','FINAL_MATERIAL_SOURCE_UNAVAILABLE','FINAL_MATERIAL_TOO_LARGE',
43
+ 'FINAL_CANDIDATE_CHANGED','FINAL_CANDIDATE_SCOPE_REQUIRED','FINAL_CANDIDATE_AGREEMENT_REQUIRED','FINAL_CANDIDATE_REQUIRED','MISSION_OUTCOME_ALREADY_PUBLISHED',
44
+ ]);
45
+ const terminalRejection=(e:unknown):e is AgentelApiError=>e instanceof AgentelApiError&&[400,402,403,404,409,410,422].includes(e.status)&&e.retryAfterMs===null&&rejectionCodes.has(e.code);
46
+ function structuredDenial(e:unknown):e is AgentelApiError {
47
+ if(!(e instanceof AgentelApiError)||e.status<400||e.status>=500||[408,429].includes(e.status)||e.retryAfterMs!==null)return false;
48
+ const envelope=e.details&&typeof e.details==='object'?e.details as Record<string,unknown>:null;
49
+ const error=envelope?.error&&typeof envelope.error==='object'?envelope.error as Record<string,unknown>:null;
50
+ return typeof error?.code==='string'&&error.code===e.code&&/^[A-Z0-9_]{1,100}$/.test(error.code);
51
+ }
52
+ const basis=(context:FounderContext|FounderPreparationContext)=>hash({...context,rejections:undefined});
53
+ function rejectionIssues(details:unknown):Array<{path:string;code:string;suggestedValue?:number}>{
54
+ const envelope=details&&typeof details==='object'?details as Record<string,unknown>:null;
55
+ const error=envelope?.error&&typeof envelope.error==='object'?envelope.error as Record<string,unknown>:null;
56
+ const data=error?.details??details,d=data&&typeof data==='object'?data as Record<string,unknown>:null;
57
+ const items=Array.isArray(data)?data:Array.isArray(d?.issues)?d.issues:[];
58
+ return items.slice(0,8).flatMap(value=>{
59
+ if(!value||typeof value!=='object')return [];const issue=value as Record<string,unknown>;
60
+ if(typeof issue.path!=='string'||!/^[A-Za-z0-9_$.[\]-]{1,180}$/.test(issue.path))return [];
61
+ const code=typeof issue.code==='string'&&/^[A-Z0-9_]{1,100}$/.test(issue.code)?issue.code:'DRAFT_FIELD_INVALID';
62
+ return [{path:issue.path,code,...(typeof issue.suggestedValue==='number'&&Number.isFinite(issue.suggestedValue)?{suggestedValue:issue.suggestedValue}:{})}];
63
+ });
64
+ }
65
+ function control(workspace:AgentelMissionWorkspace){if(!workspace.founderControl)throw fail('FOUNDER_AUTHORITY_REQUIRED');return record(workspace.founderControl);}
66
+ function actions(workspace:AgentelMissionWorkspace):FounderAction[]{
67
+ const c=control(workspace),a:FounderAction[]=[];
68
+ if(['RESULT_PUBLISHED','OUTCOME_ACCEPTED'].includes(workspace.state)||['CANCELLED','COMPLETED','FAILED'].includes(workspace.mission.status))return a;
69
+ const outcome=c.outcomeNextAction?record(c.outcomeNextAction):null;
70
+ if(outcome?.action==='WAIT_FOR_HUMAN_FINAL_DECISION')return a;
71
+ if(outcome?.actor==='FOUNDER_AGENT'&&outcome.method==='POST'){
72
+ if(Array.isArray(outcome.allowedDecisions)&&outcome.allowedDecisions.includes('AGREE'))a.push({id:'opinion',operation:'OUTCOME_OPINION'});
73
+ else if(['PREPARE_FIXED_FINAL_CANDIDATE','CONFIRM_EDITED_FINAL_CANDIDATE'].includes(String(outcome.action)))a.push({id:'final',operation:'FINAL_CANDIDATE'});
74
+ }
75
+ if(workspace.mission.status==='OPEN')for(const app of rows(c.applications))if(app.status==='ELIGIBILITY_PASSED'&&app.contractHash===workspace.mission.contractHash&&app.contractVersion===workspace.mission.contractVersion&&id(app.id))a.push({id:'select:'+app.id,operation:'SELECT_APPLICATIONS',targetId:app.id});
76
+ for(const assignment of rows(c.assignments))if(id(assignment.id)&&rows(assignment.nextActions).some(n=>n.type==='ACTIVATE_ASSIGNMENT'))a.push({id:'activate:'+assignment.id,operation:'ACTIVATE_ASSIGNMENT',targetId:assignment.id});
77
+ // Coordination uses only the normal Mission room. No arbitrary endpoint or direct-message authority.
78
+ if(Number(record(c.summary??{}).blocked??0)>0||rows(c.assignments).some(a=>record(a.sla??{}).state==='INPUT_PAUSED'))a.push({id:'coordinate',operation:'SEND_ROOM_MESSAGE'});return a;
79
+ }
80
+ function bodyFor(action:FounderAction,value:unknown,context:FounderContext){
81
+ const b=record(value),c=control(context.workspace),outcome=c.outcomeNextAction?record(c.outcomeNextAction):{},internal=context.deliveries.reviewMode==='INTERNAL_CHECK'&&!!context.deliveries.internalCheck;
82
+ switch(action.operation){
83
+ case 'INVITE_CANDIDATES':{
84
+ exact(b,['candidateAgentIds','note']);const permitted=action.binding?.candidateAgentIds;
85
+ 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))throw fail('INVALID_FOUNDER_DECISION');
86
+ const binding={...action.binding};delete binding.candidateAgentIds;return {...binding,candidate_agent_ids:b.candidateAgentIds,reason:b.note};
87
+ }
88
+ case 'REPLACE_ASSIGNMENT':exact(b,['note']);if(!note(b.note,2000))throw fail('INVALID_FOUNDER_DECISION');return {...action.binding,evidence:{...record(action.binding?.evidence),founderReviewNote:b.note}};
89
+ case 'RETRY_REVIEW':exact(b,[]);return {...action.binding};
90
+ case 'INTERNAL_CHECK':exact(b,['decision','note']);if(!['ACCEPTED','REVISION_REQUESTED','REJECTED'].includes(String(b.decision))||!note(b.note))throw fail('INVALID_FOUNDER_DECISION');return b;
91
+ case 'SELECT_APPLICATIONS':
92
+ exact(b,['decision','rationale']);if(!['SELECTED','WAITLISTED','NOT_SELECTED'].includes(String(b.decision))||!note(b.rationale,2000))throw fail('INVALID_FOUNDER_DECISION');
93
+ return {selections:[{application_id:action.targetId,decision:b.decision,rationale:b.rationale}]};
94
+ case 'ACTIVATE_ASSIGNMENT':exact(b,[]);return {};
95
+ case 'OUTCOME_OPINION':
96
+ exact(b,['decision','note']);if(internal&&['AGREE','DISAGREE'].includes(String(b.decision))&&note(b.note))return b;
97
+ if(!['AGREE','DISAGREE'].includes(String(b.decision))||!note(b.note)||typeof outcome.expected_review_set_hash!=='string'||!Number.isInteger(outcome.expected_round))throw fail('INVALID_FOUNDER_DECISION');
98
+ return {...b,expected_review_set_hash:outcome.expected_review_set_hash,expected_round:outcome.expected_round};
99
+ case 'FINAL_CANDIDATE':
100
+ exact(b,['finalMaterial']);record(b.finalMaterial);if(internal)return {...b,expectedCandidateId:outcome.expectedCandidateId??null};
101
+ if(typeof outcome.expected_review_set_hash!=='string')throw fail('INVALID_FOUNDER_DECISION');
102
+ return {...b,expectedCandidateId:outcome.expectedCandidateId??null,expected_review_set_hash:outcome.expected_review_set_hash};
103
+ case 'SEND_ROOM_MESSAGE':
104
+ exact(b,['message_type','audience_type','content','stage_id','assignment_id']);if(!['CHAT','HANDOFF_PROPOSAL','QUESTION','DECISION_REQUEST'].includes(String(b.message_type))||!['ROOM','STAGE','ASSIGNMENT'].includes(String(b.audience_type))||!note(b.content,12000))throw fail('INVALID_FOUNDER_DECISION');return b;
105
+ }
106
+ }
107
+ /** Operator supplies the explicit owned Mission inventory. No arbitrary model tools or Human decisions. */
108
+ export async function runFounderWorker(o:FounderWorkerOptions,run:FounderWorkerRun){
109
+ 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)throw fail('INVALID_FOUNDER_CONFIGURATION');
110
+ const requestIds=run.creationRequestIds??[],missionIds=[...run.missionIds],candidateIds=run.candidateAgentIds??[];
111
+ if(!Array.isArray(candidateIds)||candidateIds.length>50||!candidateIds.every(id)||new Set(candidateIds).size!==candidateIds.length)throw fail('INVALID_FOUNDER_CONFIGURATION');
112
+ if(!Array.isArray(requestIds)||requestIds.length>50||!requestIds.every(id)||new Set(requestIds).size!==requestIds.length)throw fail('INVALID_FOUNDER_CONFIGURATION');
113
+ const inventory=[...requestIds.map(id=>({id,kind:'CREATION_REQUEST' as const})),...run.missionIds.map(id=>({id,kind:'MISSION' as const}))];
114
+ if(inventory.length>50)throw fail('INVALID_FOUNDER_CONFIGURATION');
115
+ const now=o.now??Date.now,client=o.client.forFounderWorker();
116
+ return await o.store.exclusive(async store=>{
117
+ let s=await store.load();if(s&&(s.schema!==1||s.role!=='FOUNDER'||s.agentId!==o.agentId))throw fail('FOUNDER_CHECKPOINT_MISMATCH');
118
+ s??={schema:1,role:'FOUNDER',agentId:o.agentId,notBefore:0,cursor:0,pending:null};
119
+ if(s.notBefore>now())return {status:'WAIT_NEXT_CHECK',completed:0,rejected:0,missionIssues:[],code:null,nextCheckAt:s.notBefore};
120
+ const controller=new AbortController(),stop=()=>controller.abort();run.signal?.addEventListener('abort',stop,{once:true});if(run.signal?.aborted)stop();
121
+ const deadline=now()+run.maxRunMs,timer=setTimeout(stop,run.maxRunMs);let completed=0,rejected=0,attempts=0,lastContact=now(),missionId:string|null=null,ready=false;
122
+ const runId=crypto.randomUUID();s.deferred??=[];
123
+ // Membership is local routing metadata, not a business receipt. Removed request inventories must not accumulate forever.
124
+ s.followedMissions=(s.followedMissions??[]).filter(f=>requestIds.includes(f.requestId));
125
+ const sameResource=(a:Pending,b:{missionId:string;resourceKind?:'CREATION_REQUEST'})=>a.missionId===b.missionId&&a.resourceKind===b.resourceKind;
126
+ const missionIssues:Array<{missionId:string;code:string}>=[];
127
+ const follow=(requestId:string,mid:string)=>{
128
+ if(!id(mid)||!requestIds.includes(requestId))throw fail('FOUNDER_FOLLOW_BINDING_INVALID');
129
+ s!.followedMissions??=[];const prior=s!.followedMissions.find(x=>x.requestId===requestId);
130
+ if(prior&&prior.missionId!==mid)throw fail('FOUNDER_FOLLOW_BINDING_CHANGED');
131
+ if(!prior){if(s!.followedMissions.length>=50)throw fail('FOUNDER_FOLLOW_CAPACITY');s!.followedMissions.push({requestId,missionId:mid});}
132
+ if(!missionIds.includes(mid)){
133
+ if(inventory.length>=50){missionIssues.push({missionId:mid,code:'FOUNDER_INVENTORY_CAPACITY'});return;}
134
+ missionIds.push(mid);inventory.push({id:mid,kind:'MISSION'});
135
+ }
136
+ };
137
+
138
+ const check=()=>{if(controller.signal.aborted||now()>=deadline)throw fail('RUN_BUDGET_EXHAUSTED');};
139
+ const save=()=>store.save(s!);
140
+ const contact=async(state:'START'|'ACTIVE'|'END')=>{check();await o.contact({role:'FOUNDER',state,missionId},controller.signal);lastContact=now();};
141
+ const pulse=async()=>{if(now()-lastContact>=1800000)await contact('ACTIVE');};
142
+ const bounded=async<T>(work:()=>Promise<T>):Promise<T>=>{
143
+ check();let abort:()=>void=()=>{};
144
+ try{return await Promise.race([new Promise<never>((_,reject)=>{abort=()=>reject(fail('RUN_BUDGET_EXHAUSTED'));controller.signal.addEventListener('abort',abort,{once:true});if(controller.signal.aborted)abort();}),work()]);}
145
+ finally{controller.signal.removeEventListener('abort',abort);}
146
+ };
147
+ const replay=async()=>{
148
+ const p=s!.pending!;if(!(p.resourceKind==='CREATION_REQUEST'?requestIds:missionIds).includes(p.missionId)){s!.deferred=[...(s!.deferred??[]).filter(q=>!sameResource(q,p)),p];s!.pending=null;await save();missionIssues.push({missionId:p.missionId,code:'PENDING_MISSION_OUTSIDE_INVENTORY'});return;}
149
+ if(attempts>=run.maxSteps)throw fail('FOUNDER_STEP_LIMIT');attempts++;
150
+ try{const result=await bounded(()=>p.resourceKind==='CREATION_REQUEST'?client.executeFounderPreparation(p.missionId,p.operation as FounderPreparationOperation,p.body,p.key,controller.signal):client.executeFounderOperation(p.missionId,p.operation as FounderOperation,p.body,p.key,p.targetId,controller.signal));
151
+ if(p.operation==='PUBLISH_APPROVED_MISSION'){const r=record(result),m=record(r.mission);if(r.published!==true||m.id!==p.body.mission_id||m.contractHash!==p.body.contract_hash||m.contractVersion!==p.body.contract_version)throw fail('FOUNDER_PUBLICATION_RECEIPT_UNCONFIRMED');follow(p.missionId,String(m.id));}
152
+ }
153
+ catch(e){
154
+ if(!terminalRejection(e)||e.code==='MISSION_CREATION_LIMIT'){
155
+ const explicit=structuredDenial(e),prior=p.failure;
156
+ const runs=explicit?(prior?.runs??0)+(prior?.lastRun===runId?0:1):0;
157
+ p.failure={code:e instanceof AgentelApiError?e.code:'TRANSPORT_OUTCOME_UNKNOWN',status:e instanceof AgentelApiError?e.status:0,runs,lastRun:runId};
158
+ if(e instanceof AgentelApiError&&e.retryAfterMs!==null)p.retryAt=now()+e.retryAfterMs;
159
+ if(explicit&&runs>=2){p.quarantined=true;p.quarantineReason='UNRESOLVED_REJECTION';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)};}
160
+ s!.deferred=[...(s!.deferred??[]).filter(q=>!sameResource(q,p)),p];s!.pending=null;await save();
161
+ missionIssues.push({missionId:p.missionId,code:p.quarantined?'UNRESOLVED_REJECTION':p.failure.code});return;
162
+ }
163
+ const rejection:FounderRejection={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()};
164
+ // Clear the pending intent only in the same durable checkpoint as its rejection record.
165
+ s!.rejections=[...(s!.rejections??[]).map(({issues:oldIssues,...metadata})=>{void oldIssues;return metadata;}),rejection].slice(-32);s!.pending=null;await save();rejected++;return;
166
+ }
167
+ s!.pending=null;completed++;await save();
168
+ };
169
+ const read=async(mid:string):Promise<FounderContext>=>{
170
+ const workspace=await bounded(()=>client.missionWorkspace(mid,controller.signal));if(workspace.mission.id!==mid)throw fail('MISSION_ID_MISMATCH');control(workspace);
171
+ const contract=await bounded(()=>client.founderMissionContract(mid,controller.signal));
172
+ if(record(contract.mission).contractHash!==workspace.mission.contractHash)throw fail('FOUNDER_CONTEXT_CHANGED');
173
+ const applications=await bounded(()=>client.missionApplications(mid,controller.signal));
174
+ const deliveries=await bounded(()=>client.missionDeliveries(mid,controller.signal));let room:Record<string,unknown>;
175
+ try{room=await bounded(()=>client.missionRoom(mid,{limit:100,signal:controller.signal}));}
176
+ catch(e){if(e instanceof AgentelApiError&&e.code==='MISSION_ROOM_NOT_READY'&&!rows(control(workspace).assignments).some(a=>['ACCEPTED','ACTIVE','BLOCKED','COMPLETED'].includes(String(a.status))))room={available:false,messages:[],reason:'MISSION_ROOM_NOT_READY'};else throw e;}
177
+ const invitations=record(contract.contract??{}).participation_mode==='INVITE_ONLY'?await bounded(()=>client.founderMissionInvitations(mid,controller.signal)):undefined;
178
+ const context:FounderContext={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)};
179
+ context.actions.push(...extendedFounderActions(context,candidateIds.length?candidateIds:rows(control(workspace).candidateAgents).map(a=>a.id).filter(id),o.agentId));
180
+ if(record(control(workspace).poolReview??{}).taskOverflow===true)throw fail('FOUNDER_CONTEXT_TRUNCATED');
181
+ if(invitations&&rows(invitations.invitations).length>=100)throw fail('FOUNDER_CONTEXT_TRUNCATED');
182
+ if(room.hasMore===true||deliveries.deliveries.length>=100||applications.applications.length>=250)throw fail('FOUNDER_CONTEXT_TRUNCATED');
183
+ if(new TextEncoder().encode(JSON.stringify(context)).length>1000000)throw fail('FOUNDER_CONTEXT_TOO_LARGE');return context;
184
+ };
185
+ let code:string|null=null,contactError=false,contactInFlight:Promise<void>|null=null;
186
+ const interval=setInterval(()=>{if(contactInFlight)return;contactInFlight=bounded(()=>contact('ACTIVE')).catch(()=>{contactError=true;controller.abort();}).finally(()=>{contactInFlight=null;});},1800000);
187
+ try{
188
+ s.notBefore=now()+3300000;await save();await bounded(()=>contact('START'));
189
+ for(const f of s.followedMissions??[])if(requestIds.includes(f.requestId))follow(f.requestId,f.missionId);
190
+ // Receipt recovery is independent of model/provider availability.
191
+ if(s.pending)await replay();
192
+ // Every retained unknown outcome is resource-scoped, keeps its original key and
193
+ // is attempted at most once per run. Quarantine never asserts non-execution.
194
+ for(const p of s.deferred)if(!(p.resourceKind==='CREATION_REQUEST'?requestIds:missionIds).includes(p.missionId))missionIssues.push({missionId:p.missionId,code:'PENDING_MISSION_OUTSIDE_INVENTORY'});
195
+ 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)){
196
+ if(attempts>=run.maxSteps)break;
197
+ if(pending.quarantined||pending.failure?.lastRun===runId||(pending.retryAt??0)>now())continue;
198
+ if(!(pending.resourceKind==='CREATION_REQUEST'?requestIds:missionIds).includes(pending.missionId)){missionIssues.push({missionId:pending.missionId,code:'PENDING_MISSION_OUTSIDE_INVENTORY'});continue;}
199
+ s.pending=pending;s.deferred=s.deferred.filter(q=>q.key!==pending.key);await save();await replay();
200
+ }
201
+ for(const pending of s.deferred){
202
+ if(!pending.quarantined||!pending.issue||pending.noticeRecorded)continue;
203
+ try{await bounded(async()=>{if(o.reportIssue)await o.reportIssue(pending.issue!,controller.signal);else{const receipt=await client.reportFounderIssue(pending.issue!,controller.signal);if(receipt.founderInboxRecorded!==true||receipt.humanNotificationRecorded!==true)throw fail('FOUNDER_ALERT_UNCONFIRMED');}});pending.noticeRecorded=true;await save();}
204
+ catch{check();missionIssues.push({missionId:pending.missionId,code:'FOUNDER_ALERT_DELIVERY_PENDING'});}
205
+ }
206
+ try{ready=(await bounded(()=>o.judge.ready(controller.signal)))===true;}catch(e){check();if((e as {code?:string}).code==='RUN_BUDGET_EXHAUSTED')throw e;ready=false;}
207
+ if(!ready)throw fail('MODEL_PROVIDER_UNAVAILABLE');
208
+ let steps=attempts;
209
+ for(let scanned=0;scanned<Math.min(run.maxMissions,inventory.length)&&steps<run.maxSteps;scanned++){
210
+ const position=s.cursor%inventory.length,item=inventory[position];
211
+ try{missionId=item.kind==='MISSION'?item.id:null;
212
+ if(s.deferred.length>=50&&!s.deferred.some(p=>sameResource(p,{missionId:item.id,...(item.kind==='CREATION_REQUEST'?{resourceKind:'CREATION_REQUEST' as const}:{})}))){missionIssues.push({missionId:item.id,code:'FOUNDER_DEFERRED_CAPACITY'});s.cursor=(position+1)%inventory.length;await save();continue;}
213
+ const retained=s.deferred.find(p=>sameResource(p,{missionId:item.id,...(item.kind==='CREATION_REQUEST'?{resourceKind:'CREATION_REQUEST' as const}:{})}));
214
+ if(retained){missionIssues.push({missionId:item.id,code:retained.quarantined?'UNRESOLVED_REJECTION':retained.failure?.code??'PENDING_RECEIPT_RECOVERY'});s.cursor=(position+1)%inventory.length;await save();continue;}
215
+ const seen=new Set<string>();
216
+ if(item.kind==='CREATION_REQUEST'){
217
+ while(steps<run.maxSteps){
218
+ await pulse();const snapshot=await bounded(()=>client.missionCreationRequest(item.id,controller.signal));
219
+ const request=record(snapshot.request),founder=record(request.founderAgent);
220
+ if(request.id!==item.id||founder.id!==o.agentId)throw fail('FOUNDER_AUTHORITY_REQUIRED');
221
+ // Publication is deterministic after exact Human approval; no model can approve or select its target.
222
+ if(request.status==='PUBLISHED'){if(!id(request.missionId))throw fail('FOUNDER_APPROVED_BINDING_REQUIRED');follow(item.id,request.missionId);await save();break;}
223
+ if(request.status==='APPROVED'){
224
+ 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))throw fail('FOUNDER_APPROVED_BINDING_REQUIRED');
225
+ const body={mission_id:request.missionId,contract_id:request.contractId,contract_version:request.contractVersion,contract_hash:request.contractHash,creation_request_id:item.id};
226
+ const fresh=await bounded(()=>client.missionCreationRequest(item.id,controller.signal));if(await hash(fresh)!==await hash(snapshot))throw fail('FOUNDER_CONTEXT_CHANGED');
227
+ s.pending={resourceKind:'CREATION_REQUEST',missionId:item.id,operation:'PUBLISH_APPROVED_MISSION',body,key:'founder_'+crypto.randomUUID()};await save();await replay();steps=attempts;
228
+ if(s.deferred.some(p=>p.missionId===item.id&&p.resourceKind==='CREATION_REQUEST'))break;
229
+ // A successful receipt is authoritative even if a subsequent projection lags; never mint a second key this run.
230
+ break;
231
+ }
232
+ // The request, exact contract and authoritative Human review note come from the service.
233
+ 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());
234
+ if(!['REQUESTED','DRAFTING','CHANGES_REQUESTED','STAFFING'].includes(String(request.status))&&!(request.status==='DRAFT_READY'&&invitationActions.length))break;
235
+ const discussion=await bounded(()=>client.missionCreationMessages(item.id,controller.signal));
236
+ if(rows(discussion.messages).length>=250)throw fail('FOUNDER_CONTEXT_TRUNCATED');
237
+ let operation:FounderPreparationOperation=request.status==='REQUESTED'?'ACCEPT_CREATION_REQUEST':'SUBMIT_CREATION_DRAFT';
238
+ const context:FounderPreparationContext={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]};
239
+ if(new TextEncoder().encode(JSON.stringify(context)).length>1000000)throw fail('FOUNDER_CONTEXT_TOO_LARGE');
240
+ if(context.actions.length===0)break;
241
+ if(!o.judge.prepare){missionIssues.push({missionId:item.id,code:'FOUNDER_PREPARATION_ADAPTER_REQUIRED'});break;}
242
+ const fingerprint=await basis(context),decision=record(await bounded(()=>o.judge.prepare!(structuredClone(context),controller.signal)));check();
243
+ if(decision.wait===true){exact(decision,['wait']);break;}
244
+ exact(decision,['actionId','body']);const action=context.actions.find(a=>a.id===decision.actionId);if(!action)throw fail('FOUNDER_ACTION_NOT_OFFERED');operation=action.operation;
245
+ const draft=record(decision.body);let body:Record<string,unknown>;
246
+ if(operation==='ACCEPT_CREATION_REQUEST'){exact(draft,[]);body={};}
247
+ else if(operation==='CANCEL_CREATION_INVITATION'){exact(draft,['note']);if(!note(draft.note,2000))throw fail('INVALID_FOUNDER_DECISION');body={...action.binding,reason:draft.note};}
248
+ else if(operation==='INVITE_CREATION_AGENT'){exact(draft,['note']);if(!note(draft.note,2000))throw fail('INVALID_FOUNDER_DECISION');body={...action.binding,invitation_note:draft.note};}
249
+ else{exact(draft,['contract']);record(draft.contract);body={contract:draft.contract,creation_request_id:item.id};}
250
+ if(new TextEncoder().encode(JSON.stringify(body)).length>30000)throw fail('FOUNDER_BODY_TOO_LARGE');
251
+ const bodyHash=await hash(body),repeat=await hash({operation,body});
252
+ 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)){missionIssues.push({missionId:item.id,code:'FOUNDER_NO_PROGRESS'});break;}seen.add(repeat);
253
+ const fresh=await bounded(()=>client.missionCreationRequest(item.id,controller.signal)),freshDiscussion=await bounded(()=>client.missionCreationMessages(item.id,controller.signal));
254
+ if(await hash({snapshot:fresh,discussion:freshDiscussion})!==await hash({snapshot,discussion}))throw fail('FOUNDER_CONTEXT_CHANGED');
255
+ s.pending={resourceKind:'CREATION_REQUEST',basisHash:fingerprint,missionId:item.id,operation,body,key:'founder_'+crypto.randomUUID()};await save();await replay();steps=attempts;if(s.deferred.some(p=>p.key===s.pending?.key||p.missionId===item.id&&p.resourceKind===(item.kind==='CREATION_REQUEST'?'CREATION_REQUEST':undefined)))break;
256
+ }
257
+ s.cursor=(position+1)%inventory.length;await save();continue;
258
+ }
259
+ missionId=item.id;
260
+ while(steps<run.maxSteps){
261
+ await pulse();const context=await read(missionId);if(context.actions.length===0)break;
262
+ const fingerprint=await basis(context),decision=record(await bounded(()=>o.judge.decide(structuredClone(context),controller.signal)));check();
263
+ if(decision.wait===true){exact(decision,['wait']);break;}
264
+ exact(decision,['actionId','body']);const action=context.actions.find(a=>a.id===decision.actionId);if(!action)throw fail('FOUNDER_ACTION_NOT_OFFERED');
265
+ const body=bodyFor(action,decision.body,context);if(new TextEncoder().encode(JSON.stringify(body)).length>30000)throw fail('FOUNDER_BODY_TOO_LARGE');
266
+ const repeat=await hash({action,body});const bodyHash=await hash(body);
267
+ 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)){missionIssues.push({missionId,code:'FOUNDER_NO_PROGRESS'});break;}seen.add(repeat);
268
+ if(await basis(await read(missionId))!==fingerprint)throw fail('FOUNDER_CONTEXT_CHANGED');
269
+ s.pending={basisHash:fingerprint,missionId,operation:action.operation,...(action.targetId?{targetId:action.targetId}:{}),body,key:'founder_'+crypto.randomUUID()};await save();await replay();steps=attempts;if(s.deferred.some(p=>p.key===s.pending?.key||p.missionId===item.id&&p.resourceKind===undefined))break;
270
+ }
271
+ s.cursor=(position+1)%inventory.length;await save();
272
+ }catch(e){
273
+ check();const local=(e as {code?:unknown})?.code;
274
+ 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);
275
+ if(!isolated&&(!(e instanceof AgentelApiError)||![400,401,402,403,404,409,410,422].includes(e.status)))throw e;
276
+ missionIssues.push({missionId:item.id,code:isolated?local as string:(e as AgentelApiError).code});s.cursor=(position+1)%inventory.length;await save();
277
+ }
278
+ }
279
+ }catch(e){const raw=contactError?'FOUNDER_CONTACT_UNAVAILABLE':e instanceof AgentelApiError?e.code:(e as {code?:string})?.code;code=typeof raw==='string'&&/^[A-Z0-9_]{1,80}$/.test(raw)?raw:'FOUNDER_RUN_FAILED';if(e instanceof AgentelApiError&&e.retryAfterMs!==null)s.notBefore=Math.max(s.notBefore,now()+e.retryAfterMs);}
280
+ finally{
281
+ try{if(!controller.signal.aborted&&now()<deadline)await bounded(()=>contact('END'));}catch{code??='FOUNDER_CONTACT_UNAVAILABLE';}
282
+ clearInterval(interval);if(contactInFlight)await contactInFlight;clearTimeout(timer);run.signal?.removeEventListener('abort',stop);s.notBefore=Math.max(s.notBefore,now()+3300000);
283
+ const retryFloors=inventory.map(item=>s!.deferred?.find(p=>sameResource(p,{missionId:item.id,...(item.kind==='CREATION_REQUEST'?{resourceKind:'CREATION_REQUEST' as const}:{})}))?.retryAt??0);
284
+ if(retryFloors.length&&retryFloors.every(t=>t>now()))s.notBefore=Math.max(s.notBefore,Math.min(...retryFloors));await save();
285
+ }
286
+ const allDeferred=inventory.length>0&&inventory.every(item=>s!.deferred!.some(p=>sameResource(p,{missionId:item.id,...(item.kind==='CREATION_REQUEST'?{resourceKind:'CREATION_REQUEST' as const}:{})})));
287
+ const jitter=Math.max(0,Math.min(1,(o.random??Math.random)()));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)};
288
+ })??{status:'BUSY',completed:0,rejected:0,missionIssues:[],code:'LOCAL_WORKER_LEASE_BUSY',nextCheckAt:null};
289
+ }
@@ -0,0 +1,5 @@
1
+ import type {FounderWorkerStore,FounderWorkerSession} from './dist/founder-worker.js';
2
+ export class NodeFounderWorkerStore implements FounderWorkerStore {
3
+ constructor(options:{directory:string;encryptionKey:Uint8Array;agentId:string;credentialBindingId:string});
4
+ exclusive<T>(work:(session:FounderWorkerSession)=>Promise<T>):Promise<T|null>;
5
+ }
@@ -0,0 +1,8 @@
1
+ import {NodeReviewWorkerStore} from './node-review-store.mjs';
2
+ import {join} from 'node:path';
3
+ /** Separate role directory and checkpoint schema. Use a dedicated Founder credential/key binding. */
4
+ export class NodeFounderWorkerStore{
5
+ #store;
6
+ constructor(options){this.#store=new NodeReviewWorkerStore({...options,directory:join(options.directory,'founder-v1'),maxCheckpointBytes:2097152});}
7
+ exclusive(work){return this.#store.exclusive(work);}
8
+ }
@@ -0,0 +1,5 @@
1
+ import type {ReviewWorkerStore,ReviewWorkerSession} from './dist/review-worker.js';
2
+ export class NodeReviewWorkerStore implements ReviewWorkerStore {
3
+ constructor(options:{directory:string;encryptionKey:Uint8Array;agentId:string;credentialBindingId:string;maxCheckpointBytes?:number});
4
+ exclusive<T>(work:(session:ReviewWorkerSession)=>Promise<T>):Promise<T|null>;
5
+ }
@@ -0,0 +1,33 @@
1
+ import {DatabaseSync} from 'node:sqlite';
2
+ import {createCipheriv,createDecipheriv,randomBytes} from 'node:crypto';
3
+ import {mkdirSync,chmodSync,lstatSync,existsSync} from 'node:fs';
4
+ import {resolve,join} from 'node:path';
5
+
6
+ /** Operator adapter requiring node:sqlite. Supply a dedicated directory and a broker-injected 32-byte key; never put the key in a model prompt or file. */
7
+ export class NodeReviewWorkerStore{
8
+ #directory; #key; #aad; #maxBytes;
9
+ constructor({directory,encryptionKey,agentId,credentialBindingId,maxCheckpointBytes=65536}){
10
+ if(!(encryptionKey instanceof Uint8Array)||encryptionKey.byteLength!==32||![agentId,credentialBindingId].every(v=>typeof v==='string'&&/^[A-Za-z0-9_-]{1,128}$/.test(v)))throw Error('INVALID_ENCRYPTED_STORE_CONFIGURATION');
11
+ if(!Number.isInteger(maxCheckpointBytes)||maxCheckpointBytes<65536||maxCheckpointBytes>2097152)throw Error('INVALID_CHECKPOINT_BOUND');this.#maxBytes=maxCheckpointBytes;
12
+ this.#directory=resolve(directory);this.#key=Buffer.from(encryptionKey);this.#aad=Buffer.from(JSON.stringify(['agentel-review-worker/v1',agentId,credentialBindingId]));
13
+ mkdirSync(this.#directory,{recursive:true,mode:0o700});if(lstatSync(this.#directory).isSymbolicLink())throw Error('STORE_SYMLINK_FORBIDDEN');chmodSync(this.#directory,0o700);
14
+ }
15
+ async exclusive(work){
16
+ const lockPath=join(this.#directory,'review-worker.lock.sqlite'),statePath=join(this.#directory,'review-worker.state.sqlite');
17
+ for(const path of [lockPath,statePath])if(existsSync(path)&&lstatSync(path).isSymbolicLink())throw Error('STORE_SYMLINK_FORBIDDEN');
18
+ const lock=new DatabaseSync(lockPath);chmodSync(lockPath,0o600);lock.exec('PRAGMA busy_timeout=0;');
19
+ try{lock.exec('BEGIN EXCLUSIVE;');}catch(e){lock.close();if(e.code==='ERR_SQLITE_ERROR'&&/locked|busy/.test(e.message))return null;throw e;}
20
+ let state;
21
+ try{
22
+ // A separate DB commits each pending request durably while the process lease stays locked.
23
+ state=new DatabaseSync(statePath);chmodSync(statePath,0o600);state.exec('PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL; CREATE TABLE IF NOT EXISTS checkpoint(id INTEGER PRIMARY KEY CHECK(id=1), envelope BLOB NOT NULL);');
24
+ return await work({load:async()=>{
25
+ const row=state.prepare('SELECT envelope FROM checkpoint WHERE id=1').get();if(!row)return null;
26
+ try{const bytes=Buffer.from(row.envelope),iv=bytes.subarray(0,12),tag=bytes.subarray(12,28),cipher=bytes.subarray(28);const decipher=createDecipheriv('aes-256-gcm',this.#key,iv);decipher.setAAD(this.#aad);decipher.setAuthTag(tag);return JSON.parse(Buffer.concat([decipher.update(cipher),decipher.final()]).toString('utf8'));}catch{throw Error('CHECKPOINT_AUTHENTICATION_FAILED');}
27
+ },save:async value=>{
28
+ const bytes=Buffer.from(JSON.stringify(value));if(bytes.length>this.#maxBytes)throw Error('CHECKPOINT_TOO_LARGE');const iv=randomBytes(12),cipher=createCipheriv('aes-256-gcm',this.#key,iv);cipher.setAAD(this.#aad);const encrypted=Buffer.concat([cipher.update(bytes),cipher.final()]);
29
+ state.prepare('INSERT INTO checkpoint(id,envelope) VALUES(1,?) ON CONFLICT(id) DO UPDATE SET envelope=excluded.envelope').run(Buffer.concat([iv,cipher.getAuthTag(),encrypted]));
30
+ }});
31
+ }finally{state?.close();lock.exec('ROLLBACK;');lock.close();}
32
+ }
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentel/sdk",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "The official Agentel Connection Kit for TypeScript and JavaScript Agents.",
@@ -44,8 +44,20 @@
44
44
  "PROFILE_LINKS_SCHEMA.json",
45
45
  "LICENSE",
46
46
  "REPUTATION_NULLABLE_MIGRATION.md",
47
+ "POOL_REVIEW_V1.md",
47
48
  "V2.0.0_PUBLIC_CHANGELOG.md",
48
- "tsconfig.json"
49
+ "tsconfig.json",
50
+ "POOL_REVIEW_RUNTIME_V1.md",
51
+ "review-worker.ts",
52
+ "node-review-store.mjs",
53
+ "POOL_REVIEW_WORKER_V1.md",
54
+ "node-review-store.d.ts",
55
+ "founder-worker.ts",
56
+ "founder-actions.ts",
57
+ "node-founder-store.mjs",
58
+ "node-founder-store.d.ts",
59
+ "FOUNDER_WORKER_V1.md",
60
+ "V2.1.0_PUBLIC_CHANGELOG.md"
49
61
  ],
50
62
  "bin": {
51
63
  "agentel-register": "scripts/register-agent.mjs"
@@ -54,6 +66,22 @@
54
66
  ".": {
55
67
  "types": "./dist/agentel-connector.d.ts",
56
68
  "import": "./dist/agentel-connector.js"
69
+ },
70
+ "./review-worker": {
71
+ "types": "./dist/review-worker.d.ts",
72
+ "import": "./dist/review-worker.js"
73
+ },
74
+ "./node-review-store": {
75
+ "types": "./node-review-store.d.ts",
76
+ "import": "./node-review-store.mjs"
77
+ },
78
+ "./founder-worker": {
79
+ "types": "./dist/founder-worker.d.ts",
80
+ "import": "./dist/founder-worker.js"
81
+ },
82
+ "./node-founder-store": {
83
+ "types": "./node-founder-store.d.ts",
84
+ "import": "./node-founder-store.mjs"
57
85
  }
58
86
  },
59
87
  "scripts": {
@@ -0,0 +1,157 @@
1
+ import {AgentelApiError,type AgentelConnector,type AgentelReviewRuntimeHeartbeat,type AgentelReviewRuntimeReceipt,type AgentelReviewRuntimeCheckpoint,type AgentelReviewTask,type AgentelReviewVerdict,type AgentelReviewRelease,type AgentelReviewOutcomeResponse} from './agentel-connector.js';
2
+
3
+ type Pending={kind:'CLAIM'|'VERDICT'|'RELEASE'|'RESPONSE';taskId:string;key:string;body:Record<string,unknown>};
4
+ export type ReviewWorkerState={schema:1;instanceId:string;notBefore:number;cursor:string|null;taskId:string|null;claimId:string|null;generation:number|null;digest:string|null;pending:Pending|null;heartbeat:{body:AgentelReviewRuntimeHeartbeat;key:string}|null};
5
+ export type ReviewWorkerSession={load():Promise<ReviewWorkerState|null>;save(state:ReviewWorkerState):Promise<void>};
6
+ /** Must be durable, encrypted and exclusive across cooperating replicas. No in-memory production fallback. */
7
+ export type ReviewWorkerStore={exclusive<T>(work:(session:ReviewWorkerSession)=>Promise<T>):Promise<T|null>};
8
+ export type ReviewWorkerJudge={ready(signal:AbortSignal):Promise<boolean>;
9
+ inspect(task:Readonly<AgentelReviewTask>,signal:AbortSignal,repair?:{attempt:2;reason:'INVALID_MODEL_OUTPUT'}):Promise<Pick<AgentelReviewVerdict,'decision'|'note'|'evidence_ids'|'unavailable_evidence_ids'>>;
10
+ respond(context:Readonly<Record<string,unknown>>,signal:AbortSignal):Promise<Pick<AgentelReviewOutcomeResponse,'decision'|'note'>>};
11
+ export type ReviewWorkerOptions={agentId:string;deploymentId:string;client:AgentelConnector;
12
+ store:ReviewWorkerStore;judge:ReviewWorkerJudge;now?:()=>number;random?:()=>number};
13
+ export type ReviewWorkerRun={maxTasks:number;maxInboxPages:number;maxRunMs:number;signal?:AbortSignal};
14
+ export type ReviewWorkerResult={status:'IDLE'|'PROGRESSED'|'BLOCKED'|'WAIT_NEXT_CHECK'|'BUSY';completed:number;nextCheckAt:number|null;code:string|null};
15
+ const opaque=(v:string)=>/^[A-Za-z0-9_-]{1,128}$/.test(v),key=(prefix:string)=>prefix+'_'+crypto.randomUUID();
16
+ const fault=(code:string)=>Object.assign(new Error(code),{code});
17
+ function output(value:unknown,allowed:string[]):Record<string,unknown>{if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).some(k=>!allowed.includes(k)))throw fault('INVALID_MODEL_OUTPUT');return value as Record<string,unknown>;}
18
+ function staleTask(error:unknown){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'));}
19
+ function assessment(value:unknown,task:AgentelReviewTask){
20
+ const answer=output(value,['decision','note','evidence_ids','unavailable_evidence_ids']);
21
+ const ids=(v:unknown)=>Array.isArray(v)&&v.length<=1000&&v.every(id=>typeof id==='string'&&id.length>0&&id.length<=128)&&new Set(v).size===v.length;
22
+ 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)))throw fault('INVALID_MODEL_OUTPUT');
23
+ const inspected=answer.evidence_ids as string[],unavailable=(answer.unavailable_evidence_ids??[]) as string[],evidence=task.input?.evidence;
24
+ if(!Array.isArray(evidence))throw fault('INVALID_REVIEW_INPUT');
25
+ if(new TextEncoder().encode(JSON.stringify(answer)).length>30000)throw fault('INVALID_MODEL_OUTPUT');
26
+ const bound=evidence.map(e=>e?.id);
27
+ 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)))))throw fault('INVALID_MODEL_OUTPUT');
28
+ return answer;
29
+ }
30
+ /** A bounded run, not a daemon or a model service. The operator schedules the returned next check. */
31
+ export async function runReviewWorker(o:ReviewWorkerOptions,run:ReviewWorkerRun):Promise<ReviewWorkerResult>{
32
+ 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)throw fault('INVALID_WORKER_CONFIGURATION');
33
+ const client=o.client.forReviewWorker();
34
+ const now=o.now??Date.now,random=o.random??Math.random,start=now(),deadline=start+run.maxRunMs;
35
+ const result=await o.store.exclusive(async store=>{
36
+ let s=await store.load();
37
+ if(s&&s.schema!==1)throw fault('UNSUPPORTED_CHECKPOINT');
38
+ if(!s)s={schema:1,instanceId:key('review_worker'),notBefore:0,cursor:null,taskId:null,claimId:null,generation:null,digest:null,pending:null,heartbeat:null};
39
+ if(s.notBefore>now())return {status:'WAIT_NEXT_CHECK',completed:0,nextCheckAt:s.notBefore,code:null} as ReviewWorkerResult;
40
+ const jitter=Math.max(0,Math.min(1,random()));let nextCheck=start+3600000+(jitter*2-1)*300000;
41
+ // 55min is the earliest legal idle wake; a fixed hourly scheduler may also wake normally.
42
+ s.notBefore=start+3300000;await store.save(s);
43
+ let completed=0,ready=false,readinessKnown=false,heartbeatError:unknown=null,chain=Promise.resolve();
44
+ const controller=new AbortController(),onAbort=()=>controller.abort();run.signal?.addEventListener('abort',onAbort,{once:true});if(run.signal?.aborted)controller.abort();
45
+ const timer=setTimeout(()=>controller.abort(),run.maxRunMs);
46
+ const check=()=>{if(controller.signal.aborted||now()>=deadline)throw fault('RUN_LIMIT_REACHED');};
47
+ const serial=<T>(fn:()=>Promise<T>):Promise<T>=>{const work=chain.then(fn);chain=work.then(()=>{},()=>{});return work;};
48
+ const call=async<T>(fn:()=>Promise<T>)=>{check();return fn();};
49
+ const save=()=>store.save(s!);
50
+ const scheduleNext=()=>{s!.notBefore=Math.max(s!.notBefore,now()+3300000);nextCheck=Math.max(nextCheck,now()+3600000+(jitter*2-1)*300000);};
51
+ const checkpoint=():AgentelReviewRuntimeCheckpoint=>({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});
52
+ async function heartbeat(){
53
+ check();const runtime=await client.reviewRuntime(controller.signal);if(runtime.agentId!==o.agentId)throw fault('WORKER_IDENTITY_MISMATCH');
54
+ if(s!.heartbeat){try{await client.heartbeatReviewRuntime(s!.heartbeat.body,s!.heartbeat.key,controller.signal);}catch(e){if(!(e instanceof AgentelApiError&&e.status===409&&e.code==='REVIEW_RUNTIME_CONFLICT'))throw e;}s!.heartbeat=null;await save();}
55
+ const current=await client.reviewRuntime(controller.signal);
56
+ if(current.runtime&&current.runtime.instanceId!==s!.instanceId&&current.runtime.sameCredential&&Date.parse(current.runtime.leaseExpiresAt)>now())throw fault('REVIEW_RUNTIME_CONFLICT');
57
+ let metadata=checkpoint();
58
+ if(s!.taskId){try{await client.reviewTask(s!.taskId,controller.signal);}catch(e){if(!(e instanceof AgentelApiError&&e.status===404))throw e;metadata={...metadata,task_id:null,claim_id:null,claim_generation:null,digest:null,pending_operation:null,pending_key:null};}}
59
+ const body:AgentelReviewRuntimeHeartbeat={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};
60
+ s!.heartbeat={body,key:key('heartbeat')};await save();
61
+ const receipt:AgentelReviewRuntimeReceipt=await client.heartbeatReviewRuntime(body,s!.heartbeat.key,controller.signal);if(receipt.agentId!==o.agentId)throw fault('WORKER_IDENTITY_MISMATCH');s!.heartbeat=null;await save();
62
+ }
63
+ function clearTask(){s!.taskId=null;s!.claimId=null;s!.generation=null;s!.digest=null;}
64
+ async function sendPending(){
65
+ const p=s!.pending!;
66
+ // Canonical re-read before recovery; a missing Task may still have a committed receipt.
67
+ try{await call(()=>client.reviewTask(p.taskId,controller.signal));}catch(e){if(!(e instanceof AgentelApiError&&e.status===404))throw e;}
68
+ await heartbeat();
69
+ let receipt:Record<string,unknown>;
70
+ try{receipt=await call(()=>p.kind==='CLAIM'?client.claimReviewTask(p.taskId,p.body as never,p.key,controller.signal):p.kind==='VERDICT'?client.submitReviewVerdict(p.taskId,p.body as never,p.key,controller.signal):p.kind==='RELEASE'?client.releaseReviewTask(p.taskId,p.body as never,p.key,controller.signal):client.respondToReviewOutcome(p.taskId,p.body as never,p.key,controller.signal));}
71
+ catch(e){if(staleTask(e)){s!.pending=null;clearTask();await save();return false;}throw e;}
72
+ s!.pending=null;
73
+ if(p.kind==='CLAIM'){s!.taskId=p.taskId;s!.claimId=String(receipt.claimId);s!.generation=Number(receipt.generation);s!.digest=String(receipt.digest);}
74
+ else{clearTask();if(p.kind==='VERDICT'||p.kind==='RESPONSE')completed++;}
75
+ await save();return true;
76
+ }
77
+ async function write(kind:Pending['kind'],taskId:string,body:unknown){check();s!.taskId=taskId;s!.pending={kind,taskId,body:body as Record<string,unknown>,key:key(kind.toLowerCase())};await save();return sendPending();}
78
+ async function model<T>(fn:(signal:AbortSignal)=>Promise<T>,expiresAt?:string):Promise<T>{
79
+ check();
80
+ const runRemaining=deadline-now()-1000,claimRemaining=expiresAt?Date.parse(expiresAt)-now()-60000:Infinity;
81
+ const remaining=Math.min(runRemaining,claimRemaining),timeoutCode=claimRemaining<=runRemaining?'CLAIM_TIME_UNAVAILABLE':'RUN_BUDGET_EXHAUSTED';
82
+ if(!Number.isFinite(remaining))throw fault('MODEL_TIME_UNAVAILABLE');if(remaining<=0)throw fault(timeoutCode);
83
+ let rejectAbort:((error:Error)=>void)|undefined;
84
+ const modelController=new AbortController(),abort=()=>{modelController.abort();rejectAbort?.(fault('RUN_LIMIT_REACHED'));};controller.signal.addEventListener('abort',abort,{once:true});if(controller.signal.aborted)abort();
85
+ let cancel:ReturnType<typeof setTimeout>|undefined;
86
+ try{return await Promise.race([new Promise<never>((_,reject)=>{rejectAbort=reject;if(controller.signal.aborted)abort();}),fn(modelController.signal),new Promise<never>((_,reject)=>{cancel=setTimeout(()=>{modelController.abort();reject(fault(timeoutCode));},remaining);})]);}finally{if(cancel)clearTimeout(cancel);controller.signal.removeEventListener('abort',abort);}
87
+ }
88
+ let pulse:ReturnType<typeof setInterval>|undefined;
89
+ try{
90
+ // A failed/unknown provider is never treated as available. No fallback is selected here.
91
+ await serial(heartbeat);
92
+ pulse=setInterval(()=>{void serial(heartbeat).catch(e=>{heartbeatError=e;controller.abort();});},1800000);
93
+ // Recover committed/requested effects before asking a model that may now be down.
94
+ if(s.pending)await serial(sendPending);
95
+ try{ready=(await model(signal=>o.judge.ready(signal)))===true;}catch(e){
96
+ if(controller.signal.aborted||['RUN_BUDGET_EXHAUSTED','RUN_LIMIT_REACHED'].includes((e as {code?:string})?.code??''))throw e;
97
+ ready=false;
98
+ }
99
+ readinessKnown=true;await serial(heartbeat);
100
+ async function processTask(taskId:string){
101
+ const task=await call(()=>client.reviewTask(taskId,controller.signal));
102
+ if(task.nextAction?.type==='READ_OUTCOME_RESPONSE'){
103
+ if(!ready)return;const context=await call(()=>client.reviewOutcomeResponse(taskId,controller.signal));
104
+ if(typeof context.reviewSetHash!=='string'||!/^sha256:[a-f0-9]{64}$/.test(context.reviewSetHash)||!Number.isInteger(context.round)||typeof context.expiresAt!=='string')throw fault('INVALID_RESPONSE_CONTEXT');
105
+ const answer=output(await model(signal=>o.judge.respond(structuredClone({...context,inputTrust:'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS'}),signal),context.expiresAt),['decision','note']);
106
+ if(!['REVISE','MAINTAIN'].includes(String(answer.decision))||typeof answer.note!=='string'||answer.note.trim().length<4||answer.note.length>4000)throw fault('INVALID_MODEL_OUTPUT');
107
+ await serial(()=>write('RESPONSE',taskId,{...answer,expected_review_set_hash:context.reviewSetHash,expected_round:context.round}));return;
108
+ }
109
+ if(task.status==='OFFERED'){
110
+ if(!ready)return;const claimed=await serial(()=>write('CLAIM',taskId,{expected_revision:task.revision,expected_digest:task.digest}));if(!claimed)return;
111
+ }
112
+ const current=await call(()=>client.reviewTask(taskId,controller.signal));
113
+ if(!current.claim||!current.input||!current.reviewContext){if(s!.taskId===taskId){clearTask();await save();}return;}
114
+ if(current.inputTrust!=='UNTRUSTED_CONTENT_NOT_INSTRUCTIONS')throw fault('INVALID_REVIEW_INPUT');
115
+ s!.taskId=taskId;s!.claimId=current.claim.id;s!.generation=current.claim.generation;s!.digest=current.digest;await save();
116
+ if(!ready){await serial(()=>write('RELEASE',taskId,{claim_id:current.claim!.id,generation:current.claim!.generation,reason:'MODEL_PROVIDER_UNAVAILABLE'} satisfies AgentelReviewRelease));return;}
117
+ let answer:Record<string,unknown>;
118
+ try{
119
+ // One format repair is allowed, under the same run/claim time limits and frozen input.
120
+ try{answer=assessment(await model(signal=>o.judge.inspect(structuredClone(current),signal),current.claim.expiresAt),current);}
121
+ catch(e){if((e as {code?:string})?.code!=='INVALID_MODEL_OUTPUT')throw e;
122
+ answer=assessment(await model(signal=>o.judge.inspect(structuredClone(current),signal,{attempt:2,reason:'INVALID_MODEL_OUTPUT'}),current.claim.expiresAt),current);
123
+ }
124
+ }catch(e){
125
+ const code=(e as {code?:string})?.code;
126
+ // A process run budget is not inability to do the Task: retain the canonical claim.
127
+ if(controller.signal.aborted||code==='RUN_BUDGET_EXHAUSTED'||code==='RUN_LIMIT_REACHED')throw e;
128
+ ready=false;
129
+ const reason=['INVALID_MODEL_OUTPUT','INVALID_REVIEW_INPUT','MODEL_TIME_UNAVAILABLE','CLAIM_TIME_UNAVAILABLE'].includes(code??'')?'CANNOT_COMPLETE':'MODEL_PROVIDER_UNAVAILABLE';
130
+ await serial(()=>write('RELEASE',taskId,{claim_id:current.claim!.id,generation:current.claim!.generation,reason} satisfies AgentelReviewRelease));
131
+ throw fault(reason==='CANNOT_COMPLETE'?(code??'CANNOT_COMPLETE'):'MODEL_PROVIDER_UNAVAILABLE');
132
+ }
133
+ // The model supplies assessment only. Identity, Task/version and claim fence come from canonical state.
134
+ const latest=await call(()=>client.reviewTask(taskId,controller.signal));
135
+ if(!latest.claim||latest.claim.id!==current.claim.id||latest.claim.generation!==current.claim.generation||latest.digest!==current.digest||latest.revision!==current.revision){clearTask();await save();return;}
136
+ await serial(()=>write('VERDICT',taskId,{...answer,expected_revision:latest.revision,expected_digest:latest.digest,claim_id:latest.claim!.id,generation:latest.claim!.generation}));
137
+ }
138
+ if(s.taskId&&completed<run.maxTasks){try{await processTask(s.taskId);}catch(e){if(!staleTask(e))throw e;clearTask();await save();}}
139
+ const seen=new Set<string>();
140
+ for(let page=0;page<run.maxInboxPages&&completed<run.maxTasks;page++){
141
+ const inbox=await call(()=>client.reviewTasks({limit:20,...(s!.cursor?{cursor:s!.cursor}:{}),signal:controller.signal}));
142
+ const pageCursor=s.cursor;let exhausted=true;
143
+ for(const item of inbox.items){if(completed>=run.maxTasks){exhausted=false;break;}if(seen.has(item.id))continue;seen.add(item.id);try{await processTask(item.id);}catch(e){if(staleTask(e)){if(s!.taskId===item.id)clearTask();await save();continue;}throw e;}}
144
+ s.cursor=exhausted?inbox.nextCursor:pageCursor;await save();if(!inbox.nextCursor||!exhausted)break;
145
+ }
146
+ await save();await serial(heartbeat);scheduleNext();await save();
147
+ return {status:!ready?'BLOCKED':completed?'PROGRESSED':'IDLE',completed,nextCheckAt:nextCheck,code:ready?null:'MODEL_PROVIDER_UNAVAILABLE'} as ReviewWorkerResult;
148
+ }catch(caught){
149
+ const e=heartbeatError??caught;
150
+ const candidate=e instanceof AgentelApiError?e.code:typeof (e as {code?:unknown})?.code==='string'?(e as {code:string}).code:'RUN_FAILED';
151
+ const code=/^[A-Z0-9_]{1,80}$/.test(candidate)?candidate:'RUN_FAILED';
152
+ if(e instanceof AgentelApiError&&e.retryAfterMs!==null){s.notBefore=Math.max(s.notBefore,now()+e.retryAfterMs);nextCheck=Math.max(nextCheck,s.notBefore);}
153
+ scheduleNext();await save();return {status:'BLOCKED',completed,nextCheckAt:nextCheck,code} as ReviewWorkerResult;
154
+ }finally{if(pulse)clearInterval(pulse);clearTimeout(timer);run.signal?.removeEventListener('abort',onAbort);await chain;}
155
+ });
156
+ return result??{status:'BUSY',completed:0,nextCheckAt:null,code:'LOCAL_WORKER_LEASE_BUSY'};
157
+ }