UNPKG

agents

Version:

A home for your AI agents

2,374 lines 82.9 kB
import "../../../types.js";
import { m as estimateStringTokens, p as estimateMessageTokens, t as COMPACTION_PREFIX } from "../../../compaction-helpers-iiKMr2TQ.js";
import { z } from "zod";
//#region src/experimental/memory/session/search.ts
/**
* Check if a provider is a SearchProvider (has a `search` method).
*/
function isSearchProvider(provider) {
	return typeof provider === "object" && provider !== null && "search" in provider && typeof provider.search === "function";
}
/**
* SearchProvider backed by Durable Object SQLite with FTS5.
*
* - `get()` returns a count of indexed entries
* - `search(query)` full-text search using FTS5
* - `set(key, content)` indexes or replaces content under a key
*
* Each instance uses a namespaced FTS5 table to avoid collisions
* with the session message search.
*
* @example
* ```ts
* Session.create(this)
*   .withContext("knowledge", {
*     provider: new AgentSearchProvider(this)
*   })
* ```
*/
var AgentSearchProvider = class {
	constructor(agent) {
		this.label = "";
		this.initialized = false;
		this.agent = agent;
	}
	init(label) {
		this.label = label;
	}
	ensureTable() {
		if (this.initialized) return;
		this.agent.sql`
      CREATE TABLE IF NOT EXISTS cf_agents_search_entries (
        label TEXT NOT NULL,
        key TEXT NOT NULL,
        content TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (label, key)
      )
    `;
		this.agent.sql`
      CREATE VIRTUAL TABLE IF NOT EXISTS cf_agents_search_fts
      USING fts5(
        label UNINDEXED,
        key UNINDEXED,
        content,
        tokenize='porter unicode61'
      )
    `;
		this.initialized = true;
	}
	async get() {
		this.ensureTable();
		const count = this.agent.sql`
      SELECT COUNT(*) as count FROM cf_agents_search_entries
      WHERE label = ${this.label}
    `[0]?.count ?? 0;
		if (count === 0) return null;
		return `${count} entries indexed.`;
	}
	async search(query) {
		this.ensureTable();
		const sanitized = query.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, "\"\"")}"`).join(" ");
		if (!sanitized) return null;
		try {
			const rows = this.agent.sql`
        SELECT f.key, f.content
        FROM cf_agents_search_fts f
        WHERE cf_agents_search_fts MATCH ${sanitized}
          AND f.label = ${this.label}
        ORDER BY rank
        LIMIT 10
      `;
			if (rows.length === 0) return null;
			return rows.map((r) => `[${r.key}]\n${r.content}`).join("\n\n");
		} catch {
			return null;
		}
	}
	async set(key, content) {
		this.ensureTable();
		this.deleteFTS(key);
		this.agent.sql`
      INSERT INTO cf_agents_search_entries (label, key, content)
      VALUES (${this.label}, ${key}, ${content})
      ON CONFLICT(label, key) DO UPDATE SET
        content = ${content},
        updated_at = CURRENT_TIMESTAMP
    `;
		this.agent.sql`
      INSERT INTO cf_agents_search_fts (label, key, content)
      VALUES (${this.label}, ${key}, ${content})
    `;
	}
	deleteFTS(key) {
		const rows = this.agent.sql`
      SELECT rowid FROM cf_agents_search_fts
      WHERE key = ${key} AND label = ${this.label}
    `;
		for (const row of rows) this.agent.sql`DELETE FROM cf_agents_search_fts WHERE rowid = ${row.rowid}`;
	}
};
//#endregion
//#region src/experimental/memory/session/skills.ts
/**
* Check if a provider is a SkillProvider (has a `load` method).
*/
function isSkillProvider(provider) {
	return typeof provider === "object" && provider !== null && "load" in provider && typeof provider.load === "function";
}
/**
* SkillProvider backed by an R2 bucket.
*
* - `get()` returns a metadata listing of all skills (key + description)
* - `load(key)` fetches a skill's full content
* - `set(key, content, description?)` writes a skill
*
* Descriptions are pulled from R2 custom metadata (`description` key).
* If a prefix is provided, it is prepended on storage operations and
* stripped from keys in metadata. `keys`, when provided, is matched against
* these prefix-relative keys.
*
* @example
* ```ts
* const skills = new R2SkillProvider(env.SKILLS_BUCKET, {
*   prefix: "skills/",
*   keys: ["code-review", "debugging"]
* });
* ```
*/
var R2SkillProvider = class {
	constructor(bucket, options) {
		this.bucket = bucket;
		this.prefix = options?.prefix ?? "";
		this.keys = options?.keys?.length ? new Set(options.keys) : null;
	}
	async get() {
		const entries = [];
		let cursor;
		let truncated = true;
		while (truncated) {
			const listed = await this.bucket.list({
				prefix: this.prefix,
				cursor,
				include: ["customMetadata"]
			});
			for (const obj of listed.objects) {
				const key = obj.key.slice(this.prefix.length);
				if (!this.allowsKey(key)) continue;
				const desc = obj.customMetadata?.description;
				entries.push(`- ${key}${desc ? `: ${desc}` : ""}`);
			}
			truncated = listed.truncated;
			cursor = listed.truncated ? listed.cursor : void 0;
		}
		return entries.length > 0 ? entries.join("\n") : null;
	}
	async load(key) {
		if (!this.allowsKey(key)) return null;
		const obj = await this.bucket.get(this.prefix + key);
		if (!obj) return null;
		return obj.text();
	}
	async set(key, content, description) {
		await this.bucket.put(this.prefix + key, content, { customMetadata: description ? { description } : void 0 });
	}
	allowsKey(key) {
		return this.keys === null || this.keys.has(key);
	}
};
//#endregion
//#region src/experimental/memory/session/context.ts
function slugify(text) {
	return text.slice(0, 60).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
function stableHash(text) {
	let hash = 2166136261;
	for (let i = 0; i < text.length; i++) {
		hash ^= text.charCodeAt(i);
		hash = Math.imul(hash, 16777619);
	}
	return (hash >>> 0).toString(36);
}
function contextEntryKey(metadataTitle, content) {
	if (metadataTitle?.trim()) return slugify(metadataTitle) || `entry-${stableHash(metadataTitle)}`;
	return `${slugify(content) || "entry"}-${stableHash(content)}`;
}
/**
* Check if a provider is writable (has a `set` method).
*/
function isWritableProvider(provider) {
	return typeof provider === "object" && provider !== null && "set" in provider && typeof provider.set === "function";
}
/**
* Manages context blocks with frozen snapshot support.
*/
var ContextBlocks = class {
	constructor(configs, promptStore) {
		this.blocks = /* @__PURE__ */ new Map();
		this.snapshot = null;
		this.loaded = false;
		this._loadedSkills = /* @__PURE__ */ new Set();
		this._onUnloadSkill = null;
		this.configs = configs;
		this.promptStore = promptStore ?? null;
	}
	/**
	* Register a callback invoked when a skill is unloaded.
	* Session uses this to update the stored tool result message.
	*/
	setUnloadCallback(cb) {
		this._onUnloadSkill = cb;
	}
	isLoaded() {
		return this.loaded;
	}
	/**
	* Load all blocks from their providers.
	* Called once at session init.
	*/
	async load() {
		for (const config of this.configs) {
			if (config.provider?.init) config.provider.init(config.label);
			const content = config.provider ? await config.provider.get() ?? "" : "";
			const skill = config.provider ? isSkillProvider(config.provider) : false;
			const searchable = config.provider ? isSearchProvider(config.provider) : false;
			const writable = config.provider ? isWritableProvider(config.provider) || skill && !!config.provider.set || searchable && !!config.provider.set : false;
			this.blocks.set(config.label, {
				label: config.label,
				description: config.description,
				content,
				tokens: estimateStringTokens(content),
				maxTokens: config.maxTokens,
				writable,
				isSkill: skill,
				isSearchable: searchable
			});
		}
		this.loaded = true;
	}
	/**
	* Dynamically register a new context block after initialization.
	* Used by extensions to contribute context at runtime.
	*
	* If blocks have already been loaded, the new block's provider is
	* initialized and loaded immediately. The snapshot is NOT updated
	* automatically — call `refreshSystemPrompt()` to rebuild.
	*/
	async addBlock(config) {
		if (!this.loaded) await this.load();
		if (this.configs.some((c) => c.label === config.label)) throw new Error(`Block "${config.label}" already exists`);
		this.configs.push(config);
		if (config.provider?.init) config.provider.init(config.label);
		const content = config.provider ? await config.provider.get() ?? "" : "";
		const skill = config.provider ? isSkillProvider(config.provider) : false;
		const searchable = config.provider ? isSearchProvider(config.provider) : false;
		const writable = config.provider ? isWritableProvider(config.provider) || skill && !!config.provider.set || searchable && !!config.provider.set : false;
		const block = {
			label: config.label,
			description: config.description,
			content,
			tokens: estimateStringTokens(content),
			maxTokens: config.maxTokens,
			writable,
			isSkill: skill,
			isSearchable: searchable
		};
		this.blocks.set(config.label, block);
		return block;
	}
	/**
	* Remove a dynamically registered context block.
	* Used during extension unload cleanup.
	*
	* Returns true if the block existed and was removed.
	* The snapshot is NOT updated automatically — call
	* `refreshSystemPrompt()` to rebuild.
	*
	* Note: loaded skills for this block are cleaned up from the
	* tracking set but the skill unload callback is NOT fired
	* (history reclamation is skipped — appropriate for full
	* extension removal).
	*/
	removeBlock(label) {
		const idx = this.configs.findIndex((c) => c.label === label);
		if (idx === -1) return false;
		this.configs.splice(idx, 1);
		this.blocks.delete(label);
		for (const id of this._loadedSkills) if (id.startsWith(`${label}:`)) this._loadedSkills.delete(id);
		return true;
	}
	/**
	* Get a block by label.
	*/
	getBlock(label) {
		return this.blocks.get(label) ?? null;
	}
	/**
	* Get all blocks.
	*/
	getBlocks() {
		return Array.from(this.blocks.values());
	}
	/**
	* Set block content. Writes to provider immediately.
	* Does NOT update the frozen snapshot.
	*/
	async setBlock(label, content) {
		if (!this.loaded) await this.load();
		const config = this.configs.find((c) => c.label === label);
		const existing = this.blocks.get(label);
		if (!existing?.writable) throw new Error(`Block "${label}" is readonly`);
		if (existing.isSkill || existing.isSearchable) throw new Error(`Block "${label}" is a keyed provider. Use setSkill() or setSearchEntry() instead.`);
		const tokens = estimateStringTokens(content);
		const maxTokens = config?.maxTokens ?? existing?.maxTokens;
		if (maxTokens !== void 0 && tokens > maxTokens) throw new Error(`Block "${label}" exceeds maxTokens: ${tokens} > ${maxTokens}`);
		const block = {
			label,
			description: config?.description ?? existing?.description,
			content,
			tokens,
			maxTokens,
			writable: true,
			isSkill: false,
			isSearchable: false
		};
		this.blocks.set(label, block);
		if (config?.provider && isWritableProvider(config.provider)) await config.provider.set(content);
		return block;
	}
	/**
	* Set a skill entry within a skill block.
	*/
	async setSkill(label, key, content, description) {
		if (!this.loaded) await this.load();
		const config = this.configs.find((c) => c.label === label);
		const existing = this.blocks.get(label);
		if (!existing?.isSkill) throw new Error(`Block "${label}" is not a skill provider`);
		const provider = config?.provider;
		if (!provider || !isSkillProvider(provider) || !provider.set) throw new Error(`Block "${label}" does not support writes`);
		await provider.set(key, content, description);
		const metadata = await provider.get();
		if (metadata) {
			existing.content = metadata;
			existing.tokens = estimateStringTokens(metadata);
		}
	}
	/**
	* Load a skill's full content from a skill block.
	*/
	async loadSkill(label, key) {
		if (!this.loaded) await this.load();
		const config = this.configs.find((c) => c.label === label);
		if (!config?.provider || !isSkillProvider(config.provider)) throw new Error(`Block "${label}" is not a skill provider`);
		const content = await config.provider.load(key);
		if (content !== null) this._loadedSkills.add(`${label}:${key}`);
		return content;
	}
	/**
	* Unload a previously loaded skill. Updates the stored tool result
	* message via the unload callback (set by Session).
	*/
	unloadSkill(label, key) {
		const id = `${label}:${key}`;
		if (!this._loadedSkills.has(id)) return false;
		this._loadedSkills.delete(id);
		this._onUnloadSkill?.(label, key);
		return true;
	}
	/**
	* Get the set of currently loaded skill keys (as "label:key" strings).
	*/
	getLoadedSkillKeys() {
		return this._loadedSkills;
	}
	/**
	* Restore loaded skill tracking from a set of "label:key" strings.
	* Used by Session to reconstruct state after hibernation.
	*/
	restoreLoadedSkills(skillIds) {
		this._loadedSkills = new Set(skillIds);
	}
	/**
	* Clear all loaded skill tracking. Called when messages are cleared.
	*/
	clearSkillState() {
		this._loadedSkills.clear();
	}
	/**
	* Index a search entry within a searchable block.
	*/
	async setSearchEntry(label, key, content) {
		if (!this.loaded) await this.load();
		const config = this.configs.find((c) => c.label === label);
		const existing = this.blocks.get(label);
		if (!existing?.isSearchable) throw new Error(`Block "${label}" is not a search provider`);
		const provider = config?.provider;
		if (!provider || !isSearchProvider(provider) || !provider.set) throw new Error(`Block "${label}" does not support writes`);
		await provider.set(key, content);
		existing.content = await provider.get() ?? "";
		existing.tokens = estimateStringTokens(existing.content);
	}
	/**
	* Search a searchable block.
	*/
	async searchContext(label, query) {
		if (!this.loaded) await this.load();
		const config = this.configs.find((c) => c.label === label);
		if (!config?.provider || !isSearchProvider(config.provider)) throw new Error(`Block "${label}" is not a search provider`);
		return config.provider.search(query);
	}
	/**
	* Append content to a block.
	*/
	async appendToBlock(label, content) {
		if (!this.loaded) await this.load();
		const existing = this.blocks.get(label);
		if (!existing) throw new Error(`Block "${label}" not found`);
		const needsSep = existing.content.length > 0 && !content.startsWith("\n");
		return this.setBlock(label, existing.content + (needsSep ? "\n" : "") + content);
	}
	/**
	* Get the system prompt string with context blocks.
	*
	* Returns a frozen snapshot: first call renders and caches,
	* subsequent calls return the same string (preserves LLM prefix cache).
	* Call refreshSnapshot() to re-render after block changes take effect.
	*/
	toSystemPrompt() {
		if (!this.loaded) throw new Error("Context blocks not loaded. Call load() first.");
		if (this.snapshot !== null) return this.snapshot;
		return this.captureSnapshot();
	}
	/**
	* Force re-render the snapshot from current block state.
	*/
	refreshSnapshot() {
		return this.captureSnapshot();
	}
	renderPrompt() {
		const parts = [];
		const sep = "═".repeat(46);
		for (const block of this.blocks.values()) {
			if (!block.content && !block.writable && !block.isSearchable && !block.isSkill) continue;
			let header = block.label.toUpperCase();
			if (block.description) header += ` (${block.description})`;
			if (block.maxTokens) {
				const pct = Math.round(block.tokens / block.maxTokens * 100);
				header += ` [${pct}% — ${block.tokens}/${block.maxTokens} tokens]`;
			}
			if (block.isSearchable) header += " [searchable]";
			else if (block.isSkill) header += " [loadable]";
			else if (!block.writable) header += " [readonly]";
			else header += " [writable]";
			parts.push(`${sep}\n${header}\n${sep}\n${block.content}`);
		}
		return parts.join("\n\n");
	}
	captureSnapshot() {
		this.snapshot = this.renderPrompt();
		return this.snapshot;
	}
	/**
	* Get writable blocks (for tool description).
	*/
	getWritableBlocks() {
		return Array.from(this.blocks.values()).filter((b) => b.writable);
	}
	/**
	* Check if any skill providers are registered.
	*/
	hasSkillBlocks() {
		return Array.from(this.blocks.values()).some((b) => b.isSkill);
	}
	/**
	* Check whether any CONFIGURED provider is skill-capable, without
	* requiring `load()` to have run. Used by Session to decide whether the
	* init-time loaded-skill restore scan is needed at all.
	*/
	hasSkillCapableConfigs() {
		return this.configs.some((c) => c.provider !== void 0 && isSkillProvider(c.provider));
	}
	/**
	* Get skill block labels.
	*/
	getSkillLabels() {
		return Array.from(this.blocks.values()).filter((b) => b.isSkill).map((b) => b.label);
	}
	/**
	* Check if any search providers are registered.
	*/
	hasSearchBlocks() {
		return Array.from(this.blocks.values()).some((b) => b.isSearchable);
	}
	/**
	* Get searchable block labels.
	*/
	getSearchLabels() {
		return Array.from(this.blocks.values()).filter((b) => b.isSearchable).map((b) => b.label);
	}
	/**
	* Return the cached system prompt. If no cached prompt exists,
	* loads blocks from providers, renders, and persists to the store.
	* Subsequent calls return the stored value without re-rendering.
	*/
	async freezeSystemPrompt() {
		if (this.promptStore) {
			const stored = await this.promptStore.get();
			if (stored !== null) return stored;
		}
		if (!this.loaded) await this.load();
		const prompt = this.toSystemPrompt();
		if (this.promptStore) await this.promptStore.set(prompt);
		return prompt;
	}
	/**
	* Return the prompt text used for token estimation without persisting a new
	* frozen prompt to the prompt store.
	*
	* This still reads an existing cached prompt when present, so estimates match
	* the prompt that inference would reuse. If no cached prompt exists, it loads
	* providers and renders the current blocks without freezing the snapshot.
	*/
	async getSystemPromptForEstimate() {
		if (this.snapshot !== null) return this.snapshot;
		if (this.promptStore) {
			const stored = await this.promptStore.get();
			if (stored !== null) return stored;
		}
		if (!this.loaded) await this.load();
		return this.renderPrompt();
	}
	/**
	* Force reload blocks from providers, re-render the system prompt,
	* and persist to the store. Use this after block content has changed
	* or to invalidate the cached prompt.
	*/
	async refreshSystemPrompt() {
		this.loaded = false;
		await this.load();
		const prompt = this.refreshSnapshot();
		if (this.promptStore) await this.promptStore.set(prompt);
		return prompt;
	}
	/**
	* AI tools for context blocks.
	*
	* Auto-wired based on provider capabilities:
	* - `set_context` — when any block is writable
	* - `load_context` — when any block is a skill provider
	* - `search_context` — when any block is a search provider
	*/
	async tools() {
		if (!this.loaded) await this.load();
		const writable = this.getWritableBlocks();
		const hasSkills = this.hasSkillBlocks();
		const hasSearch = this.hasSearchBlocks();
		const toolSet = {};
		if (writable.length > 0) {
			const blockDescriptions = writable.map((b) => {
				const kind = b.isSkill ? "skill collection, keyed entries" : b.isSearchable ? "searchable, keyed entries" : "writable";
				return `- "${b.label}" (${kind}): ${b.description ?? "no description"}`;
			});
			const keyedBlocks = writable.filter((b) => b.isSkill || b.isSearchable);
			const properties = {
				label: {
					type: "string",
					enum: writable.map((b) => b.label),
					description: "Block label to write to"
				},
				content: {
					type: "string",
					description: "The main content to write to the block."
				},
				action: {
					type: "string",
					enum: ["replace", "append"],
					description: "replace (default) or append"
				}
			};
			if (keyedBlocks.length > 0) properties.metadata = {
				type: "object",
				description: "Optional metadata for keyed entries (skill collections, searchable blocks: " + keyedBlocks.map((b) => `"${b.label}"`).join(", ") + "). Short content doesn't need metadata; longer loadable entries (skills) benefit from a title and description so the model can pick the right one without loading it.",
				properties: {
					title: {
						type: "string",
						description: "Short title. Used as a stable identifier — entries with the same title are updated in place, different titles create new entries."
					},
					description: {
						type: "string",
						description: "One-line summary shown alongside the title in the system prompt so the model can decide when to load the entry."
					}
				}
			};
			const metadataHint = keyedBlocks.length > 0 ? "\n\nFor keyed blocks (skill collections / searchable), pass `metadata: { title, description }` — title stabilises updates, description helps the model pick entries. Metadata is optional; short content rarely needs it, long loadable entries benefit most." : "";
			toolSet.set_context = {
				description: `Write to a context block. Available blocks:\n${blockDescriptions.join("\n")}\n\nWrites are durable and persist across sessions.${metadataHint}`,
				inputSchema: z.fromJSONSchema({
					type: "object",
					properties,
					required: ["label", "content"]
				}),
				execute: async ({ label, content, metadata, action }) => {
					try {
						const block = this.blocks.get(label);
						if (!block) return `Error: block "${label}" not found`;
						if (block.isSkill || block.isSearchable) {
							const title = metadata?.title;
							const description = metadata?.description;
							const key = contextEntryKey(title, content);
							if (block.isSkill) await this.setSkill(label, key, content, description ?? title);
							else await this.setSearchEntry(label, key, content);
							return `Indexed "${key}" in ${label}.`;
						}
						const updated = action === "append" ? await this.appendToBlock(label, content) : await this.setBlock(label, content);
						return `Written to ${label}. Usage: ${updated.maxTokens ? `${Math.round(updated.tokens / updated.maxTokens * 100)}% (${updated.tokens}/${updated.maxTokens} tokens)` : `${updated.tokens} tokens`}`;
					} catch (err) {
						return `Error: ${err instanceof Error ? err.message : String(err)}`;
					}
				}
			};
		}
		if (hasSkills) {
			const skillLabels = this.getSkillLabels();
			toolSet.load_context = {
				description: "Load a document from a skill block by key. Available skill blocks: " + skillLabels.map((l) => `"${l}"`).join(", ") + ". Check the system prompt for available keys.",
				inputSchema: z.fromJSONSchema({
					type: "object",
					properties: {
						label: {
							type: "string",
							enum: skillLabels,
							description: "Skill block label"
						},
						key: {
							type: "string",
							description: "Skill key to load"
						}
					},
					required: ["label", "key"]
				}),
				execute: async ({ label, key }) => {
					try {
						if (!skillLabels.includes(label)) return `Error: "${label}" is not a skill block. Skill blocks: ${skillLabels.join(", ")}`;
						return await this.loadSkill(label, key) ?? `Not found: ${key}`;
					} catch (err) {
						return `Error: ${err instanceof Error ? err.message : String(err)}`;
					}
				}
			};
			const loadedList = [...this._loadedSkills];
			toolSet.unload_context = {
				description: "Unload a previously loaded skill to free context space. The skill remains available for re-loading." + (loadedList.length > 0 ? " Currently loaded: " + loadedList.join(", ") + "." : " No skills currently loaded."),
				inputSchema: z.fromJSONSchema({
					type: "object",
					properties: {
						label: {
							type: "string",
							enum: skillLabels,
							description: "Skill block label"
						},
						key: {
							type: "string",
							description: "Skill key to unload"
						}
					},
					required: ["label", "key"]
				}),
				execute: async ({ label, key }) => {
					if (!skillLabels.includes(label)) return `Error: "${label}" is not a skill block. Skill blocks: ${skillLabels.join(", ")}`;
					if (!this.unloadSkill(label, key)) return `Skill "${key}" is not currently loaded in "${label}".`;
					return `Unloaded "${key}" from ${label}. Context reclaimed.`;
				}
			};
		}
		if (hasSearch) {
			const searchLabels = this.getSearchLabels();
			toolSet.search_context = {
				description: "Search for information in a searchable context block. ONLY these blocks are searchable: " + searchLabels.map((l) => `"${l}"`).join(", ") + ". Other blocks cannot be searched.",
				inputSchema: z.fromJSONSchema({
					type: "object",
					properties: {
						label: {
							type: "string",
							enum: searchLabels,
							description: "Searchable block label"
						},
						query: {
							type: "string",
							description: "Search query"
						}
					},
					required: ["label", "query"]
				}),
				execute: async ({ label, query }) => {
					try {
						if (!searchLabels.includes(label)) return `Error: "${label}" is not searchable. Searchable blocks: ${searchLabels.join(", ")}`;
						return await this.searchContext(label, query) ?? "No results found.";
					} catch (err) {
						return `Error: ${err instanceof Error ? err.message : String(err)}`;
					}
				}
			};
		}
		return toolSet;
	}
};
//#endregion
//#region src/experimental/memory/session/providers/agent.ts
/**
* Bounds for each content-hydration query on a history path.
*
* Message rows can be up to ~1.8MB each (see ROW_MAX_BYTES in agents/chat),
* so content is fetched in bounded batches rather than one statement to keep
* any per-statement buffering in the SQLite layer small. In workerd the
* SQLite allocator shares the isolate's memory budget with the JS heap —
* oversized transient result sets surface as SQLITE_NOMEM (#1710).
*
* Chunks are bounded by BOTH row count and cumulative stored bytes (sizes
* come from the path row stats, so no content is read to compute them).
* Without the byte bound, 50 near-cap rows could still materialize ~90MB
* in a single statement.
*/
const HISTORY_CONTENT_CHUNK_SIZE = 50;
const HISTORY_CONTENT_CHUNK_BYTES = 4 * 1024 * 1024;
var AgentSessionProvider = class {
	/**
	* @param agent - Agent or any object with a `sql` tagged template method
	* @param sessionId - Optional session ID to isolate multiple sessions in the same DO.
	*                    Messages are filtered by session_id within shared tables.
	*/
	constructor(agent, sessionId) {
		this.initialized = false;
		this.activeLeafId = void 0;
		this.agent = agent;
		this.sessionId = sessionId ?? "";
	}
	ensureTable() {
		if (this.initialized) return;
		this.agent.sql`
      CREATE TABLE IF NOT EXISTS assistant_messages (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL DEFAULT '',
        parent_id TEXT,
        role TEXT NOT NULL,
        content TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
      )
    `;
		this.agent.sql`
      CREATE INDEX IF NOT EXISTS idx_assistant_msg_parent
      ON assistant_messages(parent_id)
    `;
		this.agent.sql`
      CREATE INDEX IF NOT EXISTS idx_assistant_msg_session
      ON assistant_messages(session_id)
    `;
		this.agent.sql`
      CREATE TABLE IF NOT EXISTS assistant_compactions (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL DEFAULT '',
        summary TEXT NOT NULL,
        from_message_id TEXT NOT NULL,
        to_message_id TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
      )
    `;
		this.agent.sql`
      CREATE VIRTUAL TABLE IF NOT EXISTS assistant_fts
      USING fts5(id UNINDEXED, session_id UNINDEXED, role UNINDEXED, content, tokenize='porter unicode61')
    `;
		this.agent.sql`
      CREATE TABLE IF NOT EXISTS assistant_config (
        session_id TEXT NOT NULL,
        key TEXT NOT NULL,
        value TEXT NOT NULL,
        PRIMARY KEY (session_id, key)
      )
    `;
		this.initialized = true;
	}
	getMessage(id) {
		this.ensureTable();
		const rows = this.agent.sql`
      SELECT content FROM assistant_messages WHERE id = ${id} AND session_id = ${this.sessionId}
    `;
		return rows.length > 0 ? this.parse(rows[0].content) : null;
	}
	getHistory(leafId) {
		this.ensureTable();
		const leaf = leafId ? this.leafRowById(leafId) : this.latestLeafRow();
		if (!leaf) return [];
		const messages = this.messagesByPathStats(this.pathRowStats(leaf.id));
		const compactions = this.getCompactions();
		if (compactions.length === 0) return messages;
		return this.applyCompactions(messages, compactions);
	}
	getRecentHistory(leafId, maxContentBytes, minRecentMessages = 1) {
		this.ensureTable();
		const leaf = leafId ? this.leafRowById(leafId) : this.latestLeafRow();
		if (!leaf) return {
			messages: [],
			truncated: false,
			totalContentBytes: 0
		};
		const stats = this.pathRowStats(leaf.id);
		const totalContentBytes = stats.reduce((sum, row) => sum + row.bytes, 0);
		const minRecent = Math.max(1, Math.floor(minRecentMessages));
		let start = stats.length - 1;
		let used = stats[start]?.bytes ?? 0;
		while (start > 0 && (stats.length - start < minRecent || used + stats[start - 1].bytes <= maxContentBytes)) {
			start--;
			used += stats[start].bytes;
		}
		const messages = this.messagesByPathStats(stats.slice(start));
		const compactions = this.getCompactions();
		return {
			messages: compactions.length === 0 ? messages : this.applyCompactions(messages, compactions),
			truncated: start > 0,
			totalContentBytes
		};
	}
	getHistoryRowStats(leafId) {
		this.ensureTable();
		const leaf = leafId ? this.leafRowById(leafId) : this.latestLeafRow();
		return leaf ? this.pathRowStats(leaf.id) : [];
	}
	getLatestLeaf() {
		this.ensureTable();
		const row = this.latestLeafRow();
		return row ? this.getMessage(row.id) : null;
	}
	getBranches(messageId) {
		this.ensureTable();
		const rows = this.agent.sql`
      SELECT content FROM assistant_messages
      WHERE parent_id = ${messageId} AND session_id = ${this.sessionId} ORDER BY created_at ASC
    `;
		return this.parseRows(rows);
	}
	getPathLength(leafId) {
		this.ensureTable();
		const leaf = leafId ? this.agent.sql`
          SELECT id FROM assistant_messages WHERE id = ${leafId} AND session_id = ${this.sessionId}
        `[0] : this.latestLeafRow();
		if (!leaf) return 0;
		return this.agent.sql`
      WITH RECURSIVE path AS (
        SELECT id, parent_id, 0 as depth FROM assistant_messages WHERE id = ${leaf.id}
        UNION ALL
        SELECT m.id, m.parent_id, p.depth + 1 FROM assistant_messages m
        JOIN path p ON m.id = p.parent_id
        WHERE m.session_id = ${this.sessionId} AND p.depth < 10000
      )
      SELECT COUNT(*) as count FROM path
    `[0]?.count ?? 0;
	}
	appendMessage(message, parentId) {
		this.ensureTable();
		if (this.agent.sql`
      SELECT id FROM assistant_messages WHERE id = ${message.id} AND session_id = ${this.sessionId}
    `.length > 0) return;
		let parent = parentId !== void 0 ? parentId : this.latestLeafRow()?.id ?? null;
		if (parent) {
			if (this.agent.sql`
        SELECT id FROM assistant_messages WHERE id = ${parent} AND session_id = ${this.sessionId}
      `.length === 0) parent = null;
		}
		const json = JSON.stringify(message);
		this.agent.sql`
      INSERT INTO assistant_messages (id, session_id, parent_id, role, content)
      VALUES (${message.id}, ${this.sessionId}, ${parent}, ${message.role}, ${json})
    `;
		this.indexFTS(message);
		this.activeLeafId = message.id;
	}
	updateMessage(message) {
		this.ensureTable();
		this.agent.sql`
      UPDATE assistant_messages SET content = ${JSON.stringify(message)}
      WHERE id = ${message.id} AND session_id = ${this.sessionId}
    `;
		this.indexFTS(message);
	}
	deleteMessages(messageIds) {
		this.ensureTable();
		for (const id of messageIds) {
			this.agent.sql`DELETE FROM assistant_messages WHERE id = ${id} AND session_id = ${this.sessionId}`;
			this.deleteFTS(id);
		}
		if (typeof this.activeLeafId === "string" && messageIds.includes(this.activeLeafId)) this.activeLeafId = void 0;
	}
	clearMessages() {
		this.ensureTable();
		this.agent.sql`DELETE FROM assistant_messages WHERE session_id = ${this.sessionId}`;
		this.agent.sql`DELETE FROM assistant_compactions WHERE session_id = ${this.sessionId}`;
		const ftsRows = this.agent.sql`
      SELECT rowid FROM assistant_fts WHERE session_id = ${this.sessionId}
    `;
		for (const row of ftsRows) this.agent.sql`DELETE FROM assistant_fts WHERE rowid = ${row.rowid}`;
		this.activeLeafId = void 0;
	}
	addCompaction(summary, fromMessageId, toMessageId) {
		this.ensureTable();
		const id = crypto.randomUUID();
		this.agent.sql`
      INSERT INTO assistant_compactions (id, session_id, summary, from_message_id, to_message_id)
      VALUES (${id}, ${this.sessionId}, ${summary}, ${fromMessageId}, ${toMessageId})
    `;
		return {
			id,
			summary,
			fromMessageId,
			toMessageId,
			createdAt: (/* @__PURE__ */ new Date()).toISOString()
		};
	}
	getCompactions() {
		this.ensureTable();
		return this.agent.sql`
      SELECT * FROM assistant_compactions WHERE session_id = ${this.sessionId} ORDER BY created_at ASC
    `.map((r) => ({
			id: r.id,
			summary: r.summary,
			fromMessageId: r.from_message_id,
			toMessageId: r.to_message_id,
			createdAt: r.created_at
		}));
	}
	searchMessages(query, limit = 20) {
		this.ensureTable();
		const sanitized = `"${query.replace(/"/g, "\"\"")}"`;
		try {
			return this.agent.sql`
        SELECT f.id, f.role, f.content FROM assistant_fts f
        INNER JOIN assistant_messages m ON m.id = f.id AND m.session_id = f.session_id
        WHERE assistant_fts MATCH ${sanitized} AND f.session_id = ${this.sessionId}
        ORDER BY rank LIMIT ${limit}
      `.map((r) => ({
				id: r.id,
				role: r.role,
				content: r.content
			}));
		} catch {
			return [];
		}
	}
	latestLeafRow() {
		if (this.activeLeafId !== void 0) {
			if (this.agent.sql`
        SELECT m.id FROM assistant_messages m
        WHERE m.id = ${this.activeLeafId} AND m.session_id = ${this.sessionId}
          AND NOT EXISTS (
            SELECT 1 FROM assistant_messages c
            WHERE c.parent_id = ${this.activeLeafId}
              AND c.session_id = ${this.sessionId}
          )
      `.length > 0) return { id: this.activeLeafId };
			this.activeLeafId = void 0;
		}
		const rows = this.agent.sql`
      SELECT m.id FROM assistant_messages m
      LEFT JOIN assistant_messages c ON c.parent_id = m.id AND c.session_id = ${this.sessionId}
      WHERE c.id IS NULL AND m.session_id = ${this.sessionId}
      ORDER BY m.created_at DESC, m.rowid DESC LIMIT 1
    `;
		this.activeLeafId = rows[0]?.id;
		return rows[0] ?? null;
	}
	leafRowById(leafId) {
		return this.agent.sql`
      SELECT id FROM assistant_messages WHERE id = ${leafId} AND session_id = ${this.sessionId}
    `[0] ?? null;
	}
	/**
	* The active branch path as (id, role, content size) rows, root → leaf.
	*
	* Recurses over (id, parent_id) only. Carrying `content` through the
	* recursive queue AND the ORDER BY sorter materializes the entire
	* transcript several times over inside SQLite's allocator, which in
	* workerd shares the isolate's memory budget with the JS heap — large
	* media-heavy sessions then fail with SQLITE_NOMEM on wake (#1710).
	* Content is fetched separately in bounded chunks (`messagesByPathStats`).
	*/
	pathRowStats(leafId) {
		return this.agent.sql`
      WITH RECURSIVE path(id, parent_id, depth) AS (
        SELECT id, parent_id, 0 FROM assistant_messages WHERE id = ${leafId}
        UNION ALL
        SELECT m.id, m.parent_id, p.depth + 1 FROM assistant_messages m
        JOIN path p ON m.id = p.parent_id
        WHERE m.session_id = ${this.sessionId} AND p.depth < 10000
      )
      SELECT path.id AS id, am.role AS role, LENGTH(CAST(am.content AS BLOB)) AS bytes
      FROM path JOIN assistant_messages am ON am.id = path.id
      ORDER BY path.depth DESC
    `;
	}
	/**
	* Fetch and parse message content for an ordered list of path rows.
	*
	* Content is read in chunks bounded by both row count and cumulative
	* stored bytes (no ORDER BY — SQLite streams rows without materializing
	* the result set) and reassembled in path order. Rows that fail to parse
	* are skipped, matching previous behavior.
	*/
	messagesByPathStats(rows) {
		const contentById = /* @__PURE__ */ new Map();
		const fetchChunk = (ids) => {
			const fetched = this.agent.sql`
        SELECT id, content FROM assistant_messages
        WHERE session_id = ${this.sessionId}
          AND id IN (SELECT value FROM json_each(${JSON.stringify(ids)}))
      `;
			for (const row of fetched) contentById.set(row.id, row.content);
		};
		let chunk = [];
		let chunkBytes = 0;
		for (const row of rows) {
			if (chunk.length > 0 && (chunk.length >= HISTORY_CONTENT_CHUNK_SIZE || chunkBytes + row.bytes > HISTORY_CONTENT_CHUNK_BYTES)) {
				fetchChunk(chunk);
				chunk = [];
				chunkBytes = 0;
			}
			chunk.push(row.id);
			chunkBytes += row.bytes;
		}
		if (chunk.length > 0) fetchChunk(chunk);
		const result = [];
		for (const row of rows) {
			const content = contentById.get(row.id);
			if (content === void 0) continue;
			const msg = this.parse(content);
			if (msg) result.push(msg);
		}
		return result;
	}
	indexFTS(message) {
		const text = message.parts.filter((p) => p.type === "text").map((p) => p.text).join(" ");
		this.deleteFTS(message.id);
		if (text) this.agent.sql`
        INSERT INTO assistant_fts (id, session_id, role, content)
        VALUES (${message.id}, ${this.sessionId}, ${message.role}, ${text})
      `;
	}
	deleteFTS(id) {
		const rows = this.agent.sql`
      SELECT rowid FROM assistant_fts WHERE id = ${id} AND session_id = ${this.sessionId}
    `;
		for (const row of rows) this.agent.sql`DELETE FROM assistant_fts WHERE rowid = ${row.rowid}`;
	}
	applyCompactions(messages, compactions) {
		const ids = messages.map((m) => m.id);
		const result = [];
		let i = 0;
		while (i < messages.length) {
			const matching = compactions.filter((c) => c.fromMessageId === ids[i]);
			const comp = matching.length > 1 ? matching[matching.length - 1] : matching[0];
			if (comp) {
				const endIdx = ids.indexOf(comp.toMessageId);
				if (endIdx >= i) {
					result.push({
						id: `${COMPACTION_PREFIX}${comp.id}`,
						role: "assistant",
						parts: [{
							type: "text",
							text: comp.summary
						}],
						createdAt: /* @__PURE__ */ new Date()
					});
					i = endIdx + 1;
					continue;
				}
			}
			result.push(messages[i]);
			i++;
		}
		return result;
	}
	parse(json) {
		try {
			const msg = JSON.parse(json);
			if (typeof msg?.id === "string" && typeof msg?.role === "string" && Array.isArray(msg?.parts)) return msg;
		} catch {}
		return null;
	}
	parseRows(rows) {
		const result = [];
		for (const row of rows) {
			const msg = this.parse(row.content);
			if (msg) result.push(msg);
		}
		return result;
	}
};
//#endregion
//#region src/experimental/memory/session/providers/agent-context.ts
var AgentContextProvider = class {
	constructor(agent, label) {
		this.initialized = false;
		this.agent = agent;
		this.label = label ?? "";
	}
	init(label) {
		if (!this.label) this.label = label;
	}
	ensureTable() {
		if (this.initialized) return;
		this.agent.sql`
      CREATE TABLE IF NOT EXISTS cf_agents_context_blocks (
        label TEXT PRIMARY KEY,
        content TEXT NOT NULL,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
      )
    `;
		this.initialized = true;
	}
	async get() {
		this.ensureTable();
		return this.agent.sql`
      SELECT content FROM cf_agents_context_blocks WHERE label = ${this.label}
    `[0]?.content ?? null;
	}
	async set(content) {
		this.ensureTable();
		this.agent.sql`
      INSERT INTO cf_agents_context_blocks (label, content)
      VALUES (${this.label}, ${content})
      ON CONFLICT(label) DO UPDATE SET content = ${content}, updated_at = CURRENT_TIMESTAMP
    `;
	}
};
//#endregion
//#region src/experimental/memory/session/session.ts
function isBroadcaster(obj) {
	return typeof obj === "object" && obj !== null && "broadcast" in obj && typeof obj.broadcast === "function";
}
function isSqlProvider(arg) {
	return "sql" in arg && typeof arg.sql === "function";
}
var Session = class Session {
	constructor(storage, options) {
		this._warnedCompactionNoOp = false;
		this._ready = false;
		this._skillScanRan = false;
		this._warnedNoRecentHistorySupport = false;
		this.storage = storage;
		this.context = new ContextBlocks(options?.context ?? [], options?.promptStore);
		this._tokenCounter = options?.tokenCounter;
		this._compactionErrorHandler = options?.onCompactionError;
		this._ready = true;
	}
	/**
	* Chainable session creation with auto-wired providers.
	*
	* Pass a `SqlProvider` (Agent with `sql` method) for auto-wired SQLite,
	* or a `SessionProvider` directly for custom storage (Postgres, etc.).
	*
	* @example
	* ```ts
	* // Auto-wired SQLite (DO Agent)
	* const session = Session.create(this)
	*   .withContext("soul", { provider: { get: async () => "You are helpful." } })
	*   .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
	*   .withCachedPrompt();
	*
	* // Skills from R2 (on-demand loading via load_context tool)
	* const session = Session.create(this)
	*   .withContext("skills", {
	*     provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" })
	*   })
	*   .withCachedPrompt();
	*
	* // Custom storage provider (Postgres, etc.)
	* const session = Session.create(postgresProvider)
	*   .withContext("memory", {
	*     maxTokens: 1100,
	*     provider: new PostgresContextProvider(conn, "memory")
	*   })
	*   .withCachedPrompt(new PostgresContextProvider(conn, "_prompt"));
	* ```
	*/
	static create(provider) {
		const session = Object.create(Session.prototype);
		if (isSqlProvider(provider)) {
			session._agent = provider;
			if (isBroadcaster(provider)) session._broadcaster = provider;
		} else session._storageProvider = provider;
		session._pending = [];
		session._ready = false;
		return session;
	}
	forSession(sessionId) {
		this._sessionId = sessionId;
		return this;
	}
	withContext(label, options) {
		this._pending.push({
			label,
			options: options ?? {}
		});
		return this;
	}
	withCachedPrompt(provider) {
		this._cachedPrompt = provider ?? true;
		return this;
	}
	/**
	* Register a compaction function. Called by `compact()` to compress
	* message history into a summary overlay.
	*/
	onCompaction(fn) {
		this._compactionFn = fn;
		return this;
	}
	/**
	* Auto-compact when estimated token count exceeds the threshold.
	* Checked after each `appendMessage`. Requires `onCompaction()`.
	*
	* By default this uses a Workers-safe heuristic over stored messages plus
	* the Session-managed frozen system prompt. Provide `tokenCounter` when you
	* have model-reported usage or a tokenizer and need a stricter budget.
	*/
	compactAfter(tokenThreshold, options) {
		this._tokenThreshold = tokenThreshold;
		if (options?.tokenCounter) this._tokenCounter = options.tokenCounter;
		return this;
	}
	/**
	* Handle failures from the automatic `compactAfter()` trigger.
	*
	* Manual `compact()` still reports errors through the existing session error
	* broadcast path.
	*/
	onCompactionError(handler) {
		this._compactionErrorHandler = handler;
		return this;
	}
	/**
	* @internal
	* Framework hook for cache-owning callers that need to mirror message
	* storage changes. Application code should use the normal Session methods.
	*/
	internal_onMessagesChanged(listener) {
		this._messageChangeListener = listener ?? void 0;
		return this;
	}
	_ensureReady() {
		if (this._ready) return;
		const configs = (this._pending ?? []).map(({ label, options: opts }) => {
			let provider = opts.provider;
			if (!provider && this._agent) {
				const key = this._sessionId ? `${label}_${this._sessionId}` : label;
				provider = new AgentContextProvider(this._agent, key);
			}
			return {
				label,
				description: opts.description,
				maxTokens: opts.maxTokens,
				provider
			};
		});
		let promptStore;
		if (this._cachedPrompt === true && this._agent) {
			const key = this._sessionId ? `_system_prompt_${this._sessionId}` : "_system_prompt";
			promptStore = new AgentContextProvider(this._agent, key);
		} else if (this._cachedPrompt && this._cachedPrompt !== true) promptStore = this._cachedPrompt;
		if (this._storageProvider) this.storage = this._storageProvider;
		else if (this._agent) this.storage = new AgentSessionProvider(this._agent, this._sessionId);
		else throw new Error("Session.create() requires a SqlProvider or SessionProvider");
		this.context = new ContextBlocks(configs, promptStore);
		this.context.setUnloadCallback((label, key) => {
			this._reclaimLoadedSkill(label, key).catch(() => {});
		});
		this._ready = true;
		this._restorePromise = this._restoreLoadedSkills().catch(() => {});
	}
	/**
	* Await the background skill-restore kicked off by `_ensureReady()`.
	* Idempotent and cheap — every async public method calls this so that
	* `_loadedSkills` reflects conversation history before any read or write.
	*/
	async _ensureRestored() {
		this._ensureReady();
		if (this._restorePromise) await this._restorePromise;
	}
	async _notifyMessagesChanged(event) {
		await this._messageChangeListener?.(event);
	}
	/**
	* Reconstruct which skills are loaded by scanning conversation history
	* for load_context tool results that haven't been unloaded.
	* Runs once per init to survive hibernation / eviction, including for
	* async SessionProviders (e.g. Postgres) where we must `await` history.
	*
	* Skipped entirely when no skill-capable provider is configured —
	* `load_context` results can only exist when a skill block was registered,
	* and the scan would otherwise read the whole transcript on every wake,
	* bypassing byte-budgeted hydration (#1710). A skill block added later via
	* `addContext()` triggers the scan at that point instead.
	*/
	async _restoreLoadedSkills() {
		if (!this.context.hasSkillCapableConfigs()) return;
		await this._scanHistoryForLoadedSkills();
	}
	/**
	* Scan stored history for load/unload_context tool results and restore
	* the loaded-skill tracking set.
	*
	* Memory-bounded when the provider supports `getHistoryRowStats`: rows
	* are enumerated without content, then only assistant rows are fetched
	* and scanned ONE AT A TIME — peak memory is a single message instead of
	* the whole transcript. Falls back to a full `getHistory()` read for
	* providers without row stats (e.g. Postgres).
	*
	* Note: the bounded path scans raw path rows, so `load_context` results
	* inside compacted ranges are still seen (the full-read path hides them
	* behind compaction overlays). That superset is intentional — the stored
	* tool result still exists and can be reclaimed by `unloadSkill`.
	*/
	async _scanHistoryForLoadedSkills() {
		this._skillScanRan = true;
		const loaded = /* @__PURE__ */ new Set();
		const scanMessage = (msg) => {
			if (msg.role !== "assistant") return;
			for (const part of msg.parts) if (part.toolName === "load_context" && part.state === "output-available") {
				const input = part.input;
				if (input?.label && input?.key) {
					const id = `${input.label}:${input.key}`;
					if (typeof part.output === "string" && part.output.startsWith("[skill unloaded:")) loaded.delete(id);
					else loaded.add(id);
				}
			} else if (part.toolName === "unload_context" && part.state === "output-available") {
				const input = part.input;
				if (input?.label && input?.key) loaded.delete(`${input.label}:${input.key}`);
			}
		};
		if (this.storage.getHistoryRowStats) {
			const stats = await this.storage.getHistoryRowStats();
			for (const row of stats) {
				if (row.role !== "assistant") continue;
				const msg = await this.storage.getMessage(row.id);
				if (msg) scanMessage(msg);
			}
		} else for (const msg of await this.storage.getHistory()) scanMessage(msg);
		if (loaded.size > 0) this.context.restoreLoadedSkills(loaded);
	}
	/**
	* Reclaim context-window tokens consumed by a previously loaded skill.
	*
	* When a skill is loaded via the `load_context` tool, its full body is
	* embedded as that tool call's `output-available` result inside the
	* assistant message — which means every subsequent turn replays the
	* entire skill as part of the conversation history and pays for it in
	* input tokens.
	*
	* This method walks back through history, finds the matching
	* `load_context` tool result for `(label, key)`, and replaces its bulky
	* `output` with a short marker `[skill unloaded: <key>]`. The skill
	* content is dropped from future turns and the tokens are reclaimed.
	* The skill itself stays available to reload via `load_context`.
	*/
	async _reclaimLoadedSkill(label, key) {
		const history = await this.storage.getHistory();
		for (let i = history.length - 1; i >= 0; i--) {
			const msg = history[i];
			if (msg.role !== "assistant") continue;
			let changed = false;
			const newParts = msg.parts.map((part) => {
				if (part.toolName === "load_context" && part.state === "output-available") {
					const input = part.input;
					if (input?.label === label && input?.key === key) {
						changed = true;
						return {
							...part,
							output: `[skill unloaded: ${key}]`
						};
					}
				}
				return part;
			});
			if (changed) {
				await this.updateMessage({
					...msg,
					parts: newParts
				});
				return;
			}
		}
	}
	async getHistory(leafId) {
		await this._ensureRestored();
		return this.storage.getHistory(leafId);
	}
	/**
	* Byte-budgeted read of the most recent messages on the active branch
	* path (always at least the leaf message, and at least
	* `minRecentMessages` when the path is long enough). Lets hosts hydrate
	* a bounded window instead of the full transcript so wake-time memory
	* scales with the budget rather than total session history (#1710).
	*
	* Falls back to a full (untruncated) read when the provider doesn't
	* implement `getRecentHistory`. The fallback reports honest metadata
	* (`truncated: false` and the real serialized size) and warns once so a
	* host relying on the budget knows it is not being enforced.
	*/
	async getRecentHistory(maxContentBytes, minRecentMessages = 1) {
		await this._ensureRestored();
		if (this.storage.getRecentHistory) return this.storage.getRecentHistory(null, maxContentBytes, minRecentMessages);
		if (!this._warnedNoRecentHistorySupport) {
			this._warnedNoRecentHistorySupport = true;
			console.warn("[Session] The configured SessionProvider does not implement getRecentHistory; the requested byte budget cannot be enforced and the FULL history was loaded. Implement getRecentHistory (and getHistoryRowStats) on the provider to bound hydration.");
		}
		const messages = await this.storage.getHistory();
		let totalContentBytes = 0;
		for (const message of messages) totalContentBytes += JSON.stringify(message).length;
		return {
			messages,
			truncated: false,
			totalContentBytes
		};
	}
	/**
	* Per-row stored sizes for the active branch path (root → leaf) WITHOUT
	* loading message content, or `null` when the provider doesn't support it.
	* Lets hosts find oversized rows (e.g. inline base64 media) and process
	* them one at a time with bounded memory.
	*/
	async getHistoryRowStats() {
		await this._ensureRestored();
		if (!this.storage.getHistoryRowStats) return null;
		return this.storage.getHistoryRowStats();
	}
	async getMessage(id) {
		await this._ensureRestored();
		return this.storage.getMessage(id);
	}
	async getLatestLeaf() {
		await this._ensureRestored();
		return this.storage.getLatestLeaf();
	}
	async getBranches(messageId) {
		await this._ensureRestored();
		return this.storage.getBranches(messageId);
	}
	async getPathLength(leafId) {
		await this._ensureRestored();
		return this.storage.getPathLength(leafId);
	}
	_broadcast(type, data) {
		if (!this._broadcaster) return;
		this._broadcaster.broadcast(JSON.stringify({
			type,
			...data
		}));
	}
	_shouldEstimateTokens() {
		return Boolean(this._broadcaster || this._tokenThreshold != null && this._compactionFn);
	}
	async _estimateTokenCount() {
		const messages = await this.getHistory();
		const systemPrompt = await this.context.getSystemPromptForEstimate();
		if (this._tokenCounter) {
			if (!this.context.isLoaded()) await this.context.load();
			const contextBlocks = this.context.getBlocks();
			const estimate = await this._tokenCounter({
				messages,
				systemPrompt,
				contextBlocks
			});
			return Number.isFinite(estimate) ? Math.max(0, Math.ceil(estimate)) : 0;
		}
		return estimateMessageTokens(messages) + estimateStringTokens(systemPrompt);
	}
	async _handleAutoCompactionError(error) {
		const message = error instanceof Error ? error.message : String(error);
		if (this._compactionErrorHandler) try {
			await this._compactionErrorHandler(error);
		} catch (handlerError) {
			const handlerMessage = handlerError instanceof Error ? handlerError.message : String(handlerError);
			console.warn(`Session auto-compaction error handler failed: ${handlerMessage}`);
		}
		else console.warn(`Session auto-compaction failed: ${message}`);
		this._emitError(message);
	}
	async _emitStatus(phase, extra) {
		let tokenEstimate = 0;
		if (this._shouldEstimateTokens()) try {
			tokenEstimate = await this._estimateTokenCount();
		} catch (err) {
			await this._handleAutoCompactionError(err);
		}
		this._broadcast("cf_agent_session", {
			phase,
			tokenEstimate,
			tokenThreshold: this._tokenThreshold ?? null,
			...extra
		});
		return tokenEstimate;
	}
	_emitError(error) {
		this._broadcast("cf_agent_session_error", { error });
	}
	async appendMessage(message, parentId) {
		await this._appendMessage(message, parentId);
	}
	async _appendMessage(message, parentId) {
		await this._ensureRestored();
		if (await this.storage.getMessage(message.id)) {
			await this._emitStatus("idle");
			await this._notifyMessagesChanged({
				type: "append",
				message,
				parentId,
				inserted: false
			});
			return;
		}
		await this.storage.appendMessage(message, parentId);
		const tokenEstimate = await this._emitStatus("idle");
		let compacted = false;
		if (this._tokenThreshold != null && this._compactionFn && tokenEstimate > this._tokenThreshold) try {
			compacted = Boolean(await this.compact());
			if (!compacted && !this._warnedCompactionNoOp) {
				this._warnedCompactionNoOp = true;
				console.warn(`[Session] Auto-compaction fired (~${tokenEstimate} tokens > ${this._tokenThreshold}) but the compaction function returned null, so history was not shortened. ` + (this._tokenCounter ? `A tokenCounter is configured and now flows to the boundary logic, but it is invoked per-message there — a whole-prompt/usage counter (e.g. returning a fixed usage.inputTokens regardless of which messages are passed) degrades the tail budget to minTailMessages and can still no-op. Pass a per-message CompactOptions.tokenCounter for precise tail budgeting.` : `If your history is tool-heavy, configure a tokenCounter on compactAfter() — it flows to createCompactFunction's boundary logic automatically.`));
			} else if (compacted) this._warnedCompactionNoOp = false;
		} catch (err) {
			await this._handleAutoCompactionError(err);
		}
		if (!compacted) await this._notifyMessagesChanged({
			type: "append",
			message,
			parentId,
			inserted: true
		});
	}
	async updateMessage(message) {
		await this._ensureRestored();
		await this.storage.updateMessage(message);
		await this._emitStatus("idle");
		await this._notifyMessagesChanged({
			type: "update",
			message
		});
	}
	/**
	* @internal
	* Rewrite a stored message WITHOUT the public-write side effects: no
	* token-estimate status broadcast (which reads the FULL history) and no
	* auto-compaction check. For framework maintenance passes that rewrite
	* many rows with bounded memory — e.g. media eviction (#1710) — where the
	* per-row full-history estimate would reintroduce the memory pressure the
	* pass exists to remove. The message-change listener still fires so a
	* cache-owning host stays coherent. Application code should use
	* `updateMessage`.
	*/
	async internal_rewriteMessage(message) {
		await this._ensureRestored();
		await this.storage.updateMessage(message);
		await this._notifyMessagesChanged({
			type: "update",
			message
		});
	}
	async deleteMessages(messageIds) {
		await this._ensureRestored();
		await this.storage.deleteMessages(messageIds);
		await this._emitStatus("idle");
		await this._notifyMessagesChanged({
			type: "delete",
			messageIds
		});
	}
	async clearMessages() {
		await this._ensureRestored();
		await this.storage.clearMessages();
		this.context.clearSkillState();
		await this.context.refreshSystemPrompt();
		await this._emitStatus("idle");
		await this._notifyMessagesChanged({ type: "clear" });
	}
	async addCompaction(summary, fromMessageId, toMessageId) {
		await this._ensureRestored();
		return this.storage.addCompaction(summary, fromMessageId, toMessageId);
	}
	async getCompactions() {
		await this._ensureRestored();
		return this.storage.getCompactions();
	}
	/**
	* Run the registered compaction function and store the result as an overlay.
	* Requires `onCompaction()` to be called first.
	*/
	async compact() {
		await this._ensureRestored();
		if (!this._compactionFn) throw new Error("No compaction function registered. Call onCompaction() first.");
		const tokensBefore = await this._emitStatus("compacting");
		let result;
		try {
			result = await this._compactionFn(await this.getHistory(), { tokenCounter: this._tokenCounter });
		} catch (err) {
			this._emitError(err instanceof Error ? err.message : String(err));
			return null;
		}
		if (!result) {
			await this._emitStatus("idle");
			return null;
		}
		if (!new Set((await this.getHistory()).map((m) => m.id)).has(result.toMessageId)) {
			await this._emitStatus("idle");
			return null;
		}
		const existing = await this.getCompactions();
		const fromId = existing.length > 0 ? existing[0].fromMessageId : result.fromMessageId;
		await this.addCompaction(result.summary, fromId, result.toMessageId);
		await this.refreshSystemPrompt();
		await this._emitStatus("idle", { compacted: { tokensBefore } });
		await this._notifyMessagesChanged({ type: "compact" });
		return {
			...result,
			fromMessageId: fromId
		};
	}
	getContextBlock(label) {
		this._ensureReady();
		return this.context.getBlock(label);
	}
	getContextBlocks() {
		this._ensureReady();
		return this.context.getBlocks();
	}
	async replaceContextBlock(label, content) {
		await this._ensureRestored();
		return this.context.setBlock(label, content);
	}
	async appendContextBlock(label, content) {
		await this._ensureRestored();
		return this.context.appendToBlock(label, content);
	}
	/**
	* Dynamically register a new context block after session initialization.
	*
	* This is a **builder / runtime API**, not an LLM tool. The LLM writes
	* into existing context blocks via the `set_context` tool (see
	* `ContextBlocks.tools()`); it cannot declare new blocks itself. This
	* method is how extension / host code contributes blocks at runtime
	* (e.g. an extension's `onLoad` handler registering its own memory block).
	*
	* The block's provider is initialized and loaded immediately.
	* Call `refreshSystemPrompt()` afterward to include the new block
	* in the system prompt.
	*
	* Note: When called without a provider, auto-wires to SQLite via
	* AgentContextProvider. Requires the session to have been created
	* via `Session.create(agent)` (not the direct constructor).
	*/
	async addContext(label, options) {
		await this._ensureRestored();
		const opts = options ?? {};
		let provider = opts.provider;
		if (!provider) {
			if (!this._agent) throw new Error(`addContext("${label}") requires an explicit provider when Session uses a SessionProvider`);
			const key = this._sessionId ? `${label}_${this._sessionId}` : label;
			provider = new AgentContextProvider(this._agent, key);
		}
		const block = await this.context.addBlock({
			label,
			description: opts.description,
			maxTokens: opts.maxTokens,
			provider
		});
		if (block.isSkill && !this._skillScanRan) await this._scanHistoryForLoadedSkills();
		return block;
	}
	/**
	* Remove a dynamically registered context block.
	* Used during extension unload cleanup.
	*
	* Returns true if the block existed and was removed.
	* Call `refreshSystemPrompt()` afterward to rebuild the prompt
	* without the removed block.
	*/
	removeContext(label) {
		this._ensureReady();
		return this.context.removeBlock(label);
	}
	/**
	* Unload a previously loaded skill, reclaiming context space.
	* The tool result in conversation history is replaced with a short marker.
	*
	* Async so that the session's background skill-state restore (which
	* reads conversation history) is awaited first — otherwise a freshly
	* rehydrated DO could report "not loaded" for a skill that's actually
	* present in history.
	*/
	async unloadSkill(label, key) {
		await this._ensureRestored();
		return this.context.unloadSkill(label, key);
	}
	/**
	* Get currently loaded skill keys (as "label:key" strings).
	* Async for the same reason as `unloadSkill` — must wait for restore.
	*/
	async getLoadedSkillKeys() {
		await this._ensureRestored();
		return this.context.getLoadedSkillKeys();
	}
	async freezeSystemPrompt() {
		await this._ensureRestored();
		return this.context.freezeSystemPrompt();
	}
	async refreshSystemPrompt() {
		await this._ensureRestored();
		return this.context.refreshSystemPrompt();
	}
	async search(query, options) {
		await this._ensureRestored();
		if (!this.storage.searchMessages) throw new Error("Session provider does not support search");
		return this.storage.searchMessages(query, options?.limit ?? 20);
	}
	/** Returns set_context and load_context tools. */
	async tools() {
		await this._ensureRestored();
		return this.context.tools();
	}
};
//#endregion
//#region src/experimental/memory/session/manager.ts
var SessionManager = class SessionManager {
	constructor(agent, _options = {}) {
		this._pending = [];
		this._sessions = /* @__PURE__ */ new Map();
		this._tableReady = false;
		this._ready = false;
		this.agent = agent;
		this._ready = true;
		this._ensureTable();
	}
	/**
	* Chainable SessionManager creation with auto-wired context for all sessions.
	*
	* @example
	* ```ts
	* const manager = SessionManager.create(this)
	*   .withContext("soul", { provider: { get: async () => "You are helpful." } })
	*   .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
	*   .withCachedPrompt()
	*   .compactAfter(100_000);
	*
	* // Each getSession(id) auto-creates namespaced providers:
	* //   memory key: "memory_<sessionId>"
	* //   prompt key: "_system_prompt_<sessionId>"
	* const session = manager.getSession("chat-123");
	* ```
	*/
	static create(agent) {
		const mgr = Object.create(SessionManager.prototype);
		mgr.agent = agent;
		mgr._pending = [];
		mgr._compactionFn = null;
		mgr._tokenThreshold = void 0;
		mgr._tokenCounter = void 0;
		mgr._compactionErrorHandler = void 0;
		mgr._sessions = /* @__PURE__ */ new Map();
		mgr._tableReady = false;
		mgr._ready = false;
		return mgr;
	}
	withContext(label, options) {
		this._pending.push({
			label,
			options: options ?? {}
		});
		return this;
	}
	withCachedPrompt(provider) {
		this._cachedPrompt = provider ?? true;
		return this;
	}
	/**
	* Register a compaction function propagated to all sessions.
	* Called by `Session.compact()` to compress message history.
	*/
	onCompaction(fn) {
		this._compactionFn = fn;
		return this;
	}
	/**
	* Auto-compact when estimated token count exceeds the threshold.
	* Propagated to all sessions. Requires `onCompaction()`.
	*/
	compactAfter(tokenThreshold, options) {
		this._tokenThreshold = tokenThreshold;
		if (options?.tokenCounter) this._tokenCounter = options.tokenCounter;
		return this;
	}
	/**
	* Handle failures from automatic compaction in managed sessions.
	*/
	onCompactionError(handler) {
		this._compactionErrorHandler = handler;
		return this;
	}
	/**
	* Add a searchable context block that searches conversation history
	* across all sessions managed by this manager.
	*
	* The model can use `search_context` to find relevant messages from
	* any session. The block is readonly (no `set`).
	*
	* @example
	* ```ts
	* SessionManager.create(this)
	*   .withContext("memory", { maxTokens: 1100 })
	*   .withSearchableHistory("history")
	*   .withCachedPrompt();
	* ```
	*/
	withSearchableHistory(label) {
		this._historyLabel = label;
		return this;
	}
	_ensureReady() {
		if (this._ready) return;
		this._ready = true;
		this._ensureTable();
	}
	_ensureTable() {
		if (this._tableReady) return;
		this.agent.sql`
      CREATE TABLE IF NOT EXISTS assistant_sessions (
        id TEXT PRIMARY KEY,
        name TEXT NOT NULL,
        parent_session_id TEXT,
        model TEXT,
        source TEXT,
        input_tokens INTEGER DEFAULT 0,
        output_tokens INTEGER DEFAULT 0,
        estimated_cost REAL DEFAULT 0,
        end_reason TEXT,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
      )
    `;
		this.agent.sql`
      CREATE VIRTUAL TABLE IF NOT EXISTS assistant_fts
      USING fts5(id UNINDEXED, session_id UNINDEXED, role UNINDEXED, content, tokenize='porter unicode61')
    `;
		this._tableReady = true;
	}
	_createHistoryProvider() {
		const mgr = this;
		return {
			async get() {
				const sessions = mgr.list();
				if (sessions.length === 0) return null;
				return `${sessions.length} session${sessions.length === 1 ? "" : "s"} available for search.`;
			},
			async search(query) {
				const results = mgr.search(query, { limit: 10 });
				if (results.length === 0) return null;
				return results.map((r) => `[${r.role}] ${r.content}`).join("\n---\n");
			}
		};
	}
	/** Get or create the Session instance for a session ID. */
	getSession(sessionId) {
		this._ensureReady();
		let session = this._sessions.get(sessionId);
		if (!session) {
			const s = Session.create(this.agent).forSession(sessionId);
			for (const { label, options } of this._pending) s.withContext(label, options);
			if (this._cachedPrompt === true) s.withCachedPrompt();
			else if (this._cachedPrompt) s.withCachedPrompt(this._cachedPrompt);
			if (this._historyLabel) s.withContext(this._historyLabel, {
				description: "Cross-session conversation history",
				provider: this._createHistoryProvider()
			});
			if (this._compactionFn) s.onCompaction(this._compactionFn);
			if (this._tokenThreshold != null) s.compactAfter(this._tokenThreshold, { tokenCounter: this._tokenCounter });
			if (this._compactionErrorHandler) s.onCompactionError(this._compactionErrorHandler);
			session = s;
			this._sessions.set(sessionId, session);
		}
		return session;
	}
	create(name, opts) {
		this._ensureReady();
		const id = crypto.randomUUID();
		this.agent.sql`
      INSERT INTO assistant_sessions (id, name, parent_session_id, model, source)
      VALUES (${id}, ${name}, ${opts?.parentSessionId ?? null}, ${opts?.model ?? null}, ${opts?.source ?? null})
    `;
		return this.get(id);
	}
	get(sessionId) {
		this._ensureReady();
		return this.agent.sql`
      SELECT * FROM assistant_sessions WHERE id = ${sessionId}
    `[0] ?? null;
	}
	list() {
		this._ensureReady();
		return this.agent.sql`
      SELECT * FROM assistant_sessions ORDER BY updated_at DESC
    `;
	}
	async delete(sessionId) {
		await this.getSession(sessionId).clearMessages();
		this.agent.sql`DELETE FROM assistant_sessions WHERE id = ${sessionId}`;
		this._sessions.delete(sessionId);
	}
	rename(sessionId, name) {
		this._ensureReady();
		this.agent.sql`
      UPDATE assistant_sessions SET name = ${name}, updated_at = CURRENT_TIMESTAMP
      WHERE id = ${sessionId}
    `;
	}
	async append(sessionId, message, parentId) {
		await this.getSession(sessionId).appendMessage(message, parentId);
		this._touch(sessionId);
		return message.id;
	}
	async upsert(sessionId, message, parentId) {
		const session = this.getSession(sessionId);
		if (await session.getMessage(message.id)) await session.updateMessage(message);
		else await session.appendMessage(message, parentId);
		this._touch(sessionId);
		return message.id;
	}
	async appendAll(sessionId, messages, parentId) {
		const session = this.getSession(sessionId);
		let lastParent = parentId ?? null;
		for (const msg of messages) {
			await session.appendMessage(msg, lastParent);
			lastParent = msg.id;
		}
		this._touch(sessionId);
		return lastParent;
	}
	async getHistory(sessionId, leafId) {
		return this.getSession(sessionId).getHistory(leafId);
	}
	async getMessageCount(sessionId) {
		return this.getSession(sessionId).getPathLength();
	}
	async clearMessages(sessionId) {
		await this.getSession(sessionId).clearMessages();
		this._touch(sessionId);
	}
	async deleteMessages(sessionId, messageIds) {
		await this.getSession(sessionId).deleteMessages(messageIds);
		this._touch(sessionId);
	}
	async getBranches(sessionId, messageId) {
		return this.getSession(sessionId).getBranches(messageId);
	}
	/**
	* Fork a session at a specific message, creating a new session
	* with the history up to that point copied over.
	*/
	async fork(sessionId, atMessageId, newName) {
		const info = this.create(newName, { parentSessionId: sessionId });
		const history = await this.getSession(sessionId).getHistory(atMessageId);
		const newSession = this.getSession(info.id);
		let parentId = null;
		for (const msg of history) {
			const newId = crypto.randomUUID();
			const copy = {
				...msg,
				id: newId
			};
			await newSession.appendMessage(copy, parentId);
			parentId = newId;
		}
		this._touch(info.id);
		return info;
	}
	async addCompaction(sessionId, summary, fromId, toId) {
		return this.getSession(sessionId).addCompaction(summary, fromId, toId);
	}
	async getCompactions(sessionId) {
		return this.getSession(sessionId).getCompactions();
	}
	async compactAndSplit(sessionId, summary, newName) {
		const old = this.get(sessionId);
		this.agent.sql`
      UPDATE assistant_sessions SET end_reason = 'compaction', updated_at = CURRENT_TIMESTAMP
      WHERE id = ${sessionId}
    `;
		const info = this.create(newName ?? old?.name ?? "Compacted", {
			parentSessionId: sessionId,
			model: old?.model ?? void 0,
			source: old?.source ?? void 0
		});
		await this.append(info.id, {
			id: crypto.randomUUID(),
			role: "assistant",
			parts: [{
				type: "text",
				text: `[Context from previous session]\n\n${summary}`
			}]
		});
		return info;
	}
	addUsage(sessionId, inputTokens, outputTokens, cost) {
		this._ensureReady();
		this.agent.sql`
      UPDATE assistant_sessions SET
        input_tokens = input_tokens + ${inputTokens},
        output_tokens = output_tokens + ${outputTokens},
        estimated_cost = estimated_cost + ${cost},
        updated_at = CURRENT_TIMESTAMP
      WHERE id = ${sessionId}
    `;
	}
	search(query, options) {
		this._ensureReady();
		const limit = options?.limit ?? 20;
		const sanitized = query.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, "\"\"")}"`).join(" ");
		if (!sanitized) return [];
		try {
			return this.agent.sql`
        SELECT id, role, content FROM assistant_fts
        WHERE assistant_fts MATCH ${sanitized}
        ORDER BY rank LIMIT ${limit}
      `.map((r) => ({
				id: r.id,
				role: r.role,
				content: r.content,
				createdAt: ""
			}));
		} catch {
			return [];
		}
	}
	tools() {
		return { session_search: {
			description: "Search past conversations for relevant context. Searches across all sessions.",
			inputSchema: z.fromJSONSchema({
				type: "object",
				properties: { query: {
					type: "string",
					description: "Search query"
				} },
				required: ["query"]
			}),
			execute: async ({ query }) => {
				try {
					const results = this.search(query, { limit: 10 });
					if (results.length === 0) return "No results found.";
					return results.map((r) => `[${r.role}] ${r.content}`).join("\n---\n");
				} catch (err) {
					return `Error: ${err instanceof Error ? err.message : String(err)}`;
				}
			}
		} };
	}
	_touch(sessionId) {
		this.agent.sql`
      UPDATE assistant_sessions SET updated_at = CURRENT_TIMESTAMP
      WHERE id = ${sessionId}
    `;
	}
};
//#endregion
//#region src/experimental/memory/session/providers/postgres-adapter.ts
function isPostgresConnection(client) {
	return typeof client.execute === "function";
}
/**
* Normalise an incoming client into a `PostgresConnection`. When given a
* `pg`-style client we translate `?` placeholders to `$1, $2, ...` so the
* providers can keep using the portable `?` syntax internally.
*/
function toPostgresConnection(client) {
	if (isPostgresConnection(client)) return client;
	const pg = client;
	return { async execute(query, args) {
		let idx = 0;
		const pgQuery = query.replace(/\?/g, () => `$${++idx}`);
		return { rows: (await pg.query(pgQuery, args ?? [])).rows };
	} };
}
//#endregion
//#region src/experimental/memory/session/providers/postgres.ts
var PostgresSessionProvider = class {
	/**
	* @param client A raw `pg.Client` (recommended) or any `PostgresConnection`.
	*   Must already be connected — this provider never opens or closes the
	*   underlying client.
	* @param sessionId Session identifier. Different ids are fully isolated
	*   rows within the shared tables. Defaults to `""`.
	*/
	constructor(client, sessionId) {
		this.conn = toPostgresConnection(client);
		this.sessionId = sessionId ?? "";
	}
	async getMessage(id) {
		const { rows } = await this.conn.execute("SELECT content FROM assistant_messages WHERE id = ? AND session_id = ?", [id, this.sessionId]);
		return rows.length > 0 ? this.parse(rows[0].content) : null;
	}
	async getHistory(leafId) {
		const leaf = leafId ? (await this.conn.execute("SELECT id FROM assistant_messages WHERE id = ? AND session_id = ?", [leafId, this.sessionId])).rows[0] : await this.latestLeafRow();
		if (!leaf) return [];
		const { rows } = await this.conn.execute(`WITH RECURSIVE path AS (
        SELECT id, parent_id, content, 0 as depth FROM assistant_messages WHERE id = ? AND session_id = ?
        UNION ALL
        SELECT m.id, m.parent_id, m.content, p.depth + 1 FROM assistant_messages m
        JOIN path p ON m.id = p.parent_id
        WHERE m.session_id = ? AND p.depth < 10000
      )
      SELECT content FROM path ORDER BY depth DESC`, [
			leaf.id,
			this.sessionId,
			this.sessionId
		]);
		const messages = this.parseRows(rows);
		const compactions = await this.getCompactions();
		if (compactions.length === 0) return messages;
		return this.applyCompactions(messages, compactions);
	}
	async getLatestLeaf() {
		const row = await this.latestLeafRow();
		return row ? this.parse(row.content) : null;
	}
	async getBranches(messageId) {
		const { rows } = await this.conn.execute("SELECT content FROM assistant_messages WHERE parent_id = ? AND session_id = ? ORDER BY created_at ASC", [messageId, this.sessionId]);
		return this.parseRows(rows);
	}
	async getPathLength(leafId) {
		const leaf = leafId ? (await this.conn.execute("SELECT id FROM assistant_messages WHERE id = ? AND session_id = ?", [leafId, this.sessionId])).rows[0] : await this.latestLeafRow();
		if (!leaf) return 0;
		const { rows } = await this.conn.execute(`WITH RECURSIVE path AS (
        SELECT id, parent_id, 0 as depth FROM assistant_messages WHERE id = ? AND session_id = ?
        UNION ALL
        SELECT m.id, m.parent_id, p.depth + 1 FROM assistant_messages m
        JOIN path p ON m.id = p.parent_id
        WHERE m.session_id = ? AND p.depth < 10000
      )
      SELECT COUNT(*) as count FROM path`, [
			leaf.id,
			this.sessionId,
			this.sessionId
		]);
		return Number(rows[0]?.count ?? 0);
	}
	async appendMessage(message, parentId) {
		let parent = parentId !== void 0 ? parentId : (await this.latestLeafRow())?.id ?? null;
		if (parent) {
			const { rows } = await this.conn.execute("SELECT id FROM assistant_messages WHERE id = ? AND session_id = ?", [parent, this.sessionId]);
			if (rows.length === 0) parent = null;
		}
		const json = JSON.stringify(message);
		const text = this.extractSearchableText(json);
		await this.conn.execute(`INSERT INTO assistant_messages (id, session_id, parent_id, role, content, text_content)
       VALUES (?, ?, ?, ?, ?, ?)
       ON CONFLICT (session_id, id) DO NOTHING`, [
			message.id,
			this.sessionId,
			parent,
			message.role,
			json,
			text
		]);
	}
	async updateMessage(message) {
		const json = JSON.stringify(message);
		await this.conn.execute("UPDATE assistant_messages SET content = ?, text_content = ? WHERE id = ? AND session_id = ?", [
			json,
			this.extractSearchableText(json),
			message.id,
			this.sessionId
		]);
	}
	async deleteMessages(messageIds) {
		for (const id of messageIds) await this.conn.execute("DELETE FROM assistant_messages WHERE id = ? AND session_id = ?", [id, this.sessionId]);
	}
	async clearMessages() {
		await this.conn.execute("DELETE FROM assistant_messages WHERE session_id = ?", [this.sessionId]);
		await this.conn.execute("DELETE FROM assistant_compactions WHERE session_id = ?", [this.sessionId]);
	}
	async addCompaction(summary, fromMessageId, toMessageId) {
		const id = crypto.randomUUID();
		await this.conn.execute("INSERT INTO assistant_compactions (id, session_id, summary, from_message_id, to_message_id) VALUES (?, ?, ?, ?, ?)", [
			id,
			this.sessionId,
			summary,
			fromMessageId,
			toMessageId
		]);
		return {
			id,
			summary,
			fromMessageId,
			toMessageId,
			createdAt: (/* @__PURE__ */ new Date()).toISOString()
		};
	}
	async getCompactions() {
		const { rows } = await this.conn.execute("SELECT * FROM assistant_compactions WHERE session_id = ? ORDER BY created_at ASC", [this.sessionId]);
		return rows.map((r) => ({
			id: r.id,
			summary: r.summary,
			fromMessageId: r.from_message_id,
			toMessageId: r.to_message_id,
			createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : String(r.created_at)
		}));
	}
	async searchMessages(query, limit = 20) {
		const { rows } = await this.conn.execute(`SELECT id, role, text_content FROM assistant_messages
       WHERE session_id = ? AND content_tsv @@ plainto_tsquery('english', ?)
       ORDER BY ts_rank(content_tsv, plainto_tsquery('english', ?)) DESC
       LIMIT ?`, [
			this.sessionId,
			query,
			query,
			limit
		]);
		return rows.map((r) => ({
			id: r.id,
			role: r.role,
			content: r.text_content ?? "",
			createdAt: ""
		}));
	}
	async latestLeafRow() {
		const { rows } = await this.conn.execute(`SELECT m.id, m.content FROM assistant_messages m
       LEFT JOIN assistant_messages c ON c.parent_id = m.id AND c.session_id = ?
       WHERE c.id IS NULL AND m.session_id = ?
       ORDER BY m.created_at DESC LIMIT 1`, [this.sessionId, this.sessionId]);
		return rows[0] ?? null;
	}
	applyCompactions(messages, compactions) {
		const ids = messages.map((m) => m.id);
		const result = [];
		let i = 0;
		while (i < messages.length) {
			const matching = compactions.filter((c) => c.fromMessageId === ids[i]);
			const comp = matching.length > 1 ? matching[matching.length - 1] : matching[0];
			if (comp) {
				const endIdx = ids.indexOf(comp.toMessageId);
				if (endIdx >= i) {
					result.push({
						id: `compaction_${comp.id}`,
						role: "assistant",
						parts: [{
							type: "text",
							text: comp.summary
						}],
						createdAt: /* @__PURE__ */ new Date()
					});
					i = endIdx + 1;
					continue;
				}
			}
			result.push(messages[i]);
			i++;
		}
		return result;
	}
	parse(json) {
		try {
			const msg = JSON.parse(json);
			if (typeof msg?.id === "string" && typeof msg?.role === "string" && Array.isArray(msg?.parts)) return msg;
		} catch {}
		return null;
	}
	parseRows(rows) {
		const result = [];
		for (const row of rows) {
			const msg = this.parse(row.content);
			if (msg) result.push(msg);
		}
		return result;
	}
	/**
	* Extract just the human-readable text from a message's JSON blob
	* and store it in `text_content`, which feeds the generated `content_tsv`
	* column used for FTS. The full structured message (parts, tool calls,
	* metadata) is still stored verbatim in `content` — this is the source
	* of truth. Indexing the raw JSON would return FTS hits on keys like
	* `"role"`, `"parts"`, `"dynamic-tool"`, etc.
	*/
	extractSearchableText(json) {
		const msg = this.parse(json);
		if (!msg) return json;
		return msg.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
	}
};
//#endregion
//#region src/experimental/memory/session/providers/postgres-context.ts
var PostgresContextProvider = class {
	/**
	* @param client A raw `pg.Client` (recommended) or any `PostgresConnection`.
	*   Must already be connected.
	* @param label Block label used as the primary key row in
	*   `cf_agents_context_blocks`. Pass a session-scoped label (e.g.
	*   `` `memory_${sessionId}` ``) for per-session state.
	*/
	constructor(client, label) {
		this.conn = toPostgresConnection(client);
		this.label = label;
	}
	async get() {
		const { rows } = await this.conn.execute("SELECT content FROM cf_agents_context_blocks WHERE label = ?", [this.label]);
		return rows[0]?.content ?? null;
	}
	async set(content) {
		await this.conn.execute(`INSERT INTO cf_agents_context_blocks (label, content)
       VALUES (?, ?)
       ON CONFLICT (label) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW()`, [this.label, content]);
	}
};
//#endregion
//#region src/experimental/memory/session/providers/postgres-search.ts
var PostgresSearchProvider = class {
	/**
	* @param client A raw `pg.Client` (recommended) or any `PostgresConnection`.
	*   Must already be connected.
	*/
	constructor(client) {
		this.label = "";
		this.conn = toPostgresConnection(client);
	}
	init(label) {
		this.label = label;
	}
	async get() {
		const { rows } = await this.conn.execute("SELECT COUNT(*) as count FROM cf_agents_search_entries WHERE label = ?", [this.label]);
		const count = Number(rows[0]?.count ?? 0);
		if (count === 0) return null;
		return `${count} entries indexed.`;
	}
	async search(query) {
		if (!query.trim()) return null;
		const { rows } = await this.conn.execute(`SELECT key, content FROM cf_agents_search_entries
       WHERE label = ? AND content_tsv @@ plainto_tsquery('english', ?)
       ORDER BY ts_rank(content_tsv, plainto_tsquery('english', ?)) DESC
       LIMIT 10`, [
			this.label,
			query,
			query
		]);
		if (rows.length === 0) return "No results found.";
		return rows.map((r) => `[${r.key}]\n${r.content}`).join("\n\n");
	}
	async set(key, content) {
		await this.conn.execute(`INSERT INTO cf_agents_search_entries (label, key, content)
       VALUES (?, ?, ?)
       ON CONFLICT (label, key) DO UPDATE SET
         content = EXCLUDED.content,
         updated_at = NOW()`, [
			this.label,
			key,
			content
		]);
	}
};
//#endregion
export { AgentContextProvider, AgentSearchProvider, AgentSessionProvider, PostgresContextProvider, PostgresSearchProvider, PostgresSessionProvider, R2SkillProvider, Session, SessionManager, isSearchProvider, isSkillProvider, isWritableProvider };

//# sourceMappingURL=index.js.map