UNPKG

agents

Version:

A home for your AI agents

453 lines (452 loc) 15.9 kB
import { i as getCurrentAgent, t as Agent } from "../src-BNU3ZiJM.js"; //#region src/chat-sdk/agent.ts const NEXT_CLEANUP_AT_KEY = "next_cleanup_at"; const CLEANUP_SCHEDULE_ID_KEY = "cleanup_schedule_id"; var ChatSdkStateAgent = class extends Agent { async onStart() { this.migrate(); await this.scheduleNextCleanup(); } subscribe(threadId) { this.sql` INSERT OR IGNORE INTO chat_sdk_state_subscriptions (thread_id) VALUES (${threadId}) `; } unsubscribe(threadId) { this.sql` DELETE FROM chat_sdk_state_subscriptions WHERE thread_id = ${threadId} `; } isSubscribed(threadId) { return this.sql` SELECT 1 as found FROM chat_sdk_state_subscriptions WHERE thread_id = ${threadId} LIMIT 1 `.length > 0; } async acquireLock(threadId, ttlMs) { const result = this.ctx.storage.transactionSync(() => { const now = Date.now(); this.ctx.storage.sql.exec("DELETE FROM chat_sdk_state_locks WHERE thread_id = ? AND expires_at <= ?", threadId, now); if (this.ctx.storage.sql.exec("SELECT 1 FROM chat_sdk_state_locks WHERE thread_id = ? LIMIT 1", threadId).toArray().length > 0) return null; const token = crypto.randomUUID(); const expiresAt = now + ttlMs; this.ctx.storage.sql.exec("INSERT INTO chat_sdk_state_locks (thread_id, token, expires_at) VALUES (?, ?, ?)", threadId, token, expiresAt); return { threadId, token, expiresAt }; }); await this.scheduleCleanupForExpiry(result?.expiresAt ?? null); return result; } releaseLock(threadId, token) { this.sql` DELETE FROM chat_sdk_state_locks WHERE thread_id = ${threadId} AND token = ${token} `; } async extendLock(threadId, token, ttlMs) { const result = this.ctx.storage.transactionSync(() => { const now = Date.now(); return this.ctx.storage.sql.exec(`UPDATE chat_sdk_state_locks SET expires_at = ? WHERE thread_id = ? AND token = ? AND expires_at > ? RETURNING thread_id`, now + ttlMs, threadId, token, now).toArray().length > 0; }); if (result) await this.scheduleCleanupForExpiry(Date.now() + ttlMs); return result; } forceReleaseLock(threadId) { this.sql` DELETE FROM chat_sdk_state_locks WHERE thread_id = ${threadId} `; } async enqueue(threadId, value, maxSize) { const parsed = parseQueueEntry(value); const count = this.ctx.storage.transactionSync(() => { this.ctx.storage.sql.exec("INSERT INTO chat_sdk_state_queue (thread_id, value, enqueued_at, expires_at) VALUES (?, ?, ?, ?)", threadId, value, parsed.enqueuedAt, parsed.expiresAt); this.ctx.storage.sql.exec(`DELETE FROM chat_sdk_state_queue WHERE thread_id = ? AND id NOT IN ( SELECT id FROM chat_sdk_state_queue WHERE thread_id = ? ORDER BY id DESC LIMIT ? )`, threadId, threadId, maxSize); return this.ctx.storage.sql.exec("SELECT COUNT(*) as count FROM chat_sdk_state_queue WHERE thread_id = ?", threadId).one().count; }); await this.scheduleCleanupForExpiry(parsed.expiresAt); return count; } popQueue(threadId) { return this.ctx.storage.transactionSync(() => { const now = Date.now(); this.ctx.storage.sql.exec("DELETE FROM chat_sdk_state_queue WHERE thread_id = ? AND expires_at <= ?", threadId, now); const row = this.ctx.storage.sql.exec("SELECT id, value FROM chat_sdk_state_queue WHERE thread_id = ? ORDER BY id ASC LIMIT 1", threadId).toArray()[0]; if (!row) return null; this.ctx.storage.sql.exec("DELETE FROM chat_sdk_state_queue WHERE id = ?", row.id); return row.value; }); } queueDepth(threadId) { return this.sql` SELECT COUNT(*) as count FROM chat_sdk_state_queue WHERE thread_id = ${threadId} AND expires_at > ${Date.now()} `[0]?.count ?? 0; } async listAppend(key, value, maxLength, ttlMs) { const expiresAt = ttlMs && ttlMs > 0 ? Date.now() + ttlMs : null; this.ctx.storage.transactionSync(() => { this.ctx.storage.sql.exec("INSERT INTO chat_sdk_state_lists (key, value, expires_at) VALUES (?, ?, ?)", key, value, expiresAt); if (expiresAt !== null) this.ctx.storage.sql.exec("UPDATE chat_sdk_state_lists SET expires_at = ? WHERE key = ?", expiresAt, key); if (maxLength != null && maxLength > 0) this.ctx.storage.sql.exec(`DELETE FROM chat_sdk_state_lists WHERE key = ? AND id NOT IN ( SELECT id FROM chat_sdk_state_lists WHERE key = ? ORDER BY id DESC LIMIT ? )`, key, key, maxLength); }); await this.scheduleCleanupForExpiry(expiresAt); } listGet(key) { const now = Date.now(); this.sql` DELETE FROM chat_sdk_state_lists WHERE key = ${key} AND expires_at IS NOT NULL AND expires_at <= ${now} `; return this.sql` SELECT value FROM chat_sdk_state_lists WHERE key = ${key} ORDER BY id ASC `.map((row) => row.value); } cacheGet(key) { return this.readCacheValue(key, Date.now()); } async cacheSet(key, value, ttlMs) { const expiresAt = ttlMs && ttlMs > 0 ? Date.now() + ttlMs : null; this.upsertCacheValue(key, value, expiresAt); await this.scheduleCleanupForExpiry(expiresAt); } async cacheSetIfNotExists(key, value, ttlMs) { const now = Date.now(); const inserted = this.ctx.storage.transactionSync(() => { this.ctx.storage.sql.exec("DELETE FROM chat_sdk_state_cache WHERE key = ? AND expires_at IS NOT NULL AND expires_at <= ?", key, now); if (this.readCacheValue(key, now) !== null) return false; const expiresAt = ttlMs && ttlMs > 0 ? now + ttlMs : null; this.upsertCacheValue(key, value, expiresAt); return true; }); if (inserted) { const expiresAt = ttlMs && ttlMs > 0 ? Date.now() + ttlMs : null; await this.scheduleCleanupForExpiry(expiresAt); } return inserted; } cacheDelete(key) { this.sql` DELETE FROM chat_sdk_state_cache WHERE key = ${key} `; } async cleanupExpired(payload) { const current = this.readCleanupMetadata(); if (payload?.expiresAt !== void 0 && current.nextCleanupAt !== null && payload.expiresAt !== current.nextCleanupAt) return; const now = Date.now(); this.clearCleanupMetadata(); this.sql` DELETE FROM chat_sdk_state_locks WHERE expires_at <= ${now} `; this.sql` DELETE FROM chat_sdk_state_cache WHERE expires_at IS NOT NULL AND expires_at <= ${now} `; this.sql` DELETE FROM chat_sdk_state_queue WHERE expires_at <= ${now} `; this.sql` DELETE FROM chat_sdk_state_lists WHERE expires_at IS NOT NULL AND expires_at <= ${now} `; await this.scheduleNextCleanup(); } migrate() { this.sql` CREATE TABLE IF NOT EXISTS chat_sdk_state_subscriptions ( thread_id TEXT PRIMARY KEY ) `; this.sql` CREATE TABLE IF NOT EXISTS chat_sdk_state_locks ( thread_id TEXT PRIMARY KEY, token TEXT NOT NULL, expires_at INTEGER NOT NULL ) `; this.sql` CREATE TABLE IF NOT EXISTS chat_sdk_state_cache ( key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at INTEGER ) `; this.sql` CREATE TABLE IF NOT EXISTS chat_sdk_state_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id TEXT NOT NULL, value TEXT NOT NULL, enqueued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL ) `; this.sql` CREATE TABLE IF NOT EXISTS chat_sdk_state_lists ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, value TEXT NOT NULL, expires_at INTEGER ) `; this.sql` CREATE TABLE IF NOT EXISTS chat_sdk_state_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) `; this.sql` CREATE INDEX IF NOT EXISTS idx_chat_sdk_state_locks_expires ON chat_sdk_state_locks(expires_at) `; this.sql` CREATE INDEX IF NOT EXISTS idx_chat_sdk_state_cache_expires ON chat_sdk_state_cache(expires_at) WHERE expires_at IS NOT NULL `; this.sql` CREATE INDEX IF NOT EXISTS idx_chat_sdk_state_queue_thread ON chat_sdk_state_queue(thread_id, id) `; this.sql` CREATE INDEX IF NOT EXISTS idx_chat_sdk_state_queue_expires ON chat_sdk_state_queue(expires_at) `; this.sql` CREATE INDEX IF NOT EXISTS idx_chat_sdk_state_lists_key ON chat_sdk_state_lists(key, id) `; this.sql` CREATE INDEX IF NOT EXISTS idx_chat_sdk_state_lists_expires ON chat_sdk_state_lists(expires_at) WHERE expires_at IS NOT NULL `; } readCacheValue(key, now) { return this.sql` SELECT value FROM chat_sdk_state_cache WHERE key = ${key} AND (expires_at IS NULL OR expires_at > ${now}) `[0]?.value ?? null; } upsertCacheValue(key, value, expiresAt) { this.sql` INSERT OR REPLACE INTO chat_sdk_state_cache (key, value, expires_at) VALUES (${key}, ${value}, ${expiresAt}) `; } async scheduleCleanupForExpiry(expiresAt) { if (expiresAt === null) return; await this.ensureCleanupScheduled(expiresAt); } async scheduleNextCleanup() { const next = this.nextExpiry(); if (next === null) { const current = this.readCleanupMetadata(); if (current.scheduleId) await this.cancelSchedule(current.scheduleId).catch(() => false); this.clearCleanupMetadata(); return; } await this.ensureCleanupScheduled(next); } async ensureCleanupScheduled(expiresAt) { const current = this.readCleanupMetadata(); if (current.nextCleanupAt !== null && current.nextCleanupAt <= expiresAt) return; if (current.scheduleId) await this.cancelSchedule(current.scheduleId).catch(() => false); const delaySeconds = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1e3)); const schedule = await this.schedule(delaySeconds, "cleanupExpired", { expiresAt }); this.writeCleanupMetadata(expiresAt, schedule.id); } nextExpiry() { return this.sql` SELECT MIN(expires_at) as expires_at FROM ( SELECT expires_at FROM chat_sdk_state_locks UNION ALL SELECT expires_at FROM chat_sdk_state_queue UNION ALL SELECT expires_at FROM chat_sdk_state_cache WHERE expires_at IS NOT NULL UNION ALL SELECT expires_at FROM chat_sdk_state_lists WHERE expires_at IS NOT NULL ) `[0]?.expires_at ?? null; } readCleanupMetadata() { const rows = this.sql` SELECT key, value FROM chat_sdk_state_metadata WHERE key IN (${NEXT_CLEANUP_AT_KEY}, ${CLEANUP_SCHEDULE_ID_KEY}) `; const values = new Map(rows.map((row) => [row.key, row.value])); const nextCleanupAt = Number(values.get(NEXT_CLEANUP_AT_KEY)); return { nextCleanupAt: Number.isFinite(nextCleanupAt) ? nextCleanupAt : null, scheduleId: values.get(CLEANUP_SCHEDULE_ID_KEY) ?? null }; } writeCleanupMetadata(expiresAt, scheduleId) { this.sql` INSERT OR REPLACE INTO chat_sdk_state_metadata (key, value) VALUES (${NEXT_CLEANUP_AT_KEY}, ${String(expiresAt)}) `; this.sql` INSERT OR REPLACE INTO chat_sdk_state_metadata (key, value) VALUES (${CLEANUP_SCHEDULE_ID_KEY}, ${scheduleId}) `; } clearCleanupMetadata() { this.sql` DELETE FROM chat_sdk_state_metadata WHERE key IN (${NEXT_CLEANUP_AT_KEY}, ${CLEANUP_SCHEDULE_ID_KEY}) `; } }; function parseQueueEntry(value) { const raw = JSON.parse(value); if (typeof raw.enqueuedAt !== "number" || typeof raw.expiresAt !== "number") throw new Error("ChatSdkStateAgent expected QueueEntry JSON with numeric TTLs"); return { enqueuedAt: raw.enqueuedAt, expiresAt: raw.expiresAt }; } //#endregion //#region src/chat-sdk/adapter.ts const THREAD_STATE_PREFIX = "thread-state:"; const CHANNEL_STATE_PREFIX = "channel-state:"; const MESSAGE_HISTORY_PREFIX = "msg-history:"; const TRANSCRIPTS_USER_PREFIX = "transcripts:user:"; function parseStoredJson(raw, label) { try { return JSON.parse(raw); } catch (error) { throw new Error(`ChatSdkStateAdapter expected JSON-encoded ${label}`, { cause: error }); } } function defaultThreadShard(threadId) { return threadId.split(":").slice(0, 2).join(":") || "default"; } function defaultKeyShard(key, shardThread = defaultThreadShard) { for (const prefix of [ THREAD_STATE_PREFIX, CHANNEL_STATE_PREFIX, MESSAGE_HISTORY_PREFIX, TRANSCRIPTS_USER_PREFIX ]) if (key.startsWith(prefix)) return shardThread(key.slice(prefix.length)); } var ChatSdkStateAdapter = class { constructor(options = {}) { this.connected = false; const parent = options.parent ?? getCurrentAgent().agent; if (!parent) throw new Error("ChatSdkStateAdapter requires a parent Agent. Pass `parent` or create it inside an Agent context."); this.parent = parent; this.agentClass = options.agent ?? ChatSdkStateAgent; this.defaultName = options.name ?? "default"; this.keyShard = options.keyShard; this.shardKey = options.shardKey ?? defaultThreadShard; } async connect() { this.connected = true; } async disconnect() { this.connected = false; } async subscribe(threadId) { await (await this.stateAgent(threadId)).subscribe(threadId); } async unsubscribe(threadId) { await (await this.stateAgent(threadId)).unsubscribe(threadId); } async isSubscribed(threadId) { return (await this.stateAgent(threadId)).isSubscribed(threadId); } async acquireLock(threadId, ttlMs) { return (await this.stateAgent(threadId)).acquireLock(threadId, ttlMs); } async releaseLock(lock) { await (await this.stateAgent(lock.threadId)).releaseLock(lock.threadId, lock.token); } async extendLock(lock, ttlMs) { return (await this.stateAgent(lock.threadId)).extendLock(lock.threadId, lock.token, ttlMs); } async forceReleaseLock(threadId) { await (await this.stateAgent(threadId)).forceReleaseLock(threadId); } async enqueue(threadId, entry, maxSize) { return (await this.stateAgent(threadId)).enqueue(threadId, JSON.stringify(entry), maxSize); } async dequeue(threadId) { const raw = await (await this.stateAgent(threadId)).popQueue(threadId); return raw === null ? null : parseStoredJson(raw, `queue entry for ${threadId}`); } async queueDepth(threadId) { return (await this.stateAgent(threadId)).queueDepth(threadId); } async appendToList(key, value, options) { await (await this.stateAgentForKey(key)).listAppend(key, JSON.stringify(value), options?.maxLength, options?.ttlMs); } async getList(key) { return (await (await this.stateAgentForKey(key)).listGet(key)).map((value) => parseStoredJson(value, `list entry for ${key}`)); } async get(key) { const raw = await (await this.stateAgentForKey(key)).cacheGet(key); return raw === null ? null : parseStoredJson(raw, `cache key ${key}`); } async set(key, value, ttlMs) { await (await this.stateAgentForKey(key)).cacheSet(key, JSON.stringify(value), ttlMs); } async setIfNotExists(key, value, ttlMs) { return (await this.stateAgentForKey(key)).cacheSetIfNotExists(key, JSON.stringify(value), ttlMs); } async delete(key) { await (await this.stateAgentForKey(key)).cacheDelete(key); } async stateAgent(threadId) { this.ensureConnected(); const name = threadId ? this.shardKey(threadId) : this.defaultName; return this.parent.subAgent(this.agentClass, name); } async stateAgentForKey(key) { this.ensureConnected(); const name = this.keyShard?.(key) ?? defaultKeyShard(key, this.shardKey) ?? this.defaultName; return this.parent.subAgent(this.agentClass, name); } ensureConnected() { if (!this.connected) throw new Error("ChatSdkStateAdapter is not connected"); } }; //#endregion //#region src/chat-sdk/index.ts function createChatSdkState(options = {}) { return new ChatSdkStateAdapter(options); } //#endregion export { ChatSdkStateAdapter, ChatSdkStateAgent, createChatSdkState, defaultKeyShard, defaultThreadShard }; //# sourceMappingURL=index.js.map