/** * Two independent synthetic agents and one verified human, using the real REST API. * Start a loopback gateway with FEATURE_COLLABORATION=on (or test) and a local mail sink. * Sign into /workspace/, link the verified owner's ID to the synthetic tenant * through the documented operator command, and create a project in the website. * For PGlite, stop the gateway before the owner CLI and restart it afterward; * only one process may use that data directory. PostgreSQL supports both clients. * Provide CP_BASE_URL, CP_PROJECT_ID, CP_OWNER_ID and CP_API_KEY through a private * environment. Run: node examples/agent-collaboration.mjs * Approve the concrete local-only action in the workspace when prompted. * Credentials are never printed. This example never deploys, sends mail, charges * a card, executes supplied code, or dereferences an artifact URL. */ import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import { pathToFileURL } from 'node:url'; const operations = ['project.get','project.update','project.history','project.checkpoint','project.checkpoints','project.handoff', 'agent.upsert','agent.get','agent.list','agent.heartbeat','task.create','task.get','task.list','task.claim','task.renew','task.update', 'decision.create','decision.get','decision.consume']; function loopback(raw) { const url=new URL(raw); if(!['http:','https:'].includes(url.protocol) || !['localhost','127.0.0.1','[::1]'].includes(url.hostname) ||url.username||url.password||url.pathname!=='/'||url.search||url.hash) throw new Error('This synthetic example requires a loopback gateway origin'); return url.origin; } /** A test harness may supply onDecision(record), performing the genuine human HTTP * action using its separately authenticated session. It cannot bypass the API. */ export async function runAgentJourney({baseUrl='http://127.0.0.1:8080',projectId,ownerId,apiKey,onDecision,log=console.log}) { const origin=loopback(baseUrl); if(!projectId||!ownerId||!apiKey)throw new Error('Set CP_PROJECT_ID, CP_OWNER_ID and CP_API_KEY for the local synthetic tenant'); async function request(path,body,bearer) { const response=await fetch(`${origin}${path}`,{method:'POST',headers:{'content-type':'application/json',authorization:`Bearer ${bearer}`}, body:JSON.stringify(body),signal:AbortSignal.timeout(15_000)}); const value=await response.json(); if(!response.ok) {const error=new Error(`Collaboration request failed: ${value.error?.code??response.status}`);error.code=value.error?.code;throw error;} return value; } const runId=randomUUID().slice(0,8); async function agent(role) { const id=`${role}-${runId}`; const issued=await request('/v1/agent-token',{agentId:id,ops:['history'],channels:[],ttlSec:900, collaboration:{ops:operations,projects:[projectId]}},apiKey); return {id,call:(operation,input)=>request(`/v1/collaboration/${operation}`,{projectId,...input},issued.token)}; } const planner=await agent('planner'),reviewer=await agent('reviewer'); for(const actor of [planner,reviewer]) await actor.call('agent.upsert',{expectedRevision:0,description:`Synthetic ${actor.id}`, skills:actor===reviewer?['review']:['planning'],inputs:['project brief'],outputs:['bounded review'],availability:'available',evidence:[]}); const directory=await planner.call('agent.list',{search:reviewer.id}); assert.ok(directory.items.some(card=>card.id===reviewer.id),'The authorized reviewer is discoverable'); let task=await planner.call('task.create',{objective:'Review a synthetic patch and record one local result',expectedResult:'A structured local review receipt', recipient:reviewer.id,requiredSkills:['review'],deadline:Date.now()+20*60_000,idempotencyKey:`task-${runId}`,context:[]}); task=await reviewer.call('task.claim',{id:task.id,expectedRevision:task.revision}); const fence=task.lease.fence; assert.ok(fence,'Claim returns the private worker fence'); task=await reviewer.call('task.update',{id:task.id,expectedRevision:task.revision,fence,state:'Working',progress:'Reviewing a local fixture',message:'I own this attempt',evidence:[]}); let notebook=await reviewer.call('project.get',{}); notebook=await reviewer.call('project.update',{expectedRevision:notebook.revision,title:notebook.data.title,objective:notebook.data.objective, constraints:notebook.data.constraints,questions:notebook.data.questions,artifacts:notebook.data.artifacts, entries:[...notebook.data.entries.map(({text,status,sources})=>({text,status,sources})), {text:`Synthetic review ${runId} is ready for its human decision`,status:'suggestion',sources:[]}]}); const checkpoint=await reviewer.call('project.checkpoint',{expectedRevision:notebook.revision,label:`Before human review ${runId}`,idempotencyKey:`checkpoint-${runId}`}); task=await reviewer.call('task.update',{id:task.id,expectedRevision:task.revision,fence,state:'Needs input',blocker:'Waiting for the exact local action to be reviewed',evidence:[]}); const action='Record a synthetic local review result',target=`memory-fixture-${runId}`,actionRevision='synthetic-v1'; let decision=await reviewer.call('decision.create',{action,target,actionRevision,evidence:[],options:['Record the local result','Keep investigating'], expiresAt:Date.now()+5*60_000,idempotencyKey:`decision-${runId}`,taskId:task.id,fence}); const expectedDigest=decision.data.digest; log(`Open ${origin}/workspace/ and review decision ${decision.id}. This authorizes one synthetic local result.`); if(onDecision)await onDecision(decision); let nextRenew=Date.now()+30_000; while(decision.data.state==='Pending') { if(Date.now()>=decision.data.expiresAt)throw new Error('The human decision expired; start a new request'); if(Date.now()>=nextRenew) {await reviewer.call('task.renew',{id:task.id,fence});nextRenew=Date.now()+30_000;} await new Promise(resolve=>setTimeout(resolve,500)); decision=await reviewer.call('decision.get',{id:decision.id}); } assert.equal(decision.data.state,'Approved','A rejection or request for changes does not authorize execution'); assert.equal(decision.data.requesterId,reviewer.id); assert.equal(decision.data.decision.actorId,ownerId,'The expected verified owner decided'); assert.equal(decision.data.action,action);assert.equal(decision.data.target,target);assert.equal(decision.data.actionRevision,actionRevision); assert.equal(decision.data.digest,expectedDigest);assert.ok(decision.data.expiresAt>Date.now()); assert.equal(decision.data.decision.option,'Record the local result'); const consumption={id:decision.id,action,target,actionRevision,digest:expectedDigest,option:decision.data.decision.option,idempotencyKey:`consume-${runId}`}; const consumed=await reviewer.call('decision.consume',consumption); const retried=await reviewer.call('decision.consume',consumption); assert.equal(retried.data.consumption.id,consumed.data.consumption.id,'A lost acknowledgement recovers the same reservation'); await assert.rejects(reviewer.call('decision.consume',{...consumption,idempotencyKey:`replay-${runId}`}),error=>error.code==='conflict'); await assert.rejects(reviewer.call('decision.consume',{...consumption,action:'A different action'}),error=>error.code==='conflict'); // This deliberately trivial in-memory action is the entire execution surface. // A real external runner needs its own durable execution/recovery protocol. const localResult={receipt:consumed.data.consumption.id,review:'Synthetic fixture passed'}; task=await reviewer.call('task.update',{id:task.id,expectedRevision:task.revision,fence,state:'Working',blocker:'',progress:'Human decision consumed; explicitly resumed',evidence:[]}); const complete={id:task.id,expectedRevision:task.revision,fence,state:'Completed',idempotencyKey:`complete-${runId}`, result:{summary:localResult.review,artifacts:[]},message:'Synthetic work completed after exact approval',evidence:[]}; const completed=await reviewer.call('task.update',complete); const completionRetry=await reviewer.call('task.update',complete); assert.deepEqual(completed.data.receipt,completionRetry.data.receipt); const resumed=await planner.call('project.handoff',{checkpointId:checkpoint.id,limit:50}); assert.equal(resumed.checkpoint.data.notebook.revision,notebook.revision); assert.ok(resumed.changes.some(event=>event.kind==='decision'),'Fresh agent sees decisions after the checkpoint'); const finalTask=await planner.call('task.get',{id:task.id});assert.equal(finalTask.data.state,'Completed'); log('Two agents, one human decision, one authorization reservation and one durable completion receipt verified.'); return {projectId,taskId:task.id,checkpointId:checkpoint.id,decisionId:decision.id,completion:completed.data.receipt,authorization:localResult.receipt}; } if(process.argv[1] && import.meta.url===pathToFileURL(process.argv[1]).href) { runAgentJourney({baseUrl:process.env.CP_BASE_URL,projectId:process.env.CP_PROJECT_ID,ownerId:process.env.CP_OWNER_ID,apiKey:process.env.CP_API_KEY}) .catch(error=>{console.error(error.message);process.exitCode=1;}); }