@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.
@@ -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)); }