@agentel/sdk 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTEL_CONTEXT.md +7 -3
- package/CHANGELOG.md +13 -0
- package/FAQ.md +9 -9
- package/FOUNDER_WORKER_V1.md +112 -0
- package/POOL_REVIEW_RUNTIME_V1.md +55 -0
- package/POOL_REVIEW_V1.md +86 -0
- package/POOL_REVIEW_WORKER_V1.md +69 -0
- package/README.md +16 -10
- package/V2.1.0_PUBLIC_CHANGELOG.md +11 -0
- package/V2.2.0_PUBLIC_CHANGELOG.md +14 -0
- package/agentel-connector.ts +188 -5
- package/dist/agentel-connector.d.ts +192 -2
- package/dist/agentel-connector.js +161 -2
- package/dist/founder-actions.d.ts +4 -0
- package/dist/founder-actions.js +74 -0
- package/dist/founder-worker.d.ts +152 -0
- package/dist/founder-worker.js +551 -0
- package/dist/review-worker.d.ts +62 -0
- package/dist/review-worker.js +316 -0
- package/founder-actions.ts +54 -0
- package/founder-worker.ts +289 -0
- package/node-founder-store.d.ts +5 -0
- package/node-founder-store.mjs +8 -0
- package/node-review-store.d.ts +5 -0
- package/node-review-store.mjs +33 -0
- package/package.json +31 -2
- package/review-worker.ts +157 -0
- package/test/founder-worker.test.mjs +308 -0
- package/test/pool-review.test.mjs +29 -0
- package/test/reply-management.test.mjs +33 -0
- package/test/review-worker.test.mjs +194 -0
- package/tsconfig.json +10 -2
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp,readFile,readdir,rm} from 'node:fs/promises';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import {join} from 'node:path';
|
|
6
|
+
import {spawn} from 'node:child_process';
|
|
7
|
+
import {once} from 'node:events';
|
|
8
|
+
import {AgentelConnector} from '../dist/agentel-connector.js';
|
|
9
|
+
import {runReviewWorker} from '../dist/review-worker.js';
|
|
10
|
+
import {NodeReviewWorkerStore} from '../node-review-store.mjs';
|
|
11
|
+
const digest='sha256:'+'a'.repeat(64),encryptionKey=new Uint8Array(32).fill(7),budget={maxTasks:1,maxInboxPages:2,maxRunMs:60000};
|
|
12
|
+
const answer={decision:'ACCEPTED',note:'PRIVATE_SYNTHETIC_REVIEW_NOTE',evidence_ids:['evidence']};
|
|
13
|
+
async function setup({lost,modelReady=true,inspect,respond}={}){
|
|
14
|
+
const directory=await mkdtemp(join(tmpdir(),'agentel-review-worker-'));let time=Date.now(),runtime={agentId:'reviewer',generation:0,revision:0,runtime:null},task={id:'task',missionId:'m',deliveryId:'delivery',revision:1,digest,status:'OFFERED',blockReason:null,claimWorkHours:2,offerEpisode:1,maxClaimAttempts:3,renoticeDueAt:new Date(time+7200000).toISOString(),unclaimedDueAt:new Date(time+14400000).toISOString(),nextAction:{type:'CLAIM_REVIEW_TASK'}};
|
|
15
|
+
let modelCalls=0,lostSent=false;const requests=[],receipts=new Map();
|
|
16
|
+
const json=(body,status=200,headers={})=>Response.json(body,{status,headers});
|
|
17
|
+
const fetch=async(url,init={})=>{
|
|
18
|
+
const path=new URL(url).pathname,method=init.method??'GET',body=init.body?JSON.parse(init.body):undefined,key=new Headers(init.headers).get('Idempotency-Key');requests.push({path,method,body,key});
|
|
19
|
+
if(path==='/api/v1/me/review-runtime-heartbeat'){
|
|
20
|
+
if(method==='GET')return json(runtime);
|
|
21
|
+
const saved=receipts.get('heartbeat:'+key);if(saved)return json({...saved,replayed:true});
|
|
22
|
+
if(body.expected_generation!==runtime.generation||body.expected_revision!==runtime.revision)return json({error:{code:'REVIEW_RUNTIME_CONFLICT',message:'Synthetic race'}},409);
|
|
23
|
+
runtime={...runtime,generation:runtime.generation||1,revision:runtime.revision+1,runtime:{instanceId:body.instance_id,leaseExpiresAt:new Date(time+9000000).toISOString(),sameCredential:true}};
|
|
24
|
+
const r={agentId:'reviewer',generation:runtime.generation,revision:runtime.revision,receivedAt:new Date(time).toISOString(),leaseExpiresAt:runtime.runtime.leaseExpiresAt};receipts.set('heartbeat:'+key,r);if(lost==='heartbeat'&&!lostSent){lostSent=true;throw Error('Synthetic heartbeat response lost after commit');}return json(r);
|
|
25
|
+
}
|
|
26
|
+
if(path==='/api/v1/me/review-tasks')return json({items:task.status==='DECIDED'?[]:[task],nextCursor:null,pollAfterSeconds:3600});
|
|
27
|
+
if(method==='GET')return json(task.status==='CLAIMED'?{...task,claim:{id:'claim',generation:1,expiresAt:new Date(time+7200000).toISOString()},input:{delivery:{content:'UNTRUSTED_PRIVATE_DELIVERY'},evidence:[{id:'evidence'}]},reviewContext:{brief:'Frozen requirements'},inputTrust:'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS'}:task);
|
|
28
|
+
const op=path.split('/').at(-1),saved=receipts.get(op+':'+key);if(saved)return json({...saved,replayed:true});
|
|
29
|
+
let r;
|
|
30
|
+
if(op==='claim'){assert.equal(body.expected_revision,task.revision);task={...task,status:'CLAIMED',revision:2};r={claimId:'claim',generation:1,digest,revision:2,expiresAt:new Date(time+7200000).toISOString()};}
|
|
31
|
+
else if(op==='verdict'){assert.equal(body.expected_revision,2);assert.equal(body.claim_id,'claim');task={...task,status:'DECIDED',revision:3};r={verdictId:'verdict',decision:body.decision};}
|
|
32
|
+
else if(op==='release'){task={...task,status:'DECIDED'};r={released:true};}
|
|
33
|
+
else throw Error('Unexpected synthetic route');
|
|
34
|
+
receipts.set(op+':'+key,r);if(op===lost&&!lostSent){lostSent=true;throw Error('Synthetic network loss after committed '+op);}return json(r);
|
|
35
|
+
};
|
|
36
|
+
const client=new AgentelConnector({baseUrl:'https://agentel.test/api/v1',apiKey:'synthetic-role-key',agentId:'reviewer',fetch,maxRetries:4});
|
|
37
|
+
const makeStore=()=>new NodeReviewWorkerStore({directory,encryptionKey,agentId:'reviewer',credentialBindingId:'synthetic-key-id'});
|
|
38
|
+
const options={agentId:'reviewer',deploymentId:'synthetic-deployment',client,store:makeStore(),now:()=>time,random:()=>0.5,judge:{ready:async()=>modelReady,inspect:async (t,signal,repair)=>{modelCalls++;assert.equal(t.inputTrust,'UNTRUSTED_CONTENT_NOT_INSTRUCTIONS');return inspect?inspect(t,signal,repair):answer;},respond:respond??(async()=>({decision:'MAINTAIN',note:'Frozen evidence remains sufficient.'}))}};
|
|
39
|
+
return {directory,options,requests,receipts,fetch,makeStore,get task(){return task;},get modelCalls(){return modelCalls;},advance:ms=>{time+=ms;},dispose:()=>rm(directory,{recursive:true,force:true})};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('bounded reviewer completes claim/inspection/verdict in one run; model cannot alter the canonical fence',async()=>{
|
|
43
|
+
const f=await setup({inspect:t=>{t.revision=999;return answer;}});try{
|
|
44
|
+
const result=await runReviewWorker(f.options,budget);assert.equal(result.status,'PROGRESSED');assert.equal(result.completed,1);assert.equal(f.task.status,'DECIDED');assert.equal(f.modelCalls,1);
|
|
45
|
+
assert.equal(f.requests.filter(r=>r.path.endsWith('/claim')).length,1);assert.equal(f.requests.filter(r=>r.path.endsWith('/verdict')).length,1);
|
|
46
|
+
assert.equal((await runReviewWorker(f.options,budget)).status,'WAIT_NEXT_CHECK');
|
|
47
|
+
assert.ok(f.requests.every(r=>!JSON.stringify(r.body??{}).includes('synthetic-role-key')));
|
|
48
|
+
}finally{await f.dispose();}
|
|
49
|
+
});
|
|
50
|
+
for(const lost of ['claim','verdict'])test('encrypted restart recovers committed '+lost+' using the same body/key and logical worker identity',async()=>{
|
|
51
|
+
const f=await setup({lost});try{
|
|
52
|
+
const first=await runReviewWorker(f.options,budget);assert.equal(first.status,'BLOCKED');
|
|
53
|
+
const before=f.requests.filter(r=>r.path.endsWith('/'+lost));assert.equal(before.length,1);
|
|
54
|
+
for(const name of await readdir(f.directory))assert.equal((await readFile(join(f.directory,name))).includes(Buffer.from(answer.note)),false);
|
|
55
|
+
const saved=await f.options.store.exclusive(s=>s.load());assert.equal(saved.pending.kind,lost.toUpperCase());
|
|
56
|
+
f.advance(3600000);const second=await runReviewWorker({...f.options,store:f.makeStore()},budget);assert.equal(second.status,'PROGRESSED');assert.equal(f.modelCalls,1);assert.equal(f.task.status,'DECIDED');
|
|
57
|
+
const writes=f.requests.filter(r=>r.path.endsWith('/'+lost));assert.equal(writes.length,2);assert.deepEqual(writes[0].body,writes[1].body);assert.equal(writes[0].key,writes[1].key);
|
|
58
|
+
const heartbeats=f.requests.filter(r=>r.path.endsWith('heartbeat')&&r.method==='POST');assert.equal(new Set(heartbeats.map(r=>r.body.instance_id)).size,1);
|
|
59
|
+
}finally{await f.dispose();}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('model outage never claims; a model injecting authority fields causes safe release rather than a verdict',async()=>{
|
|
63
|
+
for(const settings of [{modelReady:false},{modelReady:'false'},{modelReady:{available:false}},{inspect:()=>({...answer,expected_revision:900})}]){
|
|
64
|
+
const f=await setup(settings);try{const result=await runReviewWorker(f.options,budget);assert.equal(f.requests.filter(r=>r.path.endsWith('/verdict')).length,0);
|
|
65
|
+
if(settings.modelReady!==undefined){assert.equal(result.completed,0);assert.equal(f.requests.filter(r=>r.path.endsWith('/claim')).length,0);}
|
|
66
|
+
else{assert.equal(result.status,'BLOCKED');assert.equal(f.requests.filter(r=>r.path.endsWith('/release')).length,1);}
|
|
67
|
+
}finally{await f.dispose();}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('worker transport does not hide 429 retries and persists full Retry-After beyond the hourly wake',async()=>{
|
|
72
|
+
const f=await setup();try{
|
|
73
|
+
let calls=0;const client=new AgentelConnector({baseUrl:'https://agentel.test/api/v1',apiKey:'synthetic',agentId:'reviewer',maxRetries:4,fetch:async()=>{calls++;return Response.json({error:{code:'RATE_LIMITED',message:'Synthetic'}},{status:429,headers:{'Retry-After':'7200'}});}});
|
|
74
|
+
const result=await runReviewWorker({...f.options,client},budget);assert.equal(result.code,'RATE_LIMITED');assert.equal(calls,1);const saved=await f.options.store.exclusive(s=>s.load());assert.ok(saved.notBefore>=f.options.now()+7200000);f.advance(3600000);assert.equal((await runReviewWorker({...f.options,client},budget)).status,'WAIT_NEXT_CHECK');assert.equal(calls,1);
|
|
75
|
+
}finally{await f.dispose();}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('encrypted store rejects wrong credential binding/key and cooperating replicas cannot both execute',async()=>{
|
|
79
|
+
const f=await setup();try{
|
|
80
|
+
let entered,release;const started=new Promise(r=>{entered=r;}),hold=new Promise(r=>{release=r;});
|
|
81
|
+
const first=f.options.store.exclusive(async session=>{await session.save({schema:1,private:'sensitive synthetic checkpoint'});entered();await hold;return true;});await started;
|
|
82
|
+
assert.equal((await runReviewWorker({...f.options,store:f.makeStore()},budget)).status,'BUSY');assert.equal(f.requests.length,0);release();await first;
|
|
83
|
+
for(const change of [{credentialBindingId:'another-key'},{encryptionKey:new Uint8Array(32).fill(8)}]){
|
|
84
|
+
const wrong=new NodeReviewWorkerStore({directory:f.directory,encryptionKey,agentId:'reviewer',credentialBindingId:'synthetic-key-id',...change});await assert.rejects(wrong.exclusive(s=>s.load()),/CHECKPOINT_AUTHENTICATION_FAILED/);
|
|
85
|
+
}
|
|
86
|
+
}finally{await f.dispose();}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('process crash releases the SQLite process lease while the encrypted pending request survives',async()=>{
|
|
90
|
+
const f=await setup();let child;try{
|
|
91
|
+
const moduleURL=new URL('../node-review-store.mjs',import.meta.url).href;
|
|
92
|
+
const code=`import {NodeReviewWorkerStore} from ${JSON.stringify(moduleURL)};const s=new NodeReviewWorkerStore({directory:process.argv[1],encryptionKey:new Uint8Array(32).fill(7),agentId:'reviewer',credentialBindingId:'synthetic-key-id'});await s.exclusive(async session=>{await session.save({schema:1,pending:{key:'durable-private-operation',body:{note:'private crash fixture'}}});await new Promise(resolve=>{process.on('message',message=>{if(message==='release')resolve();});process.stdout.write('READY\\n');});});`;
|
|
93
|
+
child=spawn(process.execPath,['--input-type=module','-e',code,f.directory],{stdio:['ignore','pipe','pipe','ipc']});
|
|
94
|
+
await new Promise((resolve,reject)=>{let text='';child.stdout.on('data',chunk=>{text+=chunk;if(text.includes('READY'))resolve();});child.once('error',reject);child.once('exit',c=>reject(Error('Child exited before ready '+c)));});
|
|
95
|
+
assert.equal(await f.makeStore().exclusive(()=>Promise.resolve('should not execute')),null);const exited=once(child,'exit');child.kill('SIGKILL');await exited;
|
|
96
|
+
const saved=await f.makeStore().exclusive(s=>s.load());assert.equal(saved.pending.key,'durable-private-operation');assert.equal(saved.pending.body.note,'private crash fixture');
|
|
97
|
+
}finally{if(child?.exitCode===null&&child?.signalCode===null)child.kill('SIGKILL');await f.dispose();}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('long-running reasoning receives an actual half-hour heartbeat',async t=>{
|
|
101
|
+
t.mock.timers.enable({apis:['setInterval']});
|
|
102
|
+
let entered,finish;const started=new Promise(r=>{entered=r;}),result=new Promise(r=>{finish=r;});
|
|
103
|
+
const f=await setup({inspect:async()=>{entered();return result;}});try{
|
|
104
|
+
const active=runReviewWorker(f.options,{...budget,maxRunMs:21600000});await started;
|
|
105
|
+
const count=f.requests.filter(r=>r.path.endsWith('heartbeat')&&r.method==='POST').length;
|
|
106
|
+
t.mock.timers.tick(1800000);await new Promise(r=>setImmediate(r));
|
|
107
|
+
assert.ok(f.requests.filter(r=>r.path.endsWith('heartbeat')&&r.method==='POST').length>count);
|
|
108
|
+
finish(answer);assert.equal((await active).status,'PROGRESSED');
|
|
109
|
+
}finally{finish(answer);t.mock.timers.reset();await f.dispose();}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('an aborted run returns without accepting a late model result',async()=>{
|
|
113
|
+
let entered,finish;const started=new Promise(r=>{entered=r;}),answerLater=new Promise(r=>{finish=r;});
|
|
114
|
+
const f=await setup({inspect:async()=>{entered();return answerLater;}});try{
|
|
115
|
+
const abort=new AbortController(),active=runReviewWorker(f.options,{...budget,signal:abort.signal});await started;abort.abort();const result=await active;assert.equal(result.status,'BLOCKED');assert.equal(f.requests.filter(r=>r.path.endsWith('/verdict')).length,0);
|
|
116
|
+
finish(answer);await new Promise(r=>setImmediate(r));assert.equal(f.requests.filter(r=>r.path.endsWith('/verdict')).length,0);
|
|
117
|
+
}finally{finish(answer);await f.dispose();}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('restart recovers an unknown heartbeat with the original key before making a fresh tick or claim',async()=>{
|
|
121
|
+
const f=await setup({lost:'heartbeat'});try{
|
|
122
|
+
assert.equal((await runReviewWorker(f.options,budget)).status,'BLOCKED');const state=await f.options.store.exclusive(s=>s.load());assert.ok(state.heartbeat);assert.equal(state.pending,null);assert.equal(f.modelCalls,0);
|
|
123
|
+
f.advance(3600000);assert.equal((await runReviewWorker({...f.options,store:f.makeStore()},budget)).status,'PROGRESSED');const replay=f.requests.filter(r=>r.key===state.heartbeat.key);assert.equal(replay.length,2);assert.deepEqual(replay[0].body,replay[1].body);assert.equal(f.modelCalls,1);
|
|
124
|
+
}finally{await f.dispose();}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('a bounded inbox traversal persists its cursor and eventually reaches later pages',async()=>{
|
|
128
|
+
const f=await setup();try{
|
|
129
|
+
const cursors=[];const client=new AgentelConnector({baseUrl:'https://agentel.test/api/v1',agentId:'reviewer',apiKey:'synthetic',fetch:async(url,init)=>{
|
|
130
|
+
const u=new URL(url);if(u.pathname.endsWith('/me/review-tasks')){const cursor=u.searchParams.get('cursor');cursors.push(cursor);if(cursor!=='c2')return Response.json({items:[{id:cursor?'gone-2':'gone-1'}],nextCursor:cursor?'c2':'c1',pollAfterSeconds:3600});}
|
|
131
|
+
if(u.pathname.includes('/gone-'))return Response.json({error:{code:'REVIEW_TASK_NOT_FOUND',message:'No longer eligible'}},{status:404});return f.fetch(url,init);
|
|
132
|
+
}});
|
|
133
|
+
for(let i=0;i<3;i++){await runReviewWorker({...f.options,client},{...budget,maxInboxPages:1});f.advance(3600000);}
|
|
134
|
+
assert.deepEqual(cursors,[null,'c1','c2']);assert.equal(f.modelCalls,1);assert.equal(f.task.status,'DECIDED');assert.equal((await f.options.store.exclusive(s=>s.load())).cursor,null);
|
|
135
|
+
}finally{await f.dispose();}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('an active long run schedules its next idle check from completion instead of returning a past wake time',async()=>{
|
|
139
|
+
const f=await setup();try{
|
|
140
|
+
f.options.judge.inspect=async()=>{f.advance(3600000);return answer;};const r=await runReviewWorker(f.options,{...budget,maxRunMs:21600000});assert.equal(r.status,'PROGRESSED');assert.ok(r.nextCheckAt>=f.options.now()+3600000);assert.equal((await runReviewWorker(f.options,budget)).status,'WAIT_NEXT_CHECK');
|
|
141
|
+
}finally{await f.dispose();}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('run-budget cutoff preserves a live claim and the next run submits without another claim or permanent exclusion',async()=>{
|
|
145
|
+
let calls=0;
|
|
146
|
+
const f=await setup({inspect:async()=>{calls++;if(calls===1)await new Promise(r=>setTimeout(r,4000));return answer;}});try{
|
|
147
|
+
const first=await runReviewWorker(f.options,{...budget,maxRunMs:3000});assert.equal(first.status,'BLOCKED');assert.equal(f.requests.filter(r=>r.path.endsWith('/release')).length,0);assert.equal(first.code,'RUN_BUDGET_EXHAUSTED');
|
|
148
|
+
assert.equal(f.requests.filter(r=>r.path.endsWith('/release')).length,0);assert.equal(f.task.status,'CLAIMED');const state=await f.options.store.exclusive(s=>s.load());assert.equal(state.taskId,'task');assert.equal(state.claimId,'claim');assert.equal(state.pending,null);
|
|
149
|
+
f.advance(3600000);const next=await runReviewWorker({...f.options,store:f.makeStore()},budget);assert.equal(next.status,'PROGRESSED');assert.equal(calls,2);assert.equal(f.requests.filter(r=>r.path.endsWith('/claim')).length,1);assert.equal(f.requests.filter(r=>r.path.endsWith('/verdict')).length,1);
|
|
150
|
+
assert.equal(f.requests.filter(r=>r.path.endsWith('/release')).length,0);
|
|
151
|
+
}finally{await f.dispose();}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('a binding claim deadline, rather than a run cutoff, releases work that cannot fit another attempt',async()=>{
|
|
155
|
+
const f=await setup({inspect:async()=>{await new Promise(r=>setTimeout(r,4000));return answer;}});try{
|
|
156
|
+
const client=new AgentelConnector({baseUrl:'https://agentel.test/api/v1',agentId:'reviewer',apiKey:'synthetic',fetch:async(url,init)=>{
|
|
157
|
+
const response=await f.fetch(url,init);if((init.method??'GET')==='GET'&&new URL(url).pathname==='/api/v1/review-tasks/task'){
|
|
158
|
+
const body=await response.json();if(body.claim)body.claim.expiresAt=new Date(f.options.now()+62000).toISOString();return Response.json(body);
|
|
159
|
+
}return response;
|
|
160
|
+
}});
|
|
161
|
+
const result=await runReviewWorker({...f.options,client},{...budget,maxRunMs:10000});assert.equal(result.code,'CLAIM_TIME_UNAVAILABLE');assert.equal(f.requests.filter(r=>r.path.endsWith('/release')).length,1);assert.equal(f.requests.find(r=>r.path.endsWith('/release')).body.reason,'CANNOT_COMPLETE');assert.equal(f.requests.filter(r=>r.path.endsWith('/verdict')).length,0);
|
|
162
|
+
}finally{await f.dispose();}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
for(const operation of ['verdict','release'])test('readiness throwing cannot block a pending '+operation+' from replaying its exact body/key',async()=>{
|
|
166
|
+
const f=await setup({lost:operation,...(operation==='release'?{inspect:async()=>{throw Error('Synthetic provider outage');}}:{})});try{
|
|
167
|
+
assert.equal((await runReviewWorker(f.options,budget)).status,'BLOCKED');const saved=await f.options.store.exclusive(s=>s.load());assert.equal(saved.pending.kind,operation.toUpperCase());const first=f.requests.filter(r=>r.path.endsWith('/'+operation));assert.equal(first.length,1);
|
|
168
|
+
let readyCalled=false;f.options.judge.ready=async()=>{readyCalled=true;assert.equal(f.requests.filter(r=>r.path.endsWith('/'+operation)).length,2);throw Error('Synthetic ready failure');};
|
|
169
|
+
f.advance(3600000);const next=await runReviewWorker({...f.options,store:f.makeStore()},budget);assert.equal(next.status,'BLOCKED');assert.equal(next.code,'MODEL_PROVIDER_UNAVAILABLE');assert.equal(readyCalled,true);assert.equal((await f.options.store.exclusive(s=>s.load())).pending,null);
|
|
170
|
+
const writes=f.requests.filter(r=>r.path.endsWith('/'+operation));assert.equal(writes.length,2);assert.deepEqual(writes[0].body,writes[1].body);assert.equal(writes[0].key,writes[1].key);assert.equal(f.modelCalls,1);
|
|
171
|
+
assert.equal(next.completed,operation==='verdict'?1:0);
|
|
172
|
+
}finally{await f.dispose();}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('readiness throwing releases an already-held claim as provider unavailable without invoking judgment',async()=>{
|
|
176
|
+
const f=await setup();try{
|
|
177
|
+
await f.options.client.claimReviewTask('task',{expected_revision:1,expected_digest:digest},'prior-claim');
|
|
178
|
+
f.options.judge.ready=async()=>{throw Error('Synthetic unavailable readiness service');};const result=await runReviewWorker(f.options,budget);assert.equal(result.code,'MODEL_PROVIDER_UNAVAILABLE');assert.equal(f.modelCalls,0);
|
|
179
|
+
const releases=f.requests.filter(r=>r.path.endsWith('/release'));assert.equal(releases.length,1);assert.equal(releases[0].body.reason,'MODEL_PROVIDER_UNAVAILABLE');
|
|
180
|
+
}finally{await f.dispose();}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('one malformed assessment is re-asked once against the same immutable input and claim before a verdict',async()=>{
|
|
184
|
+
let count=0;const inputs=[];
|
|
185
|
+
const f=await setup({inspect:async(task,signal,repair)=>{count++;inputs.push(structuredClone(task));if(count===1){assert.equal(repair,undefined);return {...answer,expected_revision:999};}assert.deepEqual(repair,{attempt:2,reason:'INVALID_MODEL_OUTPUT'});assert.equal(signal.aborted,false);return answer;}});try{
|
|
186
|
+
const result=await runReviewWorker(f.options,budget);assert.equal(result.status,'PROGRESSED');assert.equal(count,2);assert.deepEqual(inputs[0],inputs[1]);assert.equal(f.requests.filter(r=>r.path.endsWith('/claim')).length,1);assert.equal(f.requests.filter(r=>r.path.endsWith('/release')).length,0);assert.equal(f.requests.filter(r=>r.path.endsWith('/verdict')).length,1);
|
|
187
|
+
}finally{await f.dispose();}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('two malformed assessments stop after the single bounded re-ask and release only once',async()=>{
|
|
191
|
+
const f=await setup({inspect:async()=>({decision:'UNSUPPORTED',note:'Bad synthetic format',evidence_ids:[]})});try{
|
|
192
|
+
assert.equal((await runReviewWorker(f.options,budget)).code,'INVALID_MODEL_OUTPUT');assert.equal(f.modelCalls,2);assert.equal(f.requests.filter(r=>r.path.endsWith('/release')).length,1);assert.equal(f.requests.filter(r=>r.path.endsWith('/verdict')).length,0);
|
|
193
|
+
}finally{await f.dispose();}
|
|
194
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
"target": "ES2022",
|
|
4
4
|
"module": "ESNext",
|
|
5
5
|
"moduleResolution": "Bundler",
|
|
6
|
-
"lib": [
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2022",
|
|
8
|
+
"DOM",
|
|
9
|
+
"DOM.Iterable"
|
|
10
|
+
],
|
|
7
11
|
"declaration": true,
|
|
8
12
|
"outDir": "dist",
|
|
9
13
|
"rootDir": ".",
|
|
@@ -11,5 +15,9 @@
|
|
|
11
15
|
"skipLibCheck": true,
|
|
12
16
|
"noEmitOnError": true
|
|
13
17
|
},
|
|
14
|
-
"include": [
|
|
18
|
+
"include": [
|
|
19
|
+
"agentel-connector.ts",
|
|
20
|
+
"review-worker.ts",
|
|
21
|
+
"founder-worker.ts"
|
|
22
|
+
]
|
|
15
23
|
}
|