@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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NormalizedSession } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read Cursor chat sessions out of a `state.vscdb` sqlite database (read-only). Returns one NormalizedSession per
|
|
4
|
+
* composer that has at least one real (non-meta) turn. Never throws on a malformed/locked db — returns []. The db
|
|
5
|
+
* is opened read-only, so it is safe to run against a live Cursor profile.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseCursorStateVscdb(dbPath: string): NormalizedSession[];
|
|
8
|
+
/** True for a path that looks like a Cursor globalStorage state.vscdb. */
|
|
9
|
+
export declare function isCursorStateVscdbPath(path: string): boolean;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { correlateToolNames, isMetaText } from './types.js';
|
|
3
|
+
// node:sqlite is a recent Node built-in exposed only as `node:sqlite` (no bare `sqlite` alias). Load it through
|
|
4
|
+
// createRequire so bundlers (vitest/vite) don't statically strip the prefix and fail resolution — mirrors the
|
|
5
|
+
// pattern already used by evolver-core/src/mailbox/store.ts.
|
|
6
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
7
|
+
// ── Cursor state.vscdb conversation extraction ───────────────────────────────
|
|
8
|
+
//
|
|
9
|
+
// VERIFICATION STATUS / COVERAGE (honest scope — read before extending):
|
|
10
|
+
// Cursor stores chat in ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb (sqlite, can be
|
|
11
|
+
// >1.85GB; node:sqlite opens it read-only with no CLI). Two tables: `ItemTable` (workbench KV) and
|
|
12
|
+
// `cursorDiskKV` (the conversation store). The conversation lives in `cursorDiskKV` under these keys:
|
|
13
|
+
//
|
|
14
|
+
// composerData:<composerId> -> session header. Fields we rely on:
|
|
15
|
+
// { composerId, createdAt, unifiedMode, modelConfig:{modelName,...},
|
|
16
|
+
// fullConversationHeadersOnly: [ { bubbleId, type } ], // ORDERED message list
|
|
17
|
+
// conversationMap?: { <bubbleId>: <bubble> } } // sometimes inlined
|
|
18
|
+
// bubbleId:<composerId>:<bubbleId> -> a single message bubble (when not inlined in conversationMap).
|
|
19
|
+
//
|
|
20
|
+
// A bubble:
|
|
21
|
+
// { type: 1|2, text, richText?, thinking?:{text}|string, toolFormerData?:{name,rawArgs|params,result},
|
|
22
|
+
// tokenCount?:{inputTokens,outputTokens} }
|
|
23
|
+
// type === 1 -> user, type === 2 -> assistant. (Cursor's MessageType enum.)
|
|
24
|
+
//
|
|
25
|
+
// COVERED here: opening the db read-only; enumerating composers; ordering messages by
|
|
26
|
+
// fullConversationHeadersOnly; reading bubbles from BOTH the inlined conversationMap and the separate
|
|
27
|
+
// bubbleId:<composer>:<bubble> rows; extracting user/assistant text, assistant `thinking` as a reasoning turn,
|
|
28
|
+
// and `toolFormerData` as a tool_use + tool_result pair; per-bubble token counts; session model/createdAt.
|
|
29
|
+
//
|
|
30
|
+
// NOT yet covered / known gaps (the local machine's DB had only EMPTY composers, so these are schema-documented
|
|
31
|
+
// but not golden-verified against real populated bubbles): Cursor's code-edit "diff" capability blocks
|
|
32
|
+
// (codeBlockData / originalFileStates) are NOT reconstructed into before/after diffs; sub-composer / best-of-N
|
|
33
|
+
// branch bubbles are read flat (no tree structure); attachment/context payloads are ignored. Extend with a real
|
|
34
|
+
// populated-DB fixture before trusting those.
|
|
35
|
+
function isRecord(value) {
|
|
36
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
function asString(value) {
|
|
39
|
+
return typeof value === 'string' ? value : '';
|
|
40
|
+
}
|
|
41
|
+
function finiteNumber(value) {
|
|
42
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
43
|
+
}
|
|
44
|
+
function bubbleThinkingText(bubble) {
|
|
45
|
+
const thinking = bubble['thinking'];
|
|
46
|
+
if (typeof thinking === 'string')
|
|
47
|
+
return thinking;
|
|
48
|
+
if (isRecord(thinking)) {
|
|
49
|
+
const text = thinking['text'] ?? thinking['content'];
|
|
50
|
+
if (typeof text === 'string')
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
function bubbleToolTurns(bubble, toolUseId) {
|
|
56
|
+
const tool = bubble['toolFormerData'];
|
|
57
|
+
if (!isRecord(tool))
|
|
58
|
+
return [];
|
|
59
|
+
const name = asString(tool['name']) || asString(tool['tool']) || 'unknown';
|
|
60
|
+
const rawArgs = tool['rawArgs'] ?? tool['params'] ?? tool['args'];
|
|
61
|
+
const result = tool['result'];
|
|
62
|
+
const turns = [{
|
|
63
|
+
role: 'assistant',
|
|
64
|
+
text: '',
|
|
65
|
+
toolName: name,
|
|
66
|
+
toolUseId,
|
|
67
|
+
...(rawArgs !== undefined ? { toolInput: rawArgs } : {}),
|
|
68
|
+
isMeta: false,
|
|
69
|
+
}];
|
|
70
|
+
if (result !== undefined) {
|
|
71
|
+
const resultText = typeof result === 'string' ? result : JSON.stringify(result);
|
|
72
|
+
turns.push({
|
|
73
|
+
role: 'tool',
|
|
74
|
+
text: '',
|
|
75
|
+
toolName: name,
|
|
76
|
+
toolUseId,
|
|
77
|
+
toolResult: resultText,
|
|
78
|
+
isMeta: false,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return turns;
|
|
82
|
+
}
|
|
83
|
+
function bubbleToTurns(bubble, bubbleId) {
|
|
84
|
+
if (!isRecord(bubble))
|
|
85
|
+
return [];
|
|
86
|
+
const type = finiteNumber(bubble['type']);
|
|
87
|
+
const inputTokens = isRecord(bubble['tokenCount']) ? finiteNumber(bubble['tokenCount']['inputTokens']) : undefined;
|
|
88
|
+
const outputTokens = isRecord(bubble['tokenCount']) ? finiteNumber(bubble['tokenCount']['outputTokens']) : undefined;
|
|
89
|
+
const text = asString(bubble['text']);
|
|
90
|
+
const turns = [];
|
|
91
|
+
if (type === 1) {
|
|
92
|
+
// user bubble
|
|
93
|
+
turns.push({ role: 'user', text, isMeta: isMetaText(text) });
|
|
94
|
+
return turns;
|
|
95
|
+
}
|
|
96
|
+
// assistant (type === 2) or unknown -> treat as assistant content
|
|
97
|
+
const thinking = bubbleThinkingText(bubble);
|
|
98
|
+
if (thinking) {
|
|
99
|
+
turns.push({ role: 'assistant', text: thinking, reasoning: true, isMeta: false });
|
|
100
|
+
}
|
|
101
|
+
if (text) {
|
|
102
|
+
turns.push({
|
|
103
|
+
role: 'assistant',
|
|
104
|
+
text,
|
|
105
|
+
...(inputTokens !== undefined ? { inputTokens } : {}),
|
|
106
|
+
...(outputTokens !== undefined ? { outputTokens } : {}),
|
|
107
|
+
isMeta: isMetaText(text),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
turns.push(...bubbleToolTurns(bubble, bubbleId));
|
|
111
|
+
return turns;
|
|
112
|
+
}
|
|
113
|
+
function conversationHeaders(composer) {
|
|
114
|
+
const headers = composer['fullConversationHeadersOnly'];
|
|
115
|
+
if (!Array.isArray(headers))
|
|
116
|
+
return [];
|
|
117
|
+
return headers
|
|
118
|
+
.map((header) => (isRecord(header) ? { bubbleId: asString(header['bubbleId']), type: finiteNumber(header['type']) } : { bubbleId: '' }))
|
|
119
|
+
.filter((header) => header.bubbleId);
|
|
120
|
+
}
|
|
121
|
+
function composerToSession(composer, composerId, readBubble) {
|
|
122
|
+
const headers = conversationHeaders(composer);
|
|
123
|
+
const conversationMap = isRecord(composer['conversationMap']) ? composer['conversationMap'] : undefined;
|
|
124
|
+
const orderedBubbleIds = headers.length > 0
|
|
125
|
+
? headers.map((header) => header.bubbleId)
|
|
126
|
+
: (conversationMap ? Object.keys(conversationMap) : []);
|
|
127
|
+
const turns = [];
|
|
128
|
+
for (const bubbleId of orderedBubbleIds) {
|
|
129
|
+
const bubble = (conversationMap && conversationMap[bubbleId] !== undefined)
|
|
130
|
+
? conversationMap[bubbleId]
|
|
131
|
+
: readBubble(composerId, bubbleId);
|
|
132
|
+
turns.push(...bubbleToTurns(bubble, bubbleId));
|
|
133
|
+
}
|
|
134
|
+
const model = isRecord(composer['modelConfig']) ? asString(composer['modelConfig']['modelName']) : '';
|
|
135
|
+
const createdAt = finiteNumber(composer['createdAt']);
|
|
136
|
+
return {
|
|
137
|
+
turns: correlateToolNames(turns),
|
|
138
|
+
sessionId: composerId,
|
|
139
|
+
provider: 'cursor',
|
|
140
|
+
...(model ? { model } : {}),
|
|
141
|
+
...(createdAt !== undefined ? { startedAt: new Date(createdAt).toISOString() } : {}),
|
|
142
|
+
clientSource: 'cursor',
|
|
143
|
+
sourceRecord: { composerId, ...(model ? { model } : {}) },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read Cursor chat sessions out of a `state.vscdb` sqlite database (read-only). Returns one NormalizedSession per
|
|
148
|
+
* composer that has at least one real (non-meta) turn. Never throws on a malformed/locked db — returns []. The db
|
|
149
|
+
* is opened read-only, so it is safe to run against a live Cursor profile.
|
|
150
|
+
*/
|
|
151
|
+
export function parseCursorStateVscdb(dbPath) {
|
|
152
|
+
let db;
|
|
153
|
+
try {
|
|
154
|
+
const { DatabaseSync } = nodeRequire('node:sqlite');
|
|
155
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
156
|
+
const composerRows = db
|
|
157
|
+
.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'")
|
|
158
|
+
.all();
|
|
159
|
+
const bubbleStmt = db.prepare('SELECT value FROM cursorDiskKV WHERE key = ?');
|
|
160
|
+
const readBubble = (composerId, bubbleId) => {
|
|
161
|
+
try {
|
|
162
|
+
const row = bubbleStmt.get(`bubbleId:${composerId}:${bubbleId}`);
|
|
163
|
+
if (!row || typeof row.value !== 'string')
|
|
164
|
+
return undefined;
|
|
165
|
+
return JSON.parse(row.value);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const sessions = [];
|
|
172
|
+
for (const row of composerRows) {
|
|
173
|
+
if (typeof row.value !== 'string')
|
|
174
|
+
continue;
|
|
175
|
+
let composer;
|
|
176
|
+
try {
|
|
177
|
+
composer = JSON.parse(row.value);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!isRecord(composer))
|
|
183
|
+
continue;
|
|
184
|
+
const composerId = asString(row.key).replace(/^composerData:/, '') || asString(composer['composerId']);
|
|
185
|
+
const session = composerToSession(composer, composerId, readBubble);
|
|
186
|
+
if (session.turns.some((turn) => turn.isMeta !== true))
|
|
187
|
+
sessions.push(session);
|
|
188
|
+
}
|
|
189
|
+
return sessions;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
try {
|
|
196
|
+
db?.close();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
/* ignore close errors */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** True for a path that looks like a Cursor globalStorage state.vscdb. */
|
|
204
|
+
export function isCursorStateVscdbPath(path) {
|
|
205
|
+
return /(^|[/\\])state\.vscdb$/i.test(path) && /[/\\]Cursor[/\\]/i.test(path);
|
|
206
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export interface NormalizedTurn {
|
|
2
|
+
role: 'user' | 'assistant' | 'tool' | 'system';
|
|
3
|
+
text: string;
|
|
4
|
+
timestamp?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
inputTokens?: number;
|
|
7
|
+
outputTokens?: number;
|
|
8
|
+
toolName?: string;
|
|
9
|
+
toolInput?: unknown;
|
|
10
|
+
/** Anthropic content blocks carry the tool name on tool_use and reference it by id on tool_result;
|
|
11
|
+
* this id lets us correlate the two so a failing tool_result can be attributed to its tool. */
|
|
12
|
+
toolUseId?: string;
|
|
13
|
+
toolResult?: string;
|
|
14
|
+
errorMessage?: string;
|
|
15
|
+
reasoning?: boolean;
|
|
16
|
+
/** A thinking block that was present in the source but carried no text AND no signature/encrypted payload.
|
|
17
|
+
* We KEEP the turn (instead of silently dropping it) and flag it so buyers can tell "the model emitted an
|
|
18
|
+
* empty thinking block" apart from "we dropped the reasoning during collection". */
|
|
19
|
+
thinkingEmpty?: boolean;
|
|
20
|
+
reasoningSignature?: string;
|
|
21
|
+
encryptedSignature?: string;
|
|
22
|
+
encryptedContent?: unknown;
|
|
23
|
+
sourceRecord?: unknown;
|
|
24
|
+
rawRow?: unknown;
|
|
25
|
+
metadata?: unknown;
|
|
26
|
+
usage?: unknown;
|
|
27
|
+
risk?: unknown;
|
|
28
|
+
fidelity?: unknown;
|
|
29
|
+
confidentiality?: unknown;
|
|
30
|
+
isMeta: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface NormalizedNativeCall {
|
|
33
|
+
provider?: string;
|
|
34
|
+
timestamp?: string;
|
|
35
|
+
request_time?: string;
|
|
36
|
+
response_time?: string;
|
|
37
|
+
ttfb_ms?: number;
|
|
38
|
+
request_headers?: unknown;
|
|
39
|
+
response_headers?: unknown;
|
|
40
|
+
transport?: unknown;
|
|
41
|
+
transport_metadata?: unknown;
|
|
42
|
+
request_body?: unknown;
|
|
43
|
+
response_body?: unknown;
|
|
44
|
+
metadata?: unknown;
|
|
45
|
+
usage?: unknown;
|
|
46
|
+
risk?: unknown;
|
|
47
|
+
fidelity?: unknown;
|
|
48
|
+
confidentiality?: unknown;
|
|
49
|
+
sourceRecord?: unknown;
|
|
50
|
+
rawRow?: unknown;
|
|
51
|
+
}
|
|
52
|
+
export interface NormalizedSession {
|
|
53
|
+
turns: NormalizedTurn[];
|
|
54
|
+
sessionId?: string;
|
|
55
|
+
provider?: string;
|
|
56
|
+
model?: string;
|
|
57
|
+
tools?: unknown;
|
|
58
|
+
startedAt?: string;
|
|
59
|
+
nativeCalls?: NormalizedNativeCall[];
|
|
60
|
+
clientSource?: string;
|
|
61
|
+
systemPrompt?: string;
|
|
62
|
+
metadata?: unknown;
|
|
63
|
+
usage?: unknown;
|
|
64
|
+
risk?: unknown;
|
|
65
|
+
fidelity?: unknown;
|
|
66
|
+
confidentiality?: unknown;
|
|
67
|
+
sourceRecord?: unknown;
|
|
68
|
+
rawRows?: unknown[];
|
|
69
|
+
}
|
|
70
|
+
export interface SessionLogAdapter {
|
|
71
|
+
readonly agent: string;
|
|
72
|
+
detect(path: string): boolean;
|
|
73
|
+
parse(rawChunk: string): NormalizedTurn[];
|
|
74
|
+
parseSession?(rawChunk: string): NormalizedSession;
|
|
75
|
+
parseSessions?(rawChunk: string): NormalizedSession[];
|
|
76
|
+
}
|
|
77
|
+
export interface JsonlParseStats {
|
|
78
|
+
rowsScanned: number;
|
|
79
|
+
rowsRead: number;
|
|
80
|
+
invalidJson: number;
|
|
81
|
+
}
|
|
82
|
+
export declare const META_MARKERS: string[];
|
|
83
|
+
export declare function isMetaText(text: string): boolean;
|
|
84
|
+
export declare function parseJsonlLinesWithStats(chunk: string): {
|
|
85
|
+
rows: Record<string, unknown>[];
|
|
86
|
+
stats: JsonlParseStats;
|
|
87
|
+
};
|
|
88
|
+
export declare function parseJsonlLines(chunk: string): Record<string, unknown>[];
|
|
89
|
+
/** content: string | array of {type:text|tool_use|tool_result} → NormalizedTurn[]. */
|
|
90
|
+
export declare function extractContent(role: NormalizedTurn['role'], content: unknown): NormalizedTurn[];
|
|
91
|
+
/**
|
|
92
|
+
* Backfill toolName onto tool_result turns by correlating tool_use_id → the originating tool_use's name.
|
|
93
|
+
* Anthropic content blocks only name the tool on tool_use; the tool_result references it by id, so without
|
|
94
|
+
* this a failing tool_result has no tool attribution. No-op for turns that carry no toolUseId.
|
|
95
|
+
*/
|
|
96
|
+
export declare function correlateToolNames(turns: NormalizedTurn[]): NormalizedTurn[];
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export const META_MARKERS = ['HEARTBEAT_OK', 'NO_REPLY', 'NO_RESPONSE_NEEDED', '[META]'];
|
|
2
|
+
export function isMetaText(text) {
|
|
3
|
+
const t = text.trim();
|
|
4
|
+
return t.length === 0 || META_MARKERS.some((m) => t === m || t.startsWith(m));
|
|
5
|
+
}
|
|
6
|
+
export function parseJsonlLinesWithStats(chunk) {
|
|
7
|
+
const out = [];
|
|
8
|
+
const stats = { rowsScanned: 0, rowsRead: 0, invalidJson: 0 };
|
|
9
|
+
for (const l of chunk.split('\n')) {
|
|
10
|
+
if (!l.trim())
|
|
11
|
+
continue;
|
|
12
|
+
stats.rowsScanned += 1;
|
|
13
|
+
try {
|
|
14
|
+
const o = JSON.parse(l);
|
|
15
|
+
if (o && typeof o === 'object') {
|
|
16
|
+
out.push(o);
|
|
17
|
+
stats.rowsRead += 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
stats.invalidJson += 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { rows: out, stats };
|
|
25
|
+
}
|
|
26
|
+
export function parseJsonlLines(chunk) {
|
|
27
|
+
return parseJsonlLinesWithStats(chunk).rows;
|
|
28
|
+
}
|
|
29
|
+
function stringifyResult(c) {
|
|
30
|
+
if (typeof c === 'string')
|
|
31
|
+
return c;
|
|
32
|
+
if (Array.isArray(c))
|
|
33
|
+
return c.map((x) => (typeof x === 'string' ? x : x['text'] ?? JSON.stringify(x))).join('\n');
|
|
34
|
+
return JSON.stringify(c);
|
|
35
|
+
}
|
|
36
|
+
/** content: string | array of {type:text|tool_use|tool_result} → NormalizedTurn[]. */
|
|
37
|
+
export function extractContent(role, content) {
|
|
38
|
+
if (typeof content === 'string')
|
|
39
|
+
return [{ role, text: content, isMeta: isMetaText(content) }];
|
|
40
|
+
if (!Array.isArray(content))
|
|
41
|
+
return [];
|
|
42
|
+
const turns = [];
|
|
43
|
+
for (const part of content) {
|
|
44
|
+
if (!part || typeof part !== 'object')
|
|
45
|
+
continue;
|
|
46
|
+
const p = part;
|
|
47
|
+
const ptype = p['type'];
|
|
48
|
+
if (ptype === 'text' && typeof p['text'] === 'string')
|
|
49
|
+
turns.push({ role, text: p['text'], isMeta: isMetaText(p['text']) });
|
|
50
|
+
else if (ptype === 'thinking') {
|
|
51
|
+
const text = typeof p['thinking'] === 'string' ? p['thinking'] : (typeof p['text'] === 'string' ? p['text'] : '');
|
|
52
|
+
const reasoningSignature = typeof p['signature'] === 'string' ? p['signature'] : undefined;
|
|
53
|
+
const encryptedSignature = typeof p['encrypted_signature'] === 'string'
|
|
54
|
+
? p['encrypted_signature']
|
|
55
|
+
: (typeof p['encryptedSignature'] === 'string' ? p['encryptedSignature'] : undefined);
|
|
56
|
+
const encryptedContent = p['encrypted_content'] ?? p['encryptedContent'];
|
|
57
|
+
// Previously an empty thinking block (no text, no signature, no encrypted payload) was dropped entirely,
|
|
58
|
+
// which made "model thought nothing" indistinguishable from "we lost the reasoning". Keep it and flag it.
|
|
59
|
+
const isEmptyThinking = !text && !reasoningSignature && !encryptedSignature && encryptedContent === undefined;
|
|
60
|
+
turns.push({
|
|
61
|
+
role: 'assistant',
|
|
62
|
+
text,
|
|
63
|
+
reasoning: true,
|
|
64
|
+
...(isEmptyThinking ? { thinkingEmpty: true } : {}),
|
|
65
|
+
...(reasoningSignature ? { reasoningSignature } : {}),
|
|
66
|
+
...(encryptedSignature ? { encryptedSignature } : {}),
|
|
67
|
+
...(encryptedContent !== undefined ? { encryptedContent } : {}),
|
|
68
|
+
isMeta: false,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else if (ptype === 'tool_use') {
|
|
72
|
+
turns.push({
|
|
73
|
+
role: 'assistant',
|
|
74
|
+
text: '',
|
|
75
|
+
toolName: String(p['name'] ?? ''),
|
|
76
|
+
...(p['id'] ? { toolUseId: String(p['id']) } : {}),
|
|
77
|
+
...(p['input'] !== undefined ? { toolInput: p['input'] } : {}),
|
|
78
|
+
isMeta: false,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else if (ptype === 'tool_result') {
|
|
82
|
+
const r = stringifyResult(p['content']);
|
|
83
|
+
turns.push({ role: 'tool', text: '', toolResult: r, ...(p['tool_use_id'] ? { toolUseId: String(p['tool_use_id']) } : {}), ...(p['is_error'] ? { errorMessage: r } : {}), isMeta: false });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return turns;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Backfill toolName onto tool_result turns by correlating tool_use_id → the originating tool_use's name.
|
|
90
|
+
* Anthropic content blocks only name the tool on tool_use; the tool_result references it by id, so without
|
|
91
|
+
* this a failing tool_result has no tool attribution. No-op for turns that carry no toolUseId.
|
|
92
|
+
*/
|
|
93
|
+
export function correlateToolNames(turns) {
|
|
94
|
+
const nameById = new Map();
|
|
95
|
+
for (const t of turns)
|
|
96
|
+
if (t.toolUseId && t.toolName)
|
|
97
|
+
nameById.set(t.toolUseId, t.toolName);
|
|
98
|
+
if (nameById.size === 0)
|
|
99
|
+
return turns;
|
|
100
|
+
return turns.map((t) => (!t.toolName && t.toolUseId && nameById.has(t.toolUseId) ? { ...t, toolName: nameById.get(t.toolUseId) } : t));
|
|
101
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@evomap/evolver-runtime-adapters",
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "五 runtime 会话日志适配器 (CC/codex/cursor/kiro/opencode)",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"tag": "v2-beta"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/",
|
|
21
|
+
"README.md",
|
|
22
|
+
"package.json"
|
|
23
|
+
]
|
|
24
|
+
}
|