agents
Version:
A home for your AI agents
453 lines (452 loc) • 15.9 kB
JavaScript
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