@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
149 lines • 5.3 kB
JavaScript
export class ActionProposalWorker {
options;
leaseMs;
pollIntervalMs;
maxConcurrency;
timer;
stopped = false;
renewals = new Set();
active;
constructor(options) {
this.options = options;
this.leaseMs = options.leaseMs ?? 30000;
this.pollIntervalMs = options.pollIntervalMs ?? 1000;
this.maxConcurrency = options.maxConcurrency ?? 1;
for (const value of [this.leaseMs, this.pollIntervalMs, this.maxConcurrency])
if (!Number.isSafeInteger(value) || value <= 0)
throw new RangeError('Worker configuration must be positive safe integers');
if (this.pollIntervalMs >= this.leaseMs / 3)
throw new RangeError('Worker poll interval must be below one third of lease');
if (!options.workerId)
throw new TypeError('workerId is required');
}
start() {
if (this.timer !== undefined || this.stopped)
return;
const tick = async () => {
if (this.stopped)
return;
try {
await this.runOnce();
}
catch (error) {
this.options.onError?.(error);
}
if (!this.stopped)
this.timer = setTimeout(tick, this.pollIntervalMs);
};
this.timer = setTimeout(tick, 0);
}
async stop() {
this.stopped = true;
for (const timer of this.renewals)
clearInterval(timer);
this.renewals.clear();
if (this.timer !== undefined)
clearTimeout(this.timer);
await this.active;
}
runOnce() {
if (this.active)
return this.active;
if (this.stopped)
return Promise.resolve();
const task = this.tick();
this.active = task;
return task.finally(() => {
if (this.active === task)
this.active = undefined;
});
}
async tick() {
await this.options.store.expireActionProposals({ limit: 100 });
const work = [];
for (let index = 0; index < this.maxConcurrency && !this.stopped; index++) {
const proposal = await this.options.store.claimNextActionProposal({
workerId: this.options.workerId,
leaseMs: this.leaseMs,
});
if (!proposal)
break;
work.push(this.execute(proposal));
}
await Promise.all(work);
for (let index = 0; index < 32 && !this.stopped; index++) {
const delivery = await this.options.store.claimNextActionProposalOutcome({
workerId: this.options.workerId,
leaseMs: this.leaseMs,
});
if (!delivery)
break;
const admitted = await this.options.store.admitActionProposalOutcome(delivery.lease);
if (admitted.status === 'applied') {
try {
await this.options.onAdmitted?.(delivery.outcome);
}
catch (error) {
// The outcome is already in the thread: a broken listener must not fail the tick.
this.options.onError?.(error);
}
}
}
}
async execute(proposal) {
const lease = proposal.execution?.lease;
if (!lease)
throw new Error('Claimed action lacks execution lease');
let fenced = true;
let renewing;
const renew = () => {
if (renewing || !fenced)
return;
renewing = this.options.store
.extendActionProposalLease(proposal, proposal.id, {
token: lease.token,
generation: lease.generation,
leaseMs: this.leaseMs,
})
.then((result) => {
if (result.status !== 'applied')
fenced = false;
}, (error) => {
fenced = false;
this.options.onError?.(error);
})
.finally(() => {
renewing = undefined;
});
};
const timer = setInterval(renew, this.leaseMs / 3);
this.renewals.add(timer);
try {
const result = await this.options.executor.execute(proposal);
clearInterval(timer);
await renewing;
if (!fenced)
return;
const settled = await this.options.store.settleActionProposal(proposal, proposal.id, {
...result,
token: lease.token,
generation: lease.generation,
});
if (settled.status === 'applied' && settled.proposal) {
try {
await this.options.onSettled?.(settled.proposal);
}
catch (error) {
// The settlement is already committed: a broken listener must not fail the tick.
this.options.onError?.(error);
}
}
}
finally {
clearInterval(timer);
this.renewals.delete(timer);
await renewing;
}
}
}
//# sourceMappingURL=action-proposal-worker.js.map