UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

260 lines • 9.49 kB
import { forDialect } from '../stores/lucid-schema.js'; import { isMySql } from '../stores/sql-dialect.js'; export class InMemoryPoppyConnectionStore { #connections = new Map(); #pending = new Map(); async listConnections(owner) { return [...this.#connections.values()].filter((c) => c.owner === owner).map((c) => ({ ...c })); } async getConnection(owner, issuer) { const found = this.#connections.get(`${owner}\n${issuer}`); return found ? { ...found } : null; } async saveConnection(record) { this.#connections.set(`${record.owner}\n${record.issuer}`, { ...record }); } async deleteConnection(owner, issuer) { this.#connections.delete(`${owner}\n${issuer}`); } async createPending(record) { if (!this.#pending.has(record.id)) this.#pending.set(record.id, structuredClone(record)); } async getPending(id) { const found = this.#pending.get(id); return found ? structuredClone(found) : null; } async updatePending(id, patch) { const found = this.#pending.get(id); if (found) Object.assign(found, patch); } } export const POPPY_CONNECTION_TABLES = { connections: 'agent_poppy_connection', pending: 'agent_poppy_connect', }; export function poppyConnectionTableStatements() { const t = POPPY_CONNECTION_TABLES; return [ `CREATE TABLE IF NOT EXISTS "${t.connections}" ( "owner" VARCHAR(255) NOT NULL, "issuer" VARCHAR(1024) NOT NULL, "domain" VARCHAR(255) NOT NULL, "organization" VARCHAR(255) NOT NULL, "scope" TEXT NOT NULL, "status" VARCHAR(16) NOT NULL, "state" TEXT NOT NULL, "created_at" BIGINT NOT NULL, "updated_at" BIGINT NOT NULL, PRIMARY KEY ("owner", "issuer") )`, `CREATE TABLE IF NOT EXISTS "${t.pending}" ( "id" VARCHAR(64) PRIMARY KEY NOT NULL, "owner" VARCHAR(255) NOT NULL, "domain" VARCHAR(255) NOT NULL, "issuer" VARCHAR(1024) NOT NULL, "organization" VARCHAR(255) NOT NULL, "scope" TEXT NOT NULL, "actor" TEXT NULL, "thread_id" VARCHAR(255) NULL, "run_id" VARCHAR(255) NULL, "tool_call_id" VARCHAR(255) NULL, "flow" TEXT NULL, "status" VARCHAR(16) NOT NULL, "expires_at" BIGINT NOT NULL, "created_at" BIGINT NOT NULL )`, ]; } export async function ensurePoppyConnectionTables(db) { const mysql = isMySql(db); for (const statement of poppyConnectionTableStatements()) { await db.rawQuery(forDialect(statement, mysql)); } } const int = (v) => (typeof v === 'number' ? v : Number(v ?? 0)); const str = (v) => (v == null ? null : String(v)); /** {@link PoppyConnectionStore} over Lucid (SQLite / Postgres / MySQL); tables created on first use. */ export class LucidPoppyConnectionStore { db; #ready = null; #autoCreate; constructor(db, options = {}) { this.db = db; this.#autoCreate = options.autoCreateTables ?? true; } ready() { if (!this.#autoCreate) return Promise.resolve(); if (this.#ready === null) { this.#ready = ensurePoppyConnectionTables(this.db); this.#ready.catch(() => { this.#ready = null; }); } return this.#ready; } #connection(row) { return { owner: String(row.owner), issuer: String(row.issuer), domain: String(row.domain), organization: String(row.organization), scope: String(row.scope ?? ''), status: String(row.status), state: String(row.state), createdAt: int(row.created_at), updatedAt: int(row.updated_at), }; } async listConnections(owner) { await this.ready(); const rows = await this.db .from(POPPY_CONNECTION_TABLES.connections) .where('owner', owner) .orderBy('domain', 'asc') .select('*'); return rows.map((row) => this.#connection(row)); } async getConnection(owner, issuer) { await this.ready(); const row = await this.db .from(POPPY_CONNECTION_TABLES.connections) .where('owner', owner) .where('issuer', issuer) .first(); return row ? this.#connection(row) : null; } async saveConnection(record) { await this.ready(); const columns = { domain: record.domain, organization: record.organization, scope: record.scope, status: record.status, state: record.state, updated_at: record.updatedAt, }; const existing = await this.db .from(POPPY_CONNECTION_TABLES.connections) .where('owner', record.owner) .where('issuer', record.issuer) .first(); if (existing) { await this.db .from(POPPY_CONNECTION_TABLES.connections) .where('owner', record.owner) .where('issuer', record.issuer) .update(columns); return; } await this.db.table(POPPY_CONNECTION_TABLES.connections).insert({ owner: record.owner, issuer: record.issuer, created_at: record.createdAt, ...columns, }); } async deleteConnection(owner, issuer) { await this.ready(); await this.db .from(POPPY_CONNECTION_TABLES.connections) .where('owner', owner) .where('issuer', issuer) .delete(); } async createPending(record) { await this.ready(); if (await this.getPending(record.id)) return; await this.db.table(POPPY_CONNECTION_TABLES.pending).insert({ id: record.id, owner: record.owner, domain: record.domain, issuer: record.issuer, organization: record.organization, scope: record.scope, actor: record.actor === null ? null : JSON.stringify(record.actor), thread_id: record.threadId, run_id: record.runId, tool_call_id: record.toolCallId, flow: record.flow, status: record.status, expires_at: record.expiresAt, created_at: record.createdAt, }); } async getPending(id) { await this.ready(); const row = await this.db.from(POPPY_CONNECTION_TABLES.pending).where('id', id).first(); if (!row) return null; return { id: String(row.id), owner: String(row.owner), domain: String(row.domain), issuer: String(row.issuer), organization: String(row.organization), scope: String(row.scope ?? ''), actor: row.actor == null ? null : JSON.parse(String(row.actor)), threadId: str(row.thread_id), runId: str(row.run_id), toolCallId: str(row.tool_call_id), flow: str(row.flow), status: String(row.status), expiresAt: int(row.expires_at), createdAt: int(row.created_at), }; } async updatePending(id, patch) { await this.ready(); const row = {}; if (patch.flow !== undefined) row.flow = patch.flow; if (patch.status !== undefined) row.status = patch.status; if (Object.keys(row).length === 0) return; await this.db.from(POPPY_CONNECTION_TABLES.pending).where('id', id).update(row); } } /** {@link PoppyConnectionStore} over Redis: JSON values, an index set per owner. */ export function redisPoppyConnectionStore(redis, options = {}) { const p = options.prefix ?? 'agent:poppy-connections:'; const key = (owner, issuer) => `${p}conn:${owner}\n${issuer}`; const index = (owner) => `${p}owner:${owner}`; const pendingKey = (id) => `${p}pending:${id}`; const get = async (k) => { const raw = await redis.get(k); return raw === null ? null : JSON.parse(raw); }; return { async listConnections(owner) { const issuers = await redis.smembers(index(owner)); const found = await Promise.all(issuers.map((i) => get(key(owner, i)))); return found.filter((c) => c !== null); }, getConnection: (owner, issuer) => get(key(owner, issuer)), async saveConnection(record) { await redis.set(key(record.owner, record.issuer), JSON.stringify(record)); await redis.sadd(index(record.owner), record.issuer); }, async deleteConnection(owner, issuer) { await redis.del(key(owner, issuer)); await redis.srem(index(owner), issuer); }, async createPending(record) { const ttl = Math.max(1, Math.ceil((record.expiresAt - Date.now()) / 1000) + 3600); await redis.set(pendingKey(record.id), JSON.stringify(record), 'EX', ttl, 'NX'); }, getPending: (id) => get(pendingKey(id)), async updatePending(id, patch) { const found = await get(pendingKey(id)); if (!found) return; await redis.set(pendingKey(id), JSON.stringify({ ...found, ...patch }), 'KEEPTTL'); }, }; } //# sourceMappingURL=store.js.map