@librechat/agents 3.1.86 → 3.1.87
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 +69 -0
- package/dist/cjs/events.cjs +23 -0
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +133 -18
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +251 -53
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +1 -5
- package/dist/cjs/llm/init.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +113 -24
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +3 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +18 -5
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/openai/index.cjs +253 -0
- package/dist/cjs/openai/index.cjs.map +1 -0
- package/dist/cjs/responses/index.cjs +448 -0
- package/dist/cjs/responses/index.cjs.map +1 -0
- package/dist/cjs/run.cjs +108 -7
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1057 -0
- package/dist/cjs/session/AgentSession.cjs.map +1 -0
- package/dist/cjs/session/JsonlSessionStore.cjs +425 -0
- package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -0
- package/dist/cjs/session/handlers.cjs +221 -0
- package/dist/cjs/session/handlers.cjs.map +1 -0
- package/dist/cjs/session/ids.cjs +22 -0
- package/dist/cjs/session/ids.cjs.map +1 -0
- package/dist/cjs/session/messageSerialization.cjs +179 -0
- package/dist/cjs/session/messageSerialization.cjs.map +1 -0
- package/dist/cjs/stream.cjs +472 -11
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +1 -1
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +177 -59
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +113 -0
- package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -0
- package/dist/cjs/tools/handlers.cjs +1 -1
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/tools/streamedToolCallSeals.cjs +42 -0
- package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -0
- package/dist/esm/events.mjs +23 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +133 -18
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +251 -53
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -5
- package/dist/esm/llm/init.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +113 -25
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -2
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +5 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/openai/index.mjs +246 -0
- package/dist/esm/openai/index.mjs.map +1 -0
- package/dist/esm/responses/index.mjs +440 -0
- package/dist/esm/responses/index.mjs.map +1 -0
- package/dist/esm/run.mjs +108 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1054 -0
- package/dist/esm/session/AgentSession.mjs.map +1 -0
- package/dist/esm/session/JsonlSessionStore.mjs +422 -0
- package/dist/esm/session/JsonlSessionStore.mjs.map +1 -0
- package/dist/esm/session/handlers.mjs +219 -0
- package/dist/esm/session/handlers.mjs.map +1 -0
- package/dist/esm/session/ids.mjs +17 -0
- package/dist/esm/session/ids.mjs.map +1 -0
- package/dist/esm/session/messageSerialization.mjs +173 -0
- package/dist/esm/session/messageSerialization.mjs.map +1 -0
- package/dist/esm/stream.mjs +473 -12
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +1 -1
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +177 -59
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +107 -0
- package/dist/esm/tools/eagerEventExecution.mjs.map +1 -0
- package/dist/esm/tools/handlers.mjs +1 -1
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/tools/streamedToolCallSeals.mjs +36 -0
- package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -0
- package/dist/types/events.d.ts +1 -0
- package/dist/types/graphs/Graph.d.ts +24 -9
- package/dist/types/index.d.ts +1 -0
- package/dist/types/llm/openai/index.d.ts +1 -0
- package/dist/types/openai/index.d.ts +75 -0
- package/dist/types/responses/index.d.ts +97 -0
- package/dist/types/run.d.ts +2 -0
- package/dist/types/session/AgentSession.d.ts +32 -0
- package/dist/types/session/JsonlSessionStore.d.ts +67 -0
- package/dist/types/session/handlers.d.ts +8 -0
- package/dist/types/session/ids.d.ts +4 -0
- package/dist/types/session/index.d.ts +5 -0
- package/dist/types/session/messageSerialization.d.ts +7 -0
- package/dist/types/session/types.d.ts +191 -0
- package/dist/types/tools/ToolNode.d.ts +12 -1
- package/dist/types/tools/eagerEventExecution.d.ts +23 -0
- package/dist/types/tools/streamedToolCallSeals.d.ts +13 -0
- package/dist/types/types/hitl.d.ts +4 -0
- package/dist/types/types/run.d.ts +11 -1
- package/dist/types/types/tools.d.ts +36 -0
- package/package.json +19 -2
- package/src/__tests__/stream.eagerEventExecution.test.ts +2458 -0
- package/src/events.ts +29 -0
- package/src/graphs/Graph.ts +224 -50
- package/src/graphs/MultiAgentGraph.ts +1 -1
- package/src/graphs/__tests__/composition.smoke.test.ts +30 -0
- package/src/index.ts +3 -0
- package/src/llm/anthropic/index.ts +356 -84
- package/src/llm/anthropic/llm.spec.ts +64 -0
- package/src/llm/custom-chat-models.smoke.test.ts +175 -4
- package/src/llm/openai/contentBlocks.test.ts +35 -0
- package/src/llm/openai/deepseek.test.ts +201 -2
- package/src/llm/openai/index.ts +171 -26
- package/src/llm/openai/utils/index.ts +22 -0
- package/src/llm/openrouter/index.ts +4 -2
- package/src/openai/__tests__/openai.test.ts +337 -0
- package/src/openai/index.ts +404 -0
- package/src/responses/__tests__/responses.test.ts +652 -0
- package/src/responses/index.ts +677 -0
- package/src/run.ts +158 -8
- package/src/scripts/compare_pi_vs_ours.ts +592 -173
- package/src/scripts/session_live.ts +548 -0
- package/src/session/AgentSession.ts +1432 -0
- package/src/session/JsonlSessionStore.ts +572 -0
- package/src/session/__tests__/JsonlSessionStore.test.ts +1410 -0
- package/src/session/__tests__/handlers.test.ts +161 -0
- package/src/session/handlers.ts +272 -0
- package/src/session/ids.ts +17 -0
- package/src/session/index.ts +44 -0
- package/src/session/messageSerialization.ts +207 -0
- package/src/session/types.ts +275 -0
- package/src/specs/custom-event-await.test.ts +89 -0
- package/src/specs/summarization.test.ts +1 -1
- package/src/stream.ts +755 -48
- package/src/summarization/node.ts +1 -1
- package/src/tools/ToolNode.ts +299 -126
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +373 -0
- package/src/tools/__tests__/handlers.test.ts +2 -1
- package/src/tools/__tests__/hitl.test.ts +206 -110
- package/src/tools/eagerEventExecution.ts +153 -0
- package/src/tools/handlers.ts +8 -4
- package/src/tools/streamedToolCallSeals.ts +57 -0
- package/src/types/hitl.ts +4 -0
- package/src/types/run.ts +11 -0
- package/src/types/tools.ts +36 -0
- package/dist/cjs/llm/text.cjs +0 -69
- package/dist/cjs/llm/text.cjs.map +0 -1
- package/dist/esm/llm/text.mjs +0 -67
- package/dist/esm/llm/text.mjs.map +0 -1
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'path';
|
|
4
|
+
import {
|
|
5
|
+
access,
|
|
6
|
+
appendFile,
|
|
7
|
+
mkdir,
|
|
8
|
+
readFile,
|
|
9
|
+
readdir,
|
|
10
|
+
stat,
|
|
11
|
+
writeFile,
|
|
12
|
+
} from 'fs/promises';
|
|
13
|
+
import type {
|
|
14
|
+
CreateSessionFileOptions,
|
|
15
|
+
SessionBranchOptions,
|
|
16
|
+
SessionEntry,
|
|
17
|
+
SessionForkOptions,
|
|
18
|
+
SessionHeader,
|
|
19
|
+
SessionLabelEntry,
|
|
20
|
+
SessionListItem,
|
|
21
|
+
SessionMessageEntry,
|
|
22
|
+
SessionCheckpointEntry,
|
|
23
|
+
SessionCompactionEntry,
|
|
24
|
+
SessionRunEventEntry,
|
|
25
|
+
SessionSummaryEntry,
|
|
26
|
+
SessionStateEntry,
|
|
27
|
+
SessionTreeNode,
|
|
28
|
+
} from './types';
|
|
29
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
30
|
+
import { SystemMessage } from '@langchain/core/messages';
|
|
31
|
+
import { createEntryId, createSessionId, createTimestamp } from './ids';
|
|
32
|
+
import {
|
|
33
|
+
deserializeMessage,
|
|
34
|
+
getMessageRole,
|
|
35
|
+
serializeMessage,
|
|
36
|
+
toJsonValue,
|
|
37
|
+
} from './messageSerialization';
|
|
38
|
+
|
|
39
|
+
const SESSION_VERSION = 1;
|
|
40
|
+
const DEFAULT_SESSION_ROOT = join(
|
|
41
|
+
homedir(),
|
|
42
|
+
'.librechat',
|
|
43
|
+
'agents',
|
|
44
|
+
'sessions'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
function sanitizeCwd(cwd: string): string {
|
|
48
|
+
const normalized = resolve(cwd);
|
|
49
|
+
return createHash('sha256').update(normalized).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createSessionPath(options: CreateSessionFileOptions): string {
|
|
53
|
+
const sessionId = options.sessionId ?? createSessionId();
|
|
54
|
+
const fileName = `${new Date().toISOString().replace(/[:.]/g, '-')}_${sessionId}.jsonl`;
|
|
55
|
+
return join(DEFAULT_SESSION_ROOT, sanitizeCwd(options.cwd), fileName);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
59
|
+
try {
|
|
60
|
+
await access(path);
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseLine(line: string): SessionHeader | SessionEntry | undefined {
|
|
68
|
+
if (line.trim() === '') {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(line) as SessionHeader | SessionEntry;
|
|
73
|
+
if (parsed.type === 'session') {
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
if ('id' in parsed && 'parentId' in parsed && 'data' in parsed) {
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createHeader(options: CreateSessionFileOptions): SessionHeader {
|
|
86
|
+
return {
|
|
87
|
+
type: 'session',
|
|
88
|
+
version: SESSION_VERSION,
|
|
89
|
+
id: options.sessionId ?? createSessionId(),
|
|
90
|
+
timestamp: createTimestamp(),
|
|
91
|
+
cwd: resolve(options.cwd),
|
|
92
|
+
...(options.name != null && options.name !== ''
|
|
93
|
+
? { name: options.name }
|
|
94
|
+
: {}),
|
|
95
|
+
...(options.parentSession != null && options.parentSession !== ''
|
|
96
|
+
? { parentSession: options.parentSession }
|
|
97
|
+
: {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sortEntries(entries: SessionEntry[]): SessionEntry[] {
|
|
102
|
+
return [...entries].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class JsonlSessionStore {
|
|
106
|
+
readonly path: string;
|
|
107
|
+
readonly header: SessionHeader;
|
|
108
|
+
private entries: SessionEntry[];
|
|
109
|
+
|
|
110
|
+
private constructor(params: {
|
|
111
|
+
path: string;
|
|
112
|
+
header: SessionHeader;
|
|
113
|
+
entries: SessionEntry[];
|
|
114
|
+
}) {
|
|
115
|
+
this.path = params.path;
|
|
116
|
+
this.header = params.header;
|
|
117
|
+
this.entries = sortEntries(params.entries);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static getDefaultRoot(): string {
|
|
121
|
+
return DEFAULT_SESSION_ROOT;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static async create(
|
|
125
|
+
options: CreateSessionFileOptions & { path?: string }
|
|
126
|
+
): Promise<JsonlSessionStore> {
|
|
127
|
+
const sessionId = options.sessionId ?? createSessionId();
|
|
128
|
+
const path =
|
|
129
|
+
options.path != null && options.path !== ''
|
|
130
|
+
? resolve(options.path)
|
|
131
|
+
: createSessionPath({ ...options, sessionId });
|
|
132
|
+
const header = createHeader({ ...options, sessionId });
|
|
133
|
+
await mkdir(dirname(path), { recursive: true });
|
|
134
|
+
await writeFile(path, `${JSON.stringify(header)}\n`, {
|
|
135
|
+
encoding: 'utf8',
|
|
136
|
+
flag: 'wx',
|
|
137
|
+
});
|
|
138
|
+
return new JsonlSessionStore({ path, header, entries: [] });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static async open(pathOrId: string): Promise<JsonlSessionStore> {
|
|
142
|
+
const path = await JsonlSessionStore.resolvePath(pathOrId);
|
|
143
|
+
return JsonlSessionStore.openPath(path);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static async openPath(path: string): Promise<JsonlSessionStore> {
|
|
147
|
+
const resolved = resolve(path);
|
|
148
|
+
const raw = await readFile(resolved, 'utf8');
|
|
149
|
+
const parsed = raw
|
|
150
|
+
.split('\n')
|
|
151
|
+
.map((line) => parseLine(line))
|
|
152
|
+
.filter((line): line is SessionHeader | SessionEntry => line != null);
|
|
153
|
+
const header = parsed.find(
|
|
154
|
+
(line): line is SessionHeader => line.type === 'session'
|
|
155
|
+
);
|
|
156
|
+
if (!header) {
|
|
157
|
+
throw new Error(`Invalid session file: ${resolved}`);
|
|
158
|
+
}
|
|
159
|
+
const entries = parsed.filter(
|
|
160
|
+
(line): line is SessionEntry => line.type !== 'session'
|
|
161
|
+
);
|
|
162
|
+
return new JsonlSessionStore({ path: resolved, header, entries });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
static async resolvePath(pathOrId: string): Promise<string> {
|
|
166
|
+
const candidate = isAbsolute(pathOrId) ? pathOrId : resolve(pathOrId);
|
|
167
|
+
if (await pathExists(candidate)) {
|
|
168
|
+
return candidate;
|
|
169
|
+
}
|
|
170
|
+
const sessions = await JsonlSessionStore.listAll();
|
|
171
|
+
const matches = sessions.filter(
|
|
172
|
+
(item) =>
|
|
173
|
+
item.id.startsWith(pathOrId) || basename(item.path).includes(pathOrId)
|
|
174
|
+
);
|
|
175
|
+
if (matches.length === 1) {
|
|
176
|
+
return matches[0].path;
|
|
177
|
+
}
|
|
178
|
+
if (matches.length > 1) {
|
|
179
|
+
throw new Error(`Session id "${pathOrId}" is ambiguous`);
|
|
180
|
+
}
|
|
181
|
+
throw new Error(`Session not found: ${pathOrId}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static async list(cwd: string): Promise<SessionListItem[]> {
|
|
185
|
+
const dir = join(DEFAULT_SESSION_ROOT, sanitizeCwd(cwd));
|
|
186
|
+
return JsonlSessionStore.listDirectory(dir);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static async listAll(
|
|
190
|
+
root: string = DEFAULT_SESSION_ROOT
|
|
191
|
+
): Promise<SessionListItem[]> {
|
|
192
|
+
if (!(await pathExists(root))) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const dirs = await readdir(root, { withFileTypes: true });
|
|
196
|
+
const lists = await Promise.all(
|
|
197
|
+
dirs
|
|
198
|
+
.filter((entry) => entry.isDirectory())
|
|
199
|
+
.map((entry) => JsonlSessionStore.listDirectory(join(root, entry.name)))
|
|
200
|
+
);
|
|
201
|
+
return lists.flat().sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private static async listDirectory(dir: string): Promise<SessionListItem[]> {
|
|
205
|
+
if (!(await pathExists(dir))) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
const files = await readdir(dir, { withFileTypes: true });
|
|
209
|
+
const candidates = files
|
|
210
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
211
|
+
.map((entry) => join(dir, entry.name));
|
|
212
|
+
const items = await Promise.all(
|
|
213
|
+
candidates.map(async (path): Promise<SessionListItem | undefined> => {
|
|
214
|
+
try {
|
|
215
|
+
const store = await JsonlSessionStore.open(path);
|
|
216
|
+
return {
|
|
217
|
+
id: store.header.id,
|
|
218
|
+
path,
|
|
219
|
+
cwd: store.header.cwd,
|
|
220
|
+
timestamp: store.header.timestamp,
|
|
221
|
+
name: store.header.name,
|
|
222
|
+
leafId: store.getLeafEntry()?.id ?? null,
|
|
223
|
+
};
|
|
224
|
+
} catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
return items
|
|
230
|
+
.filter((item): item is SessionListItem => item != null)
|
|
231
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getEntries(): SessionEntry[] {
|
|
235
|
+
return [...this.entries];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getEntry(id: string): SessionEntry | undefined {
|
|
239
|
+
return this.entries.find((entry) => entry.id === id);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getChildren(id: string): SessionEntry[] {
|
|
243
|
+
return this.entries.filter((entry) => entry.parentId === id);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
getTree(): SessionTreeNode[] {
|
|
247
|
+
const byParent = new Map<string | null, SessionEntry[]>();
|
|
248
|
+
for (const entry of this.entries) {
|
|
249
|
+
const siblings = byParent.get(entry.parentId) ?? [];
|
|
250
|
+
siblings.push(entry);
|
|
251
|
+
byParent.set(entry.parentId, siblings);
|
|
252
|
+
}
|
|
253
|
+
const build = (parentId: string | null): SessionTreeNode[] =>
|
|
254
|
+
(byParent.get(parentId) ?? []).map((entry) => ({
|
|
255
|
+
entry,
|
|
256
|
+
children: build(entry.id),
|
|
257
|
+
}));
|
|
258
|
+
return build(null);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
getLeafEntry(): SessionEntry | undefined {
|
|
262
|
+
const state = [...this.entries]
|
|
263
|
+
.reverse()
|
|
264
|
+
.find(
|
|
265
|
+
(entry): entry is SessionStateEntry => entry.type === 'session_state'
|
|
266
|
+
);
|
|
267
|
+
if (state) {
|
|
268
|
+
return state.data.leafId == null
|
|
269
|
+
? undefined
|
|
270
|
+
: this.getEntry(state.data.leafId);
|
|
271
|
+
}
|
|
272
|
+
return [...this.entries]
|
|
273
|
+
.reverse()
|
|
274
|
+
.find((entry) => entry.type === 'message' || entry.type === 'summary');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getPath(
|
|
278
|
+
entryId: string | undefined = this.getLeafEntry()?.id
|
|
279
|
+
): SessionEntry[] {
|
|
280
|
+
if (entryId == null || entryId === '') {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
const byId = new Map(this.entries.map((entry) => [entry.id, entry]));
|
|
284
|
+
const path: SessionEntry[] = [];
|
|
285
|
+
let current: SessionEntry | undefined = byId.get(entryId);
|
|
286
|
+
while (current) {
|
|
287
|
+
path.push(current);
|
|
288
|
+
current =
|
|
289
|
+
current.parentId == null ? undefined : byId.get(current.parentId);
|
|
290
|
+
}
|
|
291
|
+
return path.reverse();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
getMessages(entryId?: string): BaseMessage[] {
|
|
295
|
+
const messages: BaseMessage[] = [];
|
|
296
|
+
for (const entry of this.getPath(entryId)) {
|
|
297
|
+
if (entry.type === 'message') {
|
|
298
|
+
messages.push(deserializeMessage(entry.data.message));
|
|
299
|
+
} else if (entry.type === 'summary') {
|
|
300
|
+
messages.push(new SystemMessage(entry.data.text));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return messages;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
getForkPoints(): SessionMessageEntry[] {
|
|
307
|
+
return this.entries.filter(
|
|
308
|
+
(entry): entry is SessionMessageEntry =>
|
|
309
|
+
entry.type === 'message' && entry.data.role === 'user'
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
getLabel(entryId: string): string | undefined {
|
|
314
|
+
return [...this.entries]
|
|
315
|
+
.reverse()
|
|
316
|
+
.find(
|
|
317
|
+
(entry): entry is SessionLabelEntry =>
|
|
318
|
+
entry.type === 'label' && entry.data.targetEntryId === entryId
|
|
319
|
+
)?.data.label;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async setLabel(entryId: string, label: string): Promise<SessionLabelEntry> {
|
|
323
|
+
return this.appendEntry<SessionLabelEntry>({
|
|
324
|
+
type: 'label',
|
|
325
|
+
parentId: this.getLeafEntry()?.id ?? null,
|
|
326
|
+
data: { targetEntryId: entryId, label },
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async appendMessage(
|
|
331
|
+
message: BaseMessage,
|
|
332
|
+
parentId: string | null = this.getLeafEntry()?.id ?? null
|
|
333
|
+
): Promise<SessionMessageEntry> {
|
|
334
|
+
const entry = await this.appendEntry<SessionMessageEntry>({
|
|
335
|
+
type: 'message',
|
|
336
|
+
parentId,
|
|
337
|
+
data: {
|
|
338
|
+
role: getMessageRole(message),
|
|
339
|
+
message: serializeMessage(message),
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
await this.setLeaf(entry.id);
|
|
343
|
+
return entry;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async appendRunEvent(
|
|
347
|
+
event: string,
|
|
348
|
+
payload?: unknown,
|
|
349
|
+
params?: { runId?: string; threadId?: string }
|
|
350
|
+
): Promise<SessionRunEventEntry> {
|
|
351
|
+
return this.appendEntry<SessionRunEventEntry>({
|
|
352
|
+
type: 'run_event',
|
|
353
|
+
parentId: this.getLeafEntry()?.id ?? null,
|
|
354
|
+
data: {
|
|
355
|
+
event,
|
|
356
|
+
...(params?.runId != null && params.runId !== ''
|
|
357
|
+
? { runId: params.runId }
|
|
358
|
+
: {}),
|
|
359
|
+
...(params?.threadId != null && params.threadId !== ''
|
|
360
|
+
? { threadId: params.threadId }
|
|
361
|
+
: {}),
|
|
362
|
+
...(typeof payload !== 'undefined'
|
|
363
|
+
? { payload: toJsonValue(payload) }
|
|
364
|
+
: {}),
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async setLeaf(leafId: string | null): Promise<SessionStateEntry> {
|
|
370
|
+
return this.appendEntry<SessionStateEntry>({
|
|
371
|
+
type: 'session_state',
|
|
372
|
+
parentId: leafId,
|
|
373
|
+
data: { leafId },
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async appendEntryForCompaction(params: {
|
|
378
|
+
text: string;
|
|
379
|
+
tokenCount?: number;
|
|
380
|
+
retainedEntryIds: string[];
|
|
381
|
+
summarizedEntryIds: string[];
|
|
382
|
+
instructions?: string;
|
|
383
|
+
parentId?: string | null;
|
|
384
|
+
}): Promise<SessionSummaryEntry> {
|
|
385
|
+
const tokenCount =
|
|
386
|
+
typeof params.tokenCount === 'number' &&
|
|
387
|
+
Number.isFinite(params.tokenCount) &&
|
|
388
|
+
params.tokenCount >= 0
|
|
389
|
+
? params.tokenCount
|
|
390
|
+
: undefined;
|
|
391
|
+
const entry = await this.appendEntry<SessionSummaryEntry>({
|
|
392
|
+
type: 'summary',
|
|
393
|
+
parentId:
|
|
394
|
+
'parentId' in params
|
|
395
|
+
? (params.parentId ?? null)
|
|
396
|
+
: (this.getLeafEntry()?.id ?? null),
|
|
397
|
+
data: {
|
|
398
|
+
text: params.text,
|
|
399
|
+
...(tokenCount != null ? { tokenCount } : {}),
|
|
400
|
+
retainedEntryIds: params.retainedEntryIds,
|
|
401
|
+
summarizedEntryIds: params.summarizedEntryIds,
|
|
402
|
+
...(params.instructions != null && params.instructions !== ''
|
|
403
|
+
? { instructions: params.instructions }
|
|
404
|
+
: {}),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
await this.setLeaf(entry.id);
|
|
408
|
+
return entry;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async appendCompactionEntry(params: {
|
|
412
|
+
summaryEntryId: string;
|
|
413
|
+
retainedEntryIds: string[];
|
|
414
|
+
summarizedEntryIds: string[];
|
|
415
|
+
}): Promise<SessionCompactionEntry> {
|
|
416
|
+
return this.appendEntry<SessionCompactionEntry>({
|
|
417
|
+
type: 'compaction',
|
|
418
|
+
parentId: this.getLeafEntry()?.id ?? null,
|
|
419
|
+
data: {
|
|
420
|
+
summaryEntryId: params.summaryEntryId,
|
|
421
|
+
retainedEntryIds: params.retainedEntryIds,
|
|
422
|
+
summarizedEntryIds: params.summarizedEntryIds,
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async appendCheckpoint(params: {
|
|
428
|
+
source: SessionCheckpointEntry['data']['source'];
|
|
429
|
+
threadId: string;
|
|
430
|
+
runId?: string;
|
|
431
|
+
checkpointId?: string;
|
|
432
|
+
checkpointNs?: string;
|
|
433
|
+
parentCheckpointId?: string;
|
|
434
|
+
reason?: string;
|
|
435
|
+
}): Promise<SessionCheckpointEntry> {
|
|
436
|
+
return this.appendEntry<SessionCheckpointEntry>({
|
|
437
|
+
type: 'checkpoint',
|
|
438
|
+
parentId: this.getLeafEntry()?.id ?? null,
|
|
439
|
+
data: {
|
|
440
|
+
provider: 'langgraph',
|
|
441
|
+
source: params.source,
|
|
442
|
+
threadId: params.threadId,
|
|
443
|
+
...(params.runId != null && params.runId !== ''
|
|
444
|
+
? { runId: params.runId }
|
|
445
|
+
: {}),
|
|
446
|
+
...(params.checkpointId != null && params.checkpointId !== ''
|
|
447
|
+
? { checkpointId: params.checkpointId }
|
|
448
|
+
: {}),
|
|
449
|
+
...(params.checkpointNs != null
|
|
450
|
+
? { checkpointNs: params.checkpointNs }
|
|
451
|
+
: {}),
|
|
452
|
+
...(params.parentCheckpointId != null &&
|
|
453
|
+
params.parentCheckpointId !== ''
|
|
454
|
+
? { parentCheckpointId: params.parentCheckpointId }
|
|
455
|
+
: {}),
|
|
456
|
+
...(params.reason != null && params.reason !== ''
|
|
457
|
+
? { reason: params.reason }
|
|
458
|
+
: {}),
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
getCheckpoints(threadId?: string): SessionCheckpointEntry[] {
|
|
464
|
+
return this.entries.filter(
|
|
465
|
+
(entry): entry is SessionCheckpointEntry =>
|
|
466
|
+
entry.type === 'checkpoint' &&
|
|
467
|
+
(threadId == null || entry.data.threadId === threadId)
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
getLatestCheckpoint(threadId?: string): SessionCheckpointEntry | undefined {
|
|
472
|
+
const checkpoints = this.getCheckpoints(threadId);
|
|
473
|
+
for (let i = checkpoints.length - 1; i >= 0; i--) {
|
|
474
|
+
const checkpoint = checkpoints[i];
|
|
475
|
+
if (checkpoint.data.source === 'reset') {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
return checkpoint;
|
|
479
|
+
}
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async branch(
|
|
484
|
+
entryId: string,
|
|
485
|
+
options: SessionBranchOptions = {}
|
|
486
|
+
): Promise<SessionEntry | undefined> {
|
|
487
|
+
const target = this.getBranchTarget(entryId, options.position ?? 'at');
|
|
488
|
+
await this.setLeaf(target?.id ?? null);
|
|
489
|
+
return target;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async createBranchedSession(
|
|
493
|
+
entryId: string | undefined = this.getLeafEntry()?.id,
|
|
494
|
+
options: SessionForkOptions = {}
|
|
495
|
+
): Promise<JsonlSessionStore> {
|
|
496
|
+
const target =
|
|
497
|
+
entryId != null && entryId !== ''
|
|
498
|
+
? this.getBranchTarget(entryId, options.position ?? 'at')
|
|
499
|
+
: this.getLeafEntry();
|
|
500
|
+
const newStore = await JsonlSessionStore.create({
|
|
501
|
+
cwd: options.cwd ?? this.header.cwd,
|
|
502
|
+
name: options.name ?? this.header.name,
|
|
503
|
+
parentSession: this.path,
|
|
504
|
+
});
|
|
505
|
+
const pathEntries = target ? this.getPath(target.id) : [];
|
|
506
|
+
for (const entry of pathEntries) {
|
|
507
|
+
await newStore.appendExistingEntry(entry);
|
|
508
|
+
}
|
|
509
|
+
await newStore.setLeaf(target?.id ?? null);
|
|
510
|
+
return newStore;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async clone(options: SessionForkOptions = {}): Promise<JsonlSessionStore> {
|
|
514
|
+
return this.createBranchedSession(this.getLeafEntry()?.id, {
|
|
515
|
+
...options,
|
|
516
|
+
position: 'at',
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async fork(
|
|
521
|
+
entryId: string,
|
|
522
|
+
options: SessionForkOptions = {}
|
|
523
|
+
): Promise<JsonlSessionStore> {
|
|
524
|
+
return this.createBranchedSession(entryId, {
|
|
525
|
+
...options,
|
|
526
|
+
position: options.position ?? 'before',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private getBranchTarget(
|
|
531
|
+
entryId: string,
|
|
532
|
+
position: 'before' | 'at'
|
|
533
|
+
): SessionEntry | undefined {
|
|
534
|
+
const entry = this.getEntry(entryId);
|
|
535
|
+
if (!entry) {
|
|
536
|
+
throw new Error(`Entry not found: ${entryId}`);
|
|
537
|
+
}
|
|
538
|
+
if (position === 'at') {
|
|
539
|
+
return entry;
|
|
540
|
+
}
|
|
541
|
+
return entry.parentId == null ? undefined : this.getEntry(entry.parentId);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private async appendExistingEntry(entry: SessionEntry): Promise<void> {
|
|
545
|
+
this.entries.push(entry);
|
|
546
|
+
await appendFile(this.path, `${JSON.stringify(entry)}\n`, 'utf8');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private async appendEntry<TEntry extends SessionEntry>(params: {
|
|
550
|
+
type: TEntry['type'];
|
|
551
|
+
parentId: string | null;
|
|
552
|
+
data: TEntry['data'];
|
|
553
|
+
}): Promise<TEntry> {
|
|
554
|
+
const entry = {
|
|
555
|
+
type: params.type,
|
|
556
|
+
id: createEntryId(),
|
|
557
|
+
parentId: params.parentId,
|
|
558
|
+
timestamp: createTimestamp(),
|
|
559
|
+
data: params.data,
|
|
560
|
+
} as TEntry;
|
|
561
|
+
this.entries.push(entry);
|
|
562
|
+
await appendFile(this.path, `${JSON.stringify(entry)}\n`, 'utf8');
|
|
563
|
+
return entry;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async existsOnDisk(): Promise<boolean> {
|
|
567
|
+
const fileStat = await stat(this.path).catch(() => undefined);
|
|
568
|
+
return fileStat?.isFile() === true;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export const SessionManager = JsonlSessionStore;
|