@agentex/agent 0.0.28 → 0.0.29
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/CHANGELOG.md +30 -0
- package/README.md +40 -1
- package/dist/history/fs.d.ts +36 -0
- package/dist/history/fs.d.ts.map +1 -0
- package/dist/history/fs.js +266 -0
- package/dist/history/fs.js.map +1 -0
- package/dist/history/index.d.ts +3 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/index.js +2 -0
- package/dist/history/index.js.map +1 -0
- package/dist/history/types.d.ts +75 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/history/types.js +9 -0
- package/dist/history/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/history.d.ts +3 -0
- package/dist/providers/claude/history.d.ts.map +1 -0
- package/dist/providers/claude/history.js +248 -0
- package/dist/providers/claude/history.js.map +1 -0
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +18 -0
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/codex/attach.d.ts +2 -2
- package/dist/providers/codex/attach.js +4 -4
- package/dist/providers/codex/attach.js.map +1 -1
- package/dist/providers/codex/history.d.ts +3 -0
- package/dist/providers/codex/history.d.ts.map +1 -0
- package/dist/providers/codex/history.js +350 -0
- package/dist/providers/codex/history.js.map +1 -0
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +18 -0
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.d.ts +3 -2
- package/dist/providers/codex/transcript-normalize.d.ts.map +1 -1
- package/dist/providers/codex/transcript-normalize.js +4 -3
- package/dist/providers/codex/transcript-normalize.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -1
- package/src/history/fs.ts +307 -0
- package/src/history/index.ts +18 -0
- package/src/history/types.ts +104 -0
- package/src/index.ts +18 -0
- package/src/providers/claude/history.ts +290 -0
- package/src/providers/claude/index.ts +18 -0
- package/src/providers/codex/attach.ts +4 -4
- package/src/providers/codex/history.ts +399 -0
- package/src/providers/codex/index.ts +18 -0
- package/src/providers/codex/transcript-normalize.ts +4 -3
- package/src/types.ts +5 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import type { BaseStreamEventFields, StreamEvent } from "../../types.js";
|
|
5
|
+
import type {
|
|
6
|
+
LocalHistoryDiscoverOptions,
|
|
7
|
+
LocalHistoryOps,
|
|
8
|
+
LocalHistoryProbeOptions,
|
|
9
|
+
LocalHistorySession,
|
|
10
|
+
LocalHistoryYield,
|
|
11
|
+
} from "../../history/types.js";
|
|
12
|
+
import {
|
|
13
|
+
UUID_JSONL_RE,
|
|
14
|
+
asRecord,
|
|
15
|
+
asString,
|
|
16
|
+
cleanTitle,
|
|
17
|
+
isDirectory,
|
|
18
|
+
isoDate,
|
|
19
|
+
meaningfulHumanText,
|
|
20
|
+
readJsonlRecords,
|
|
21
|
+
recentEligible,
|
|
22
|
+
sourceFingerprint,
|
|
23
|
+
textFromContent,
|
|
24
|
+
} from "../../history/fs.js";
|
|
25
|
+
import { parseStreamLine } from "./parse.js";
|
|
26
|
+
import { resolveClaudeHome } from "./transcript.js";
|
|
27
|
+
|
|
28
|
+
const TITLE_SCAN_BYTES = 64 * 1024;
|
|
29
|
+
const MAX_DISCOVERY_SCAN_BYTES = 4 * 1024 * 1024;
|
|
30
|
+
const DEFAULT_PROBE_LIMIT = 100;
|
|
31
|
+
const DISCOVERY_CONCURRENCY = 16;
|
|
32
|
+
const DROP_EVENT_TYPES = new Set([
|
|
33
|
+
"assistant_delta",
|
|
34
|
+
"thinking_delta",
|
|
35
|
+
"permission_mode",
|
|
36
|
+
"rate_limit",
|
|
37
|
+
"unknown",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function claudeHome(options?: { env?: Record<string, string> }): string {
|
|
41
|
+
return resolveClaudeHome(options?.env?.["CLAUDE_CONFIG_DIR"]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function listClaudeSessionFiles(
|
|
45
|
+
home: string,
|
|
46
|
+
options: { limit?: number; includeSubagents?: boolean } = {},
|
|
47
|
+
): Promise<string[]> {
|
|
48
|
+
const projectsRoot = path.join(home, "projects");
|
|
49
|
+
let projects;
|
|
50
|
+
try {
|
|
51
|
+
projects = await readdir(projectsRoot, { withFileTypes: true });
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
56
|
+
const files: string[] = [];
|
|
57
|
+
const cap = Math.max(1, options.limit ?? Number.MAX_SAFE_INTEGER);
|
|
58
|
+
for (const project of projects) {
|
|
59
|
+
if (files.length >= cap) break;
|
|
60
|
+
if (!project.isDirectory()) continue;
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = await readdir(path.join(projectsRoot, project.name), { withFileTypes: true });
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (files.length >= cap) break;
|
|
70
|
+
// Top-level UUID files are main sessions. Nested subagent directories and
|
|
71
|
+
// symlinks are deliberately ignored.
|
|
72
|
+
if (entry.isFile() && UUID_JSONL_RE.test(entry.name)) {
|
|
73
|
+
files.push(path.join(projectsRoot, project.name, entry.name));
|
|
74
|
+
}
|
|
75
|
+
if (options.includeSubagents && entry.isDirectory() && UUID_JSONL_RE.test(`${entry.name}.jsonl`)) {
|
|
76
|
+
const subagentsDir = path.join(projectsRoot, project.name, entry.name, "subagents");
|
|
77
|
+
let subagents;
|
|
78
|
+
try {
|
|
79
|
+
subagents = await readdir(subagentsDir, { withFileTypes: true });
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
subagents.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
for (const subagent of subagents) {
|
|
85
|
+
if (files.length >= cap) break;
|
|
86
|
+
if (subagent.isFile() && /^agent-[a-z0-9]+\.jsonl$/i.test(subagent.name)) {
|
|
87
|
+
files.push(path.join(subagentsDir, subagent.name));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return files;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function subagentParentId(transcriptPath: string): string | null {
|
|
97
|
+
if (path.basename(path.dirname(transcriptPath)) !== "subagents") return null;
|
|
98
|
+
return path.basename(path.dirname(path.dirname(transcriptPath))) || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function parentSessionMetadata(
|
|
102
|
+
transcriptPath: string,
|
|
103
|
+
): Promise<{ cwd: string | null; branch: string | null }> {
|
|
104
|
+
const parentId = subagentParentId(transcriptPath);
|
|
105
|
+
if (!parentId) return { cwd: null, branch: null };
|
|
106
|
+
const projectDir = path.dirname(path.dirname(path.dirname(transcriptPath)));
|
|
107
|
+
const parentTranscript = path.join(projectDir, `${parentId}.jsonl`);
|
|
108
|
+
for await (const line of readJsonlRecords(parentTranscript)) {
|
|
109
|
+
const cwd = absoluteCwd(asString(line.raw["cwd"]));
|
|
110
|
+
const branch = asString(line.raw["gitBranch"]);
|
|
111
|
+
if (cwd) return { cwd, branch };
|
|
112
|
+
if (line.nextOffset >= TITLE_SCAN_BYTES) break;
|
|
113
|
+
}
|
|
114
|
+
return { cwd: null, branch: null };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function claudeUserText(record: Record<string, unknown>): string | null {
|
|
118
|
+
if (record["type"] !== "user") return null;
|
|
119
|
+
const message = asRecord(record["message"]);
|
|
120
|
+
if (!message || message["role"] !== "user") return null;
|
|
121
|
+
return meaningfulHumanText(textFromContent(
|
|
122
|
+
message["content"],
|
|
123
|
+
new Set(["text", "input_text"]),
|
|
124
|
+
));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function absoluteCwd(value: string | null): string | null {
|
|
128
|
+
return value && path.isAbsolute(value) ? value : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function inspectClaudeSession(
|
|
132
|
+
transcriptPath: string,
|
|
133
|
+
options: LocalHistoryDiscoverOptions,
|
|
134
|
+
): Promise<LocalHistorySession | null> {
|
|
135
|
+
const fileIdentity = path.basename(transcriptPath, ".jsonl");
|
|
136
|
+
const parentId = subagentParentId(transcriptPath);
|
|
137
|
+
const externalSessionId = parentId
|
|
138
|
+
? `subagent:${parentId}:${fileIdentity}`
|
|
139
|
+
: fileIdentity;
|
|
140
|
+
let cwd: string | null = null;
|
|
141
|
+
let title: string | null = null;
|
|
142
|
+
let firstPrompt: string | null = null;
|
|
143
|
+
let startedAt: string | null = null;
|
|
144
|
+
let branch: string | null = null;
|
|
145
|
+
let sidechain = false;
|
|
146
|
+
|
|
147
|
+
for await (const line of readJsonlRecords(transcriptPath)) {
|
|
148
|
+
const record = line.raw;
|
|
149
|
+
if (record["isSidechain"] === true) sidechain = true;
|
|
150
|
+
cwd ??= absoluteCwd(asString(record["cwd"]));
|
|
151
|
+
startedAt ??= isoDate(record["timestamp"]);
|
|
152
|
+
branch ??= asString(record["gitBranch"]);
|
|
153
|
+
if (record["type"] === "ai-title") title ??= cleanTitle(asString(record["aiTitle"]));
|
|
154
|
+
firstPrompt ??= claudeUserText(record);
|
|
155
|
+
if (cwd && firstPrompt && (title || line.nextOffset >= TITLE_SCAN_BYTES)) break;
|
|
156
|
+
if (line.nextOffset >= MAX_DISCOVERY_SCAN_BYTES) break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!cwd && parentId) {
|
|
160
|
+
const parent = await parentSessionMetadata(transcriptPath);
|
|
161
|
+
cwd = parent.cwd;
|
|
162
|
+
branch ??= parent.branch;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if ((options.mainSessionsOnly ?? true) && sidechain) return null;
|
|
166
|
+
const hasUserMessage = firstPrompt !== null;
|
|
167
|
+
if ((options.requireUserMessage ?? true) && !hasUserMessage) return null;
|
|
168
|
+
if ((options.mainSessionsOnly ?? true) && !cwd) return null;
|
|
169
|
+
if (options.cwd && (!cwd || path.normalize(cwd) !== path.normalize(options.cwd))) return null;
|
|
170
|
+
|
|
171
|
+
const source = await sourceFingerprint(transcriptPath);
|
|
172
|
+
return {
|
|
173
|
+
version: 1,
|
|
174
|
+
providerType: "claude",
|
|
175
|
+
externalSessionId,
|
|
176
|
+
transcriptPath,
|
|
177
|
+
cwd,
|
|
178
|
+
title: title ?? cleanTitle(firstPrompt),
|
|
179
|
+
startedAt,
|
|
180
|
+
updatedAt: new Date(Number(BigInt(source.modifiedAtNs) / 1_000_000n)).toISOString(),
|
|
181
|
+
branch,
|
|
182
|
+
gitOriginUrl: null,
|
|
183
|
+
archiveState: "unknown",
|
|
184
|
+
hasUserMessage,
|
|
185
|
+
source,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function baseFields(
|
|
190
|
+
session: LocalHistorySession,
|
|
191
|
+
raw: Record<string, unknown>,
|
|
192
|
+
eventId: string,
|
|
193
|
+
): BaseStreamEventFields & { eventId: string } {
|
|
194
|
+
const message = asRecord(raw["message"]);
|
|
195
|
+
return {
|
|
196
|
+
timestamp: isoDate(raw["timestamp"], session.startedAt ?? new Date(0).toISOString())!,
|
|
197
|
+
providerType: "claude",
|
|
198
|
+
sessionId: session.externalSessionId,
|
|
199
|
+
messageId: asString(message?.["id"]),
|
|
200
|
+
eventId,
|
|
201
|
+
turnId: null,
|
|
202
|
+
parentToolCallId: asString(raw["parent_tool_use_id"]),
|
|
203
|
+
raw,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizedClaudeEvents(
|
|
208
|
+
session: LocalHistorySession,
|
|
209
|
+
raw: Record<string, unknown>,
|
|
210
|
+
text: string,
|
|
211
|
+
eventId: string,
|
|
212
|
+
): Array<StreamEvent & { eventId: string }> {
|
|
213
|
+
return parseStreamLine(text)
|
|
214
|
+
.filter((event) => !DROP_EVENT_TYPES.has(event.type))
|
|
215
|
+
.map((event) => ({
|
|
216
|
+
...event,
|
|
217
|
+
timestamp: isoDate(raw["timestamp"], event.timestamp)!,
|
|
218
|
+
providerType: "claude",
|
|
219
|
+
sessionId: session.externalSessionId,
|
|
220
|
+
eventId,
|
|
221
|
+
raw,
|
|
222
|
+
} as StreamEvent & { eventId: string }));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const claudeLocalHistory: LocalHistoryOps = {
|
|
226
|
+
async probe(options?: LocalHistoryProbeOptions) {
|
|
227
|
+
const home = claudeHome(options);
|
|
228
|
+
const homeAvailable = await isDirectory(home);
|
|
229
|
+
const files = homeAvailable
|
|
230
|
+
? await listClaudeSessionFiles(home, { limit: options?.limit ?? DEFAULT_PROBE_LIMIT })
|
|
231
|
+
: [];
|
|
232
|
+
return {
|
|
233
|
+
providerType: "claude",
|
|
234
|
+
homeAvailable,
|
|
235
|
+
historyAvailable: files.length > 0,
|
|
236
|
+
approximateCount: files.length,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async *discover(options: LocalHistoryDiscoverOptions = {}) {
|
|
241
|
+
const files = await listClaudeSessionFiles(claudeHome(options), {
|
|
242
|
+
includeSubagents: options.mainSessionsOnly === false,
|
|
243
|
+
});
|
|
244
|
+
const sessions = await recentEligible(
|
|
245
|
+
files,
|
|
246
|
+
{ limit: options.limit, concurrency: DISCOVERY_CONCURRENCY },
|
|
247
|
+
async (filePath) => {
|
|
248
|
+
try {
|
|
249
|
+
return await inspectClaudeSession(filePath, options);
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
for (const session of sessions) yield session;
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
async *read(session, options = {}): AsyncIterable<LocalHistoryYield> {
|
|
259
|
+
if (session.providerType !== "claude" || !path.isAbsolute(session.transcriptPath)) {
|
|
260
|
+
throw new Error("Invalid Claude local history session");
|
|
261
|
+
}
|
|
262
|
+
for await (const line of readJsonlRecords(
|
|
263
|
+
session.transcriptPath,
|
|
264
|
+
options.fromOffset ?? 0,
|
|
265
|
+
{ rejectChanges: true },
|
|
266
|
+
)) {
|
|
267
|
+
const eventId = asString(line.raw["uuid"])
|
|
268
|
+
?? `claude:${session.externalSessionId}:${line.lineStartOffset}`;
|
|
269
|
+
const events: LocalHistoryYield["event"][] = [];
|
|
270
|
+
const userText = claudeUserText(line.raw);
|
|
271
|
+
if (userText) {
|
|
272
|
+
events.push({ type: "user", text: userText, ...baseFields(session, line.raw, eventId) });
|
|
273
|
+
}
|
|
274
|
+
events.push(...normalizedClaudeEvents(session, line.raw, line.text, eventId));
|
|
275
|
+
for (let partIndex = 0; partIndex < events.length; partIndex++) {
|
|
276
|
+
yield {
|
|
277
|
+
event: events[partIndex]!,
|
|
278
|
+
lineStartOffset: line.lineStartOffset,
|
|
279
|
+
nextOffset: line.nextOffset,
|
|
280
|
+
partIndex,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
fingerprint(session, options) {
|
|
287
|
+
if (session.providerType !== "claude") throw new Error("Invalid Claude local history session");
|
|
288
|
+
return sourceFingerprint(session.transcriptPath, options?.sha256 ?? false);
|
|
289
|
+
},
|
|
290
|
+
};
|
|
@@ -36,6 +36,7 @@ export const claudeProvider: ProviderModule = {
|
|
|
36
36
|
goals: claudeGoalCapability,
|
|
37
37
|
durableSessions: true,
|
|
38
38
|
durableHistory: true,
|
|
39
|
+
localHistory: true,
|
|
39
40
|
},
|
|
40
41
|
// Heavy machinery (execute.ts, session.ts, attach.ts) loads lazily on first
|
|
41
42
|
// use — every ProviderModule method is already async, so this is invisible to
|
|
@@ -56,6 +57,23 @@ export const claudeProvider: ProviderModule = {
|
|
|
56
57
|
]);
|
|
57
58
|
return historyFromSessionAttachment("claude", await attachClaudeSession(record, opts));
|
|
58
59
|
},
|
|
60
|
+
localHistory: {
|
|
61
|
+
probe: (options) => import("./history.js").then((module) => module.claudeLocalHistory.probe(options)),
|
|
62
|
+
discover: (options) => ({
|
|
63
|
+
async *[Symbol.asyncIterator]() {
|
|
64
|
+
const module = await import("./history.js");
|
|
65
|
+
yield* module.claudeLocalHistory.discover(options);
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
read: (session, options) => ({
|
|
69
|
+
async *[Symbol.asyncIterator]() {
|
|
70
|
+
const module = await import("./history.js");
|
|
71
|
+
yield* module.claudeLocalHistory.read(session, options);
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
fingerprint: (session, options) => import("./history.js")
|
|
75
|
+
.then((module) => module.claudeLocalHistory.fingerprint(session, options)),
|
|
76
|
+
},
|
|
59
77
|
};
|
|
60
78
|
|
|
61
79
|
export {
|
|
@@ -79,8 +79,8 @@ async function latestTurnBoundary(
|
|
|
79
79
|
/**
|
|
80
80
|
* Read-only reattachment to a durable Codex session. Same skeleton as Claude,
|
|
81
81
|
* with two deltas: classify via the rollout's last line, and replay through
|
|
82
|
-
* `codexLineToStreamEvents
|
|
83
|
-
*
|
|
82
|
+
* `codexLineToStreamEvents`. File-backed transcript lines carry deterministic
|
|
83
|
+
* synthetic event ids so replay can be deduplicated across process restarts.
|
|
84
84
|
*/
|
|
85
85
|
export async function attachCodexSession(
|
|
86
86
|
record: SessionRecord,
|
|
@@ -142,7 +142,7 @@ export async function attachCodexSession(
|
|
|
142
142
|
transcript,
|
|
143
143
|
lastTurn,
|
|
144
144
|
// 4. Replay: map each rollout line through the normalizer; one yield per
|
|
145
|
-
// produced event, all sharing the line's offset
|
|
145
|
+
// produced event, all sharing the line's offset and source event id.
|
|
146
146
|
catchUp(catchOpts?: CatchUpOptions): AsyncIterable<CatchUpYield> {
|
|
147
147
|
if (!transcript) return EMPTY;
|
|
148
148
|
const filePath = transcript.filePath;
|
|
@@ -154,7 +154,7 @@ export async function attachCodexSession(
|
|
|
154
154
|
...(catchOpts?.fromOffset !== undefined ? { fromOffset: catchOpts.fromOffset } : {}),
|
|
155
155
|
})) {
|
|
156
156
|
for (const event of codexLineToStreamEvents(line, { sessionId: sid })) {
|
|
157
|
-
yield { event, offset, eventId:
|
|
157
|
+
yield { event, offset, eventId: event.eventId };
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
},
|