@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
151 lines • 5.32 kB
JavaScript
/** Process-local {@link A2aStore} — tests and single-instance dev. */
export class InMemoryA2aStore {
#contexts = new Map();
#messages = new Map();
async getContext(id) {
const found = this.#contexts.get(id);
return found ? { ...found } : null;
}
async createContext(context) {
this.#contexts.set(context.id, { ...context });
}
async bindAccount(id, accountRef) {
const context = this.#contexts.get(id);
if (!context)
return false;
if (context.accountRef === null)
context.accountRef = accountRef;
return context.accountRef === accountRef;
}
async claimMessage(contextId, messageId) {
const key = `${contextId}\n${messageId}`;
const found = this.#messages.get(key);
if (!found) {
this.#messages.set(key, { done: false });
return { status: 'claimed' };
}
return found.done ? { status: 'done', reply: found.reply } : { status: 'pending' };
}
async completeMessage(contextId, messageId, reply) {
this.#messages.set(`${contextId}\n${messageId}`, { done: true, reply });
}
async releaseMessage(contextId, messageId) {
this.#messages.delete(`${contextId}\n${messageId}`);
}
}
export const A2A_TABLES = {
contexts: 'agent_a2a_context',
messages: 'agent_a2a_message',
};
/**
* `CREATE TABLE IF NOT EXISTS` DDL for the two A2A tables — same portable dialect as the agent
* tables (quoted identifiers, epoch-ms `BIGINT`, `TEXT` JSON). The message table's primary key IS the
* idempotency lock: two deliveries of one `messageId` race on the insert and one loses.
*/
export function a2aTableStatements() {
const t = A2A_TABLES;
return [
`CREATE TABLE IF NOT EXISTS "${t.contexts}" (
"id" VARCHAR(255) PRIMARY KEY NOT NULL,
"brand" VARCHAR(255) NOT NULL,
"agent_issuer" VARCHAR(2048) NOT NULL,
"agent_sub" VARCHAR(255) NOT NULL,
"account_ref" VARCHAR(255) NULL,
"created_at" BIGINT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS "${t.messages}" (
"context_id" VARCHAR(255) NOT NULL,
"message_id" VARCHAR(255) NOT NULL,
"reply" TEXT NULL,
"created_at" BIGINT NOT NULL,
PRIMARY KEY ("context_id", "message_id")
)`,
];
}
/** Create the A2A tables if missing. Idempotent. */
export async function ensureA2aTables(db) {
for (const statement of a2aTableStatements()) {
await db.rawQuery(statement);
}
}
/** {@link A2aStore} over Lucid (any of SQLite / Postgres / MySQL). */
export class LucidA2aStore {
db;
constructor(db) {
this.db = db;
}
async getContext(id) {
const row = await this.db.from(A2A_TABLES.contexts).where('id', id).first();
if (!row)
return null;
return {
id: String(row.id),
brand: String(row.brand),
agentIssuer: String(row.agent_issuer),
agentSub: String(row.agent_sub),
accountRef: row.account_ref == null ? null : String(row.account_ref),
};
}
async createContext(context) {
await this.db.table(A2A_TABLES.contexts).insert({
id: context.id,
brand: context.brand,
agent_issuer: context.agentIssuer,
agent_sub: context.agentSub,
account_ref: context.accountRef,
created_at: Date.now(),
});
}
async bindAccount(id, accountRef) {
await this.db
.from(A2A_TABLES.contexts)
.where('id', id)
.whereNull('account_ref')
.update({ account_ref: accountRef });
// Re-read rather than trust the update count: a concurrent bind to the SAME account also wins.
const context = await this.getContext(id);
return context?.accountRef === accountRef;
}
async claimMessage(contextId, messageId) {
try {
await this.db.table(A2A_TABLES.messages).insert({
context_id: contextId,
message_id: messageId,
reply: null,
created_at: Date.now(),
});
return { status: 'claimed' };
}
catch (error) {
const row = await this.#message(contextId, messageId);
if (!row)
throw error;
return row.reply == null
? { status: 'pending' }
: { status: 'done', reply: JSON.parse(String(row.reply)) };
}
}
async completeMessage(contextId, messageId, reply) {
await this.db
.from(A2A_TABLES.messages)
.where('context_id', contextId)
.where('message_id', messageId)
.update({ reply: JSON.stringify(reply) });
}
async releaseMessage(contextId, messageId) {
await this.db
.from(A2A_TABLES.messages)
.where('context_id', contextId)
.where('message_id', messageId)
.whereNull('reply')
.delete();
}
#message(contextId, messageId) {
return this.db
.from(A2A_TABLES.messages)
.where('context_id', contextId)
.where('message_id', messageId)
.first();
}
}
//# sourceMappingURL=store.js.map