agents 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/dist/{agent-tool-types-CzGGB-20.d.ts → agent-routing-DE5zmCQ8.d.ts} +1844 -1336
- package/dist/agent-routing.d.ts +14 -0
- package/dist/agent-routing.js +187 -0
- package/dist/agent-routing.js.map +1 -0
- package/dist/agent-tool-types.d.ts +26 -26
- package/dist/{agent-tools-zR2d5uij.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
- package/dist/agent-tools.d.ts +21 -21
- package/dist/agent-tools.js +2 -1
- package/dist/agent-tools.js.map +1 -1
- package/dist/browser/ai.js +6 -2
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
- package/dist/callable-decorator.d.ts +18 -0
- package/dist/callable-decorator.js +71 -0
- package/dist/callable-decorator.js.map +1 -0
- package/dist/capability-BjSKYpzg.js +42 -0
- package/dist/capability-BjSKYpzg.js.map +1 -0
- package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
- package/dist/channel-Bnm4S7T2.d.ts +491 -0
- package/dist/channels/ai-sdk.d.ts +46 -0
- package/dist/channels/ai-sdk.js +120 -0
- package/dist/channels/ai-sdk.js.map +1 -0
- package/dist/channels/email.d.ts +95 -0
- package/dist/channels/email.js +323 -0
- package/dist/channels/email.js.map +1 -0
- package/dist/channels/index.d.ts +233 -0
- package/dist/channels/index.js +608 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/slack.d.ts +140 -0
- package/dist/channels/slack.js +614 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/tanstack-ai.d.ts +39 -0
- package/dist/channels/tanstack-ai.js +17 -0
- package/dist/channels/tanstack-ai.js.map +1 -0
- package/dist/channels/telegram.d.ts +106 -0
- package/dist/channels/telegram.js +427 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/voice.d.ts +45 -0
- package/dist/channels/voice.js +122 -0
- package/dist/channels/voice.js.map +1 -0
- package/dist/chat/index.d.ts +2328 -2015
- package/dist/chat/index.js +891 -521
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +14 -1
- package/dist/chat/react.js +82 -52
- package/dist/chat/react.js.map +1 -1
- package/dist/chat/transport.js +1 -1
- package/dist/chat-sdk/index.d.ts +7 -7
- package/dist/chat-sdk/index.js +1 -1
- package/dist/{client-zqKcsyFa.js → client-jagG8a9_.js} +129 -37
- package/dist/client-jagG8a9_.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/{cloudflare-BduZwmYK.js → cloudflare-Dzvc7V2N.js} +10 -3
- package/dist/{cloudflare-BduZwmYK.js.map → cloudflare-Dzvc7V2N.js.map} +1 -1
- package/dist/context/index.d.ts +216 -0
- package/dist/context/index.js +454 -0
- package/dist/context/index.js.map +1 -0
- package/dist/current-agent-Da_C9a3b.d.ts +266 -0
- package/dist/current-agent-DhoDkSnH.js +51 -0
- package/dist/current-agent-DhoDkSnH.js.map +1 -0
- package/dist/diagnostics-BzvaX2UT.js +45 -0
- package/dist/diagnostics-BzvaX2UT.js.map +1 -0
- package/dist/diagnostics-C4jcz3VK.js +360 -0
- package/dist/diagnostics-C4jcz3VK.js.map +1 -0
- package/dist/{do-oauth-client-provider-VTZj2VtM.d.ts → do-oauth-client-provider-Tmf1vgKz.d.ts} +2 -2
- package/dist/{email-CL27preh.d.ts → email-7TatiTnl.d.ts} +38 -9
- package/dist/email-send.d.ts +15 -0
- package/dist/email-send.js +32 -0
- package/dist/email-send.js.map +1 -0
- package/dist/email.d.ts +14 -10
- package/dist/email.js.map +1 -1
- package/dist/{handler-stateless-C_bo-Ytq.d.ts → handler-stateless-DxYpJ_XF.d.ts} +3 -3
- package/dist/{handler-stateless-CIkKPETH.js → handler-stateless-VvrWSAVA.js} +5 -5
- package/dist/handler-stateless-VvrWSAVA.js.map +1 -0
- package/dist/index-BB0kqhIz.d.ts +101 -0
- package/dist/index-XDkuQ7zm.d.ts +89 -0
- package/dist/{index-BRnybD6X.d.ts → index-YSKgfgg9.d.ts} +21 -31
- package/dist/index.d.ts +113 -101
- package/dist/index.js +11 -7234
- package/dist/ingress-BfetZbMO.js +83 -0
- package/dist/ingress-BfetZbMO.js.map +1 -0
- package/dist/internal-CYlgHl1l.js +59 -0
- package/dist/internal-CYlgHl1l.js.map +1 -0
- package/dist/internal_context-BlxFEWfn.d.ts +19 -0
- package/dist/internal_context.d.ts +10 -4
- package/dist/internal_context.js +1 -10
- package/dist/{client-invoker-BNSZxAkv.d.ts → invoker-CG0_p_Wq.d.ts} +2 -2
- package/dist/{client-invoker-VNZ7X0nn.js → invoker-CHMnoxIA.js} +2 -2
- package/dist/invoker-CHMnoxIA.js.map +1 -0
- package/dist/lifecycle/index.d.ts +66 -0
- package/dist/lifecycle/index.js +4 -0
- package/dist/lifecycle-CMRGjZdw.js +1299 -0
- package/dist/lifecycle-CMRGjZdw.js.map +1 -0
- package/dist/mcp/{do-oauth-client-provider.d.ts → client/do-oauth-client-provider.d.ts} +1 -1
- package/dist/mcp/{do-oauth-client-provider.js → client/do-oauth-client-provider.js} +1 -1
- package/dist/mcp/client/do-oauth-client-provider.js.map +1 -0
- package/dist/mcp/client/index.d.ts +42 -0
- package/dist/mcp/{client.js → client/index.js} +1 -1
- package/dist/mcp/{x402.d.ts → client/x402.d.ts} +2 -2
- package/dist/mcp/{x402.js → client/x402.js} +2 -2
- package/dist/mcp/client/x402.js.map +1 -0
- package/dist/mcp/index.d.ts +36 -36
- package/dist/mcp/index.js +14 -16
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/{server.d.ts → server/index.d.ts} +1 -1
- package/dist/mcp/{server.js → server/index.js} +1 -1
- package/dist/observability/ai/index.js +50 -35
- package/dist/observability/ai/index.js.map +1 -1
- package/dist/observability/index.d.ts +4 -4
- package/dist/observability/index.js +3 -50
- package/dist/observability/index.js.map +1 -1
- package/dist/{protocol-Dqc2MQxo.js → protocol-B0nh6KNf.js} +19 -21
- package/dist/protocol-B0nh6KNf.js.map +1 -0
- package/dist/react.d.ts +4 -4
- package/dist/react.js +1 -1
- package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +13 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +137 -0
- package/dist/routing/index.js +244 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sanitize-D9TujEK8.js +79 -0
- package/dist/sanitize-D9TujEK8.js.map +1 -0
- package/dist/schedule.d.ts +25 -94
- package/dist/schedule.js +1 -98
- package/dist/schedule.js.map +1 -1
- package/dist/scheduler-DD9NdYbF.js +665 -0
- package/dist/scheduler-DD9NdYbF.js.map +1 -0
- package/dist/scheduler-Dwh85ZGl.d.ts +223 -0
- package/dist/schedules/index.d.ts +22 -0
- package/dist/schedules/index.js +2 -0
- package/dist/schedules/parser.d.ts +79 -0
- package/dist/schedules/parser.js +103 -0
- package/dist/schedules/parser.js.map +1 -0
- package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +441 -0
- package/dist/sessions/index.js +2063 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/skills/index.d.ts +99 -0
- package/dist/skills/index.js +254 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/sql-error-CPY-GXyI.d.ts +12 -0
- package/dist/sql-error.d.ts +2 -0
- package/dist/sql-error.js +16 -0
- package/dist/sql-error.js.map +1 -0
- package/dist/src-DlSHshb2.js +6963 -0
- package/dist/src-DlSHshb2.js.map +1 -0
- package/dist/streams/index.d.ts +120 -0
- package/dist/streams/index.js +107 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams-D6tJ0NN9.d.ts +370 -0
- package/dist/streams-DZKgAj9b.js +709 -0
- package/dist/streams-DZKgAj9b.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/surface-bZZJqBka.js +17 -0
- package/dist/surface-bZZJqBka.js.map +1 -0
- package/dist/tasks/index.d.ts +64 -0
- package/dist/tasks/index.js +2 -0
- package/dist/tasks-BRJ5zgya.d.ts +517 -0
- package/dist/tasks-ylZgBjhj.js +1656 -0
- package/dist/tasks-ylZgBjhj.js.map +1 -0
- package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
- package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
- package/dist/text-stream-CpdiKrJB.js +272 -0
- package/dist/text-stream-CpdiKrJB.js.map +1 -0
- package/dist/tokens-nHAKcN6M.js +52 -0
- package/dist/tokens-nHAKcN6M.js.map +1 -0
- package/dist/tool-schema-CBjGPrsQ.js +31 -0
- package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
- package/dist/types-B7LojTe4.d.ts +202 -0
- package/dist/types-_Faxb570.d.ts +439 -0
- package/dist/voice/client.d.ts +226 -0
- package/dist/voice/client.js +932 -0
- package/dist/voice/client.js.map +1 -0
- package/dist/voice/errors.d.ts +43 -0
- package/dist/voice/errors.js +41 -0
- package/dist/voice/errors.js.map +1 -0
- package/dist/voice/index.d.ts +271 -0
- package/dist/voice/index.js +1812 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/react.d.ts +167 -0
- package/dist/voice/react.js +234 -0
- package/dist/voice/react.js.map +1 -0
- package/dist/voice/sfu.d.ts +71 -0
- package/dist/voice/sfu.js +157 -0
- package/dist/voice/sfu.js.map +1 -0
- package/dist/voice/text.d.ts +6 -0
- package/dist/voice/text.js +2 -0
- package/dist/voice/types.d.ts +58 -0
- package/dist/voice/types.js +18 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/workers-ai.d.ts +136 -0
- package/dist/voice/workers-ai.js +568 -0
- package/dist/voice/workers-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +192 -0
- package/dist/websockets/index.js +2 -0
- package/dist/websockets-DUfRHPRq.js +502 -0
- package/dist/websockets-DUfRHPRq.js.map +1 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +22 -22
- package/dist/workflows.js +2 -1
- package/dist/workflows.js.map +1 -1
- package/dist/{ws-chat-transport-CIoOBbO7.js → ws-chat-transport-rWwta645.js} +152 -15
- package/dist/ws-chat-transport-rWwta645.js.map +1 -0
- package/docs/agent-class.md +29 -87
- package/docs/agent-tools.md +2 -1
- package/docs/channels.md +323 -0
- package/docs/chat-agents.md +19 -25
- package/docs/context.md +131 -0
- package/docs/durable-execution.md +1 -1
- package/docs/http-websockets.md +1 -11
- package/docs/human-in-the-loop.md +1 -1
- package/docs/index.md +16 -12
- package/docs/lifecycle.md +370 -0
- package/docs/long-running-agents.md +4 -6
- package/docs/mcp-client.md +55 -0
- package/docs/mcp-servers.md +5 -1
- package/docs/observability.md +11 -11
- package/docs/resumable-streaming.md +2 -2
- package/docs/routing.md +105 -0
- package/docs/scheduling.md +175 -15
- package/docs/server-driven-messages.md +1 -1
- package/docs/sessions.md +237 -871
- package/docs/streams.md +213 -0
- package/docs/sub-agents.md +185 -125
- package/docs/tasks.md +246 -0
- package/docs/voice.md +745 -0
- package/package.json +144 -33
- package/dist/cli/index.js +0 -26
- package/dist/cli/index.js.map +0 -1
- package/dist/client-invoker-VNZ7X0nn.js.map +0 -1
- package/dist/client-zqKcsyFa.js.map +0 -1
- package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
- package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
- package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
- package/dist/experimental/memory/session/index.d.ts +0 -670
- package/dist/experimental/memory/session/index.js +0 -2374
- package/dist/experimental/memory/session/index.js.map +0 -1
- package/dist/experimental/memory/utils/index.d.ts +0 -96
- package/dist/experimental/memory/utils/index.js +0 -79
- package/dist/experimental/memory/utils/index.js.map +0 -1
- package/dist/handler-stateless-CIkKPETH.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal_context-Dg4Cgjcu.d.ts +0 -37
- package/dist/internal_context.js.map +0 -1
- package/dist/mcp/client.d.ts +0 -42
- package/dist/mcp/do-oauth-client-provider.js.map +0 -1
- package/dist/mcp/x402.js.map +0 -1
- package/dist/protocol-Dqc2MQxo.js.map +0 -1
- package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
- package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
- package/dist/ws-chat-transport-CIoOBbO7.js.map +0 -1
- /package/dist/{cli/index.d.ts → index-BVVgDSdq.d.ts} +0 -0
|
@@ -1,2374 +0,0 @@
|
|
|
1
|
-
import "../../../types.js";
|
|
2
|
-
import { m as estimateStringTokens, p as estimateMessageTokens, t as COMPACTION_PREFIX } from "../../../compaction-helpers-iiKMr2TQ.js";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
//#region src/experimental/memory/session/search.ts
|
|
5
|
-
/**
|
|
6
|
-
* Check if a provider is a SearchProvider (has a `search` method).
|
|
7
|
-
*/
|
|
8
|
-
function isSearchProvider(provider) {
|
|
9
|
-
return typeof provider === "object" && provider !== null && "search" in provider && typeof provider.search === "function";
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* SearchProvider backed by Durable Object SQLite with FTS5.
|
|
13
|
-
*
|
|
14
|
-
* - `get()` returns a count of indexed entries
|
|
15
|
-
* - `search(query)` full-text search using FTS5
|
|
16
|
-
* - `set(key, content)` indexes or replaces content under a key
|
|
17
|
-
*
|
|
18
|
-
* Each instance uses a namespaced FTS5 table to avoid collisions
|
|
19
|
-
* with the session message search.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```ts
|
|
23
|
-
* Session.create(this)
|
|
24
|
-
* .withContext("knowledge", {
|
|
25
|
-
* provider: new AgentSearchProvider(this)
|
|
26
|
-
* })
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
var AgentSearchProvider = class {
|
|
30
|
-
constructor(agent) {
|
|
31
|
-
this.label = "";
|
|
32
|
-
this.initialized = false;
|
|
33
|
-
this.agent = agent;
|
|
34
|
-
}
|
|
35
|
-
init(label) {
|
|
36
|
-
this.label = label;
|
|
37
|
-
}
|
|
38
|
-
ensureTable() {
|
|
39
|
-
if (this.initialized) return;
|
|
40
|
-
this.agent.sql`
|
|
41
|
-
CREATE TABLE IF NOT EXISTS cf_agents_search_entries (
|
|
42
|
-
label TEXT NOT NULL,
|
|
43
|
-
key TEXT NOT NULL,
|
|
44
|
-
content TEXT NOT NULL,
|
|
45
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
46
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
-
PRIMARY KEY (label, key)
|
|
48
|
-
)
|
|
49
|
-
`;
|
|
50
|
-
this.agent.sql`
|
|
51
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS cf_agents_search_fts
|
|
52
|
-
USING fts5(
|
|
53
|
-
label UNINDEXED,
|
|
54
|
-
key UNINDEXED,
|
|
55
|
-
content,
|
|
56
|
-
tokenize='porter unicode61'
|
|
57
|
-
)
|
|
58
|
-
`;
|
|
59
|
-
this.initialized = true;
|
|
60
|
-
}
|
|
61
|
-
async get() {
|
|
62
|
-
this.ensureTable();
|
|
63
|
-
const count = this.agent.sql`
|
|
64
|
-
SELECT COUNT(*) as count FROM cf_agents_search_entries
|
|
65
|
-
WHERE label = ${this.label}
|
|
66
|
-
`[0]?.count ?? 0;
|
|
67
|
-
if (count === 0) return null;
|
|
68
|
-
return `${count} entries indexed.`;
|
|
69
|
-
}
|
|
70
|
-
async search(query) {
|
|
71
|
-
this.ensureTable();
|
|
72
|
-
const sanitized = query.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, "\"\"")}"`).join(" ");
|
|
73
|
-
if (!sanitized) return null;
|
|
74
|
-
try {
|
|
75
|
-
const rows = this.agent.sql`
|
|
76
|
-
SELECT f.key, f.content
|
|
77
|
-
FROM cf_agents_search_fts f
|
|
78
|
-
WHERE cf_agents_search_fts MATCH ${sanitized}
|
|
79
|
-
AND f.label = ${this.label}
|
|
80
|
-
ORDER BY rank
|
|
81
|
-
LIMIT 10
|
|
82
|
-
`;
|
|
83
|
-
if (rows.length === 0) return null;
|
|
84
|
-
return rows.map((r) => `[${r.key}]\n${r.content}`).join("\n\n");
|
|
85
|
-
} catch {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
async set(key, content) {
|
|
90
|
-
this.ensureTable();
|
|
91
|
-
this.deleteFTS(key);
|
|
92
|
-
this.agent.sql`
|
|
93
|
-
INSERT INTO cf_agents_search_entries (label, key, content)
|
|
94
|
-
VALUES (${this.label}, ${key}, ${content})
|
|
95
|
-
ON CONFLICT(label, key) DO UPDATE SET
|
|
96
|
-
content = ${content},
|
|
97
|
-
updated_at = CURRENT_TIMESTAMP
|
|
98
|
-
`;
|
|
99
|
-
this.agent.sql`
|
|
100
|
-
INSERT INTO cf_agents_search_fts (label, key, content)
|
|
101
|
-
VALUES (${this.label}, ${key}, ${content})
|
|
102
|
-
`;
|
|
103
|
-
}
|
|
104
|
-
deleteFTS(key) {
|
|
105
|
-
const rows = this.agent.sql`
|
|
106
|
-
SELECT rowid FROM cf_agents_search_fts
|
|
107
|
-
WHERE key = ${key} AND label = ${this.label}
|
|
108
|
-
`;
|
|
109
|
-
for (const row of rows) this.agent.sql`DELETE FROM cf_agents_search_fts WHERE rowid = ${row.rowid}`;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region src/experimental/memory/session/skills.ts
|
|
114
|
-
/**
|
|
115
|
-
* Check if a provider is a SkillProvider (has a `load` method).
|
|
116
|
-
*/
|
|
117
|
-
function isSkillProvider(provider) {
|
|
118
|
-
return typeof provider === "object" && provider !== null && "load" in provider && typeof provider.load === "function";
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* SkillProvider backed by an R2 bucket.
|
|
122
|
-
*
|
|
123
|
-
* - `get()` returns a metadata listing of all skills (key + description)
|
|
124
|
-
* - `load(key)` fetches a skill's full content
|
|
125
|
-
* - `set(key, content, description?)` writes a skill
|
|
126
|
-
*
|
|
127
|
-
* Descriptions are pulled from R2 custom metadata (`description` key).
|
|
128
|
-
* If a prefix is provided, it is prepended on storage operations and
|
|
129
|
-
* stripped from keys in metadata. `keys`, when provided, is matched against
|
|
130
|
-
* these prefix-relative keys.
|
|
131
|
-
*
|
|
132
|
-
* @example
|
|
133
|
-
* ```ts
|
|
134
|
-
* const skills = new R2SkillProvider(env.SKILLS_BUCKET, {
|
|
135
|
-
* prefix: "skills/",
|
|
136
|
-
* keys: ["code-review", "debugging"]
|
|
137
|
-
* });
|
|
138
|
-
* ```
|
|
139
|
-
*/
|
|
140
|
-
var R2SkillProvider = class {
|
|
141
|
-
constructor(bucket, options) {
|
|
142
|
-
this.bucket = bucket;
|
|
143
|
-
this.prefix = options?.prefix ?? "";
|
|
144
|
-
this.keys = options?.keys?.length ? new Set(options.keys) : null;
|
|
145
|
-
}
|
|
146
|
-
async get() {
|
|
147
|
-
const entries = [];
|
|
148
|
-
let cursor;
|
|
149
|
-
let truncated = true;
|
|
150
|
-
while (truncated) {
|
|
151
|
-
const listed = await this.bucket.list({
|
|
152
|
-
prefix: this.prefix,
|
|
153
|
-
cursor,
|
|
154
|
-
include: ["customMetadata"]
|
|
155
|
-
});
|
|
156
|
-
for (const obj of listed.objects) {
|
|
157
|
-
const key = obj.key.slice(this.prefix.length);
|
|
158
|
-
if (!this.allowsKey(key)) continue;
|
|
159
|
-
const desc = obj.customMetadata?.description;
|
|
160
|
-
entries.push(`- ${key}${desc ? `: ${desc}` : ""}`);
|
|
161
|
-
}
|
|
162
|
-
truncated = listed.truncated;
|
|
163
|
-
cursor = listed.truncated ? listed.cursor : void 0;
|
|
164
|
-
}
|
|
165
|
-
return entries.length > 0 ? entries.join("\n") : null;
|
|
166
|
-
}
|
|
167
|
-
async load(key) {
|
|
168
|
-
if (!this.allowsKey(key)) return null;
|
|
169
|
-
const obj = await this.bucket.get(this.prefix + key);
|
|
170
|
-
if (!obj) return null;
|
|
171
|
-
return obj.text();
|
|
172
|
-
}
|
|
173
|
-
async set(key, content, description) {
|
|
174
|
-
await this.bucket.put(this.prefix + key, content, { customMetadata: description ? { description } : void 0 });
|
|
175
|
-
}
|
|
176
|
-
allowsKey(key) {
|
|
177
|
-
return this.keys === null || this.keys.has(key);
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
//#endregion
|
|
181
|
-
//#region src/experimental/memory/session/context.ts
|
|
182
|
-
function slugify(text) {
|
|
183
|
-
return text.slice(0, 60).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
184
|
-
}
|
|
185
|
-
function stableHash(text) {
|
|
186
|
-
let hash = 2166136261;
|
|
187
|
-
for (let i = 0; i < text.length; i++) {
|
|
188
|
-
hash ^= text.charCodeAt(i);
|
|
189
|
-
hash = Math.imul(hash, 16777619);
|
|
190
|
-
}
|
|
191
|
-
return (hash >>> 0).toString(36);
|
|
192
|
-
}
|
|
193
|
-
function contextEntryKey(metadataTitle, content) {
|
|
194
|
-
if (metadataTitle?.trim()) return slugify(metadataTitle) || `entry-${stableHash(metadataTitle)}`;
|
|
195
|
-
return `${slugify(content) || "entry"}-${stableHash(content)}`;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Check if a provider is writable (has a `set` method).
|
|
199
|
-
*/
|
|
200
|
-
function isWritableProvider(provider) {
|
|
201
|
-
return typeof provider === "object" && provider !== null && "set" in provider && typeof provider.set === "function";
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Manages context blocks with frozen snapshot support.
|
|
205
|
-
*/
|
|
206
|
-
var ContextBlocks = class {
|
|
207
|
-
constructor(configs, promptStore) {
|
|
208
|
-
this.blocks = /* @__PURE__ */ new Map();
|
|
209
|
-
this.snapshot = null;
|
|
210
|
-
this.loaded = false;
|
|
211
|
-
this._loadedSkills = /* @__PURE__ */ new Set();
|
|
212
|
-
this._onUnloadSkill = null;
|
|
213
|
-
this.configs = configs;
|
|
214
|
-
this.promptStore = promptStore ?? null;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Register a callback invoked when a skill is unloaded.
|
|
218
|
-
* Session uses this to update the stored tool result message.
|
|
219
|
-
*/
|
|
220
|
-
setUnloadCallback(cb) {
|
|
221
|
-
this._onUnloadSkill = cb;
|
|
222
|
-
}
|
|
223
|
-
isLoaded() {
|
|
224
|
-
return this.loaded;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Load all blocks from their providers.
|
|
228
|
-
* Called once at session init.
|
|
229
|
-
*/
|
|
230
|
-
async load() {
|
|
231
|
-
for (const config of this.configs) {
|
|
232
|
-
if (config.provider?.init) config.provider.init(config.label);
|
|
233
|
-
const content = config.provider ? await config.provider.get() ?? "" : "";
|
|
234
|
-
const skill = config.provider ? isSkillProvider(config.provider) : false;
|
|
235
|
-
const searchable = config.provider ? isSearchProvider(config.provider) : false;
|
|
236
|
-
const writable = config.provider ? isWritableProvider(config.provider) || skill && !!config.provider.set || searchable && !!config.provider.set : false;
|
|
237
|
-
this.blocks.set(config.label, {
|
|
238
|
-
label: config.label,
|
|
239
|
-
description: config.description,
|
|
240
|
-
content,
|
|
241
|
-
tokens: estimateStringTokens(content),
|
|
242
|
-
maxTokens: config.maxTokens,
|
|
243
|
-
writable,
|
|
244
|
-
isSkill: skill,
|
|
245
|
-
isSearchable: searchable
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
this.loaded = true;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Dynamically register a new context block after initialization.
|
|
252
|
-
* Used by extensions to contribute context at runtime.
|
|
253
|
-
*
|
|
254
|
-
* If blocks have already been loaded, the new block's provider is
|
|
255
|
-
* initialized and loaded immediately. The snapshot is NOT updated
|
|
256
|
-
* automatically — call `refreshSystemPrompt()` to rebuild.
|
|
257
|
-
*/
|
|
258
|
-
async addBlock(config) {
|
|
259
|
-
if (!this.loaded) await this.load();
|
|
260
|
-
if (this.configs.some((c) => c.label === config.label)) throw new Error(`Block "${config.label}" already exists`);
|
|
261
|
-
this.configs.push(config);
|
|
262
|
-
if (config.provider?.init) config.provider.init(config.label);
|
|
263
|
-
const content = config.provider ? await config.provider.get() ?? "" : "";
|
|
264
|
-
const skill = config.provider ? isSkillProvider(config.provider) : false;
|
|
265
|
-
const searchable = config.provider ? isSearchProvider(config.provider) : false;
|
|
266
|
-
const writable = config.provider ? isWritableProvider(config.provider) || skill && !!config.provider.set || searchable && !!config.provider.set : false;
|
|
267
|
-
const block = {
|
|
268
|
-
label: config.label,
|
|
269
|
-
description: config.description,
|
|
270
|
-
content,
|
|
271
|
-
tokens: estimateStringTokens(content),
|
|
272
|
-
maxTokens: config.maxTokens,
|
|
273
|
-
writable,
|
|
274
|
-
isSkill: skill,
|
|
275
|
-
isSearchable: searchable
|
|
276
|
-
};
|
|
277
|
-
this.blocks.set(config.label, block);
|
|
278
|
-
return block;
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Remove a dynamically registered context block.
|
|
282
|
-
* Used during extension unload cleanup.
|
|
283
|
-
*
|
|
284
|
-
* Returns true if the block existed and was removed.
|
|
285
|
-
* The snapshot is NOT updated automatically — call
|
|
286
|
-
* `refreshSystemPrompt()` to rebuild.
|
|
287
|
-
*
|
|
288
|
-
* Note: loaded skills for this block are cleaned up from the
|
|
289
|
-
* tracking set but the skill unload callback is NOT fired
|
|
290
|
-
* (history reclamation is skipped — appropriate for full
|
|
291
|
-
* extension removal).
|
|
292
|
-
*/
|
|
293
|
-
removeBlock(label) {
|
|
294
|
-
const idx = this.configs.findIndex((c) => c.label === label);
|
|
295
|
-
if (idx === -1) return false;
|
|
296
|
-
this.configs.splice(idx, 1);
|
|
297
|
-
this.blocks.delete(label);
|
|
298
|
-
for (const id of this._loadedSkills) if (id.startsWith(`${label}:`)) this._loadedSkills.delete(id);
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Get a block by label.
|
|
303
|
-
*/
|
|
304
|
-
getBlock(label) {
|
|
305
|
-
return this.blocks.get(label) ?? null;
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Get all blocks.
|
|
309
|
-
*/
|
|
310
|
-
getBlocks() {
|
|
311
|
-
return Array.from(this.blocks.values());
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Set block content. Writes to provider immediately.
|
|
315
|
-
* Does NOT update the frozen snapshot.
|
|
316
|
-
*/
|
|
317
|
-
async setBlock(label, content) {
|
|
318
|
-
if (!this.loaded) await this.load();
|
|
319
|
-
const config = this.configs.find((c) => c.label === label);
|
|
320
|
-
const existing = this.blocks.get(label);
|
|
321
|
-
if (!existing?.writable) throw new Error(`Block "${label}" is readonly`);
|
|
322
|
-
if (existing.isSkill || existing.isSearchable) throw new Error(`Block "${label}" is a keyed provider. Use setSkill() or setSearchEntry() instead.`);
|
|
323
|
-
const tokens = estimateStringTokens(content);
|
|
324
|
-
const maxTokens = config?.maxTokens ?? existing?.maxTokens;
|
|
325
|
-
if (maxTokens !== void 0 && tokens > maxTokens) throw new Error(`Block "${label}" exceeds maxTokens: ${tokens} > ${maxTokens}`);
|
|
326
|
-
const block = {
|
|
327
|
-
label,
|
|
328
|
-
description: config?.description ?? existing?.description,
|
|
329
|
-
content,
|
|
330
|
-
tokens,
|
|
331
|
-
maxTokens,
|
|
332
|
-
writable: true,
|
|
333
|
-
isSkill: false,
|
|
334
|
-
isSearchable: false
|
|
335
|
-
};
|
|
336
|
-
this.blocks.set(label, block);
|
|
337
|
-
if (config?.provider && isWritableProvider(config.provider)) await config.provider.set(content);
|
|
338
|
-
return block;
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Set a skill entry within a skill block.
|
|
342
|
-
*/
|
|
343
|
-
async setSkill(label, key, content, description) {
|
|
344
|
-
if (!this.loaded) await this.load();
|
|
345
|
-
const config = this.configs.find((c) => c.label === label);
|
|
346
|
-
const existing = this.blocks.get(label);
|
|
347
|
-
if (!existing?.isSkill) throw new Error(`Block "${label}" is not a skill provider`);
|
|
348
|
-
const provider = config?.provider;
|
|
349
|
-
if (!provider || !isSkillProvider(provider) || !provider.set) throw new Error(`Block "${label}" does not support writes`);
|
|
350
|
-
await provider.set(key, content, description);
|
|
351
|
-
const metadata = await provider.get();
|
|
352
|
-
if (metadata) {
|
|
353
|
-
existing.content = metadata;
|
|
354
|
-
existing.tokens = estimateStringTokens(metadata);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Load a skill's full content from a skill block.
|
|
359
|
-
*/
|
|
360
|
-
async loadSkill(label, key) {
|
|
361
|
-
if (!this.loaded) await this.load();
|
|
362
|
-
const config = this.configs.find((c) => c.label === label);
|
|
363
|
-
if (!config?.provider || !isSkillProvider(config.provider)) throw new Error(`Block "${label}" is not a skill provider`);
|
|
364
|
-
const content = await config.provider.load(key);
|
|
365
|
-
if (content !== null) this._loadedSkills.add(`${label}:${key}`);
|
|
366
|
-
return content;
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Unload a previously loaded skill. Updates the stored tool result
|
|
370
|
-
* message via the unload callback (set by Session).
|
|
371
|
-
*/
|
|
372
|
-
unloadSkill(label, key) {
|
|
373
|
-
const id = `${label}:${key}`;
|
|
374
|
-
if (!this._loadedSkills.has(id)) return false;
|
|
375
|
-
this._loadedSkills.delete(id);
|
|
376
|
-
this._onUnloadSkill?.(label, key);
|
|
377
|
-
return true;
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Get the set of currently loaded skill keys (as "label:key" strings).
|
|
381
|
-
*/
|
|
382
|
-
getLoadedSkillKeys() {
|
|
383
|
-
return this._loadedSkills;
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Restore loaded skill tracking from a set of "label:key" strings.
|
|
387
|
-
* Used by Session to reconstruct state after hibernation.
|
|
388
|
-
*/
|
|
389
|
-
restoreLoadedSkills(skillIds) {
|
|
390
|
-
this._loadedSkills = new Set(skillIds);
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Clear all loaded skill tracking. Called when messages are cleared.
|
|
394
|
-
*/
|
|
395
|
-
clearSkillState() {
|
|
396
|
-
this._loadedSkills.clear();
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Index a search entry within a searchable block.
|
|
400
|
-
*/
|
|
401
|
-
async setSearchEntry(label, key, content) {
|
|
402
|
-
if (!this.loaded) await this.load();
|
|
403
|
-
const config = this.configs.find((c) => c.label === label);
|
|
404
|
-
const existing = this.blocks.get(label);
|
|
405
|
-
if (!existing?.isSearchable) throw new Error(`Block "${label}" is not a search provider`);
|
|
406
|
-
const provider = config?.provider;
|
|
407
|
-
if (!provider || !isSearchProvider(provider) || !provider.set) throw new Error(`Block "${label}" does not support writes`);
|
|
408
|
-
await provider.set(key, content);
|
|
409
|
-
existing.content = await provider.get() ?? "";
|
|
410
|
-
existing.tokens = estimateStringTokens(existing.content);
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Search a searchable block.
|
|
414
|
-
*/
|
|
415
|
-
async searchContext(label, query) {
|
|
416
|
-
if (!this.loaded) await this.load();
|
|
417
|
-
const config = this.configs.find((c) => c.label === label);
|
|
418
|
-
if (!config?.provider || !isSearchProvider(config.provider)) throw new Error(`Block "${label}" is not a search provider`);
|
|
419
|
-
return config.provider.search(query);
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Append content to a block.
|
|
423
|
-
*/
|
|
424
|
-
async appendToBlock(label, content) {
|
|
425
|
-
if (!this.loaded) await this.load();
|
|
426
|
-
const existing = this.blocks.get(label);
|
|
427
|
-
if (!existing) throw new Error(`Block "${label}" not found`);
|
|
428
|
-
const needsSep = existing.content.length > 0 && !content.startsWith("\n");
|
|
429
|
-
return this.setBlock(label, existing.content + (needsSep ? "\n" : "") + content);
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Get the system prompt string with context blocks.
|
|
433
|
-
*
|
|
434
|
-
* Returns a frozen snapshot: first call renders and caches,
|
|
435
|
-
* subsequent calls return the same string (preserves LLM prefix cache).
|
|
436
|
-
* Call refreshSnapshot() to re-render after block changes take effect.
|
|
437
|
-
*/
|
|
438
|
-
toSystemPrompt() {
|
|
439
|
-
if (!this.loaded) throw new Error("Context blocks not loaded. Call load() first.");
|
|
440
|
-
if (this.snapshot !== null) return this.snapshot;
|
|
441
|
-
return this.captureSnapshot();
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Force re-render the snapshot from current block state.
|
|
445
|
-
*/
|
|
446
|
-
refreshSnapshot() {
|
|
447
|
-
return this.captureSnapshot();
|
|
448
|
-
}
|
|
449
|
-
renderPrompt() {
|
|
450
|
-
const parts = [];
|
|
451
|
-
const sep = "═".repeat(46);
|
|
452
|
-
for (const block of this.blocks.values()) {
|
|
453
|
-
if (!block.content && !block.writable && !block.isSearchable && !block.isSkill) continue;
|
|
454
|
-
let header = block.label.toUpperCase();
|
|
455
|
-
if (block.description) header += ` (${block.description})`;
|
|
456
|
-
if (block.maxTokens) {
|
|
457
|
-
const pct = Math.round(block.tokens / block.maxTokens * 100);
|
|
458
|
-
header += ` [${pct}% — ${block.tokens}/${block.maxTokens} tokens]`;
|
|
459
|
-
}
|
|
460
|
-
if (block.isSearchable) header += " [searchable]";
|
|
461
|
-
else if (block.isSkill) header += " [loadable]";
|
|
462
|
-
else if (!block.writable) header += " [readonly]";
|
|
463
|
-
else header += " [writable]";
|
|
464
|
-
parts.push(`${sep}\n${header}\n${sep}\n${block.content}`);
|
|
465
|
-
}
|
|
466
|
-
return parts.join("\n\n");
|
|
467
|
-
}
|
|
468
|
-
captureSnapshot() {
|
|
469
|
-
this.snapshot = this.renderPrompt();
|
|
470
|
-
return this.snapshot;
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Get writable blocks (for tool description).
|
|
474
|
-
*/
|
|
475
|
-
getWritableBlocks() {
|
|
476
|
-
return Array.from(this.blocks.values()).filter((b) => b.writable);
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Check if any skill providers are registered.
|
|
480
|
-
*/
|
|
481
|
-
hasSkillBlocks() {
|
|
482
|
-
return Array.from(this.blocks.values()).some((b) => b.isSkill);
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Check whether any CONFIGURED provider is skill-capable, without
|
|
486
|
-
* requiring `load()` to have run. Used by Session to decide whether the
|
|
487
|
-
* init-time loaded-skill restore scan is needed at all.
|
|
488
|
-
*/
|
|
489
|
-
hasSkillCapableConfigs() {
|
|
490
|
-
return this.configs.some((c) => c.provider !== void 0 && isSkillProvider(c.provider));
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* Get skill block labels.
|
|
494
|
-
*/
|
|
495
|
-
getSkillLabels() {
|
|
496
|
-
return Array.from(this.blocks.values()).filter((b) => b.isSkill).map((b) => b.label);
|
|
497
|
-
}
|
|
498
|
-
/**
|
|
499
|
-
* Check if any search providers are registered.
|
|
500
|
-
*/
|
|
501
|
-
hasSearchBlocks() {
|
|
502
|
-
return Array.from(this.blocks.values()).some((b) => b.isSearchable);
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Get searchable block labels.
|
|
506
|
-
*/
|
|
507
|
-
getSearchLabels() {
|
|
508
|
-
return Array.from(this.blocks.values()).filter((b) => b.isSearchable).map((b) => b.label);
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* Return the cached system prompt. If no cached prompt exists,
|
|
512
|
-
* loads blocks from providers, renders, and persists to the store.
|
|
513
|
-
* Subsequent calls return the stored value without re-rendering.
|
|
514
|
-
*/
|
|
515
|
-
async freezeSystemPrompt() {
|
|
516
|
-
if (this.promptStore) {
|
|
517
|
-
const stored = await this.promptStore.get();
|
|
518
|
-
if (stored !== null) return stored;
|
|
519
|
-
}
|
|
520
|
-
if (!this.loaded) await this.load();
|
|
521
|
-
const prompt = this.toSystemPrompt();
|
|
522
|
-
if (this.promptStore) await this.promptStore.set(prompt);
|
|
523
|
-
return prompt;
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* Return the prompt text used for token estimation without persisting a new
|
|
527
|
-
* frozen prompt to the prompt store.
|
|
528
|
-
*
|
|
529
|
-
* This still reads an existing cached prompt when present, so estimates match
|
|
530
|
-
* the prompt that inference would reuse. If no cached prompt exists, it loads
|
|
531
|
-
* providers and renders the current blocks without freezing the snapshot.
|
|
532
|
-
*/
|
|
533
|
-
async getSystemPromptForEstimate() {
|
|
534
|
-
if (this.snapshot !== null) return this.snapshot;
|
|
535
|
-
if (this.promptStore) {
|
|
536
|
-
const stored = await this.promptStore.get();
|
|
537
|
-
if (stored !== null) return stored;
|
|
538
|
-
}
|
|
539
|
-
if (!this.loaded) await this.load();
|
|
540
|
-
return this.renderPrompt();
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* Force reload blocks from providers, re-render the system prompt,
|
|
544
|
-
* and persist to the store. Use this after block content has changed
|
|
545
|
-
* or to invalidate the cached prompt.
|
|
546
|
-
*/
|
|
547
|
-
async refreshSystemPrompt() {
|
|
548
|
-
this.loaded = false;
|
|
549
|
-
await this.load();
|
|
550
|
-
const prompt = this.refreshSnapshot();
|
|
551
|
-
if (this.promptStore) await this.promptStore.set(prompt);
|
|
552
|
-
return prompt;
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* AI tools for context blocks.
|
|
556
|
-
*
|
|
557
|
-
* Auto-wired based on provider capabilities:
|
|
558
|
-
* - `set_context` — when any block is writable
|
|
559
|
-
* - `load_context` — when any block is a skill provider
|
|
560
|
-
* - `search_context` — when any block is a search provider
|
|
561
|
-
*/
|
|
562
|
-
async tools() {
|
|
563
|
-
if (!this.loaded) await this.load();
|
|
564
|
-
const writable = this.getWritableBlocks();
|
|
565
|
-
const hasSkills = this.hasSkillBlocks();
|
|
566
|
-
const hasSearch = this.hasSearchBlocks();
|
|
567
|
-
const toolSet = {};
|
|
568
|
-
if (writable.length > 0) {
|
|
569
|
-
const blockDescriptions = writable.map((b) => {
|
|
570
|
-
const kind = b.isSkill ? "skill collection, keyed entries" : b.isSearchable ? "searchable, keyed entries" : "writable";
|
|
571
|
-
return `- "${b.label}" (${kind}): ${b.description ?? "no description"}`;
|
|
572
|
-
});
|
|
573
|
-
const keyedBlocks = writable.filter((b) => b.isSkill || b.isSearchable);
|
|
574
|
-
const properties = {
|
|
575
|
-
label: {
|
|
576
|
-
type: "string",
|
|
577
|
-
enum: writable.map((b) => b.label),
|
|
578
|
-
description: "Block label to write to"
|
|
579
|
-
},
|
|
580
|
-
content: {
|
|
581
|
-
type: "string",
|
|
582
|
-
description: "The main content to write to the block."
|
|
583
|
-
},
|
|
584
|
-
action: {
|
|
585
|
-
type: "string",
|
|
586
|
-
enum: ["replace", "append"],
|
|
587
|
-
description: "replace (default) or append"
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
if (keyedBlocks.length > 0) properties.metadata = {
|
|
591
|
-
type: "object",
|
|
592
|
-
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.",
|
|
593
|
-
properties: {
|
|
594
|
-
title: {
|
|
595
|
-
type: "string",
|
|
596
|
-
description: "Short title. Used as a stable identifier — entries with the same title are updated in place, different titles create new entries."
|
|
597
|
-
},
|
|
598
|
-
description: {
|
|
599
|
-
type: "string",
|
|
600
|
-
description: "One-line summary shown alongside the title in the system prompt so the model can decide when to load the entry."
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
};
|
|
604
|
-
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." : "";
|
|
605
|
-
toolSet.set_context = {
|
|
606
|
-
description: `Write to a context block. Available blocks:\n${blockDescriptions.join("\n")}\n\nWrites are durable and persist across sessions.${metadataHint}`,
|
|
607
|
-
inputSchema: z.fromJSONSchema({
|
|
608
|
-
type: "object",
|
|
609
|
-
properties,
|
|
610
|
-
required: ["label", "content"]
|
|
611
|
-
}),
|
|
612
|
-
execute: async ({ label, content, metadata, action }) => {
|
|
613
|
-
try {
|
|
614
|
-
const block = this.blocks.get(label);
|
|
615
|
-
if (!block) return `Error: block "${label}" not found`;
|
|
616
|
-
if (block.isSkill || block.isSearchable) {
|
|
617
|
-
const title = metadata?.title;
|
|
618
|
-
const description = metadata?.description;
|
|
619
|
-
const key = contextEntryKey(title, content);
|
|
620
|
-
if (block.isSkill) await this.setSkill(label, key, content, description ?? title);
|
|
621
|
-
else await this.setSearchEntry(label, key, content);
|
|
622
|
-
return `Indexed "${key}" in ${label}.`;
|
|
623
|
-
}
|
|
624
|
-
const updated = action === "append" ? await this.appendToBlock(label, content) : await this.setBlock(label, content);
|
|
625
|
-
return `Written to ${label}. Usage: ${updated.maxTokens ? `${Math.round(updated.tokens / updated.maxTokens * 100)}% (${updated.tokens}/${updated.maxTokens} tokens)` : `${updated.tokens} tokens`}`;
|
|
626
|
-
} catch (err) {
|
|
627
|
-
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
if (hasSkills) {
|
|
633
|
-
const skillLabels = this.getSkillLabels();
|
|
634
|
-
toolSet.load_context = {
|
|
635
|
-
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.",
|
|
636
|
-
inputSchema: z.fromJSONSchema({
|
|
637
|
-
type: "object",
|
|
638
|
-
properties: {
|
|
639
|
-
label: {
|
|
640
|
-
type: "string",
|
|
641
|
-
enum: skillLabels,
|
|
642
|
-
description: "Skill block label"
|
|
643
|
-
},
|
|
644
|
-
key: {
|
|
645
|
-
type: "string",
|
|
646
|
-
description: "Skill key to load"
|
|
647
|
-
}
|
|
648
|
-
},
|
|
649
|
-
required: ["label", "key"]
|
|
650
|
-
}),
|
|
651
|
-
execute: async ({ label, key }) => {
|
|
652
|
-
try {
|
|
653
|
-
if (!skillLabels.includes(label)) return `Error: "${label}" is not a skill block. Skill blocks: ${skillLabels.join(", ")}`;
|
|
654
|
-
return await this.loadSkill(label, key) ?? `Not found: ${key}`;
|
|
655
|
-
} catch (err) {
|
|
656
|
-
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
};
|
|
660
|
-
const loadedList = [...this._loadedSkills];
|
|
661
|
-
toolSet.unload_context = {
|
|
662
|
-
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."),
|
|
663
|
-
inputSchema: z.fromJSONSchema({
|
|
664
|
-
type: "object",
|
|
665
|
-
properties: {
|
|
666
|
-
label: {
|
|
667
|
-
type: "string",
|
|
668
|
-
enum: skillLabels,
|
|
669
|
-
description: "Skill block label"
|
|
670
|
-
},
|
|
671
|
-
key: {
|
|
672
|
-
type: "string",
|
|
673
|
-
description: "Skill key to unload"
|
|
674
|
-
}
|
|
675
|
-
},
|
|
676
|
-
required: ["label", "key"]
|
|
677
|
-
}),
|
|
678
|
-
execute: async ({ label, key }) => {
|
|
679
|
-
if (!skillLabels.includes(label)) return `Error: "${label}" is not a skill block. Skill blocks: ${skillLabels.join(", ")}`;
|
|
680
|
-
if (!this.unloadSkill(label, key)) return `Skill "${key}" is not currently loaded in "${label}".`;
|
|
681
|
-
return `Unloaded "${key}" from ${label}. Context reclaimed.`;
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
if (hasSearch) {
|
|
686
|
-
const searchLabels = this.getSearchLabels();
|
|
687
|
-
toolSet.search_context = {
|
|
688
|
-
description: "Search for information in a searchable context block. ONLY these blocks are searchable: " + searchLabels.map((l) => `"${l}"`).join(", ") + ". Other blocks cannot be searched.",
|
|
689
|
-
inputSchema: z.fromJSONSchema({
|
|
690
|
-
type: "object",
|
|
691
|
-
properties: {
|
|
692
|
-
label: {
|
|
693
|
-
type: "string",
|
|
694
|
-
enum: searchLabels,
|
|
695
|
-
description: "Searchable block label"
|
|
696
|
-
},
|
|
697
|
-
query: {
|
|
698
|
-
type: "string",
|
|
699
|
-
description: "Search query"
|
|
700
|
-
}
|
|
701
|
-
},
|
|
702
|
-
required: ["label", "query"]
|
|
703
|
-
}),
|
|
704
|
-
execute: async ({ label, query }) => {
|
|
705
|
-
try {
|
|
706
|
-
if (!searchLabels.includes(label)) return `Error: "${label}" is not searchable. Searchable blocks: ${searchLabels.join(", ")}`;
|
|
707
|
-
return await this.searchContext(label, query) ?? "No results found.";
|
|
708
|
-
} catch (err) {
|
|
709
|
-
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
return toolSet;
|
|
715
|
-
}
|
|
716
|
-
};
|
|
717
|
-
//#endregion
|
|
718
|
-
//#region src/experimental/memory/session/providers/agent.ts
|
|
719
|
-
/**
|
|
720
|
-
* Bounds for each content-hydration query on a history path.
|
|
721
|
-
*
|
|
722
|
-
* Message rows can be up to ~1.8MB each (see ROW_MAX_BYTES in agents/chat),
|
|
723
|
-
* so content is fetched in bounded batches rather than one statement to keep
|
|
724
|
-
* any per-statement buffering in the SQLite layer small. In workerd the
|
|
725
|
-
* SQLite allocator shares the isolate's memory budget with the JS heap —
|
|
726
|
-
* oversized transient result sets surface as SQLITE_NOMEM (#1710).
|
|
727
|
-
*
|
|
728
|
-
* Chunks are bounded by BOTH row count and cumulative stored bytes (sizes
|
|
729
|
-
* come from the path row stats, so no content is read to compute them).
|
|
730
|
-
* Without the byte bound, 50 near-cap rows could still materialize ~90MB
|
|
731
|
-
* in a single statement.
|
|
732
|
-
*/
|
|
733
|
-
const HISTORY_CONTENT_CHUNK_SIZE = 50;
|
|
734
|
-
const HISTORY_CONTENT_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
735
|
-
var AgentSessionProvider = class {
|
|
736
|
-
/**
|
|
737
|
-
* @param agent - Agent or any object with a `sql` tagged template method
|
|
738
|
-
* @param sessionId - Optional session ID to isolate multiple sessions in the same DO.
|
|
739
|
-
* Messages are filtered by session_id within shared tables.
|
|
740
|
-
*/
|
|
741
|
-
constructor(agent, sessionId) {
|
|
742
|
-
this.initialized = false;
|
|
743
|
-
this.activeLeafId = void 0;
|
|
744
|
-
this.agent = agent;
|
|
745
|
-
this.sessionId = sessionId ?? "";
|
|
746
|
-
}
|
|
747
|
-
ensureTable() {
|
|
748
|
-
if (this.initialized) return;
|
|
749
|
-
this.agent.sql`
|
|
750
|
-
CREATE TABLE IF NOT EXISTS assistant_messages (
|
|
751
|
-
id TEXT PRIMARY KEY,
|
|
752
|
-
session_id TEXT NOT NULL DEFAULT '',
|
|
753
|
-
parent_id TEXT,
|
|
754
|
-
role TEXT NOT NULL,
|
|
755
|
-
content TEXT NOT NULL,
|
|
756
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
757
|
-
)
|
|
758
|
-
`;
|
|
759
|
-
this.agent.sql`
|
|
760
|
-
CREATE INDEX IF NOT EXISTS idx_assistant_msg_parent
|
|
761
|
-
ON assistant_messages(parent_id)
|
|
762
|
-
`;
|
|
763
|
-
this.agent.sql`
|
|
764
|
-
CREATE INDEX IF NOT EXISTS idx_assistant_msg_session
|
|
765
|
-
ON assistant_messages(session_id)
|
|
766
|
-
`;
|
|
767
|
-
this.agent.sql`
|
|
768
|
-
CREATE TABLE IF NOT EXISTS assistant_compactions (
|
|
769
|
-
id TEXT PRIMARY KEY,
|
|
770
|
-
session_id TEXT NOT NULL DEFAULT '',
|
|
771
|
-
summary TEXT NOT NULL,
|
|
772
|
-
from_message_id TEXT NOT NULL,
|
|
773
|
-
to_message_id TEXT NOT NULL,
|
|
774
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
775
|
-
)
|
|
776
|
-
`;
|
|
777
|
-
this.agent.sql`
|
|
778
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS assistant_fts
|
|
779
|
-
USING fts5(id UNINDEXED, session_id UNINDEXED, role UNINDEXED, content, tokenize='porter unicode61')
|
|
780
|
-
`;
|
|
781
|
-
this.agent.sql`
|
|
782
|
-
CREATE TABLE IF NOT EXISTS assistant_config (
|
|
783
|
-
session_id TEXT NOT NULL,
|
|
784
|
-
key TEXT NOT NULL,
|
|
785
|
-
value TEXT NOT NULL,
|
|
786
|
-
PRIMARY KEY (session_id, key)
|
|
787
|
-
)
|
|
788
|
-
`;
|
|
789
|
-
this.initialized = true;
|
|
790
|
-
}
|
|
791
|
-
getMessage(id) {
|
|
792
|
-
this.ensureTable();
|
|
793
|
-
const rows = this.agent.sql`
|
|
794
|
-
SELECT content FROM assistant_messages WHERE id = ${id} AND session_id = ${this.sessionId}
|
|
795
|
-
`;
|
|
796
|
-
return rows.length > 0 ? this.parse(rows[0].content) : null;
|
|
797
|
-
}
|
|
798
|
-
getHistory(leafId) {
|
|
799
|
-
this.ensureTable();
|
|
800
|
-
const leaf = leafId ? this.leafRowById(leafId) : this.latestLeafRow();
|
|
801
|
-
if (!leaf) return [];
|
|
802
|
-
const messages = this.messagesByPathStats(this.pathRowStats(leaf.id));
|
|
803
|
-
const compactions = this.getCompactions();
|
|
804
|
-
if (compactions.length === 0) return messages;
|
|
805
|
-
return this.applyCompactions(messages, compactions);
|
|
806
|
-
}
|
|
807
|
-
getRecentHistory(leafId, maxContentBytes, minRecentMessages = 1) {
|
|
808
|
-
this.ensureTable();
|
|
809
|
-
const leaf = leafId ? this.leafRowById(leafId) : this.latestLeafRow();
|
|
810
|
-
if (!leaf) return {
|
|
811
|
-
messages: [],
|
|
812
|
-
truncated: false,
|
|
813
|
-
totalContentBytes: 0
|
|
814
|
-
};
|
|
815
|
-
const stats = this.pathRowStats(leaf.id);
|
|
816
|
-
const totalContentBytes = stats.reduce((sum, row) => sum + row.bytes, 0);
|
|
817
|
-
const minRecent = Math.max(1, Math.floor(minRecentMessages));
|
|
818
|
-
let start = stats.length - 1;
|
|
819
|
-
let used = stats[start]?.bytes ?? 0;
|
|
820
|
-
while (start > 0 && (stats.length - start < minRecent || used + stats[start - 1].bytes <= maxContentBytes)) {
|
|
821
|
-
start--;
|
|
822
|
-
used += stats[start].bytes;
|
|
823
|
-
}
|
|
824
|
-
const messages = this.messagesByPathStats(stats.slice(start));
|
|
825
|
-
const compactions = this.getCompactions();
|
|
826
|
-
return {
|
|
827
|
-
messages: compactions.length === 0 ? messages : this.applyCompactions(messages, compactions),
|
|
828
|
-
truncated: start > 0,
|
|
829
|
-
totalContentBytes
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
getHistoryRowStats(leafId) {
|
|
833
|
-
this.ensureTable();
|
|
834
|
-
const leaf = leafId ? this.leafRowById(leafId) : this.latestLeafRow();
|
|
835
|
-
return leaf ? this.pathRowStats(leaf.id) : [];
|
|
836
|
-
}
|
|
837
|
-
getLatestLeaf() {
|
|
838
|
-
this.ensureTable();
|
|
839
|
-
const row = this.latestLeafRow();
|
|
840
|
-
return row ? this.getMessage(row.id) : null;
|
|
841
|
-
}
|
|
842
|
-
getBranches(messageId) {
|
|
843
|
-
this.ensureTable();
|
|
844
|
-
const rows = this.agent.sql`
|
|
845
|
-
SELECT content FROM assistant_messages
|
|
846
|
-
WHERE parent_id = ${messageId} AND session_id = ${this.sessionId} ORDER BY created_at ASC
|
|
847
|
-
`;
|
|
848
|
-
return this.parseRows(rows);
|
|
849
|
-
}
|
|
850
|
-
getPathLength(leafId) {
|
|
851
|
-
this.ensureTable();
|
|
852
|
-
const leaf = leafId ? this.agent.sql`
|
|
853
|
-
SELECT id FROM assistant_messages WHERE id = ${leafId} AND session_id = ${this.sessionId}
|
|
854
|
-
`[0] : this.latestLeafRow();
|
|
855
|
-
if (!leaf) return 0;
|
|
856
|
-
return this.agent.sql`
|
|
857
|
-
WITH RECURSIVE path AS (
|
|
858
|
-
SELECT id, parent_id, 0 as depth FROM assistant_messages WHERE id = ${leaf.id}
|
|
859
|
-
UNION ALL
|
|
860
|
-
SELECT m.id, m.parent_id, p.depth + 1 FROM assistant_messages m
|
|
861
|
-
JOIN path p ON m.id = p.parent_id
|
|
862
|
-
WHERE m.session_id = ${this.sessionId} AND p.depth < 10000
|
|
863
|
-
)
|
|
864
|
-
SELECT COUNT(*) as count FROM path
|
|
865
|
-
`[0]?.count ?? 0;
|
|
866
|
-
}
|
|
867
|
-
appendMessage(message, parentId) {
|
|
868
|
-
this.ensureTable();
|
|
869
|
-
if (this.agent.sql`
|
|
870
|
-
SELECT id FROM assistant_messages WHERE id = ${message.id} AND session_id = ${this.sessionId}
|
|
871
|
-
`.length > 0) return;
|
|
872
|
-
let parent = parentId !== void 0 ? parentId : this.latestLeafRow()?.id ?? null;
|
|
873
|
-
if (parent) {
|
|
874
|
-
if (this.agent.sql`
|
|
875
|
-
SELECT id FROM assistant_messages WHERE id = ${parent} AND session_id = ${this.sessionId}
|
|
876
|
-
`.length === 0) parent = null;
|
|
877
|
-
}
|
|
878
|
-
const json = JSON.stringify(message);
|
|
879
|
-
this.agent.sql`
|
|
880
|
-
INSERT INTO assistant_messages (id, session_id, parent_id, role, content)
|
|
881
|
-
VALUES (${message.id}, ${this.sessionId}, ${parent}, ${message.role}, ${json})
|
|
882
|
-
`;
|
|
883
|
-
this.indexFTS(message);
|
|
884
|
-
this.activeLeafId = message.id;
|
|
885
|
-
}
|
|
886
|
-
updateMessage(message) {
|
|
887
|
-
this.ensureTable();
|
|
888
|
-
this.agent.sql`
|
|
889
|
-
UPDATE assistant_messages SET content = ${JSON.stringify(message)}
|
|
890
|
-
WHERE id = ${message.id} AND session_id = ${this.sessionId}
|
|
891
|
-
`;
|
|
892
|
-
this.indexFTS(message);
|
|
893
|
-
}
|
|
894
|
-
deleteMessages(messageIds) {
|
|
895
|
-
this.ensureTable();
|
|
896
|
-
for (const id of messageIds) {
|
|
897
|
-
this.agent.sql`DELETE FROM assistant_messages WHERE id = ${id} AND session_id = ${this.sessionId}`;
|
|
898
|
-
this.deleteFTS(id);
|
|
899
|
-
}
|
|
900
|
-
if (typeof this.activeLeafId === "string" && messageIds.includes(this.activeLeafId)) this.activeLeafId = void 0;
|
|
901
|
-
}
|
|
902
|
-
clearMessages() {
|
|
903
|
-
this.ensureTable();
|
|
904
|
-
this.agent.sql`DELETE FROM assistant_messages WHERE session_id = ${this.sessionId}`;
|
|
905
|
-
this.agent.sql`DELETE FROM assistant_compactions WHERE session_id = ${this.sessionId}`;
|
|
906
|
-
const ftsRows = this.agent.sql`
|
|
907
|
-
SELECT rowid FROM assistant_fts WHERE session_id = ${this.sessionId}
|
|
908
|
-
`;
|
|
909
|
-
for (const row of ftsRows) this.agent.sql`DELETE FROM assistant_fts WHERE rowid = ${row.rowid}`;
|
|
910
|
-
this.activeLeafId = void 0;
|
|
911
|
-
}
|
|
912
|
-
addCompaction(summary, fromMessageId, toMessageId) {
|
|
913
|
-
this.ensureTable();
|
|
914
|
-
const id = crypto.randomUUID();
|
|
915
|
-
this.agent.sql`
|
|
916
|
-
INSERT INTO assistant_compactions (id, session_id, summary, from_message_id, to_message_id)
|
|
917
|
-
VALUES (${id}, ${this.sessionId}, ${summary}, ${fromMessageId}, ${toMessageId})
|
|
918
|
-
`;
|
|
919
|
-
return {
|
|
920
|
-
id,
|
|
921
|
-
summary,
|
|
922
|
-
fromMessageId,
|
|
923
|
-
toMessageId,
|
|
924
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
getCompactions() {
|
|
928
|
-
this.ensureTable();
|
|
929
|
-
return this.agent.sql`
|
|
930
|
-
SELECT * FROM assistant_compactions WHERE session_id = ${this.sessionId} ORDER BY created_at ASC
|
|
931
|
-
`.map((r) => ({
|
|
932
|
-
id: r.id,
|
|
933
|
-
summary: r.summary,
|
|
934
|
-
fromMessageId: r.from_message_id,
|
|
935
|
-
toMessageId: r.to_message_id,
|
|
936
|
-
createdAt: r.created_at
|
|
937
|
-
}));
|
|
938
|
-
}
|
|
939
|
-
searchMessages(query, limit = 20) {
|
|
940
|
-
this.ensureTable();
|
|
941
|
-
const sanitized = `"${query.replace(/"/g, "\"\"")}"`;
|
|
942
|
-
try {
|
|
943
|
-
return this.agent.sql`
|
|
944
|
-
SELECT f.id, f.role, f.content FROM assistant_fts f
|
|
945
|
-
INNER JOIN assistant_messages m ON m.id = f.id AND m.session_id = f.session_id
|
|
946
|
-
WHERE assistant_fts MATCH ${sanitized} AND f.session_id = ${this.sessionId}
|
|
947
|
-
ORDER BY rank LIMIT ${limit}
|
|
948
|
-
`.map((r) => ({
|
|
949
|
-
id: r.id,
|
|
950
|
-
role: r.role,
|
|
951
|
-
content: r.content
|
|
952
|
-
}));
|
|
953
|
-
} catch {
|
|
954
|
-
return [];
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
latestLeafRow() {
|
|
958
|
-
if (this.activeLeafId !== void 0) {
|
|
959
|
-
if (this.agent.sql`
|
|
960
|
-
SELECT m.id FROM assistant_messages m
|
|
961
|
-
WHERE m.id = ${this.activeLeafId} AND m.session_id = ${this.sessionId}
|
|
962
|
-
AND NOT EXISTS (
|
|
963
|
-
SELECT 1 FROM assistant_messages c
|
|
964
|
-
WHERE c.parent_id = ${this.activeLeafId}
|
|
965
|
-
AND c.session_id = ${this.sessionId}
|
|
966
|
-
)
|
|
967
|
-
`.length > 0) return { id: this.activeLeafId };
|
|
968
|
-
this.activeLeafId = void 0;
|
|
969
|
-
}
|
|
970
|
-
const rows = this.agent.sql`
|
|
971
|
-
SELECT m.id FROM assistant_messages m
|
|
972
|
-
LEFT JOIN assistant_messages c ON c.parent_id = m.id AND c.session_id = ${this.sessionId}
|
|
973
|
-
WHERE c.id IS NULL AND m.session_id = ${this.sessionId}
|
|
974
|
-
ORDER BY m.created_at DESC, m.rowid DESC LIMIT 1
|
|
975
|
-
`;
|
|
976
|
-
this.activeLeafId = rows[0]?.id;
|
|
977
|
-
return rows[0] ?? null;
|
|
978
|
-
}
|
|
979
|
-
leafRowById(leafId) {
|
|
980
|
-
return this.agent.sql`
|
|
981
|
-
SELECT id FROM assistant_messages WHERE id = ${leafId} AND session_id = ${this.sessionId}
|
|
982
|
-
`[0] ?? null;
|
|
983
|
-
}
|
|
984
|
-
/**
|
|
985
|
-
* The active branch path as (id, role, content size) rows, root → leaf.
|
|
986
|
-
*
|
|
987
|
-
* Recurses over (id, parent_id) only. Carrying `content` through the
|
|
988
|
-
* recursive queue AND the ORDER BY sorter materializes the entire
|
|
989
|
-
* transcript several times over inside SQLite's allocator, which in
|
|
990
|
-
* workerd shares the isolate's memory budget with the JS heap — large
|
|
991
|
-
* media-heavy sessions then fail with SQLITE_NOMEM on wake (#1710).
|
|
992
|
-
* Content is fetched separately in bounded chunks (`messagesByPathStats`).
|
|
993
|
-
*/
|
|
994
|
-
pathRowStats(leafId) {
|
|
995
|
-
return this.agent.sql`
|
|
996
|
-
WITH RECURSIVE path(id, parent_id, depth) AS (
|
|
997
|
-
SELECT id, parent_id, 0 FROM assistant_messages WHERE id = ${leafId}
|
|
998
|
-
UNION ALL
|
|
999
|
-
SELECT m.id, m.parent_id, p.depth + 1 FROM assistant_messages m
|
|
1000
|
-
JOIN path p ON m.id = p.parent_id
|
|
1001
|
-
WHERE m.session_id = ${this.sessionId} AND p.depth < 10000
|
|
1002
|
-
)
|
|
1003
|
-
SELECT path.id AS id, am.role AS role, LENGTH(CAST(am.content AS BLOB)) AS bytes
|
|
1004
|
-
FROM path JOIN assistant_messages am ON am.id = path.id
|
|
1005
|
-
ORDER BY path.depth DESC
|
|
1006
|
-
`;
|
|
1007
|
-
}
|
|
1008
|
-
/**
|
|
1009
|
-
* Fetch and parse message content for an ordered list of path rows.
|
|
1010
|
-
*
|
|
1011
|
-
* Content is read in chunks bounded by both row count and cumulative
|
|
1012
|
-
* stored bytes (no ORDER BY — SQLite streams rows without materializing
|
|
1013
|
-
* the result set) and reassembled in path order. Rows that fail to parse
|
|
1014
|
-
* are skipped, matching previous behavior.
|
|
1015
|
-
*/
|
|
1016
|
-
messagesByPathStats(rows) {
|
|
1017
|
-
const contentById = /* @__PURE__ */ new Map();
|
|
1018
|
-
const fetchChunk = (ids) => {
|
|
1019
|
-
const fetched = this.agent.sql`
|
|
1020
|
-
SELECT id, content FROM assistant_messages
|
|
1021
|
-
WHERE session_id = ${this.sessionId}
|
|
1022
|
-
AND id IN (SELECT value FROM json_each(${JSON.stringify(ids)}))
|
|
1023
|
-
`;
|
|
1024
|
-
for (const row of fetched) contentById.set(row.id, row.content);
|
|
1025
|
-
};
|
|
1026
|
-
let chunk = [];
|
|
1027
|
-
let chunkBytes = 0;
|
|
1028
|
-
for (const row of rows) {
|
|
1029
|
-
if (chunk.length > 0 && (chunk.length >= HISTORY_CONTENT_CHUNK_SIZE || chunkBytes + row.bytes > HISTORY_CONTENT_CHUNK_BYTES)) {
|
|
1030
|
-
fetchChunk(chunk);
|
|
1031
|
-
chunk = [];
|
|
1032
|
-
chunkBytes = 0;
|
|
1033
|
-
}
|
|
1034
|
-
chunk.push(row.id);
|
|
1035
|
-
chunkBytes += row.bytes;
|
|
1036
|
-
}
|
|
1037
|
-
if (chunk.length > 0) fetchChunk(chunk);
|
|
1038
|
-
const result = [];
|
|
1039
|
-
for (const row of rows) {
|
|
1040
|
-
const content = contentById.get(row.id);
|
|
1041
|
-
if (content === void 0) continue;
|
|
1042
|
-
const msg = this.parse(content);
|
|
1043
|
-
if (msg) result.push(msg);
|
|
1044
|
-
}
|
|
1045
|
-
return result;
|
|
1046
|
-
}
|
|
1047
|
-
indexFTS(message) {
|
|
1048
|
-
const text = message.parts.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
1049
|
-
this.deleteFTS(message.id);
|
|
1050
|
-
if (text) this.agent.sql`
|
|
1051
|
-
INSERT INTO assistant_fts (id, session_id, role, content)
|
|
1052
|
-
VALUES (${message.id}, ${this.sessionId}, ${message.role}, ${text})
|
|
1053
|
-
`;
|
|
1054
|
-
}
|
|
1055
|
-
deleteFTS(id) {
|
|
1056
|
-
const rows = this.agent.sql`
|
|
1057
|
-
SELECT rowid FROM assistant_fts WHERE id = ${id} AND session_id = ${this.sessionId}
|
|
1058
|
-
`;
|
|
1059
|
-
for (const row of rows) this.agent.sql`DELETE FROM assistant_fts WHERE rowid = ${row.rowid}`;
|
|
1060
|
-
}
|
|
1061
|
-
applyCompactions(messages, compactions) {
|
|
1062
|
-
const ids = messages.map((m) => m.id);
|
|
1063
|
-
const result = [];
|
|
1064
|
-
let i = 0;
|
|
1065
|
-
while (i < messages.length) {
|
|
1066
|
-
const matching = compactions.filter((c) => c.fromMessageId === ids[i]);
|
|
1067
|
-
const comp = matching.length > 1 ? matching[matching.length - 1] : matching[0];
|
|
1068
|
-
if (comp) {
|
|
1069
|
-
const endIdx = ids.indexOf(comp.toMessageId);
|
|
1070
|
-
if (endIdx >= i) {
|
|
1071
|
-
result.push({
|
|
1072
|
-
id: `${COMPACTION_PREFIX}${comp.id}`,
|
|
1073
|
-
role: "assistant",
|
|
1074
|
-
parts: [{
|
|
1075
|
-
type: "text",
|
|
1076
|
-
text: comp.summary
|
|
1077
|
-
}],
|
|
1078
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
1079
|
-
});
|
|
1080
|
-
i = endIdx + 1;
|
|
1081
|
-
continue;
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
result.push(messages[i]);
|
|
1085
|
-
i++;
|
|
1086
|
-
}
|
|
1087
|
-
return result;
|
|
1088
|
-
}
|
|
1089
|
-
parse(json) {
|
|
1090
|
-
try {
|
|
1091
|
-
const msg = JSON.parse(json);
|
|
1092
|
-
if (typeof msg?.id === "string" && typeof msg?.role === "string" && Array.isArray(msg?.parts)) return msg;
|
|
1093
|
-
} catch {}
|
|
1094
|
-
return null;
|
|
1095
|
-
}
|
|
1096
|
-
parseRows(rows) {
|
|
1097
|
-
const result = [];
|
|
1098
|
-
for (const row of rows) {
|
|
1099
|
-
const msg = this.parse(row.content);
|
|
1100
|
-
if (msg) result.push(msg);
|
|
1101
|
-
}
|
|
1102
|
-
return result;
|
|
1103
|
-
}
|
|
1104
|
-
};
|
|
1105
|
-
//#endregion
|
|
1106
|
-
//#region src/experimental/memory/session/providers/agent-context.ts
|
|
1107
|
-
var AgentContextProvider = class {
|
|
1108
|
-
constructor(agent, label) {
|
|
1109
|
-
this.initialized = false;
|
|
1110
|
-
this.agent = agent;
|
|
1111
|
-
this.label = label ?? "";
|
|
1112
|
-
}
|
|
1113
|
-
init(label) {
|
|
1114
|
-
if (!this.label) this.label = label;
|
|
1115
|
-
}
|
|
1116
|
-
ensureTable() {
|
|
1117
|
-
if (this.initialized) return;
|
|
1118
|
-
this.agent.sql`
|
|
1119
|
-
CREATE TABLE IF NOT EXISTS cf_agents_context_blocks (
|
|
1120
|
-
label TEXT PRIMARY KEY,
|
|
1121
|
-
content TEXT NOT NULL,
|
|
1122
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
1123
|
-
)
|
|
1124
|
-
`;
|
|
1125
|
-
this.initialized = true;
|
|
1126
|
-
}
|
|
1127
|
-
async get() {
|
|
1128
|
-
this.ensureTable();
|
|
1129
|
-
return this.agent.sql`
|
|
1130
|
-
SELECT content FROM cf_agents_context_blocks WHERE label = ${this.label}
|
|
1131
|
-
`[0]?.content ?? null;
|
|
1132
|
-
}
|
|
1133
|
-
async set(content) {
|
|
1134
|
-
this.ensureTable();
|
|
1135
|
-
this.agent.sql`
|
|
1136
|
-
INSERT INTO cf_agents_context_blocks (label, content)
|
|
1137
|
-
VALUES (${this.label}, ${content})
|
|
1138
|
-
ON CONFLICT(label) DO UPDATE SET content = ${content}, updated_at = CURRENT_TIMESTAMP
|
|
1139
|
-
`;
|
|
1140
|
-
}
|
|
1141
|
-
};
|
|
1142
|
-
//#endregion
|
|
1143
|
-
//#region src/experimental/memory/session/session.ts
|
|
1144
|
-
function isBroadcaster(obj) {
|
|
1145
|
-
return typeof obj === "object" && obj !== null && "broadcast" in obj && typeof obj.broadcast === "function";
|
|
1146
|
-
}
|
|
1147
|
-
function isSqlProvider(arg) {
|
|
1148
|
-
return "sql" in arg && typeof arg.sql === "function";
|
|
1149
|
-
}
|
|
1150
|
-
var Session = class Session {
|
|
1151
|
-
constructor(storage, options) {
|
|
1152
|
-
this._warnedCompactionNoOp = false;
|
|
1153
|
-
this._ready = false;
|
|
1154
|
-
this._skillScanRan = false;
|
|
1155
|
-
this._warnedNoRecentHistorySupport = false;
|
|
1156
|
-
this.storage = storage;
|
|
1157
|
-
this.context = new ContextBlocks(options?.context ?? [], options?.promptStore);
|
|
1158
|
-
this._tokenCounter = options?.tokenCounter;
|
|
1159
|
-
this._compactionErrorHandler = options?.onCompactionError;
|
|
1160
|
-
this._ready = true;
|
|
1161
|
-
}
|
|
1162
|
-
/**
|
|
1163
|
-
* Chainable session creation with auto-wired providers.
|
|
1164
|
-
*
|
|
1165
|
-
* Pass a `SqlProvider` (Agent with `sql` method) for auto-wired SQLite,
|
|
1166
|
-
* or a `SessionProvider` directly for custom storage (Postgres, etc.).
|
|
1167
|
-
*
|
|
1168
|
-
* @example
|
|
1169
|
-
* ```ts
|
|
1170
|
-
* // Auto-wired SQLite (DO Agent)
|
|
1171
|
-
* const session = Session.create(this)
|
|
1172
|
-
* .withContext("soul", { provider: { get: async () => "You are helpful." } })
|
|
1173
|
-
* .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
|
|
1174
|
-
* .withCachedPrompt();
|
|
1175
|
-
*
|
|
1176
|
-
* // Skills from R2 (on-demand loading via load_context tool)
|
|
1177
|
-
* const session = Session.create(this)
|
|
1178
|
-
* .withContext("skills", {
|
|
1179
|
-
* provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" })
|
|
1180
|
-
* })
|
|
1181
|
-
* .withCachedPrompt();
|
|
1182
|
-
*
|
|
1183
|
-
* // Custom storage provider (Postgres, etc.)
|
|
1184
|
-
* const session = Session.create(postgresProvider)
|
|
1185
|
-
* .withContext("memory", {
|
|
1186
|
-
* maxTokens: 1100,
|
|
1187
|
-
* provider: new PostgresContextProvider(conn, "memory")
|
|
1188
|
-
* })
|
|
1189
|
-
* .withCachedPrompt(new PostgresContextProvider(conn, "_prompt"));
|
|
1190
|
-
* ```
|
|
1191
|
-
*/
|
|
1192
|
-
static create(provider) {
|
|
1193
|
-
const session = Object.create(Session.prototype);
|
|
1194
|
-
if (isSqlProvider(provider)) {
|
|
1195
|
-
session._agent = provider;
|
|
1196
|
-
if (isBroadcaster(provider)) session._broadcaster = provider;
|
|
1197
|
-
} else session._storageProvider = provider;
|
|
1198
|
-
session._pending = [];
|
|
1199
|
-
session._ready = false;
|
|
1200
|
-
return session;
|
|
1201
|
-
}
|
|
1202
|
-
forSession(sessionId) {
|
|
1203
|
-
this._sessionId = sessionId;
|
|
1204
|
-
return this;
|
|
1205
|
-
}
|
|
1206
|
-
withContext(label, options) {
|
|
1207
|
-
this._pending.push({
|
|
1208
|
-
label,
|
|
1209
|
-
options: options ?? {}
|
|
1210
|
-
});
|
|
1211
|
-
return this;
|
|
1212
|
-
}
|
|
1213
|
-
withCachedPrompt(provider) {
|
|
1214
|
-
this._cachedPrompt = provider ?? true;
|
|
1215
|
-
return this;
|
|
1216
|
-
}
|
|
1217
|
-
/**
|
|
1218
|
-
* Register a compaction function. Called by `compact()` to compress
|
|
1219
|
-
* message history into a summary overlay.
|
|
1220
|
-
*/
|
|
1221
|
-
onCompaction(fn) {
|
|
1222
|
-
this._compactionFn = fn;
|
|
1223
|
-
return this;
|
|
1224
|
-
}
|
|
1225
|
-
/**
|
|
1226
|
-
* Auto-compact when estimated token count exceeds the threshold.
|
|
1227
|
-
* Checked after each `appendMessage`. Requires `onCompaction()`.
|
|
1228
|
-
*
|
|
1229
|
-
* By default this uses a Workers-safe heuristic over stored messages plus
|
|
1230
|
-
* the Session-managed frozen system prompt. Provide `tokenCounter` when you
|
|
1231
|
-
* have model-reported usage or a tokenizer and need a stricter budget.
|
|
1232
|
-
*/
|
|
1233
|
-
compactAfter(tokenThreshold, options) {
|
|
1234
|
-
this._tokenThreshold = tokenThreshold;
|
|
1235
|
-
if (options?.tokenCounter) this._tokenCounter = options.tokenCounter;
|
|
1236
|
-
return this;
|
|
1237
|
-
}
|
|
1238
|
-
/**
|
|
1239
|
-
* Handle failures from the automatic `compactAfter()` trigger.
|
|
1240
|
-
*
|
|
1241
|
-
* Manual `compact()` still reports errors through the existing session error
|
|
1242
|
-
* broadcast path.
|
|
1243
|
-
*/
|
|
1244
|
-
onCompactionError(handler) {
|
|
1245
|
-
this._compactionErrorHandler = handler;
|
|
1246
|
-
return this;
|
|
1247
|
-
}
|
|
1248
|
-
/**
|
|
1249
|
-
* @internal
|
|
1250
|
-
* Framework hook for cache-owning callers that need to mirror message
|
|
1251
|
-
* storage changes. Application code should use the normal Session methods.
|
|
1252
|
-
*/
|
|
1253
|
-
internal_onMessagesChanged(listener) {
|
|
1254
|
-
this._messageChangeListener = listener ?? void 0;
|
|
1255
|
-
return this;
|
|
1256
|
-
}
|
|
1257
|
-
_ensureReady() {
|
|
1258
|
-
if (this._ready) return;
|
|
1259
|
-
const configs = (this._pending ?? []).map(({ label, options: opts }) => {
|
|
1260
|
-
let provider = opts.provider;
|
|
1261
|
-
if (!provider && this._agent) {
|
|
1262
|
-
const key = this._sessionId ? `${label}_${this._sessionId}` : label;
|
|
1263
|
-
provider = new AgentContextProvider(this._agent, key);
|
|
1264
|
-
}
|
|
1265
|
-
return {
|
|
1266
|
-
label,
|
|
1267
|
-
description: opts.description,
|
|
1268
|
-
maxTokens: opts.maxTokens,
|
|
1269
|
-
provider
|
|
1270
|
-
};
|
|
1271
|
-
});
|
|
1272
|
-
let promptStore;
|
|
1273
|
-
if (this._cachedPrompt === true && this._agent) {
|
|
1274
|
-
const key = this._sessionId ? `_system_prompt_${this._sessionId}` : "_system_prompt";
|
|
1275
|
-
promptStore = new AgentContextProvider(this._agent, key);
|
|
1276
|
-
} else if (this._cachedPrompt && this._cachedPrompt !== true) promptStore = this._cachedPrompt;
|
|
1277
|
-
if (this._storageProvider) this.storage = this._storageProvider;
|
|
1278
|
-
else if (this._agent) this.storage = new AgentSessionProvider(this._agent, this._sessionId);
|
|
1279
|
-
else throw new Error("Session.create() requires a SqlProvider or SessionProvider");
|
|
1280
|
-
this.context = new ContextBlocks(configs, promptStore);
|
|
1281
|
-
this.context.setUnloadCallback((label, key) => {
|
|
1282
|
-
this._reclaimLoadedSkill(label, key).catch(() => {});
|
|
1283
|
-
});
|
|
1284
|
-
this._ready = true;
|
|
1285
|
-
this._restorePromise = this._restoreLoadedSkills().catch(() => {});
|
|
1286
|
-
}
|
|
1287
|
-
/**
|
|
1288
|
-
* Await the background skill-restore kicked off by `_ensureReady()`.
|
|
1289
|
-
* Idempotent and cheap — every async public method calls this so that
|
|
1290
|
-
* `_loadedSkills` reflects conversation history before any read or write.
|
|
1291
|
-
*/
|
|
1292
|
-
async _ensureRestored() {
|
|
1293
|
-
this._ensureReady();
|
|
1294
|
-
if (this._restorePromise) await this._restorePromise;
|
|
1295
|
-
}
|
|
1296
|
-
async _notifyMessagesChanged(event) {
|
|
1297
|
-
await this._messageChangeListener?.(event);
|
|
1298
|
-
}
|
|
1299
|
-
/**
|
|
1300
|
-
* Reconstruct which skills are loaded by scanning conversation history
|
|
1301
|
-
* for load_context tool results that haven't been unloaded.
|
|
1302
|
-
* Runs once per init to survive hibernation / eviction, including for
|
|
1303
|
-
* async SessionProviders (e.g. Postgres) where we must `await` history.
|
|
1304
|
-
*
|
|
1305
|
-
* Skipped entirely when no skill-capable provider is configured —
|
|
1306
|
-
* `load_context` results can only exist when a skill block was registered,
|
|
1307
|
-
* and the scan would otherwise read the whole transcript on every wake,
|
|
1308
|
-
* bypassing byte-budgeted hydration (#1710). A skill block added later via
|
|
1309
|
-
* `addContext()` triggers the scan at that point instead.
|
|
1310
|
-
*/
|
|
1311
|
-
async _restoreLoadedSkills() {
|
|
1312
|
-
if (!this.context.hasSkillCapableConfigs()) return;
|
|
1313
|
-
await this._scanHistoryForLoadedSkills();
|
|
1314
|
-
}
|
|
1315
|
-
/**
|
|
1316
|
-
* Scan stored history for load/unload_context tool results and restore
|
|
1317
|
-
* the loaded-skill tracking set.
|
|
1318
|
-
*
|
|
1319
|
-
* Memory-bounded when the provider supports `getHistoryRowStats`: rows
|
|
1320
|
-
* are enumerated without content, then only assistant rows are fetched
|
|
1321
|
-
* and scanned ONE AT A TIME — peak memory is a single message instead of
|
|
1322
|
-
* the whole transcript. Falls back to a full `getHistory()` read for
|
|
1323
|
-
* providers without row stats (e.g. Postgres).
|
|
1324
|
-
*
|
|
1325
|
-
* Note: the bounded path scans raw path rows, so `load_context` results
|
|
1326
|
-
* inside compacted ranges are still seen (the full-read path hides them
|
|
1327
|
-
* behind compaction overlays). That superset is intentional — the stored
|
|
1328
|
-
* tool result still exists and can be reclaimed by `unloadSkill`.
|
|
1329
|
-
*/
|
|
1330
|
-
async _scanHistoryForLoadedSkills() {
|
|
1331
|
-
this._skillScanRan = true;
|
|
1332
|
-
const loaded = /* @__PURE__ */ new Set();
|
|
1333
|
-
const scanMessage = (msg) => {
|
|
1334
|
-
if (msg.role !== "assistant") return;
|
|
1335
|
-
for (const part of msg.parts) if (part.toolName === "load_context" && part.state === "output-available") {
|
|
1336
|
-
const input = part.input;
|
|
1337
|
-
if (input?.label && input?.key) {
|
|
1338
|
-
const id = `${input.label}:${input.key}`;
|
|
1339
|
-
if (typeof part.output === "string" && part.output.startsWith("[skill unloaded:")) loaded.delete(id);
|
|
1340
|
-
else loaded.add(id);
|
|
1341
|
-
}
|
|
1342
|
-
} else if (part.toolName === "unload_context" && part.state === "output-available") {
|
|
1343
|
-
const input = part.input;
|
|
1344
|
-
if (input?.label && input?.key) loaded.delete(`${input.label}:${input.key}`);
|
|
1345
|
-
}
|
|
1346
|
-
};
|
|
1347
|
-
if (this.storage.getHistoryRowStats) {
|
|
1348
|
-
const stats = await this.storage.getHistoryRowStats();
|
|
1349
|
-
for (const row of stats) {
|
|
1350
|
-
if (row.role !== "assistant") continue;
|
|
1351
|
-
const msg = await this.storage.getMessage(row.id);
|
|
1352
|
-
if (msg) scanMessage(msg);
|
|
1353
|
-
}
|
|
1354
|
-
} else for (const msg of await this.storage.getHistory()) scanMessage(msg);
|
|
1355
|
-
if (loaded.size > 0) this.context.restoreLoadedSkills(loaded);
|
|
1356
|
-
}
|
|
1357
|
-
/**
|
|
1358
|
-
* Reclaim context-window tokens consumed by a previously loaded skill.
|
|
1359
|
-
*
|
|
1360
|
-
* When a skill is loaded via the `load_context` tool, its full body is
|
|
1361
|
-
* embedded as that tool call's `output-available` result inside the
|
|
1362
|
-
* assistant message — which means every subsequent turn replays the
|
|
1363
|
-
* entire skill as part of the conversation history and pays for it in
|
|
1364
|
-
* input tokens.
|
|
1365
|
-
*
|
|
1366
|
-
* This method walks back through history, finds the matching
|
|
1367
|
-
* `load_context` tool result for `(label, key)`, and replaces its bulky
|
|
1368
|
-
* `output` with a short marker `[skill unloaded: <key>]`. The skill
|
|
1369
|
-
* content is dropped from future turns and the tokens are reclaimed.
|
|
1370
|
-
* The skill itself stays available to reload via `load_context`.
|
|
1371
|
-
*/
|
|
1372
|
-
async _reclaimLoadedSkill(label, key) {
|
|
1373
|
-
const history = await this.storage.getHistory();
|
|
1374
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
1375
|
-
const msg = history[i];
|
|
1376
|
-
if (msg.role !== "assistant") continue;
|
|
1377
|
-
let changed = false;
|
|
1378
|
-
const newParts = msg.parts.map((part) => {
|
|
1379
|
-
if (part.toolName === "load_context" && part.state === "output-available") {
|
|
1380
|
-
const input = part.input;
|
|
1381
|
-
if (input?.label === label && input?.key === key) {
|
|
1382
|
-
changed = true;
|
|
1383
|
-
return {
|
|
1384
|
-
...part,
|
|
1385
|
-
output: `[skill unloaded: ${key}]`
|
|
1386
|
-
};
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
return part;
|
|
1390
|
-
});
|
|
1391
|
-
if (changed) {
|
|
1392
|
-
await this.updateMessage({
|
|
1393
|
-
...msg,
|
|
1394
|
-
parts: newParts
|
|
1395
|
-
});
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
async getHistory(leafId) {
|
|
1401
|
-
await this._ensureRestored();
|
|
1402
|
-
return this.storage.getHistory(leafId);
|
|
1403
|
-
}
|
|
1404
|
-
/**
|
|
1405
|
-
* Byte-budgeted read of the most recent messages on the active branch
|
|
1406
|
-
* path (always at least the leaf message, and at least
|
|
1407
|
-
* `minRecentMessages` when the path is long enough). Lets hosts hydrate
|
|
1408
|
-
* a bounded window instead of the full transcript so wake-time memory
|
|
1409
|
-
* scales with the budget rather than total session history (#1710).
|
|
1410
|
-
*
|
|
1411
|
-
* Falls back to a full (untruncated) read when the provider doesn't
|
|
1412
|
-
* implement `getRecentHistory`. The fallback reports honest metadata
|
|
1413
|
-
* (`truncated: false` and the real serialized size) and warns once so a
|
|
1414
|
-
* host relying on the budget knows it is not being enforced.
|
|
1415
|
-
*/
|
|
1416
|
-
async getRecentHistory(maxContentBytes, minRecentMessages = 1) {
|
|
1417
|
-
await this._ensureRestored();
|
|
1418
|
-
if (this.storage.getRecentHistory) return this.storage.getRecentHistory(null, maxContentBytes, minRecentMessages);
|
|
1419
|
-
if (!this._warnedNoRecentHistorySupport) {
|
|
1420
|
-
this._warnedNoRecentHistorySupport = true;
|
|
1421
|
-
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.");
|
|
1422
|
-
}
|
|
1423
|
-
const messages = await this.storage.getHistory();
|
|
1424
|
-
let totalContentBytes = 0;
|
|
1425
|
-
for (const message of messages) totalContentBytes += JSON.stringify(message).length;
|
|
1426
|
-
return {
|
|
1427
|
-
messages,
|
|
1428
|
-
truncated: false,
|
|
1429
|
-
totalContentBytes
|
|
1430
|
-
};
|
|
1431
|
-
}
|
|
1432
|
-
/**
|
|
1433
|
-
* Per-row stored sizes for the active branch path (root → leaf) WITHOUT
|
|
1434
|
-
* loading message content, or `null` when the provider doesn't support it.
|
|
1435
|
-
* Lets hosts find oversized rows (e.g. inline base64 media) and process
|
|
1436
|
-
* them one at a time with bounded memory.
|
|
1437
|
-
*/
|
|
1438
|
-
async getHistoryRowStats() {
|
|
1439
|
-
await this._ensureRestored();
|
|
1440
|
-
if (!this.storage.getHistoryRowStats) return null;
|
|
1441
|
-
return this.storage.getHistoryRowStats();
|
|
1442
|
-
}
|
|
1443
|
-
async getMessage(id) {
|
|
1444
|
-
await this._ensureRestored();
|
|
1445
|
-
return this.storage.getMessage(id);
|
|
1446
|
-
}
|
|
1447
|
-
async getLatestLeaf() {
|
|
1448
|
-
await this._ensureRestored();
|
|
1449
|
-
return this.storage.getLatestLeaf();
|
|
1450
|
-
}
|
|
1451
|
-
async getBranches(messageId) {
|
|
1452
|
-
await this._ensureRestored();
|
|
1453
|
-
return this.storage.getBranches(messageId);
|
|
1454
|
-
}
|
|
1455
|
-
async getPathLength(leafId) {
|
|
1456
|
-
await this._ensureRestored();
|
|
1457
|
-
return this.storage.getPathLength(leafId);
|
|
1458
|
-
}
|
|
1459
|
-
_broadcast(type, data) {
|
|
1460
|
-
if (!this._broadcaster) return;
|
|
1461
|
-
this._broadcaster.broadcast(JSON.stringify({
|
|
1462
|
-
type,
|
|
1463
|
-
...data
|
|
1464
|
-
}));
|
|
1465
|
-
}
|
|
1466
|
-
_shouldEstimateTokens() {
|
|
1467
|
-
return Boolean(this._broadcaster || this._tokenThreshold != null && this._compactionFn);
|
|
1468
|
-
}
|
|
1469
|
-
async _estimateTokenCount() {
|
|
1470
|
-
const messages = await this.getHistory();
|
|
1471
|
-
const systemPrompt = await this.context.getSystemPromptForEstimate();
|
|
1472
|
-
if (this._tokenCounter) {
|
|
1473
|
-
if (!this.context.isLoaded()) await this.context.load();
|
|
1474
|
-
const contextBlocks = this.context.getBlocks();
|
|
1475
|
-
const estimate = await this._tokenCounter({
|
|
1476
|
-
messages,
|
|
1477
|
-
systemPrompt,
|
|
1478
|
-
contextBlocks
|
|
1479
|
-
});
|
|
1480
|
-
return Number.isFinite(estimate) ? Math.max(0, Math.ceil(estimate)) : 0;
|
|
1481
|
-
}
|
|
1482
|
-
return estimateMessageTokens(messages) + estimateStringTokens(systemPrompt);
|
|
1483
|
-
}
|
|
1484
|
-
async _handleAutoCompactionError(error) {
|
|
1485
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1486
|
-
if (this._compactionErrorHandler) try {
|
|
1487
|
-
await this._compactionErrorHandler(error);
|
|
1488
|
-
} catch (handlerError) {
|
|
1489
|
-
const handlerMessage = handlerError instanceof Error ? handlerError.message : String(handlerError);
|
|
1490
|
-
console.warn(`Session auto-compaction error handler failed: ${handlerMessage}`);
|
|
1491
|
-
}
|
|
1492
|
-
else console.warn(`Session auto-compaction failed: ${message}`);
|
|
1493
|
-
this._emitError(message);
|
|
1494
|
-
}
|
|
1495
|
-
async _emitStatus(phase, extra) {
|
|
1496
|
-
let tokenEstimate = 0;
|
|
1497
|
-
if (this._shouldEstimateTokens()) try {
|
|
1498
|
-
tokenEstimate = await this._estimateTokenCount();
|
|
1499
|
-
} catch (err) {
|
|
1500
|
-
await this._handleAutoCompactionError(err);
|
|
1501
|
-
}
|
|
1502
|
-
this._broadcast("cf_agent_session", {
|
|
1503
|
-
phase,
|
|
1504
|
-
tokenEstimate,
|
|
1505
|
-
tokenThreshold: this._tokenThreshold ?? null,
|
|
1506
|
-
...extra
|
|
1507
|
-
});
|
|
1508
|
-
return tokenEstimate;
|
|
1509
|
-
}
|
|
1510
|
-
_emitError(error) {
|
|
1511
|
-
this._broadcast("cf_agent_session_error", { error });
|
|
1512
|
-
}
|
|
1513
|
-
async appendMessage(message, parentId) {
|
|
1514
|
-
await this._appendMessage(message, parentId);
|
|
1515
|
-
}
|
|
1516
|
-
async _appendMessage(message, parentId) {
|
|
1517
|
-
await this._ensureRestored();
|
|
1518
|
-
if (await this.storage.getMessage(message.id)) {
|
|
1519
|
-
await this._emitStatus("idle");
|
|
1520
|
-
await this._notifyMessagesChanged({
|
|
1521
|
-
type: "append",
|
|
1522
|
-
message,
|
|
1523
|
-
parentId,
|
|
1524
|
-
inserted: false
|
|
1525
|
-
});
|
|
1526
|
-
return;
|
|
1527
|
-
}
|
|
1528
|
-
await this.storage.appendMessage(message, parentId);
|
|
1529
|
-
const tokenEstimate = await this._emitStatus("idle");
|
|
1530
|
-
let compacted = false;
|
|
1531
|
-
if (this._tokenThreshold != null && this._compactionFn && tokenEstimate > this._tokenThreshold) try {
|
|
1532
|
-
compacted = Boolean(await this.compact());
|
|
1533
|
-
if (!compacted && !this._warnedCompactionNoOp) {
|
|
1534
|
-
this._warnedCompactionNoOp = true;
|
|
1535
|
-
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.`));
|
|
1536
|
-
} else if (compacted) this._warnedCompactionNoOp = false;
|
|
1537
|
-
} catch (err) {
|
|
1538
|
-
await this._handleAutoCompactionError(err);
|
|
1539
|
-
}
|
|
1540
|
-
if (!compacted) await this._notifyMessagesChanged({
|
|
1541
|
-
type: "append",
|
|
1542
|
-
message,
|
|
1543
|
-
parentId,
|
|
1544
|
-
inserted: true
|
|
1545
|
-
});
|
|
1546
|
-
}
|
|
1547
|
-
async updateMessage(message) {
|
|
1548
|
-
await this._ensureRestored();
|
|
1549
|
-
await this.storage.updateMessage(message);
|
|
1550
|
-
await this._emitStatus("idle");
|
|
1551
|
-
await this._notifyMessagesChanged({
|
|
1552
|
-
type: "update",
|
|
1553
|
-
message
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
/**
|
|
1557
|
-
* @internal
|
|
1558
|
-
* Rewrite a stored message WITHOUT the public-write side effects: no
|
|
1559
|
-
* token-estimate status broadcast (which reads the FULL history) and no
|
|
1560
|
-
* auto-compaction check. For framework maintenance passes that rewrite
|
|
1561
|
-
* many rows with bounded memory — e.g. media eviction (#1710) — where the
|
|
1562
|
-
* per-row full-history estimate would reintroduce the memory pressure the
|
|
1563
|
-
* pass exists to remove. The message-change listener still fires so a
|
|
1564
|
-
* cache-owning host stays coherent. Application code should use
|
|
1565
|
-
* `updateMessage`.
|
|
1566
|
-
*/
|
|
1567
|
-
async internal_rewriteMessage(message) {
|
|
1568
|
-
await this._ensureRestored();
|
|
1569
|
-
await this.storage.updateMessage(message);
|
|
1570
|
-
await this._notifyMessagesChanged({
|
|
1571
|
-
type: "update",
|
|
1572
|
-
message
|
|
1573
|
-
});
|
|
1574
|
-
}
|
|
1575
|
-
async deleteMessages(messageIds) {
|
|
1576
|
-
await this._ensureRestored();
|
|
1577
|
-
await this.storage.deleteMessages(messageIds);
|
|
1578
|
-
await this._emitStatus("idle");
|
|
1579
|
-
await this._notifyMessagesChanged({
|
|
1580
|
-
type: "delete",
|
|
1581
|
-
messageIds
|
|
1582
|
-
});
|
|
1583
|
-
}
|
|
1584
|
-
async clearMessages() {
|
|
1585
|
-
await this._ensureRestored();
|
|
1586
|
-
await this.storage.clearMessages();
|
|
1587
|
-
this.context.clearSkillState();
|
|
1588
|
-
await this.context.refreshSystemPrompt();
|
|
1589
|
-
await this._emitStatus("idle");
|
|
1590
|
-
await this._notifyMessagesChanged({ type: "clear" });
|
|
1591
|
-
}
|
|
1592
|
-
async addCompaction(summary, fromMessageId, toMessageId) {
|
|
1593
|
-
await this._ensureRestored();
|
|
1594
|
-
return this.storage.addCompaction(summary, fromMessageId, toMessageId);
|
|
1595
|
-
}
|
|
1596
|
-
async getCompactions() {
|
|
1597
|
-
await this._ensureRestored();
|
|
1598
|
-
return this.storage.getCompactions();
|
|
1599
|
-
}
|
|
1600
|
-
/**
|
|
1601
|
-
* Run the registered compaction function and store the result as an overlay.
|
|
1602
|
-
* Requires `onCompaction()` to be called first.
|
|
1603
|
-
*/
|
|
1604
|
-
async compact() {
|
|
1605
|
-
await this._ensureRestored();
|
|
1606
|
-
if (!this._compactionFn) throw new Error("No compaction function registered. Call onCompaction() first.");
|
|
1607
|
-
const tokensBefore = await this._emitStatus("compacting");
|
|
1608
|
-
let result;
|
|
1609
|
-
try {
|
|
1610
|
-
result = await this._compactionFn(await this.getHistory(), { tokenCounter: this._tokenCounter });
|
|
1611
|
-
} catch (err) {
|
|
1612
|
-
this._emitError(err instanceof Error ? err.message : String(err));
|
|
1613
|
-
return null;
|
|
1614
|
-
}
|
|
1615
|
-
if (!result) {
|
|
1616
|
-
await this._emitStatus("idle");
|
|
1617
|
-
return null;
|
|
1618
|
-
}
|
|
1619
|
-
if (!new Set((await this.getHistory()).map((m) => m.id)).has(result.toMessageId)) {
|
|
1620
|
-
await this._emitStatus("idle");
|
|
1621
|
-
return null;
|
|
1622
|
-
}
|
|
1623
|
-
const existing = await this.getCompactions();
|
|
1624
|
-
const fromId = existing.length > 0 ? existing[0].fromMessageId : result.fromMessageId;
|
|
1625
|
-
await this.addCompaction(result.summary, fromId, result.toMessageId);
|
|
1626
|
-
await this.refreshSystemPrompt();
|
|
1627
|
-
await this._emitStatus("idle", { compacted: { tokensBefore } });
|
|
1628
|
-
await this._notifyMessagesChanged({ type: "compact" });
|
|
1629
|
-
return {
|
|
1630
|
-
...result,
|
|
1631
|
-
fromMessageId: fromId
|
|
1632
|
-
};
|
|
1633
|
-
}
|
|
1634
|
-
getContextBlock(label) {
|
|
1635
|
-
this._ensureReady();
|
|
1636
|
-
return this.context.getBlock(label);
|
|
1637
|
-
}
|
|
1638
|
-
getContextBlocks() {
|
|
1639
|
-
this._ensureReady();
|
|
1640
|
-
return this.context.getBlocks();
|
|
1641
|
-
}
|
|
1642
|
-
async replaceContextBlock(label, content) {
|
|
1643
|
-
await this._ensureRestored();
|
|
1644
|
-
return this.context.setBlock(label, content);
|
|
1645
|
-
}
|
|
1646
|
-
async appendContextBlock(label, content) {
|
|
1647
|
-
await this._ensureRestored();
|
|
1648
|
-
return this.context.appendToBlock(label, content);
|
|
1649
|
-
}
|
|
1650
|
-
/**
|
|
1651
|
-
* Dynamically register a new context block after session initialization.
|
|
1652
|
-
*
|
|
1653
|
-
* This is a **builder / runtime API**, not an LLM tool. The LLM writes
|
|
1654
|
-
* into existing context blocks via the `set_context` tool (see
|
|
1655
|
-
* `ContextBlocks.tools()`); it cannot declare new blocks itself. This
|
|
1656
|
-
* method is how extension / host code contributes blocks at runtime
|
|
1657
|
-
* (e.g. an extension's `onLoad` handler registering its own memory block).
|
|
1658
|
-
*
|
|
1659
|
-
* The block's provider is initialized and loaded immediately.
|
|
1660
|
-
* Call `refreshSystemPrompt()` afterward to include the new block
|
|
1661
|
-
* in the system prompt.
|
|
1662
|
-
*
|
|
1663
|
-
* Note: When called without a provider, auto-wires to SQLite via
|
|
1664
|
-
* AgentContextProvider. Requires the session to have been created
|
|
1665
|
-
* via `Session.create(agent)` (not the direct constructor).
|
|
1666
|
-
*/
|
|
1667
|
-
async addContext(label, options) {
|
|
1668
|
-
await this._ensureRestored();
|
|
1669
|
-
const opts = options ?? {};
|
|
1670
|
-
let provider = opts.provider;
|
|
1671
|
-
if (!provider) {
|
|
1672
|
-
if (!this._agent) throw new Error(`addContext("${label}") requires an explicit provider when Session uses a SessionProvider`);
|
|
1673
|
-
const key = this._sessionId ? `${label}_${this._sessionId}` : label;
|
|
1674
|
-
provider = new AgentContextProvider(this._agent, key);
|
|
1675
|
-
}
|
|
1676
|
-
const block = await this.context.addBlock({
|
|
1677
|
-
label,
|
|
1678
|
-
description: opts.description,
|
|
1679
|
-
maxTokens: opts.maxTokens,
|
|
1680
|
-
provider
|
|
1681
|
-
});
|
|
1682
|
-
if (block.isSkill && !this._skillScanRan) await this._scanHistoryForLoadedSkills();
|
|
1683
|
-
return block;
|
|
1684
|
-
}
|
|
1685
|
-
/**
|
|
1686
|
-
* Remove a dynamically registered context block.
|
|
1687
|
-
* Used during extension unload cleanup.
|
|
1688
|
-
*
|
|
1689
|
-
* Returns true if the block existed and was removed.
|
|
1690
|
-
* Call `refreshSystemPrompt()` afterward to rebuild the prompt
|
|
1691
|
-
* without the removed block.
|
|
1692
|
-
*/
|
|
1693
|
-
removeContext(label) {
|
|
1694
|
-
this._ensureReady();
|
|
1695
|
-
return this.context.removeBlock(label);
|
|
1696
|
-
}
|
|
1697
|
-
/**
|
|
1698
|
-
* Unload a previously loaded skill, reclaiming context space.
|
|
1699
|
-
* The tool result in conversation history is replaced with a short marker.
|
|
1700
|
-
*
|
|
1701
|
-
* Async so that the session's background skill-state restore (which
|
|
1702
|
-
* reads conversation history) is awaited first — otherwise a freshly
|
|
1703
|
-
* rehydrated DO could report "not loaded" for a skill that's actually
|
|
1704
|
-
* present in history.
|
|
1705
|
-
*/
|
|
1706
|
-
async unloadSkill(label, key) {
|
|
1707
|
-
await this._ensureRestored();
|
|
1708
|
-
return this.context.unloadSkill(label, key);
|
|
1709
|
-
}
|
|
1710
|
-
/**
|
|
1711
|
-
* Get currently loaded skill keys (as "label:key" strings).
|
|
1712
|
-
* Async for the same reason as `unloadSkill` — must wait for restore.
|
|
1713
|
-
*/
|
|
1714
|
-
async getLoadedSkillKeys() {
|
|
1715
|
-
await this._ensureRestored();
|
|
1716
|
-
return this.context.getLoadedSkillKeys();
|
|
1717
|
-
}
|
|
1718
|
-
async freezeSystemPrompt() {
|
|
1719
|
-
await this._ensureRestored();
|
|
1720
|
-
return this.context.freezeSystemPrompt();
|
|
1721
|
-
}
|
|
1722
|
-
async refreshSystemPrompt() {
|
|
1723
|
-
await this._ensureRestored();
|
|
1724
|
-
return this.context.refreshSystemPrompt();
|
|
1725
|
-
}
|
|
1726
|
-
async search(query, options) {
|
|
1727
|
-
await this._ensureRestored();
|
|
1728
|
-
if (!this.storage.searchMessages) throw new Error("Session provider does not support search");
|
|
1729
|
-
return this.storage.searchMessages(query, options?.limit ?? 20);
|
|
1730
|
-
}
|
|
1731
|
-
/** Returns set_context and load_context tools. */
|
|
1732
|
-
async tools() {
|
|
1733
|
-
await this._ensureRestored();
|
|
1734
|
-
return this.context.tools();
|
|
1735
|
-
}
|
|
1736
|
-
};
|
|
1737
|
-
//#endregion
|
|
1738
|
-
//#region src/experimental/memory/session/manager.ts
|
|
1739
|
-
var SessionManager = class SessionManager {
|
|
1740
|
-
constructor(agent, _options = {}) {
|
|
1741
|
-
this._pending = [];
|
|
1742
|
-
this._sessions = /* @__PURE__ */ new Map();
|
|
1743
|
-
this._tableReady = false;
|
|
1744
|
-
this._ready = false;
|
|
1745
|
-
this.agent = agent;
|
|
1746
|
-
this._ready = true;
|
|
1747
|
-
this._ensureTable();
|
|
1748
|
-
}
|
|
1749
|
-
/**
|
|
1750
|
-
* Chainable SessionManager creation with auto-wired context for all sessions.
|
|
1751
|
-
*
|
|
1752
|
-
* @example
|
|
1753
|
-
* ```ts
|
|
1754
|
-
* const manager = SessionManager.create(this)
|
|
1755
|
-
* .withContext("soul", { provider: { get: async () => "You are helpful." } })
|
|
1756
|
-
* .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
|
|
1757
|
-
* .withCachedPrompt()
|
|
1758
|
-
* .compactAfter(100_000);
|
|
1759
|
-
*
|
|
1760
|
-
* // Each getSession(id) auto-creates namespaced providers:
|
|
1761
|
-
* // memory key: "memory_<sessionId>"
|
|
1762
|
-
* // prompt key: "_system_prompt_<sessionId>"
|
|
1763
|
-
* const session = manager.getSession("chat-123");
|
|
1764
|
-
* ```
|
|
1765
|
-
*/
|
|
1766
|
-
static create(agent) {
|
|
1767
|
-
const mgr = Object.create(SessionManager.prototype);
|
|
1768
|
-
mgr.agent = agent;
|
|
1769
|
-
mgr._pending = [];
|
|
1770
|
-
mgr._compactionFn = null;
|
|
1771
|
-
mgr._tokenThreshold = void 0;
|
|
1772
|
-
mgr._tokenCounter = void 0;
|
|
1773
|
-
mgr._compactionErrorHandler = void 0;
|
|
1774
|
-
mgr._sessions = /* @__PURE__ */ new Map();
|
|
1775
|
-
mgr._tableReady = false;
|
|
1776
|
-
mgr._ready = false;
|
|
1777
|
-
return mgr;
|
|
1778
|
-
}
|
|
1779
|
-
withContext(label, options) {
|
|
1780
|
-
this._pending.push({
|
|
1781
|
-
label,
|
|
1782
|
-
options: options ?? {}
|
|
1783
|
-
});
|
|
1784
|
-
return this;
|
|
1785
|
-
}
|
|
1786
|
-
withCachedPrompt(provider) {
|
|
1787
|
-
this._cachedPrompt = provider ?? true;
|
|
1788
|
-
return this;
|
|
1789
|
-
}
|
|
1790
|
-
/**
|
|
1791
|
-
* Register a compaction function propagated to all sessions.
|
|
1792
|
-
* Called by `Session.compact()` to compress message history.
|
|
1793
|
-
*/
|
|
1794
|
-
onCompaction(fn) {
|
|
1795
|
-
this._compactionFn = fn;
|
|
1796
|
-
return this;
|
|
1797
|
-
}
|
|
1798
|
-
/**
|
|
1799
|
-
* Auto-compact when estimated token count exceeds the threshold.
|
|
1800
|
-
* Propagated to all sessions. Requires `onCompaction()`.
|
|
1801
|
-
*/
|
|
1802
|
-
compactAfter(tokenThreshold, options) {
|
|
1803
|
-
this._tokenThreshold = tokenThreshold;
|
|
1804
|
-
if (options?.tokenCounter) this._tokenCounter = options.tokenCounter;
|
|
1805
|
-
return this;
|
|
1806
|
-
}
|
|
1807
|
-
/**
|
|
1808
|
-
* Handle failures from automatic compaction in managed sessions.
|
|
1809
|
-
*/
|
|
1810
|
-
onCompactionError(handler) {
|
|
1811
|
-
this._compactionErrorHandler = handler;
|
|
1812
|
-
return this;
|
|
1813
|
-
}
|
|
1814
|
-
/**
|
|
1815
|
-
* Add a searchable context block that searches conversation history
|
|
1816
|
-
* across all sessions managed by this manager.
|
|
1817
|
-
*
|
|
1818
|
-
* The model can use `search_context` to find relevant messages from
|
|
1819
|
-
* any session. The block is readonly (no `set`).
|
|
1820
|
-
*
|
|
1821
|
-
* @example
|
|
1822
|
-
* ```ts
|
|
1823
|
-
* SessionManager.create(this)
|
|
1824
|
-
* .withContext("memory", { maxTokens: 1100 })
|
|
1825
|
-
* .withSearchableHistory("history")
|
|
1826
|
-
* .withCachedPrompt();
|
|
1827
|
-
* ```
|
|
1828
|
-
*/
|
|
1829
|
-
withSearchableHistory(label) {
|
|
1830
|
-
this._historyLabel = label;
|
|
1831
|
-
return this;
|
|
1832
|
-
}
|
|
1833
|
-
_ensureReady() {
|
|
1834
|
-
if (this._ready) return;
|
|
1835
|
-
this._ready = true;
|
|
1836
|
-
this._ensureTable();
|
|
1837
|
-
}
|
|
1838
|
-
_ensureTable() {
|
|
1839
|
-
if (this._tableReady) return;
|
|
1840
|
-
this.agent.sql`
|
|
1841
|
-
CREATE TABLE IF NOT EXISTS assistant_sessions (
|
|
1842
|
-
id TEXT PRIMARY KEY,
|
|
1843
|
-
name TEXT NOT NULL,
|
|
1844
|
-
parent_session_id TEXT,
|
|
1845
|
-
model TEXT,
|
|
1846
|
-
source TEXT,
|
|
1847
|
-
input_tokens INTEGER DEFAULT 0,
|
|
1848
|
-
output_tokens INTEGER DEFAULT 0,
|
|
1849
|
-
estimated_cost REAL DEFAULT 0,
|
|
1850
|
-
end_reason TEXT,
|
|
1851
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
1852
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
1853
|
-
)
|
|
1854
|
-
`;
|
|
1855
|
-
this.agent.sql`
|
|
1856
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS assistant_fts
|
|
1857
|
-
USING fts5(id UNINDEXED, session_id UNINDEXED, role UNINDEXED, content, tokenize='porter unicode61')
|
|
1858
|
-
`;
|
|
1859
|
-
this._tableReady = true;
|
|
1860
|
-
}
|
|
1861
|
-
_createHistoryProvider() {
|
|
1862
|
-
const mgr = this;
|
|
1863
|
-
return {
|
|
1864
|
-
async get() {
|
|
1865
|
-
const sessions = mgr.list();
|
|
1866
|
-
if (sessions.length === 0) return null;
|
|
1867
|
-
return `${sessions.length} session${sessions.length === 1 ? "" : "s"} available for search.`;
|
|
1868
|
-
},
|
|
1869
|
-
async search(query) {
|
|
1870
|
-
const results = mgr.search(query, { limit: 10 });
|
|
1871
|
-
if (results.length === 0) return null;
|
|
1872
|
-
return results.map((r) => `[${r.role}] ${r.content}`).join("\n---\n");
|
|
1873
|
-
}
|
|
1874
|
-
};
|
|
1875
|
-
}
|
|
1876
|
-
/** Get or create the Session instance for a session ID. */
|
|
1877
|
-
getSession(sessionId) {
|
|
1878
|
-
this._ensureReady();
|
|
1879
|
-
let session = this._sessions.get(sessionId);
|
|
1880
|
-
if (!session) {
|
|
1881
|
-
const s = Session.create(this.agent).forSession(sessionId);
|
|
1882
|
-
for (const { label, options } of this._pending) s.withContext(label, options);
|
|
1883
|
-
if (this._cachedPrompt === true) s.withCachedPrompt();
|
|
1884
|
-
else if (this._cachedPrompt) s.withCachedPrompt(this._cachedPrompt);
|
|
1885
|
-
if (this._historyLabel) s.withContext(this._historyLabel, {
|
|
1886
|
-
description: "Cross-session conversation history",
|
|
1887
|
-
provider: this._createHistoryProvider()
|
|
1888
|
-
});
|
|
1889
|
-
if (this._compactionFn) s.onCompaction(this._compactionFn);
|
|
1890
|
-
if (this._tokenThreshold != null) s.compactAfter(this._tokenThreshold, { tokenCounter: this._tokenCounter });
|
|
1891
|
-
if (this._compactionErrorHandler) s.onCompactionError(this._compactionErrorHandler);
|
|
1892
|
-
session = s;
|
|
1893
|
-
this._sessions.set(sessionId, session);
|
|
1894
|
-
}
|
|
1895
|
-
return session;
|
|
1896
|
-
}
|
|
1897
|
-
create(name, opts) {
|
|
1898
|
-
this._ensureReady();
|
|
1899
|
-
const id = crypto.randomUUID();
|
|
1900
|
-
this.agent.sql`
|
|
1901
|
-
INSERT INTO assistant_sessions (id, name, parent_session_id, model, source)
|
|
1902
|
-
VALUES (${id}, ${name}, ${opts?.parentSessionId ?? null}, ${opts?.model ?? null}, ${opts?.source ?? null})
|
|
1903
|
-
`;
|
|
1904
|
-
return this.get(id);
|
|
1905
|
-
}
|
|
1906
|
-
get(sessionId) {
|
|
1907
|
-
this._ensureReady();
|
|
1908
|
-
return this.agent.sql`
|
|
1909
|
-
SELECT * FROM assistant_sessions WHERE id = ${sessionId}
|
|
1910
|
-
`[0] ?? null;
|
|
1911
|
-
}
|
|
1912
|
-
list() {
|
|
1913
|
-
this._ensureReady();
|
|
1914
|
-
return this.agent.sql`
|
|
1915
|
-
SELECT * FROM assistant_sessions ORDER BY updated_at DESC
|
|
1916
|
-
`;
|
|
1917
|
-
}
|
|
1918
|
-
async delete(sessionId) {
|
|
1919
|
-
await this.getSession(sessionId).clearMessages();
|
|
1920
|
-
this.agent.sql`DELETE FROM assistant_sessions WHERE id = ${sessionId}`;
|
|
1921
|
-
this._sessions.delete(sessionId);
|
|
1922
|
-
}
|
|
1923
|
-
rename(sessionId, name) {
|
|
1924
|
-
this._ensureReady();
|
|
1925
|
-
this.agent.sql`
|
|
1926
|
-
UPDATE assistant_sessions SET name = ${name}, updated_at = CURRENT_TIMESTAMP
|
|
1927
|
-
WHERE id = ${sessionId}
|
|
1928
|
-
`;
|
|
1929
|
-
}
|
|
1930
|
-
async append(sessionId, message, parentId) {
|
|
1931
|
-
await this.getSession(sessionId).appendMessage(message, parentId);
|
|
1932
|
-
this._touch(sessionId);
|
|
1933
|
-
return message.id;
|
|
1934
|
-
}
|
|
1935
|
-
async upsert(sessionId, message, parentId) {
|
|
1936
|
-
const session = this.getSession(sessionId);
|
|
1937
|
-
if (await session.getMessage(message.id)) await session.updateMessage(message);
|
|
1938
|
-
else await session.appendMessage(message, parentId);
|
|
1939
|
-
this._touch(sessionId);
|
|
1940
|
-
return message.id;
|
|
1941
|
-
}
|
|
1942
|
-
async appendAll(sessionId, messages, parentId) {
|
|
1943
|
-
const session = this.getSession(sessionId);
|
|
1944
|
-
let lastParent = parentId ?? null;
|
|
1945
|
-
for (const msg of messages) {
|
|
1946
|
-
await session.appendMessage(msg, lastParent);
|
|
1947
|
-
lastParent = msg.id;
|
|
1948
|
-
}
|
|
1949
|
-
this._touch(sessionId);
|
|
1950
|
-
return lastParent;
|
|
1951
|
-
}
|
|
1952
|
-
async getHistory(sessionId, leafId) {
|
|
1953
|
-
return this.getSession(sessionId).getHistory(leafId);
|
|
1954
|
-
}
|
|
1955
|
-
async getMessageCount(sessionId) {
|
|
1956
|
-
return this.getSession(sessionId).getPathLength();
|
|
1957
|
-
}
|
|
1958
|
-
async clearMessages(sessionId) {
|
|
1959
|
-
await this.getSession(sessionId).clearMessages();
|
|
1960
|
-
this._touch(sessionId);
|
|
1961
|
-
}
|
|
1962
|
-
async deleteMessages(sessionId, messageIds) {
|
|
1963
|
-
await this.getSession(sessionId).deleteMessages(messageIds);
|
|
1964
|
-
this._touch(sessionId);
|
|
1965
|
-
}
|
|
1966
|
-
async getBranches(sessionId, messageId) {
|
|
1967
|
-
return this.getSession(sessionId).getBranches(messageId);
|
|
1968
|
-
}
|
|
1969
|
-
/**
|
|
1970
|
-
* Fork a session at a specific message, creating a new session
|
|
1971
|
-
* with the history up to that point copied over.
|
|
1972
|
-
*/
|
|
1973
|
-
async fork(sessionId, atMessageId, newName) {
|
|
1974
|
-
const info = this.create(newName, { parentSessionId: sessionId });
|
|
1975
|
-
const history = await this.getSession(sessionId).getHistory(atMessageId);
|
|
1976
|
-
const newSession = this.getSession(info.id);
|
|
1977
|
-
let parentId = null;
|
|
1978
|
-
for (const msg of history) {
|
|
1979
|
-
const newId = crypto.randomUUID();
|
|
1980
|
-
const copy = {
|
|
1981
|
-
...msg,
|
|
1982
|
-
id: newId
|
|
1983
|
-
};
|
|
1984
|
-
await newSession.appendMessage(copy, parentId);
|
|
1985
|
-
parentId = newId;
|
|
1986
|
-
}
|
|
1987
|
-
this._touch(info.id);
|
|
1988
|
-
return info;
|
|
1989
|
-
}
|
|
1990
|
-
async addCompaction(sessionId, summary, fromId, toId) {
|
|
1991
|
-
return this.getSession(sessionId).addCompaction(summary, fromId, toId);
|
|
1992
|
-
}
|
|
1993
|
-
async getCompactions(sessionId) {
|
|
1994
|
-
return this.getSession(sessionId).getCompactions();
|
|
1995
|
-
}
|
|
1996
|
-
async compactAndSplit(sessionId, summary, newName) {
|
|
1997
|
-
const old = this.get(sessionId);
|
|
1998
|
-
this.agent.sql`
|
|
1999
|
-
UPDATE assistant_sessions SET end_reason = 'compaction', updated_at = CURRENT_TIMESTAMP
|
|
2000
|
-
WHERE id = ${sessionId}
|
|
2001
|
-
`;
|
|
2002
|
-
const info = this.create(newName ?? old?.name ?? "Compacted", {
|
|
2003
|
-
parentSessionId: sessionId,
|
|
2004
|
-
model: old?.model ?? void 0,
|
|
2005
|
-
source: old?.source ?? void 0
|
|
2006
|
-
});
|
|
2007
|
-
await this.append(info.id, {
|
|
2008
|
-
id: crypto.randomUUID(),
|
|
2009
|
-
role: "assistant",
|
|
2010
|
-
parts: [{
|
|
2011
|
-
type: "text",
|
|
2012
|
-
text: `[Context from previous session]\n\n${summary}`
|
|
2013
|
-
}]
|
|
2014
|
-
});
|
|
2015
|
-
return info;
|
|
2016
|
-
}
|
|
2017
|
-
addUsage(sessionId, inputTokens, outputTokens, cost) {
|
|
2018
|
-
this._ensureReady();
|
|
2019
|
-
this.agent.sql`
|
|
2020
|
-
UPDATE assistant_sessions SET
|
|
2021
|
-
input_tokens = input_tokens + ${inputTokens},
|
|
2022
|
-
output_tokens = output_tokens + ${outputTokens},
|
|
2023
|
-
estimated_cost = estimated_cost + ${cost},
|
|
2024
|
-
updated_at = CURRENT_TIMESTAMP
|
|
2025
|
-
WHERE id = ${sessionId}
|
|
2026
|
-
`;
|
|
2027
|
-
}
|
|
2028
|
-
search(query, options) {
|
|
2029
|
-
this._ensureReady();
|
|
2030
|
-
const limit = options?.limit ?? 20;
|
|
2031
|
-
const sanitized = query.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, "\"\"")}"`).join(" ");
|
|
2032
|
-
if (!sanitized) return [];
|
|
2033
|
-
try {
|
|
2034
|
-
return this.agent.sql`
|
|
2035
|
-
SELECT id, role, content FROM assistant_fts
|
|
2036
|
-
WHERE assistant_fts MATCH ${sanitized}
|
|
2037
|
-
ORDER BY rank LIMIT ${limit}
|
|
2038
|
-
`.map((r) => ({
|
|
2039
|
-
id: r.id,
|
|
2040
|
-
role: r.role,
|
|
2041
|
-
content: r.content,
|
|
2042
|
-
createdAt: ""
|
|
2043
|
-
}));
|
|
2044
|
-
} catch {
|
|
2045
|
-
return [];
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
tools() {
|
|
2049
|
-
return { session_search: {
|
|
2050
|
-
description: "Search past conversations for relevant context. Searches across all sessions.",
|
|
2051
|
-
inputSchema: z.fromJSONSchema({
|
|
2052
|
-
type: "object",
|
|
2053
|
-
properties: { query: {
|
|
2054
|
-
type: "string",
|
|
2055
|
-
description: "Search query"
|
|
2056
|
-
} },
|
|
2057
|
-
required: ["query"]
|
|
2058
|
-
}),
|
|
2059
|
-
execute: async ({ query }) => {
|
|
2060
|
-
try {
|
|
2061
|
-
const results = this.search(query, { limit: 10 });
|
|
2062
|
-
if (results.length === 0) return "No results found.";
|
|
2063
|
-
return results.map((r) => `[${r.role}] ${r.content}`).join("\n---\n");
|
|
2064
|
-
} catch (err) {
|
|
2065
|
-
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
} };
|
|
2069
|
-
}
|
|
2070
|
-
_touch(sessionId) {
|
|
2071
|
-
this.agent.sql`
|
|
2072
|
-
UPDATE assistant_sessions SET updated_at = CURRENT_TIMESTAMP
|
|
2073
|
-
WHERE id = ${sessionId}
|
|
2074
|
-
`;
|
|
2075
|
-
}
|
|
2076
|
-
};
|
|
2077
|
-
//#endregion
|
|
2078
|
-
//#region src/experimental/memory/session/providers/postgres-adapter.ts
|
|
2079
|
-
function isPostgresConnection(client) {
|
|
2080
|
-
return typeof client.execute === "function";
|
|
2081
|
-
}
|
|
2082
|
-
/**
|
|
2083
|
-
* Normalise an incoming client into a `PostgresConnection`. When given a
|
|
2084
|
-
* `pg`-style client we translate `?` placeholders to `$1, $2, ...` so the
|
|
2085
|
-
* providers can keep using the portable `?` syntax internally.
|
|
2086
|
-
*/
|
|
2087
|
-
function toPostgresConnection(client) {
|
|
2088
|
-
if (isPostgresConnection(client)) return client;
|
|
2089
|
-
const pg = client;
|
|
2090
|
-
return { async execute(query, args) {
|
|
2091
|
-
let idx = 0;
|
|
2092
|
-
const pgQuery = query.replace(/\?/g, () => `$${++idx}`);
|
|
2093
|
-
return { rows: (await pg.query(pgQuery, args ?? [])).rows };
|
|
2094
|
-
} };
|
|
2095
|
-
}
|
|
2096
|
-
//#endregion
|
|
2097
|
-
//#region src/experimental/memory/session/providers/postgres.ts
|
|
2098
|
-
var PostgresSessionProvider = class {
|
|
2099
|
-
/**
|
|
2100
|
-
* @param client A raw `pg.Client` (recommended) or any `PostgresConnection`.
|
|
2101
|
-
* Must already be connected — this provider never opens or closes the
|
|
2102
|
-
* underlying client.
|
|
2103
|
-
* @param sessionId Session identifier. Different ids are fully isolated
|
|
2104
|
-
* rows within the shared tables. Defaults to `""`.
|
|
2105
|
-
*/
|
|
2106
|
-
constructor(client, sessionId) {
|
|
2107
|
-
this.conn = toPostgresConnection(client);
|
|
2108
|
-
this.sessionId = sessionId ?? "";
|
|
2109
|
-
}
|
|
2110
|
-
async getMessage(id) {
|
|
2111
|
-
const { rows } = await this.conn.execute("SELECT content FROM assistant_messages WHERE id = ? AND session_id = ?", [id, this.sessionId]);
|
|
2112
|
-
return rows.length > 0 ? this.parse(rows[0].content) : null;
|
|
2113
|
-
}
|
|
2114
|
-
async getHistory(leafId) {
|
|
2115
|
-
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();
|
|
2116
|
-
if (!leaf) return [];
|
|
2117
|
-
const { rows } = await this.conn.execute(`WITH RECURSIVE path AS (
|
|
2118
|
-
SELECT id, parent_id, content, 0 as depth FROM assistant_messages WHERE id = ? AND session_id = ?
|
|
2119
|
-
UNION ALL
|
|
2120
|
-
SELECT m.id, m.parent_id, m.content, p.depth + 1 FROM assistant_messages m
|
|
2121
|
-
JOIN path p ON m.id = p.parent_id
|
|
2122
|
-
WHERE m.session_id = ? AND p.depth < 10000
|
|
2123
|
-
)
|
|
2124
|
-
SELECT content FROM path ORDER BY depth DESC`, [
|
|
2125
|
-
leaf.id,
|
|
2126
|
-
this.sessionId,
|
|
2127
|
-
this.sessionId
|
|
2128
|
-
]);
|
|
2129
|
-
const messages = this.parseRows(rows);
|
|
2130
|
-
const compactions = await this.getCompactions();
|
|
2131
|
-
if (compactions.length === 0) return messages;
|
|
2132
|
-
return this.applyCompactions(messages, compactions);
|
|
2133
|
-
}
|
|
2134
|
-
async getLatestLeaf() {
|
|
2135
|
-
const row = await this.latestLeafRow();
|
|
2136
|
-
return row ? this.parse(row.content) : null;
|
|
2137
|
-
}
|
|
2138
|
-
async getBranches(messageId) {
|
|
2139
|
-
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]);
|
|
2140
|
-
return this.parseRows(rows);
|
|
2141
|
-
}
|
|
2142
|
-
async getPathLength(leafId) {
|
|
2143
|
-
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();
|
|
2144
|
-
if (!leaf) return 0;
|
|
2145
|
-
const { rows } = await this.conn.execute(`WITH RECURSIVE path AS (
|
|
2146
|
-
SELECT id, parent_id, 0 as depth FROM assistant_messages WHERE id = ? AND session_id = ?
|
|
2147
|
-
UNION ALL
|
|
2148
|
-
SELECT m.id, m.parent_id, p.depth + 1 FROM assistant_messages m
|
|
2149
|
-
JOIN path p ON m.id = p.parent_id
|
|
2150
|
-
WHERE m.session_id = ? AND p.depth < 10000
|
|
2151
|
-
)
|
|
2152
|
-
SELECT COUNT(*) as count FROM path`, [
|
|
2153
|
-
leaf.id,
|
|
2154
|
-
this.sessionId,
|
|
2155
|
-
this.sessionId
|
|
2156
|
-
]);
|
|
2157
|
-
return Number(rows[0]?.count ?? 0);
|
|
2158
|
-
}
|
|
2159
|
-
async appendMessage(message, parentId) {
|
|
2160
|
-
let parent = parentId !== void 0 ? parentId : (await this.latestLeafRow())?.id ?? null;
|
|
2161
|
-
if (parent) {
|
|
2162
|
-
const { rows } = await this.conn.execute("SELECT id FROM assistant_messages WHERE id = ? AND session_id = ?", [parent, this.sessionId]);
|
|
2163
|
-
if (rows.length === 0) parent = null;
|
|
2164
|
-
}
|
|
2165
|
-
const json = JSON.stringify(message);
|
|
2166
|
-
const text = this.extractSearchableText(json);
|
|
2167
|
-
await this.conn.execute(`INSERT INTO assistant_messages (id, session_id, parent_id, role, content, text_content)
|
|
2168
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
2169
|
-
ON CONFLICT (session_id, id) DO NOTHING`, [
|
|
2170
|
-
message.id,
|
|
2171
|
-
this.sessionId,
|
|
2172
|
-
parent,
|
|
2173
|
-
message.role,
|
|
2174
|
-
json,
|
|
2175
|
-
text
|
|
2176
|
-
]);
|
|
2177
|
-
}
|
|
2178
|
-
async updateMessage(message) {
|
|
2179
|
-
const json = JSON.stringify(message);
|
|
2180
|
-
await this.conn.execute("UPDATE assistant_messages SET content = ?, text_content = ? WHERE id = ? AND session_id = ?", [
|
|
2181
|
-
json,
|
|
2182
|
-
this.extractSearchableText(json),
|
|
2183
|
-
message.id,
|
|
2184
|
-
this.sessionId
|
|
2185
|
-
]);
|
|
2186
|
-
}
|
|
2187
|
-
async deleteMessages(messageIds) {
|
|
2188
|
-
for (const id of messageIds) await this.conn.execute("DELETE FROM assistant_messages WHERE id = ? AND session_id = ?", [id, this.sessionId]);
|
|
2189
|
-
}
|
|
2190
|
-
async clearMessages() {
|
|
2191
|
-
await this.conn.execute("DELETE FROM assistant_messages WHERE session_id = ?", [this.sessionId]);
|
|
2192
|
-
await this.conn.execute("DELETE FROM assistant_compactions WHERE session_id = ?", [this.sessionId]);
|
|
2193
|
-
}
|
|
2194
|
-
async addCompaction(summary, fromMessageId, toMessageId) {
|
|
2195
|
-
const id = crypto.randomUUID();
|
|
2196
|
-
await this.conn.execute("INSERT INTO assistant_compactions (id, session_id, summary, from_message_id, to_message_id) VALUES (?, ?, ?, ?, ?)", [
|
|
2197
|
-
id,
|
|
2198
|
-
this.sessionId,
|
|
2199
|
-
summary,
|
|
2200
|
-
fromMessageId,
|
|
2201
|
-
toMessageId
|
|
2202
|
-
]);
|
|
2203
|
-
return {
|
|
2204
|
-
id,
|
|
2205
|
-
summary,
|
|
2206
|
-
fromMessageId,
|
|
2207
|
-
toMessageId,
|
|
2208
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2209
|
-
};
|
|
2210
|
-
}
|
|
2211
|
-
async getCompactions() {
|
|
2212
|
-
const { rows } = await this.conn.execute("SELECT * FROM assistant_compactions WHERE session_id = ? ORDER BY created_at ASC", [this.sessionId]);
|
|
2213
|
-
return rows.map((r) => ({
|
|
2214
|
-
id: r.id,
|
|
2215
|
-
summary: r.summary,
|
|
2216
|
-
fromMessageId: r.from_message_id,
|
|
2217
|
-
toMessageId: r.to_message_id,
|
|
2218
|
-
createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : String(r.created_at)
|
|
2219
|
-
}));
|
|
2220
|
-
}
|
|
2221
|
-
async searchMessages(query, limit = 20) {
|
|
2222
|
-
const { rows } = await this.conn.execute(`SELECT id, role, text_content FROM assistant_messages
|
|
2223
|
-
WHERE session_id = ? AND content_tsv @@ plainto_tsquery('english', ?)
|
|
2224
|
-
ORDER BY ts_rank(content_tsv, plainto_tsquery('english', ?)) DESC
|
|
2225
|
-
LIMIT ?`, [
|
|
2226
|
-
this.sessionId,
|
|
2227
|
-
query,
|
|
2228
|
-
query,
|
|
2229
|
-
limit
|
|
2230
|
-
]);
|
|
2231
|
-
return rows.map((r) => ({
|
|
2232
|
-
id: r.id,
|
|
2233
|
-
role: r.role,
|
|
2234
|
-
content: r.text_content ?? "",
|
|
2235
|
-
createdAt: ""
|
|
2236
|
-
}));
|
|
2237
|
-
}
|
|
2238
|
-
async latestLeafRow() {
|
|
2239
|
-
const { rows } = await this.conn.execute(`SELECT m.id, m.content FROM assistant_messages m
|
|
2240
|
-
LEFT JOIN assistant_messages c ON c.parent_id = m.id AND c.session_id = ?
|
|
2241
|
-
WHERE c.id IS NULL AND m.session_id = ?
|
|
2242
|
-
ORDER BY m.created_at DESC LIMIT 1`, [this.sessionId, this.sessionId]);
|
|
2243
|
-
return rows[0] ?? null;
|
|
2244
|
-
}
|
|
2245
|
-
applyCompactions(messages, compactions) {
|
|
2246
|
-
const ids = messages.map((m) => m.id);
|
|
2247
|
-
const result = [];
|
|
2248
|
-
let i = 0;
|
|
2249
|
-
while (i < messages.length) {
|
|
2250
|
-
const matching = compactions.filter((c) => c.fromMessageId === ids[i]);
|
|
2251
|
-
const comp = matching.length > 1 ? matching[matching.length - 1] : matching[0];
|
|
2252
|
-
if (comp) {
|
|
2253
|
-
const endIdx = ids.indexOf(comp.toMessageId);
|
|
2254
|
-
if (endIdx >= i) {
|
|
2255
|
-
result.push({
|
|
2256
|
-
id: `compaction_${comp.id}`,
|
|
2257
|
-
role: "assistant",
|
|
2258
|
-
parts: [{
|
|
2259
|
-
type: "text",
|
|
2260
|
-
text: comp.summary
|
|
2261
|
-
}],
|
|
2262
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
2263
|
-
});
|
|
2264
|
-
i = endIdx + 1;
|
|
2265
|
-
continue;
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
result.push(messages[i]);
|
|
2269
|
-
i++;
|
|
2270
|
-
}
|
|
2271
|
-
return result;
|
|
2272
|
-
}
|
|
2273
|
-
parse(json) {
|
|
2274
|
-
try {
|
|
2275
|
-
const msg = JSON.parse(json);
|
|
2276
|
-
if (typeof msg?.id === "string" && typeof msg?.role === "string" && Array.isArray(msg?.parts)) return msg;
|
|
2277
|
-
} catch {}
|
|
2278
|
-
return null;
|
|
2279
|
-
}
|
|
2280
|
-
parseRows(rows) {
|
|
2281
|
-
const result = [];
|
|
2282
|
-
for (const row of rows) {
|
|
2283
|
-
const msg = this.parse(row.content);
|
|
2284
|
-
if (msg) result.push(msg);
|
|
2285
|
-
}
|
|
2286
|
-
return result;
|
|
2287
|
-
}
|
|
2288
|
-
/**
|
|
2289
|
-
* Extract just the human-readable text from a message's JSON blob
|
|
2290
|
-
* and store it in `text_content`, which feeds the generated `content_tsv`
|
|
2291
|
-
* column used for FTS. The full structured message (parts, tool calls,
|
|
2292
|
-
* metadata) is still stored verbatim in `content` — this is the source
|
|
2293
|
-
* of truth. Indexing the raw JSON would return FTS hits on keys like
|
|
2294
|
-
* `"role"`, `"parts"`, `"dynamic-tool"`, etc.
|
|
2295
|
-
*/
|
|
2296
|
-
extractSearchableText(json) {
|
|
2297
|
-
const msg = this.parse(json);
|
|
2298
|
-
if (!msg) return json;
|
|
2299
|
-
return msg.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
2300
|
-
}
|
|
2301
|
-
};
|
|
2302
|
-
//#endregion
|
|
2303
|
-
//#region src/experimental/memory/session/providers/postgres-context.ts
|
|
2304
|
-
var PostgresContextProvider = class {
|
|
2305
|
-
/**
|
|
2306
|
-
* @param client A raw `pg.Client` (recommended) or any `PostgresConnection`.
|
|
2307
|
-
* Must already be connected.
|
|
2308
|
-
* @param label Block label used as the primary key row in
|
|
2309
|
-
* `cf_agents_context_blocks`. Pass a session-scoped label (e.g.
|
|
2310
|
-
* `` `memory_${sessionId}` ``) for per-session state.
|
|
2311
|
-
*/
|
|
2312
|
-
constructor(client, label) {
|
|
2313
|
-
this.conn = toPostgresConnection(client);
|
|
2314
|
-
this.label = label;
|
|
2315
|
-
}
|
|
2316
|
-
async get() {
|
|
2317
|
-
const { rows } = await this.conn.execute("SELECT content FROM cf_agents_context_blocks WHERE label = ?", [this.label]);
|
|
2318
|
-
return rows[0]?.content ?? null;
|
|
2319
|
-
}
|
|
2320
|
-
async set(content) {
|
|
2321
|
-
await this.conn.execute(`INSERT INTO cf_agents_context_blocks (label, content)
|
|
2322
|
-
VALUES (?, ?)
|
|
2323
|
-
ON CONFLICT (label) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW()`, [this.label, content]);
|
|
2324
|
-
}
|
|
2325
|
-
};
|
|
2326
|
-
//#endregion
|
|
2327
|
-
//#region src/experimental/memory/session/providers/postgres-search.ts
|
|
2328
|
-
var PostgresSearchProvider = class {
|
|
2329
|
-
/**
|
|
2330
|
-
* @param client A raw `pg.Client` (recommended) or any `PostgresConnection`.
|
|
2331
|
-
* Must already be connected.
|
|
2332
|
-
*/
|
|
2333
|
-
constructor(client) {
|
|
2334
|
-
this.label = "";
|
|
2335
|
-
this.conn = toPostgresConnection(client);
|
|
2336
|
-
}
|
|
2337
|
-
init(label) {
|
|
2338
|
-
this.label = label;
|
|
2339
|
-
}
|
|
2340
|
-
async get() {
|
|
2341
|
-
const { rows } = await this.conn.execute("SELECT COUNT(*) as count FROM cf_agents_search_entries WHERE label = ?", [this.label]);
|
|
2342
|
-
const count = Number(rows[0]?.count ?? 0);
|
|
2343
|
-
if (count === 0) return null;
|
|
2344
|
-
return `${count} entries indexed.`;
|
|
2345
|
-
}
|
|
2346
|
-
async search(query) {
|
|
2347
|
-
if (!query.trim()) return null;
|
|
2348
|
-
const { rows } = await this.conn.execute(`SELECT key, content FROM cf_agents_search_entries
|
|
2349
|
-
WHERE label = ? AND content_tsv @@ plainto_tsquery('english', ?)
|
|
2350
|
-
ORDER BY ts_rank(content_tsv, plainto_tsquery('english', ?)) DESC
|
|
2351
|
-
LIMIT 10`, [
|
|
2352
|
-
this.label,
|
|
2353
|
-
query,
|
|
2354
|
-
query
|
|
2355
|
-
]);
|
|
2356
|
-
if (rows.length === 0) return "No results found.";
|
|
2357
|
-
return rows.map((r) => `[${r.key}]\n${r.content}`).join("\n\n");
|
|
2358
|
-
}
|
|
2359
|
-
async set(key, content) {
|
|
2360
|
-
await this.conn.execute(`INSERT INTO cf_agents_search_entries (label, key, content)
|
|
2361
|
-
VALUES (?, ?, ?)
|
|
2362
|
-
ON CONFLICT (label, key) DO UPDATE SET
|
|
2363
|
-
content = EXCLUDED.content,
|
|
2364
|
-
updated_at = NOW()`, [
|
|
2365
|
-
this.label,
|
|
2366
|
-
key,
|
|
2367
|
-
content
|
|
2368
|
-
]);
|
|
2369
|
-
}
|
|
2370
|
-
};
|
|
2371
|
-
//#endregion
|
|
2372
|
-
export { AgentContextProvider, AgentSearchProvider, AgentSessionProvider, PostgresContextProvider, PostgresSearchProvider, PostgresSessionProvider, R2SkillProvider, Session, SessionManager, isSearchProvider, isSkillProvider, isWritableProvider };
|
|
2373
|
-
|
|
2374
|
-
//# sourceMappingURL=index.js.map
|