@evomap/evolver-runtime-adapters 2.0.0-beta.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/dist/adapters.d.ts +9 -0
- package/dist/adapters.js +1037 -0
- package/dist/cursorState.d.ts +9 -0
- package/dist/cursorState.js +206 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.js +101 -0
- package/package.json +24 -0
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
import { parseJsonlLines, extractContent, isMetaText, correlateToolNames } from './types.js';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function finiteNumber(value) {
|
|
6
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
function firstNumber(record, keys) {
|
|
9
|
+
for (const key of keys) {
|
|
10
|
+
const value = finiteNumber(record[key]);
|
|
11
|
+
if (value !== undefined)
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
function firstString(record, keys) {
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
const value = record[key];
|
|
19
|
+
if (typeof value === 'string' && value.trim())
|
|
20
|
+
return value.trim();
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
function parseJsonish(value) {
|
|
25
|
+
if (typeof value !== 'string')
|
|
26
|
+
return value;
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('[')))
|
|
29
|
+
return value;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(trimmed);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function parsedRecord(value) {
|
|
38
|
+
const parsed = parseJsonish(value);
|
|
39
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
40
|
+
}
|
|
41
|
+
function hasOwn(record, key) {
|
|
42
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
43
|
+
}
|
|
44
|
+
function firstPresent(record, keys) {
|
|
45
|
+
for (const key of keys)
|
|
46
|
+
if (hasOwn(record, key))
|
|
47
|
+
return record[key];
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function firstPresentFrom(records, keys) {
|
|
51
|
+
for (const record of records) {
|
|
52
|
+
const value = firstPresent(record, keys);
|
|
53
|
+
if (value !== undefined)
|
|
54
|
+
return parseJsonish(value);
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function firstStringFrom(records, keys) {
|
|
59
|
+
for (const record of records) {
|
|
60
|
+
const value = firstString(record, keys);
|
|
61
|
+
if (value !== undefined)
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
function mergeSessionMetadata(current, next) {
|
|
67
|
+
const nativeCalls = [...(current.nativeCalls ?? []), ...(next.nativeCalls ?? [])];
|
|
68
|
+
const rawRows = [...(current.rawRows ?? []), ...(next.rawRows ?? [])];
|
|
69
|
+
return {
|
|
70
|
+
...current,
|
|
71
|
+
...(!current.sessionId && next.sessionId ? { sessionId: next.sessionId } : {}),
|
|
72
|
+
...(!current.provider && next.provider ? { provider: next.provider } : {}),
|
|
73
|
+
...(!current.model && next.model ? { model: next.model } : {}),
|
|
74
|
+
...(current.tools === undefined && next.tools !== undefined ? { tools: next.tools } : {}),
|
|
75
|
+
...(!current.startedAt && next.startedAt ? { startedAt: next.startedAt } : {}),
|
|
76
|
+
...(!current.clientSource && next.clientSource ? { clientSource: next.clientSource } : {}),
|
|
77
|
+
...(!current.systemPrompt && next.systemPrompt ? { systemPrompt: next.systemPrompt } : {}),
|
|
78
|
+
...(current.metadata === undefined && next.metadata !== undefined ? { metadata: next.metadata } : {}),
|
|
79
|
+
...(current.usage === undefined && next.usage !== undefined ? { usage: next.usage } : {}),
|
|
80
|
+
...(current.risk === undefined && next.risk !== undefined ? { risk: next.risk } : {}),
|
|
81
|
+
...(current.fidelity === undefined && next.fidelity !== undefined ? { fidelity: next.fidelity } : {}),
|
|
82
|
+
...(current.confidentiality === undefined && next.confidentiality !== undefined ? { confidentiality: next.confidentiality } : {}),
|
|
83
|
+
...(current.sourceRecord === undefined && next.sourceRecord !== undefined ? { sourceRecord: next.sourceRecord } : {}),
|
|
84
|
+
...(rawRows.length > 0 ? { rawRows } : {}),
|
|
85
|
+
...(nativeCalls.length > 0 ? { nativeCalls } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function sourceTurnMetadata(source) {
|
|
89
|
+
if (!source)
|
|
90
|
+
return {};
|
|
91
|
+
const parsedMeta = parsedRecord(source['meta']);
|
|
92
|
+
const parsedMetadata = parsedRecord(source['metadata']);
|
|
93
|
+
const sourceRecords = [source, parsedMetadata, parsedMeta].filter((record) => isRecord(record));
|
|
94
|
+
const usageValue = firstPresentFrom(sourceRecords, ['usage']);
|
|
95
|
+
const usage = isRecord(usageValue) ? usageValue : source;
|
|
96
|
+
const model = firstStringFrom(sourceRecords, ['model', 'model_name', 'modelName']);
|
|
97
|
+
const inputTokens = firstNumber(usage, ['input_tokens', 'inputTokens', 'prompt_tokens', 'promptTokens']);
|
|
98
|
+
const outputTokens = firstNumber(usage, ['output_tokens', 'outputTokens', 'completion_tokens', 'completionTokens']);
|
|
99
|
+
const metadata = parsedMetadata ?? parsedMeta;
|
|
100
|
+
return {
|
|
101
|
+
...(model ? { model } : {}),
|
|
102
|
+
...(inputTokens !== undefined ? { inputTokens } : {}),
|
|
103
|
+
...(outputTokens !== undefined ? { outputTokens } : {}),
|
|
104
|
+
sourceRecord: source,
|
|
105
|
+
rawRow: source,
|
|
106
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
107
|
+
...(usageValue !== undefined ? { usage: usageValue } : {}),
|
|
108
|
+
...(firstPresentFrom(sourceRecords, ['risk']) !== undefined ? { risk: firstPresentFrom(sourceRecords, ['risk']) } : {}),
|
|
109
|
+
...(firstPresentFrom(sourceRecords, ['fidelity']) !== undefined ? { fidelity: firstPresentFrom(sourceRecords, ['fidelity']) } : {}),
|
|
110
|
+
...(firstPresentFrom(sourceRecords, ['confidentiality']) !== undefined ? { confidentiality: firstPresentFrom(sourceRecords, ['confidentiality']) } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function hasUsage(metadata) {
|
|
114
|
+
return metadata.inputTokens !== undefined || metadata.outputTokens !== undefined;
|
|
115
|
+
}
|
|
116
|
+
function withSourceMetadata(turns, source) {
|
|
117
|
+
if (turns.length === 0)
|
|
118
|
+
return turns;
|
|
119
|
+
const metadata = sourceTurnMetadata(source);
|
|
120
|
+
let usageAttached = false;
|
|
121
|
+
return turns.map((turn) => {
|
|
122
|
+
const out = {
|
|
123
|
+
...turn,
|
|
124
|
+
...(metadata.model ? { model: metadata.model } : {}),
|
|
125
|
+
...(metadata.sourceRecord !== undefined ? { sourceRecord: metadata.sourceRecord } : {}),
|
|
126
|
+
...(metadata.rawRow !== undefined ? { rawRow: metadata.rawRow } : {}),
|
|
127
|
+
...(metadata.metadata !== undefined ? { metadata: metadata.metadata } : {}),
|
|
128
|
+
...(metadata.usage !== undefined ? { usage: metadata.usage } : {}),
|
|
129
|
+
...(metadata.risk !== undefined ? { risk: metadata.risk } : {}),
|
|
130
|
+
...(metadata.fidelity !== undefined ? { fidelity: metadata.fidelity } : {}),
|
|
131
|
+
...(metadata.confidentiality !== undefined ? { confidentiality: metadata.confidentiality } : {}),
|
|
132
|
+
};
|
|
133
|
+
if (!usageAttached && turn.isMeta !== true && hasUsage(metadata)) {
|
|
134
|
+
if (metadata.inputTokens !== undefined)
|
|
135
|
+
out.inputTokens = metadata.inputTokens;
|
|
136
|
+
if (metadata.outputTokens !== undefined)
|
|
137
|
+
out.outputTokens = metadata.outputTokens;
|
|
138
|
+
usageAttached = true;
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// VERIFICATION BAR: an adapter only ships once its parse() is checked against a REAL session log from that tool
|
|
144
|
+
// — a trimmed, sanitized sample + a golden test (see the golden tests in adapters.test.ts). A guessed schema is
|
|
145
|
+
// worse than no adapter: it silently yields 0 turns on real logs while looking supported. Today: claude-code +
|
|
146
|
+
// codex + cursor are verified (codex against codex-cli 0.137.0 rollout logs; cursor against real
|
|
147
|
+
// ~/.cursor/projects/*/agent-transcripts/*.jsonl). opencode/kiro stay removed until each has a real-log golden test.
|
|
148
|
+
// claude-code AND cursor share the Anthropic content-block transcript shape: one JSONL record per turn,
|
|
149
|
+
// { role|type: 'user'|'assistant', message: { content: [ {type:'text',text} | {type:'tool_use',name,id?} |
|
|
150
|
+
// {type:'tool_result',...} ] } }. correlateToolNames backfills a tool_result's tool name from its tool_use id.
|
|
151
|
+
function anthropicStyleTranscript(chunk) {
|
|
152
|
+
return correlateToolNames(parseJsonlLines(chunk).flatMap((obj) => {
|
|
153
|
+
const type = (obj['type'] ?? obj['role']);
|
|
154
|
+
if (type !== 'user' && type !== 'assistant')
|
|
155
|
+
return []; // non-turn records (no role/type) → skipped
|
|
156
|
+
const msg = isRecord(obj['message']) ? obj['message'] : undefined;
|
|
157
|
+
const timestamp = typeof obj['timestamp'] === 'string'
|
|
158
|
+
? obj['timestamp']
|
|
159
|
+
: (typeof msg?.['timestamp'] === 'string' ? msg['timestamp'] : undefined);
|
|
160
|
+
const metadataSource = msg ? { ...obj, ...msg } : obj;
|
|
161
|
+
return withSourceMetadata(extractContent(type, msg?.['content'] ?? obj['content']), metadataSource)
|
|
162
|
+
.map((turn) => (timestamp ? { ...turn, timestamp } : turn));
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
// codex content items are {type:'input_text'|'output_text'|'text', text}; flatten to a single string.
|
|
166
|
+
const CODEX_TEXT_TYPES = new Set(['input_text', 'output_text', 'text']);
|
|
167
|
+
function codexMessageText(content) {
|
|
168
|
+
if (typeof content === 'string')
|
|
169
|
+
return content;
|
|
170
|
+
if (!Array.isArray(content))
|
|
171
|
+
return '';
|
|
172
|
+
return content
|
|
173
|
+
.filter((p) => !!p && typeof p === 'object')
|
|
174
|
+
.filter((p) => CODEX_TEXT_TYPES.has(String(p['type'])))
|
|
175
|
+
.map((p) => (typeof p['text'] === 'string' ? p['text'] : ''))
|
|
176
|
+
.join('');
|
|
177
|
+
}
|
|
178
|
+
// codex injects scaffolding envelopes (not human/model-authored) — treat them as meta so distillation skips them.
|
|
179
|
+
const CODEX_META_TAGS = ['<environment_context>', '<permissions instructions>', '<user_instructions>'];
|
|
180
|
+
// Shell-style tool outputs lead with `Exit code: N` — a non-zero code is a tool FAILURE. Downstream signal
|
|
181
|
+
// extraction keys error signals off `errorMessage`, so surface it there (not just toolResult), mirroring claude's
|
|
182
|
+
// is_error. Shared by the codex adapter and the generic chat adapter (both wrap shell-style tool output).
|
|
183
|
+
function exitCodeFailed(output) {
|
|
184
|
+
const m = /^Exit code:\s*(-?\d+)/.exec(output);
|
|
185
|
+
return m ? Number(m[1]) !== 0 : false;
|
|
186
|
+
}
|
|
187
|
+
function stringifyReasoningSummary(summary) {
|
|
188
|
+
if (typeof summary === 'string')
|
|
189
|
+
return summary;
|
|
190
|
+
if (!Array.isArray(summary))
|
|
191
|
+
return '';
|
|
192
|
+
return summary.map((item) => {
|
|
193
|
+
if (typeof item === 'string')
|
|
194
|
+
return item;
|
|
195
|
+
if (!item || typeof item !== 'object')
|
|
196
|
+
return '';
|
|
197
|
+
const row = item;
|
|
198
|
+
for (const key of ['text', 'summary', 'content']) {
|
|
199
|
+
const value = row[key];
|
|
200
|
+
if (typeof value === 'string')
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
return '';
|
|
204
|
+
}).filter(Boolean).join('\n');
|
|
205
|
+
}
|
|
206
|
+
function withoutKeys(obj, keys) {
|
|
207
|
+
const out = {};
|
|
208
|
+
for (const [key, value] of Object.entries(obj))
|
|
209
|
+
if (!keys.includes(key))
|
|
210
|
+
out[key] = value;
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
function codexToolUseId(p) {
|
|
214
|
+
for (const key of ['call_id', 'id', 'tool_call_id', 'tool_use_id']) {
|
|
215
|
+
const value = p[key];
|
|
216
|
+
if (typeof value === 'string' && value)
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
function codexNativeToolTurn(p) {
|
|
222
|
+
const type = String(p['type'] ?? '');
|
|
223
|
+
if (!type)
|
|
224
|
+
return null;
|
|
225
|
+
const isOutput = /(?:_output|output)$/.test(type);
|
|
226
|
+
const isCall = /(?:_call|call)$/.test(type);
|
|
227
|
+
if (!isCall && !isOutput)
|
|
228
|
+
return null;
|
|
229
|
+
const payload = withoutKeys(p, ['type', 'id', 'call_id', 'tool_call_id', 'tool_use_id']);
|
|
230
|
+
const toolUseId = codexToolUseId(p);
|
|
231
|
+
if (isOutput) {
|
|
232
|
+
return {
|
|
233
|
+
role: 'tool',
|
|
234
|
+
text: '',
|
|
235
|
+
toolName: type,
|
|
236
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
237
|
+
toolResult: JSON.stringify(payload),
|
|
238
|
+
isMeta: false,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
role: 'assistant',
|
|
243
|
+
text: '',
|
|
244
|
+
toolName: type,
|
|
245
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
246
|
+
toolInput: payload,
|
|
247
|
+
isMeta: false,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function normalizeChatRole(rawRole) {
|
|
251
|
+
if (rawRole === 'developer')
|
|
252
|
+
return 'system';
|
|
253
|
+
if (rawRole === 'human')
|
|
254
|
+
return 'user';
|
|
255
|
+
if (rawRole === 'model')
|
|
256
|
+
return 'assistant';
|
|
257
|
+
if (rawRole === 'function')
|
|
258
|
+
return 'tool';
|
|
259
|
+
if (['user', 'assistant', 'tool', 'system'].includes(rawRole))
|
|
260
|
+
return rawRole;
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
// FIX-8: extract a SESSION-level system prompt for Anthropic-shaped transcripts. Claude stores the system prompt
|
|
264
|
+
// out-of-band in some versions: a dedicated `{type:'system'}` record, a record carrying a top-level
|
|
265
|
+
// `systemPrompt`/`system` string, or a leading message with role 'system'. When present we surface it at session
|
|
266
|
+
// level so buyers can see the operating instructions; when absent (current Claude transcripts persist no system
|
|
267
|
+
// record) parseSession simply omits it. We scan a bounded prefix — the system record is always near the top.
|
|
268
|
+
function anthropicSessionSystemPrompt(chunk) {
|
|
269
|
+
const rows = parseJsonlLines(chunk).slice(0, 50);
|
|
270
|
+
for (const row of rows) {
|
|
271
|
+
if (typeof row['systemPrompt'] === 'string' && row['systemPrompt'].trim())
|
|
272
|
+
return row['systemPrompt'];
|
|
273
|
+
if (typeof row['system'] === 'string' && row['system'].trim())
|
|
274
|
+
return row['system'];
|
|
275
|
+
const type = (row['type'] ?? row['role']);
|
|
276
|
+
const msg = isRecord(row['message']) ? row['message'] : undefined;
|
|
277
|
+
const role = (msg?.['role'] ?? row['role']);
|
|
278
|
+
if (type === 'system' || role === 'system') {
|
|
279
|
+
const content = msg?.['content'] ?? row['content'];
|
|
280
|
+
if (typeof content === 'string' && content.trim())
|
|
281
|
+
return content;
|
|
282
|
+
if (Array.isArray(content)) {
|
|
283
|
+
const text = content
|
|
284
|
+
.filter((p) => isRecord(p))
|
|
285
|
+
.map((p) => (typeof p['text'] === 'string' ? p['text'] : ''))
|
|
286
|
+
.join('');
|
|
287
|
+
if (text.trim())
|
|
288
|
+
return text;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
function anthropicStyleSession(chunk) {
|
|
295
|
+
const systemPrompt = anthropicSessionSystemPrompt(chunk);
|
|
296
|
+
return {
|
|
297
|
+
turns: anthropicStyleTranscript(chunk),
|
|
298
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
export const claudeCodeAdapter = {
|
|
302
|
+
agent: 'claude-code',
|
|
303
|
+
detect: (p) => /\.claude[/\\]projects[/\\].*\.jsonl$/.test(p) || /claude.*\.jsonl$/i.test(p),
|
|
304
|
+
parse: anthropicStyleTranscript,
|
|
305
|
+
parseSession: anthropicStyleSession,
|
|
306
|
+
};
|
|
307
|
+
// Verified against real cursor agent-transcripts (~/.cursor/projects/<proj>/agent-transcripts/<uuid>.jsonl):
|
|
308
|
+
// same Anthropic content-block shape as claude-code — observed blocks are text + tool_use (no tool_result). Other
|
|
309
|
+
// .jsonl that live under .cursor (eval datasets: task_id/canonical_solution, no role) carry no turn and parse to [].
|
|
310
|
+
export const cursorAdapter = {
|
|
311
|
+
agent: 'cursor',
|
|
312
|
+
detect: (p) => /\.cursor[/\\].*\.jsonl$/.test(p) || /cursor.*\.jsonl$/i.test(p),
|
|
313
|
+
parse: anthropicStyleTranscript,
|
|
314
|
+
};
|
|
315
|
+
// Verified against codex-cli 0.137.0 rollout logs (~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl). Each line is
|
|
316
|
+
// {timestamp, type, payload}. Codex writes the SAME conversation twice: a high-level `event_msg` stream
|
|
317
|
+
// (user_message/agent_message) and the authoritative `response_item` stream (full roles + tool calls + outputs).
|
|
318
|
+
// We parse ONLY `response_item` — parsing both would double-count every user/assistant turn. A tool call and its
|
|
319
|
+
// output share a `call_id` (codex's analogue of claude's tool_use_id), so correlateToolNames backfills the
|
|
320
|
+
// tool's name onto its (possibly failing) output turn — preserving tool attribution for signal extraction.
|
|
321
|
+
function codexTranscript(chunk) {
|
|
322
|
+
return correlateToolNames(parseJsonlLines(chunk).flatMap((obj) => {
|
|
323
|
+
if (obj['type'] !== 'response_item')
|
|
324
|
+
return [];
|
|
325
|
+
const p = obj['payload'];
|
|
326
|
+
if (!p)
|
|
327
|
+
return [];
|
|
328
|
+
const callId = codexToolUseId(p);
|
|
329
|
+
const timestamp = typeof obj['timestamp'] === 'string' ? obj['timestamp'] : undefined;
|
|
330
|
+
const stamp = (turns) => timestamp ? turns.map((turn) => ({ ...turn, timestamp })) : turns;
|
|
331
|
+
const annotate = (turns) => stamp(withSourceMetadata(turns, p));
|
|
332
|
+
switch (p['type']) {
|
|
333
|
+
case 'message': {
|
|
334
|
+
const raw = String(p['role'] ?? 'user');
|
|
335
|
+
// codex 'developer' role = injected system instruction envelope.
|
|
336
|
+
const role = normalizeChatRole(raw) ?? 'user';
|
|
337
|
+
const text = codexMessageText(p['content']);
|
|
338
|
+
if (!text)
|
|
339
|
+
return [];
|
|
340
|
+
const meta = role === 'system' || isMetaText(text) || CODEX_META_TAGS.some((t) => text.startsWith(t));
|
|
341
|
+
return annotate([{ role, text, isMeta: meta }]);
|
|
342
|
+
}
|
|
343
|
+
case 'function_call': // shell_command etc. — model-issued tool call
|
|
344
|
+
case 'custom_tool_call': // apply_patch etc.
|
|
345
|
+
return annotate([{
|
|
346
|
+
role: 'assistant',
|
|
347
|
+
text: '',
|
|
348
|
+
toolName: String(p['name'] ?? ''),
|
|
349
|
+
...(callId ? { toolUseId: String(callId) } : {}),
|
|
350
|
+
...(p['arguments'] !== undefined ? { toolInput: p['arguments'] } : {}),
|
|
351
|
+
...(p['input'] !== undefined ? { toolInput: p['input'] } : {}),
|
|
352
|
+
isMeta: false,
|
|
353
|
+
}]);
|
|
354
|
+
case 'function_call_output':
|
|
355
|
+
case 'custom_tool_call_output': {
|
|
356
|
+
const out = typeof p['output'] === 'string' ? p['output'] : JSON.stringify(p['output'] ?? '');
|
|
357
|
+
return annotate([{ role: 'tool', text: '', toolResult: out, ...(callId ? { toolUseId: String(callId) } : {}), ...(exitCodeFailed(out) ? { errorMessage: out } : {}), isMeta: false }]);
|
|
358
|
+
}
|
|
359
|
+
case 'reasoning': {
|
|
360
|
+
const text = stringifyReasoningSummary(p['summary']);
|
|
361
|
+
const reasoningSignature = typeof p['signature'] === 'string' ? p['signature'] : undefined;
|
|
362
|
+
const encryptedSignature = typeof p['encrypted_signature'] === 'string'
|
|
363
|
+
? p['encrypted_signature']
|
|
364
|
+
: (typeof p['encryptedSignature'] === 'string' ? p['encryptedSignature'] : undefined);
|
|
365
|
+
const encryptedContent = p['encrypted_content'] ?? p['encryptedContent'];
|
|
366
|
+
const metadata = sourceTurnMetadata(p);
|
|
367
|
+
if (!text && !reasoningSignature && !encryptedSignature && encryptedContent === undefined && !metadata.model && !hasUsage(metadata))
|
|
368
|
+
return [];
|
|
369
|
+
return annotate([{
|
|
370
|
+
role: 'assistant',
|
|
371
|
+
text,
|
|
372
|
+
reasoning: true,
|
|
373
|
+
...(reasoningSignature ? { reasoningSignature } : {}),
|
|
374
|
+
...(encryptedSignature ? { encryptedSignature } : {}),
|
|
375
|
+
...(encryptedContent !== undefined ? { encryptedContent } : {}),
|
|
376
|
+
isMeta: false,
|
|
377
|
+
}]);
|
|
378
|
+
}
|
|
379
|
+
default: // token_count, etc. → drop
|
|
380
|
+
{
|
|
381
|
+
const nativeTurn = codexNativeToolTurn(p);
|
|
382
|
+
return nativeTurn ? annotate([nativeTurn]) : [];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
// FIX-8: codex injects its system instruction as the first `developer`/`system` message (mapped to a meta
|
|
388
|
+
// system turn by codexTranscript). Surface that as the session-level systemPrompt as well.
|
|
389
|
+
function codexSession(chunk) {
|
|
390
|
+
const turns = codexTranscript(chunk);
|
|
391
|
+
const systemTurn = turns.find((turn) => turn.role === 'system' && turn.text.trim());
|
|
392
|
+
return {
|
|
393
|
+
turns,
|
|
394
|
+
...(systemTurn ? { systemPrompt: systemTurn.text } : {}),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
export const codexAdapter = {
|
|
398
|
+
agent: 'codex',
|
|
399
|
+
// real files are rollout-<ts>-<uuid>.jsonl under .codex/sessions|archived_sessions; keep the generic codex*.jsonl too.
|
|
400
|
+
detect: (p) => /\.codex[/\\].*\.jsonl$/.test(p) || /(^|[/\\])rollout-.*\.jsonl$/i.test(p) || /codex.*\.jsonl$/i.test(p),
|
|
401
|
+
parse: codexTranscript,
|
|
402
|
+
parseSession: codexSession,
|
|
403
|
+
};
|
|
404
|
+
// ── Gemini CLI adapter ───────────────────────────────────────────────────────
|
|
405
|
+
// Verified against real Gemini CLI session files at
|
|
406
|
+
// ~/.gemini/tmp/<projectHash>/chats/session-<ts>-<id>.{json,jsonl}. The session
|
|
407
|
+
// file is ONE JSON document:
|
|
408
|
+
// { sessionId, projectHash, startTime, lastUpdated, messages: [...], summary }
|
|
409
|
+
// (We deliberately NEVER read ~/.gemini/tmp/<hash>/logs.json — it is a terse
|
|
410
|
+
// event log, not the conversation.)
|
|
411
|
+
// Each message: { id, timestamp, type: 'user'|'gemini'|'info', content: string,
|
|
412
|
+
// thoughts?: [{subject, description, timestamp}], tokens?: {input,output,...,total},
|
|
413
|
+
// model?: string, toolCalls?: [{ id, name, args, result: [{functionResponse:{...,response}}] }] }.
|
|
414
|
+
// - type 'user' -> user turn (content is a string or [{text}] parts array)
|
|
415
|
+
// - type 'gemini' -> assistant turn; `thoughts` -> a reasoning turn (subject + description),
|
|
416
|
+
// `toolCalls` -> tool_use turn(s) + paired tool_result turn(s).
|
|
417
|
+
// - type 'info' -> CLI scaffolding (e.g. update banner) -> meta, dropped from distillation.
|
|
418
|
+
function geminiContentToString(content) {
|
|
419
|
+
if (typeof content === 'string')
|
|
420
|
+
return content;
|
|
421
|
+
if (Array.isArray(content)) {
|
|
422
|
+
return content
|
|
423
|
+
.map((part) => (typeof part === 'string' ? part : (isRecord(part) && typeof part['text'] === 'string' ? part['text'] : '')))
|
|
424
|
+
.join('');
|
|
425
|
+
}
|
|
426
|
+
return '';
|
|
427
|
+
}
|
|
428
|
+
function geminiThoughtsText(thoughts) {
|
|
429
|
+
if (!Array.isArray(thoughts))
|
|
430
|
+
return '';
|
|
431
|
+
return thoughts
|
|
432
|
+
.map((thought) => {
|
|
433
|
+
if (typeof thought === 'string')
|
|
434
|
+
return thought;
|
|
435
|
+
if (!isRecord(thought))
|
|
436
|
+
return '';
|
|
437
|
+
const subject = typeof thought['subject'] === 'string' ? thought['subject'] : '';
|
|
438
|
+
const description = typeof thought['description'] === 'string' ? thought['description'] : '';
|
|
439
|
+
return [subject, description].filter(Boolean).join(': ');
|
|
440
|
+
})
|
|
441
|
+
.filter(Boolean)
|
|
442
|
+
.join('\n');
|
|
443
|
+
}
|
|
444
|
+
function geminiUsageFromTokens(tokens) {
|
|
445
|
+
if (!isRecord(tokens))
|
|
446
|
+
return {};
|
|
447
|
+
const input = finiteNumber(tokens['input'] ?? tokens['inputTokens'] ?? tokens['promptTokenCount']);
|
|
448
|
+
const output = finiteNumber(tokens['output'] ?? tokens['outputTokens'] ?? tokens['candidatesTokenCount']);
|
|
449
|
+
return {
|
|
450
|
+
...(input !== undefined ? { inputTokens: input } : {}),
|
|
451
|
+
...(output !== undefined ? { outputTokens: output } : {}),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function geminiToolCallTurns(toolCalls, model) {
|
|
455
|
+
if (!Array.isArray(toolCalls))
|
|
456
|
+
return [];
|
|
457
|
+
const turns = [];
|
|
458
|
+
for (const call of toolCalls) {
|
|
459
|
+
if (!isRecord(call))
|
|
460
|
+
continue;
|
|
461
|
+
const name = typeof call['name'] === 'string' ? call['name'] : '';
|
|
462
|
+
const id = typeof call['id'] === 'string' ? call['id'] : undefined;
|
|
463
|
+
turns.push({
|
|
464
|
+
role: 'assistant',
|
|
465
|
+
text: '',
|
|
466
|
+
...(name ? { toolName: name } : {}),
|
|
467
|
+
...(id ? { toolUseId: id } : {}),
|
|
468
|
+
...(call['args'] !== undefined ? { toolInput: call['args'] } : {}),
|
|
469
|
+
...(model ? { model } : {}),
|
|
470
|
+
isMeta: false,
|
|
471
|
+
});
|
|
472
|
+
// Gemini stores the tool result inside `result: [{ functionResponse: { name, response } }]`.
|
|
473
|
+
const results = Array.isArray(call['result']) ? call['result'] : [];
|
|
474
|
+
for (const result of results) {
|
|
475
|
+
if (!isRecord(result))
|
|
476
|
+
continue;
|
|
477
|
+
const fr = isRecord(result['functionResponse']) ? result['functionResponse'] : undefined;
|
|
478
|
+
const responseValue = fr ? fr['response'] : result['response'];
|
|
479
|
+
if (responseValue === undefined && fr === undefined)
|
|
480
|
+
continue;
|
|
481
|
+
const text = typeof responseValue === 'string' ? responseValue : JSON.stringify(responseValue ?? result);
|
|
482
|
+
turns.push({
|
|
483
|
+
role: 'tool',
|
|
484
|
+
text: '',
|
|
485
|
+
...(name ? { toolName: name } : {}),
|
|
486
|
+
...(id ? { toolUseId: id } : {}),
|
|
487
|
+
toolResult: text,
|
|
488
|
+
...(exitCodeFailed(text) ? { errorMessage: text } : {}),
|
|
489
|
+
isMeta: false,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return turns;
|
|
494
|
+
}
|
|
495
|
+
function geminiMessageToTurns(message) {
|
|
496
|
+
const type = String(message['type'] ?? '');
|
|
497
|
+
const timestamp = typeof message['timestamp'] === 'string' ? message['timestamp'] : undefined;
|
|
498
|
+
const model = typeof message['model'] === 'string' ? message['model'] : undefined;
|
|
499
|
+
const stamp = (turn) => (timestamp ? { ...turn, timestamp } : turn);
|
|
500
|
+
if (type === 'user') {
|
|
501
|
+
const text = geminiContentToString(message['content']);
|
|
502
|
+
return [stamp({ role: 'user', text, isMeta: isMetaText(text) })];
|
|
503
|
+
}
|
|
504
|
+
if (type === 'gemini') {
|
|
505
|
+
const usage = geminiUsageFromTokens(message['tokens']);
|
|
506
|
+
const turns = [];
|
|
507
|
+
const thoughts = geminiThoughtsText(message['thoughts']);
|
|
508
|
+
if (thoughts) {
|
|
509
|
+
turns.push({ role: 'assistant', text: thoughts, reasoning: true, ...(model ? { model } : {}), isMeta: false });
|
|
510
|
+
}
|
|
511
|
+
const text = geminiContentToString(message['content']);
|
|
512
|
+
if (text) {
|
|
513
|
+
turns.push({
|
|
514
|
+
role: 'assistant',
|
|
515
|
+
text,
|
|
516
|
+
...(model ? { model } : {}),
|
|
517
|
+
...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),
|
|
518
|
+
...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),
|
|
519
|
+
isMeta: isMetaText(text),
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
turns.push(...geminiToolCallTurns(message['toolCalls'], model));
|
|
523
|
+
// Attach usage to the first non-meta turn even when the assistant only produced
|
|
524
|
+
// thoughts/tool calls (empty content), so token accounting is not lost.
|
|
525
|
+
if (text === '' && (usage.inputTokens !== undefined || usage.outputTokens !== undefined)) {
|
|
526
|
+
const first = turns.find((turn) => turn.isMeta !== true);
|
|
527
|
+
if (first) {
|
|
528
|
+
if (usage.inputTokens !== undefined && first.inputTokens === undefined)
|
|
529
|
+
first.inputTokens = usage.inputTokens;
|
|
530
|
+
if (usage.outputTokens !== undefined && first.outputTokens === undefined)
|
|
531
|
+
first.outputTokens = usage.outputTokens;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return turns.map(stamp);
|
|
535
|
+
}
|
|
536
|
+
// 'info' and any other CLI scaffolding records -> meta so distillation skips them.
|
|
537
|
+
const infoText = geminiContentToString(message['content']);
|
|
538
|
+
return infoText ? [stamp({ role: 'system', text: infoText, isMeta: true })] : [];
|
|
539
|
+
}
|
|
540
|
+
function geminiSessionFromValue(value) {
|
|
541
|
+
if (!isRecord(value))
|
|
542
|
+
return null;
|
|
543
|
+
const messages = Array.isArray(value['messages']) ? value['messages'] : undefined;
|
|
544
|
+
if (!messages)
|
|
545
|
+
return null;
|
|
546
|
+
const turns = correlateToolNames(messages.filter((m) => isRecord(m)).flatMap(geminiMessageToTurns));
|
|
547
|
+
const sessionId = firstString(value, ['sessionId', 'session_id', 'id']);
|
|
548
|
+
const startedAt = firstString(value, ['startTime', 'start_time', 'lastUpdated']);
|
|
549
|
+
const model = turns.find((turn) => turn.model)?.model;
|
|
550
|
+
return {
|
|
551
|
+
turns,
|
|
552
|
+
...(sessionId ? { sessionId } : {}),
|
|
553
|
+
provider: 'gemini',
|
|
554
|
+
...(model ? { model } : {}),
|
|
555
|
+
...(startedAt ? { startedAt } : {}),
|
|
556
|
+
clientSource: 'gemini-cli',
|
|
557
|
+
sourceRecord: value,
|
|
558
|
+
rawRows: [value],
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function geminiSessions(chunk) {
|
|
562
|
+
const trimmed = chunk.trim();
|
|
563
|
+
if (!trimmed)
|
|
564
|
+
return [];
|
|
565
|
+
// Gemini session files are a single JSON document. Tolerate JSONL-of-sessions too.
|
|
566
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
567
|
+
try {
|
|
568
|
+
const parsed = JSON.parse(trimmed);
|
|
569
|
+
const values = Array.isArray(parsed) ? parsed : [parsed];
|
|
570
|
+
return values
|
|
571
|
+
.map(geminiSessionFromValue)
|
|
572
|
+
.filter((session) => session !== null && session.turns.length > 0);
|
|
573
|
+
}
|
|
574
|
+
catch { /* fall through to JSONL */ }
|
|
575
|
+
}
|
|
576
|
+
return parseJsonlLines(chunk)
|
|
577
|
+
.map(geminiSessionFromValue)
|
|
578
|
+
.filter((session) => session !== null && session.turns.length > 0);
|
|
579
|
+
}
|
|
580
|
+
export const geminiAdapter = {
|
|
581
|
+
agent: 'gemini',
|
|
582
|
+
// Real path: ~/.gemini/tmp/<hash>/chats/session-<ts>-<id>.{json,jsonl}. NEVER logs.json.
|
|
583
|
+
detect: (p) => {
|
|
584
|
+
if (/(^|[/\\])logs\.json$/i.test(p))
|
|
585
|
+
return false;
|
|
586
|
+
return /\.gemini[/\\].*[/\\]chats[/\\]session-[^/\\]*\.jsonl?$/i.test(p)
|
|
587
|
+
|| /(^|[/\\])gemini[^/\\]*session[^/\\]*\.jsonl?$/i.test(p)
|
|
588
|
+
|| /(^|[/\\])session-[^/\\]*\.gemini\.jsonl?$/i.test(p);
|
|
589
|
+
},
|
|
590
|
+
parse: (chunk) => geminiSessions(chunk).flatMap((session) => session.turns),
|
|
591
|
+
parseSession: (chunk) => geminiSessions(chunk)[0] ?? { turns: [] },
|
|
592
|
+
parseSessions: geminiSessions,
|
|
593
|
+
};
|
|
594
|
+
// ── generic chat-transcript adapter ──────────────────────────────────────────
|
|
595
|
+
// The per-tool adapters above are gated on a REAL private-log golden (a guessed private schema silently yields 0
|
|
596
|
+
// turns). This one is different ON PURPOSE: it targets the DOCUMENTED, stable interchange format — OpenAI
|
|
597
|
+
// chat-completions / Anthropic messages — not a tool's private log. The "real sample" it is verified against is
|
|
598
|
+
// the published schema itself (golden tests), so it is not a guess. The payoff: any agent that can dump its
|
|
599
|
+
// conversation as standard messages joins the self-learning loop WITHOUT a bespoke adapter (this is the intake
|
|
600
|
+
// for the mcp-generic / http-agent runtimes). Registered LAST so any tool-specific path still wins; matched by an
|
|
601
|
+
// explicit on-disk intake convention: name the transcript `<name>.chat|messages|transcript.json[l]`.
|
|
602
|
+
/** Flatten a message `content` (string | OpenAI text-parts | mixed) to a plain string — for tool messages. */
|
|
603
|
+
function chatContentToString(content) {
|
|
604
|
+
if (typeof content === 'string')
|
|
605
|
+
return content;
|
|
606
|
+
if (!Array.isArray(content))
|
|
607
|
+
return '';
|
|
608
|
+
return content
|
|
609
|
+
.map((p) => (typeof p === 'string' ? p : (p && typeof p === 'object' && typeof p['text'] === 'string' ? p['text'] : '')))
|
|
610
|
+
.join('');
|
|
611
|
+
}
|
|
612
|
+
function hasGenericTurnPayload(turn) {
|
|
613
|
+
return Boolean(turn.text
|
|
614
|
+
|| turn.toolName
|
|
615
|
+
|| turn.toolInput !== undefined
|
|
616
|
+
|| turn.toolResult !== undefined
|
|
617
|
+
|| turn.errorMessage
|
|
618
|
+
|| turn.reasoning === true
|
|
619
|
+
|| turn.reasoningSignature
|
|
620
|
+
|| turn.encryptedSignature
|
|
621
|
+
|| turn.encryptedContent !== undefined);
|
|
622
|
+
}
|
|
623
|
+
/** One standard chat message → turns. Handles string/parts/Anthropic-block content (via extractContent), the
|
|
624
|
+
* OpenAI `role:'tool'` result message, and an assistant `tool_calls` array (each → a tool_use-style turn so
|
|
625
|
+
* correlateToolNames can attribute the matching tool result). A record without a known role is not a message. */
|
|
626
|
+
function chatMessageToTurns(obj) {
|
|
627
|
+
const rawRole = obj['role'];
|
|
628
|
+
if (typeof rawRole !== 'string')
|
|
629
|
+
return [];
|
|
630
|
+
const role = normalizeChatRole(rawRole);
|
|
631
|
+
if (!role)
|
|
632
|
+
return [];
|
|
633
|
+
if (role === 'tool') {
|
|
634
|
+
const text = chatContentToString(obj['content']);
|
|
635
|
+
// A failed tool result must surface on errorMessage (not just toolResult) — extractSignals mines strong tool
|
|
636
|
+
// errors from errorMessage and skips empty-text turns, so without this a failure produces no signal. Standard
|
|
637
|
+
// messages have no universal error flag, so we honor an explicit is_error/isError when the producer sets one,
|
|
638
|
+
// plus the shared shell `Exit code: N` convention (same as the codex adapter), and never guess from free text.
|
|
639
|
+
const flaggedError = obj['is_error'] === true || obj['isError'] === true;
|
|
640
|
+
const failed = flaggedError || exitCodeFailed(text);
|
|
641
|
+
return withSourceMetadata([{
|
|
642
|
+
role: 'tool', text: '', toolResult: text,
|
|
643
|
+
...(typeof obj['name'] === 'string' ? { toolName: obj['name'] } : {}),
|
|
644
|
+
...(typeof obj['tool_call_id'] === 'string' ? { toolUseId: obj['tool_call_id'] } : {}),
|
|
645
|
+
...(failed ? { errorMessage: text } : {}),
|
|
646
|
+
isMeta: false,
|
|
647
|
+
}], obj);
|
|
648
|
+
}
|
|
649
|
+
// A system message is an instruction envelope, not agent narration — mark it meta so distillation skips it
|
|
650
|
+
// (mirrors the codex adapter's developer/system handling).
|
|
651
|
+
const content = extractContent(role, obj['content']).filter(hasGenericTurnPayload);
|
|
652
|
+
const turns = role === 'system' ? content.map((t) => ({ ...t, isMeta: true })) : [...content];
|
|
653
|
+
const reasoningContent = firstString(obj, ['reasoning_content', 'reasoningContent', 'thinking']);
|
|
654
|
+
if (role === 'assistant' && reasoningContent) {
|
|
655
|
+
turns.unshift({
|
|
656
|
+
role: 'assistant',
|
|
657
|
+
text: reasoningContent,
|
|
658
|
+
reasoning: true,
|
|
659
|
+
...(firstString(obj, ['reasoning_signature', 'reasoningSignature', 'signature']) ? { reasoningSignature: firstString(obj, ['reasoning_signature', 'reasoningSignature', 'signature']) } : {}),
|
|
660
|
+
...(firstString(obj, ['encrypted_signature', 'encryptedSignature']) ? { encryptedSignature: firstString(obj, ['encrypted_signature', 'encryptedSignature']) } : {}),
|
|
661
|
+
...(obj['encrypted_content'] !== undefined ? { encryptedContent: obj['encrypted_content'] } : {}),
|
|
662
|
+
...(obj['encryptedContent'] !== undefined ? { encryptedContent: obj['encryptedContent'] } : {}),
|
|
663
|
+
isMeta: false,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
const toolCalls = obj['tool_calls'];
|
|
667
|
+
if (role === 'assistant' && Array.isArray(toolCalls)) {
|
|
668
|
+
for (const tc of toolCalls) {
|
|
669
|
+
if (!tc || typeof tc !== 'object')
|
|
670
|
+
continue;
|
|
671
|
+
const c = tc;
|
|
672
|
+
const fn = (c['function'] && typeof c['function'] === 'object' ? c['function'] : {});
|
|
673
|
+
const name = typeof fn['name'] === 'string' ? fn['name'] : (typeof c['name'] === 'string' ? c['name'] : '');
|
|
674
|
+
turns.push({
|
|
675
|
+
role: 'assistant',
|
|
676
|
+
text: '',
|
|
677
|
+
toolName: name,
|
|
678
|
+
...(typeof c['id'] === 'string' ? { toolUseId: c['id'] } : {}),
|
|
679
|
+
...(fn['arguments'] !== undefined ? { toolInput: fn['arguments'] } : {}),
|
|
680
|
+
isMeta: false,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return withSourceMetadata(turns, obj);
|
|
685
|
+
}
|
|
686
|
+
function genericChatMetadataFromRecord(record) {
|
|
687
|
+
const meta = parsedRecord(record['meta']) ?? {};
|
|
688
|
+
const metadata = parsedRecord(record['metadata']) ?? {};
|
|
689
|
+
const metadataSources = [record, metadata, meta];
|
|
690
|
+
const request = isRecord(parseJsonish(record['request'])) ? parseJsonish(record['request']) : {};
|
|
691
|
+
const requestBody = isRecord(parseJsonish(record['request_body'] ?? record['requestBody']))
|
|
692
|
+
? parseJsonish(record['request_body'] ?? record['requestBody'])
|
|
693
|
+
: {};
|
|
694
|
+
const response = isRecord(parseJsonish(record['response'])) ? parseJsonish(record['response']) : {};
|
|
695
|
+
const responseBody = isRecord(parseJsonish(record['response_body'] ?? record['responseBody']))
|
|
696
|
+
? parseJsonish(record['response_body'] ?? record['responseBody'])
|
|
697
|
+
: {};
|
|
698
|
+
const responseData = isRecord(parseJsonish(response['response_data'] ?? response['responseData']))
|
|
699
|
+
? parseJsonish(response['response_data'] ?? response['responseData'])
|
|
700
|
+
: {};
|
|
701
|
+
const responseBodyData = isRecord(parseJsonish(responseBody['response_data'] ?? responseBody['responseData']))
|
|
702
|
+
? parseJsonish(responseBody['response_data'] ?? responseBody['responseData'])
|
|
703
|
+
: {};
|
|
704
|
+
const sessionId = firstString(record, ['trajectory_id', 'trajectoryId', 'session_id', 'sessionId', 'task_id', 'taskId', 'id']);
|
|
705
|
+
const provider = firstStringFrom(metadataSources, ['provider', 'wire_api', 'wireApi', 'upstream', 'source', 'client_source', 'clientSource']);
|
|
706
|
+
const model = firstStringFrom(metadataSources, ['model', 'model_name', 'modelName', 'chosen_model', 'chosenModel'])
|
|
707
|
+
?? firstString(request, ['model', 'chosen_model', 'chosenModel'])
|
|
708
|
+
?? firstString(requestBody, ['model', 'chosen_model', 'chosenModel'])
|
|
709
|
+
?? firstString(responseData, ['model', 'chosen_model', 'chosenModel'])
|
|
710
|
+
?? firstString(responseBodyData, ['model', 'chosen_model', 'chosenModel'])
|
|
711
|
+
?? firstString(responseBody, ['model', 'chosen_model', 'chosenModel'])
|
|
712
|
+
?? firstString(response, ['model', 'chosen_model', 'chosenModel']);
|
|
713
|
+
const startedAt = firstString(record, ['created_at', 'createdAt', 'timestamp', 'request_time', 'requestTime'])
|
|
714
|
+
?? firstString(meta, ['created_at', 'createdAt', 'create_time', 'createTime', 'timestamp', 'request_time', 'requestTime']);
|
|
715
|
+
const requestTime = firstString(record, ['request_time', 'requestTime'])
|
|
716
|
+
?? firstString(meta, ['request_time', 'requestTime']);
|
|
717
|
+
const responseTime = firstString(record, ['response_time', 'responseTime'])
|
|
718
|
+
?? firstString(meta, ['response_time', 'responseTime']);
|
|
719
|
+
const clientSource = firstStringFrom(metadataSources, ['client_source', 'clientSource']);
|
|
720
|
+
const systemPrompt = firstStringFrom(metadataSources, ['system_prompt', 'systemPrompt']);
|
|
721
|
+
const tools = record['tools'] ?? request['tools'] ?? requestBody['tools'];
|
|
722
|
+
const nativeCall = nativeCallFromRecord(record, { provider, startedAt, requestTime, responseTime });
|
|
723
|
+
const metadataValue = record['metadata'] !== undefined ? parseJsonish(record['metadata']) : (record['meta'] !== undefined ? parseJsonish(record['meta']) : undefined);
|
|
724
|
+
const usage = firstPresentFrom(metadataSources, ['usage']);
|
|
725
|
+
const risk = firstPresentFrom(metadataSources, ['risk']);
|
|
726
|
+
const fidelity = firstPresentFrom(metadataSources, ['fidelity']);
|
|
727
|
+
const confidentiality = firstPresentFrom(metadataSources, ['confidentiality']);
|
|
728
|
+
return {
|
|
729
|
+
...(sessionId ? { sessionId } : {}),
|
|
730
|
+
...(provider ? { provider } : {}),
|
|
731
|
+
...(model ? { model } : {}),
|
|
732
|
+
...(tools !== undefined ? { tools } : {}),
|
|
733
|
+
...(startedAt ? { startedAt } : {}),
|
|
734
|
+
...(clientSource ? { clientSource } : {}),
|
|
735
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
736
|
+
...(metadataValue !== undefined ? { metadata: metadataValue } : {}),
|
|
737
|
+
...(usage !== undefined ? { usage } : {}),
|
|
738
|
+
...(risk !== undefined ? { risk } : {}),
|
|
739
|
+
...(fidelity !== undefined ? { fidelity } : {}),
|
|
740
|
+
...(confidentiality !== undefined ? { confidentiality } : {}),
|
|
741
|
+
sourceRecord: record,
|
|
742
|
+
rawRows: [record],
|
|
743
|
+
...(nativeCall ? { nativeCalls: [nativeCall] } : {}),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function nativeCallFromRecord(record, metadata) {
|
|
747
|
+
const hasRequest = hasOwn(record, 'request') || hasOwn(record, 'request_body') || hasOwn(record, 'requestBody');
|
|
748
|
+
const hasResponse = hasOwn(record, 'response') || hasOwn(record, 'response_body') || hasOwn(record, 'responseBody');
|
|
749
|
+
const requestHeaders = firstPresent(record, ['request_headers', 'requestHeaders']);
|
|
750
|
+
const responseHeaders = firstPresent(record, ['response_headers', 'responseHeaders']);
|
|
751
|
+
const transport = firstPresent(record, ['transport']);
|
|
752
|
+
const transportMetadata = firstPresent(record, ['transport_metadata', 'transportMetadata']);
|
|
753
|
+
const ttfbMs = finiteNumber(record['ttfb_ms'] ?? record['ttfbMs']);
|
|
754
|
+
if (!hasRequest && !hasResponse && requestHeaders === undefined && responseHeaders === undefined && transport === undefined && transportMetadata === undefined && ttfbMs === undefined)
|
|
755
|
+
return undefined;
|
|
756
|
+
const requestBody = parseJsonish(firstPresent(record, ['request', 'request_body', 'requestBody']));
|
|
757
|
+
const responseBody = parseJsonish(firstPresent(record, ['response', 'response_body', 'responseBody']));
|
|
758
|
+
const metadataValue = record['metadata'] !== undefined ? parseJsonish(record['metadata']) : (record['meta'] !== undefined ? parseJsonish(record['meta']) : undefined);
|
|
759
|
+
const meta = parsedRecord(record['meta']) ?? {};
|
|
760
|
+
const normalizedMetadata = parsedRecord(record['metadata']) ?? {};
|
|
761
|
+
const metadataSources = [record, normalizedMetadata, meta];
|
|
762
|
+
const usage = firstPresentFrom(metadataSources, ['usage']);
|
|
763
|
+
const risk = firstPresentFrom(metadataSources, ['risk']);
|
|
764
|
+
const fidelity = firstPresentFrom(metadataSources, ['fidelity']);
|
|
765
|
+
const confidentiality = firstPresentFrom(metadataSources, ['confidentiality']);
|
|
766
|
+
return {
|
|
767
|
+
...(metadata.provider ? { provider: metadata.provider } : {}),
|
|
768
|
+
...(metadata.startedAt ? { timestamp: metadata.startedAt } : {}),
|
|
769
|
+
...(metadata.requestTime ? { request_time: metadata.requestTime } : {}),
|
|
770
|
+
...(metadata.responseTime ? { response_time: metadata.responseTime } : {}),
|
|
771
|
+
...(ttfbMs !== undefined ? { ttfb_ms: ttfbMs } : {}),
|
|
772
|
+
...(requestHeaders !== undefined ? { request_headers: parseJsonish(requestHeaders) } : {}),
|
|
773
|
+
...(responseHeaders !== undefined ? { response_headers: parseJsonish(responseHeaders) } : {}),
|
|
774
|
+
...(transport !== undefined ? { transport: parseJsonish(transport) } : {}),
|
|
775
|
+
...(transportMetadata !== undefined ? { transport_metadata: parseJsonish(transportMetadata) } : {}),
|
|
776
|
+
...(hasRequest ? { request_body: requestBody } : {}),
|
|
777
|
+
...(hasResponse ? { response_body: responseBody } : {}),
|
|
778
|
+
...(metadataValue !== undefined ? { metadata: metadataValue } : {}),
|
|
779
|
+
...(usage !== undefined ? { usage } : {}),
|
|
780
|
+
...(risk !== undefined ? { risk } : {}),
|
|
781
|
+
...(fidelity !== undefined ? { fidelity } : {}),
|
|
782
|
+
...(confidentiality !== undefined ? { confidentiality } : {}),
|
|
783
|
+
sourceRecord: record,
|
|
784
|
+
rawRow: record,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function responseMessageFromBody(value) {
|
|
788
|
+
const body = parseJsonish(value);
|
|
789
|
+
if (!isRecord(body))
|
|
790
|
+
return [];
|
|
791
|
+
if (isRecord(body['message']))
|
|
792
|
+
return genericChatRecordsFromValue(body['message']).records;
|
|
793
|
+
if (Array.isArray(body['choices'])) {
|
|
794
|
+
return body['choices'].flatMap((choice) => {
|
|
795
|
+
if (!isRecord(choice))
|
|
796
|
+
return [];
|
|
797
|
+
if (isRecord(choice['message'])) {
|
|
798
|
+
const message = { ...choice['message'] };
|
|
799
|
+
if (body['model'] !== undefined && message['model'] === undefined)
|
|
800
|
+
message['model'] = body['model'];
|
|
801
|
+
if (body['usage'] !== undefined && message['usage'] === undefined)
|
|
802
|
+
message['usage'] = body['usage'];
|
|
803
|
+
return genericChatRecordsFromValue(message).records;
|
|
804
|
+
}
|
|
805
|
+
return [];
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
if (typeof body['role'] === 'string')
|
|
809
|
+
return genericChatRecordsFromValue(body).records;
|
|
810
|
+
if (body['content'] !== undefined || body['tool_calls'] !== undefined || body['usage'] !== undefined) {
|
|
811
|
+
return [{
|
|
812
|
+
role: 'assistant',
|
|
813
|
+
...(body['content'] !== undefined ? { content: body['content'] } : {}),
|
|
814
|
+
...(body['output'] !== undefined ? { content: body['output'] } : {}),
|
|
815
|
+
...(body['tool_calls'] !== undefined ? { tool_calls: body['tool_calls'] } : {}),
|
|
816
|
+
...(body['model'] !== undefined ? { model: body['model'] } : {}),
|
|
817
|
+
...(body['usage'] !== undefined ? { usage: body['usage'] } : {}),
|
|
818
|
+
}];
|
|
819
|
+
}
|
|
820
|
+
return [];
|
|
821
|
+
}
|
|
822
|
+
function candidateMessages(value) {
|
|
823
|
+
if (Array.isArray(value))
|
|
824
|
+
return value.flatMap(candidateMessages);
|
|
825
|
+
if (isRecord(value))
|
|
826
|
+
return genericChatRecordsFromValue(value).records;
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
function requestResponseRecordsFromValue(value) {
|
|
830
|
+
const request = parseJsonish(value['request'] ?? value['request_body'] ?? value['requestBody']);
|
|
831
|
+
const responseEnvelope = parseJsonish(value['response'] ?? value['response_body'] ?? value['responseBody']);
|
|
832
|
+
const response = isRecord(responseEnvelope) && (responseEnvelope['response_data'] !== undefined || responseEnvelope['responseData'] !== undefined)
|
|
833
|
+
? parseJsonish(responseEnvelope['response_data'] ?? responseEnvelope['responseData'])
|
|
834
|
+
: responseEnvelope;
|
|
835
|
+
const records = [];
|
|
836
|
+
if (isRecord(request)) {
|
|
837
|
+
if (typeof request['instructions'] === 'string')
|
|
838
|
+
records.push({ role: 'system', content: request['instructions'] });
|
|
839
|
+
if (typeof request['system_prompt'] === 'string')
|
|
840
|
+
records.push({ role: 'system', content: request['system_prompt'] });
|
|
841
|
+
if (typeof request['systemPrompt'] === 'string')
|
|
842
|
+
records.push({ role: 'system', content: request['systemPrompt'] });
|
|
843
|
+
records.push(...genericChatRecordsFromValue(request).records);
|
|
844
|
+
}
|
|
845
|
+
records.push(...responseMessageFromBody(response));
|
|
846
|
+
return records;
|
|
847
|
+
}
|
|
848
|
+
function systemPromptRecord(metadata) {
|
|
849
|
+
return metadata.systemPrompt ? [{ role: 'system', content: metadata.systemPrompt }] : [];
|
|
850
|
+
}
|
|
851
|
+
function isSessionWrapperRecord(value) {
|
|
852
|
+
return Array.isArray(value['messages'])
|
|
853
|
+
|| Array.isArray(value['turns'])
|
|
854
|
+
|| Array.isArray(value['prompt'])
|
|
855
|
+
|| value['request'] !== undefined
|
|
856
|
+
|| value['request_body'] !== undefined
|
|
857
|
+
|| value['requestBody'] !== undefined
|
|
858
|
+
|| value['response'] !== undefined
|
|
859
|
+
|| value['response_body'] !== undefined
|
|
860
|
+
|| value['responseBody'] !== undefined;
|
|
861
|
+
}
|
|
862
|
+
function genericChatRecordsFromValue(value) {
|
|
863
|
+
if (Array.isArray(value)) {
|
|
864
|
+
return value.reduce((acc, item) => {
|
|
865
|
+
const parsed = genericChatRecordsFromValue(item);
|
|
866
|
+
acc.records.push(...parsed.records);
|
|
867
|
+
acc.metadata = mergeSessionMetadata(acc.metadata, parsed.metadata);
|
|
868
|
+
return acc;
|
|
869
|
+
}, { records: [], metadata: {} });
|
|
870
|
+
}
|
|
871
|
+
if (!isRecord(value))
|
|
872
|
+
return { records: [], metadata: {} };
|
|
873
|
+
const metadata = genericChatMetadataFromRecord(value);
|
|
874
|
+
if (Array.isArray(value['prompt'])) {
|
|
875
|
+
return {
|
|
876
|
+
records: systemPromptRecord(metadata).concat(value['prompt']
|
|
877
|
+
.filter((x) => isRecord(x))
|
|
878
|
+
.concat(candidateMessages(value['candidates']))),
|
|
879
|
+
metadata,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const wrapped = value['messages'] ?? value['turns'];
|
|
883
|
+
if (Array.isArray(wrapped)) {
|
|
884
|
+
return {
|
|
885
|
+
records: systemPromptRecord(metadata).concat(wrapped.filter((x) => isRecord(x))),
|
|
886
|
+
metadata,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
const requestResponseRecords = requestResponseRecordsFromValue(value);
|
|
890
|
+
if (requestResponseRecords.length > 0)
|
|
891
|
+
return { records: requestResponseRecords, metadata };
|
|
892
|
+
if (isRecord(value['message'])) {
|
|
893
|
+
const parsed = genericChatRecordsFromValue(value['message']);
|
|
894
|
+
return { records: parsed.records, metadata: mergeSessionMetadata(metadata, parsed.metadata) };
|
|
895
|
+
}
|
|
896
|
+
return typeof value['role'] === 'string' ? { records: [value], metadata: {} } : { records: [], metadata };
|
|
897
|
+
}
|
|
898
|
+
function applyGenericSessionMetadata(turns, metadata) {
|
|
899
|
+
if (!metadata.model
|
|
900
|
+
&& metadata.metadata === undefined
|
|
901
|
+
&& metadata.usage === undefined
|
|
902
|
+
&& metadata.risk === undefined
|
|
903
|
+
&& metadata.fidelity === undefined
|
|
904
|
+
&& metadata.confidentiality === undefined
|
|
905
|
+
&& metadata.sourceRecord === undefined)
|
|
906
|
+
return turns;
|
|
907
|
+
let sessionScopedApplied = false;
|
|
908
|
+
const hasNonMetaTurn = turns.some((turn) => turn.isMeta !== true);
|
|
909
|
+
return turns.map((turn) => {
|
|
910
|
+
const out = {
|
|
911
|
+
...turn,
|
|
912
|
+
...(!turn.model && metadata.model ? { model: metadata.model } : {}),
|
|
913
|
+
};
|
|
914
|
+
if (!sessionScopedApplied && (!hasNonMetaTurn || turn.isMeta !== true)) {
|
|
915
|
+
if (out.metadata === undefined && metadata.metadata !== undefined)
|
|
916
|
+
out.metadata = metadata.metadata;
|
|
917
|
+
if (out.usage === undefined && metadata.usage !== undefined)
|
|
918
|
+
out.usage = metadata.usage;
|
|
919
|
+
if (out.risk === undefined && metadata.risk !== undefined)
|
|
920
|
+
out.risk = metadata.risk;
|
|
921
|
+
if (out.fidelity === undefined && metadata.fidelity !== undefined)
|
|
922
|
+
out.fidelity = metadata.fidelity;
|
|
923
|
+
if (out.confidentiality === undefined && metadata.confidentiality !== undefined)
|
|
924
|
+
out.confidentiality = metadata.confidentiality;
|
|
925
|
+
if (out.sourceRecord === undefined && metadata.sourceRecord !== undefined)
|
|
926
|
+
out.sourceRecord = metadata.sourceRecord;
|
|
927
|
+
if (out.rawRow === undefined && metadata.sourceRecord !== undefined)
|
|
928
|
+
out.rawRow = metadata.sourceRecord;
|
|
929
|
+
sessionScopedApplied = true;
|
|
930
|
+
}
|
|
931
|
+
return out;
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
/** Accept JSONL (one message per line), a bare JSON array of messages, a { messages|turns: [...] } wrapper (the
|
|
935
|
+
* chat-completions request-body shape), or a SINGLE message object (incl. pretty-printed multi-line — which is
|
|
936
|
+
* not valid JSONL, so it must be handled here, not fall through). */
|
|
937
|
+
function genericChatRecords(chunk) {
|
|
938
|
+
const trimmed = chunk.trim();
|
|
939
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
940
|
+
try {
|
|
941
|
+
const parsed = JSON.parse(trimmed);
|
|
942
|
+
return genericChatRecordsFromValue(parsed);
|
|
943
|
+
}
|
|
944
|
+
catch { /* not a single JSON doc → treat as JSONL below */ }
|
|
945
|
+
}
|
|
946
|
+
return parseJsonlLines(chunk).reduce((acc, row) => {
|
|
947
|
+
const parsed = genericChatRecordsFromValue(row);
|
|
948
|
+
acc.records.push(...parsed.records);
|
|
949
|
+
acc.metadata = mergeSessionMetadata(acc.metadata, parsed.metadata);
|
|
950
|
+
return acc;
|
|
951
|
+
}, { records: [], metadata: {} });
|
|
952
|
+
}
|
|
953
|
+
function genericChatSessionFromParseResult(parsed) {
|
|
954
|
+
const turns = correlateToolNames(parsed.records.flatMap(chatMessageToTurns));
|
|
955
|
+
return {
|
|
956
|
+
turns: applyGenericSessionMetadata(turns, parsed.metadata),
|
|
957
|
+
...parsed.metadata,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function genericChatSession(chunk) {
|
|
961
|
+
return genericChatSessionFromParseResult(genericChatRecords(chunk));
|
|
962
|
+
}
|
|
963
|
+
function genericChatSessionsFromValue(value) {
|
|
964
|
+
if (Array.isArray(value) && value.every((item) => isRecord(item) && isSessionWrapperRecord(item))) {
|
|
965
|
+
return value
|
|
966
|
+
.map((item) => genericChatSessionFromParseResult(genericChatRecordsFromValue(item)))
|
|
967
|
+
.filter((session) => session.turns.length > 0);
|
|
968
|
+
}
|
|
969
|
+
const session = genericChatSessionFromParseResult(genericChatRecordsFromValue(value));
|
|
970
|
+
return session.turns.length > 0 ? [session] : [];
|
|
971
|
+
}
|
|
972
|
+
function genericChatSessions(chunk) {
|
|
973
|
+
const trimmed = chunk.trim();
|
|
974
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
975
|
+
try {
|
|
976
|
+
const parsed = JSON.parse(trimmed);
|
|
977
|
+
return genericChatSessionsFromValue(parsed);
|
|
978
|
+
}
|
|
979
|
+
catch { /* not a single JSON doc → treat as JSONL below */ }
|
|
980
|
+
}
|
|
981
|
+
const rows = parseJsonlLines(chunk);
|
|
982
|
+
if (rows.length > 0 && rows.every(isSessionWrapperRecord)) {
|
|
983
|
+
return rows
|
|
984
|
+
.map((row) => genericChatSessionFromParseResult(genericChatRecordsFromValue(row)))
|
|
985
|
+
.filter((session) => session.turns.length > 0);
|
|
986
|
+
}
|
|
987
|
+
const session = genericChatSessionFromParseResult(rows.reduce((acc, row) => {
|
|
988
|
+
const parsed = genericChatRecordsFromValue(row);
|
|
989
|
+
acc.records.push(...parsed.records);
|
|
990
|
+
acc.metadata = mergeSessionMetadata(acc.metadata, parsed.metadata);
|
|
991
|
+
return acc;
|
|
992
|
+
}, { records: [], metadata: {} }));
|
|
993
|
+
return session.turns.length > 0 ? [session] : [];
|
|
994
|
+
}
|
|
995
|
+
export const genericChatAdapter = {
|
|
996
|
+
agent: 'generic-chat',
|
|
997
|
+
detect: (p) => /(^|[/\\])[^/\\]*\.(chat|messages|transcript)\.jsonl?$/i.test(p),
|
|
998
|
+
parse: (chunk) => genericChatSessions(chunk).flatMap((session) => session.turns),
|
|
999
|
+
parseSession: genericChatSession,
|
|
1000
|
+
parseSessions: genericChatSessions,
|
|
1001
|
+
};
|
|
1002
|
+
// ── Kimi (wire.jsonl) adapter ────────────────────────────────────────────────
|
|
1003
|
+
// Kimi CLI persists the raw wire conversation to `wire.jsonl`: one JSON object per line, each carrying the FULL
|
|
1004
|
+
// `messages` array (OpenAI chat-completions shape) with PLAINTEXT thinking directly captured (no encrypted/signed
|
|
1005
|
+
// reasoning — Kimi exposes it). No real local sample was available, so this is built against that documented
|
|
1006
|
+
// "one record per line, full messages + plaintext thinking" shape and reuses the generic chat message decoder
|
|
1007
|
+
// (which already understands messages[], reasoning_content/thinking, tool_calls, and the role:'tool' result).
|
|
1008
|
+
// When the same file carries multiple lines, the LAST full messages snapshot wins (it is the most complete turn
|
|
1009
|
+
// log), avoiding double-counting earlier partial snapshots.
|
|
1010
|
+
function kimiSessions(chunk) {
|
|
1011
|
+
const rows = parseJsonlLines(chunk);
|
|
1012
|
+
// Pick the row with the most messages as the authoritative conversation snapshot; fall back to all rows merged.
|
|
1013
|
+
let best;
|
|
1014
|
+
let bestLen = -1;
|
|
1015
|
+
for (const row of rows) {
|
|
1016
|
+
const messages = row['messages'];
|
|
1017
|
+
const len = Array.isArray(messages) ? messages.length : -1;
|
|
1018
|
+
if (len > bestLen) {
|
|
1019
|
+
bestLen = len;
|
|
1020
|
+
best = row;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const source = best ?? rows;
|
|
1024
|
+
const sessions = genericChatSessionsFromValue(source);
|
|
1025
|
+
return sessions.map((session) => ({ ...session, provider: session.provider ?? 'kimi', clientSource: session.clientSource ?? 'kimi' }));
|
|
1026
|
+
}
|
|
1027
|
+
export const kimiAdapter = {
|
|
1028
|
+
agent: 'kimi',
|
|
1029
|
+
detect: (p) => /(^|[/\\])wire\.jsonl$/i.test(p) || /(^|[/\\])kimi[^/\\]*\.jsonl$/i.test(p),
|
|
1030
|
+
parse: (chunk) => kimiSessions(chunk).flatMap((session) => session.turns),
|
|
1031
|
+
parseSession: (chunk) => kimiSessions(chunk)[0] ?? { turns: [] },
|
|
1032
|
+
parseSessions: kimiSessions,
|
|
1033
|
+
};
|
|
1034
|
+
// Only verified adapters are registered. opencode/kiro live in git history — re-add with a real-log fixture.
|
|
1035
|
+
// genericChatAdapter is LAST so any tool-specific path (claude/cursor/codex/gemini/kimi) resolves first.
|
|
1036
|
+
export const ADAPTERS = [claudeCodeAdapter, codexAdapter, cursorAdapter, geminiAdapter, kimiAdapter, genericChatAdapter];
|
|
1037
|
+
export function adapterForPath(path) { return ADAPTERS.find((a) => a.detect(path)); }
|