@tangle-network/harness-sessions 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +70 -2
- package/dist/builder.d.ts +116 -0
- package/dist/builder.js +332 -0
- package/dist/capture.d.ts +44 -0
- package/dist/capture.js +210 -0
- package/dist/catalog.d.ts +163 -0
- package/dist/catalog.js +163 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +115 -0
- package/dist/formats/claude-code.d.ts +9 -0
- package/dist/formats/claude-code.js +293 -0
- package/dist/formats/codex.d.ts +8 -0
- package/dist/formats/codex.js +379 -0
- package/dist/formats/factory.d.ts +13 -0
- package/dist/formats/factory.js +155 -0
- package/dist/formats/fold.d.ts +13 -0
- package/dist/formats/fold.js +5 -0
- package/dist/formats/head.d.ts +5 -0
- package/dist/formats/head.js +23 -0
- package/dist/formats/kimi.d.ts +9 -0
- package/dist/formats/kimi.js +205 -0
- package/dist/formats/opencode.d.ts +4 -0
- package/dist/formats/opencode.js +241 -0
- package/dist/formats/pi.d.ts +8 -0
- package/dist/formats/pi.js +201 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +16 -0
- package/dist/input.d.ts +18 -0
- package/dist/input.js +46 -0
- package/dist/projections/chat.d.ts +31 -0
- package/dist/projections/chat.js +79 -0
- package/dist/projections/otlp.d.ts +12 -0
- package/dist/projections/otlp.js +91 -0
- package/dist/projections/turns.d.ts +51 -0
- package/dist/projections/turns.js +136 -0
- package/dist/registry.d.ts +11 -0
- package/dist/registry.js +39 -0
- package/dist/schema.d.ts +270 -0
- package/dist/schema.js +10 -0
- package/dist/source.d.ts +67 -0
- package/dist/source.js +254 -0
- package/dist/stores.d.ts +7 -0
- package/dist/stores.js +46 -0
- package/package.json +53 -3
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code: `~/.claude/projects/<cwd slug>/<session id>.jsonl`, one JSON record per line, and
|
|
3
|
+
* each subagent's own transcript at `<session id>/subagents/agent-<agent id>.jsonl`.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code writes one `assistant` record per content block of an API response, repeating the
|
|
6
|
+
* response's `message.id`, `model` and `usage` on each; the blocks are one model call. It writes a
|
|
7
|
+
* failed call as an `assistant` record with `isApiErrorMessage` and model `<synthetic>`, and each
|
|
8
|
+
* retried API failure as a `system` record with subtype `api_error`. `message.model` is the model
|
|
9
|
+
* the API answered with.
|
|
10
|
+
*/
|
|
11
|
+
import { basename, join } from 'node:path';
|
|
12
|
+
import { SessionBuilder, count, isRecord, sessionError, str, textOf, usageOrNull } from '../builder.js';
|
|
13
|
+
import { fileRecords } from '../source.js';
|
|
14
|
+
import { runFold } from './fold.js';
|
|
15
|
+
import { EVIDENCE, STORES, mtimeMs, storeGlob, walkFiles } from '../stores.js';
|
|
16
|
+
import { headRecord } from './head.js';
|
|
17
|
+
const FORMAT = 'claude-code.projects-jsonl';
|
|
18
|
+
const STORE = STORES[FORMAT];
|
|
19
|
+
const HARNESS = 'claude-code';
|
|
20
|
+
function usageOf(value) {
|
|
21
|
+
if (!isRecord(value))
|
|
22
|
+
return null;
|
|
23
|
+
const details = isRecord(value.output_tokens_details) ? value.output_tokens_details : {};
|
|
24
|
+
return usageOrNull({
|
|
25
|
+
input: count(value.input_tokens),
|
|
26
|
+
output: count(value.output_tokens),
|
|
27
|
+
reasoning: count(details.thinking_tokens),
|
|
28
|
+
cacheRead: count(value.cache_read_input_tokens),
|
|
29
|
+
cacheWrite: count(value.cache_creation_input_tokens),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const INTERRUPTED = /^\[Request interrupted by user/u;
|
|
33
|
+
export function createFold(ref, mode, stats) {
|
|
34
|
+
const subagent = ref.parentNativeSessionId !== null;
|
|
35
|
+
const builder = new SessionBuilder(HARNESS, FORMAT, EVIDENCE[FORMAT].servedModel, mode, ref.nativeSessionId, ref.parentNativeSessionId);
|
|
36
|
+
let sidechainSkipped = 0;
|
|
37
|
+
let aborted = null;
|
|
38
|
+
let errors = 0;
|
|
39
|
+
let identified = false;
|
|
40
|
+
const observe = (record) => {
|
|
41
|
+
if (!subagent && record.isSidechain === true) {
|
|
42
|
+
sidechainSkipped += 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const type = record.type;
|
|
46
|
+
if (type !== 'user' && type !== 'assistant' && type !== 'system')
|
|
47
|
+
return;
|
|
48
|
+
const at = builder.seen(record.timestamp);
|
|
49
|
+
if (builder.cwd === null && typeof record.cwd === 'string')
|
|
50
|
+
builder.cwd = record.cwd;
|
|
51
|
+
if (!identified && typeof record.sessionId === 'string') {
|
|
52
|
+
// The records name the session; a subagent's records name its parent and its agent id.
|
|
53
|
+
identified = true;
|
|
54
|
+
if (subagent && typeof record.agentId === 'string') {
|
|
55
|
+
builder.nativeSessionId = record.agentId;
|
|
56
|
+
builder.parentNativeSessionId = record.sessionId;
|
|
57
|
+
}
|
|
58
|
+
else if (!subagent) {
|
|
59
|
+
builder.nativeSessionId = record.sessionId;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const uuid = str(record.uuid) ?? `${builder.nativeSessionId}:${stats.line}`;
|
|
63
|
+
if (type === 'system') {
|
|
64
|
+
if (record.subtype !== 'api_error')
|
|
65
|
+
return;
|
|
66
|
+
// A failed API request that Claude Code retried (or gave up on). It is a model call that
|
|
67
|
+
// returned no answer; a later success supersedes it in the ending.
|
|
68
|
+
const error = isRecord(record.error) ? record.error : {};
|
|
69
|
+
const message = str(error.formatted) ?? str(error.message) ?? str(record.content) ?? 'API error';
|
|
70
|
+
builder.modelCall({
|
|
71
|
+
id: uuid,
|
|
72
|
+
at,
|
|
73
|
+
provider: null,
|
|
74
|
+
servedModel: null,
|
|
75
|
+
requestedModel: null,
|
|
76
|
+
usage: null,
|
|
77
|
+
costUsd: null,
|
|
78
|
+
stopReason: null,
|
|
79
|
+
error: sessionError('api_error', message, at, count(error.status)),
|
|
80
|
+
});
|
|
81
|
+
errors += 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const message = isRecord(record.message) ? record.message : null;
|
|
85
|
+
if (message === null) {
|
|
86
|
+
builder.gap(`record ${uuid}: ${type} record has no message`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (type === 'user') {
|
|
90
|
+
aborted = null;
|
|
91
|
+
const content = message.content;
|
|
92
|
+
if (typeof content === 'string') {
|
|
93
|
+
if (INTERRUPTED.test(content))
|
|
94
|
+
aborted = { at };
|
|
95
|
+
builder.message({
|
|
96
|
+
id: uuid,
|
|
97
|
+
role: 'user',
|
|
98
|
+
actor: record.isMeta === true || record.isCompactSummary === true ? 'injected' : subagent ? 'subagent-spawn' : 'human',
|
|
99
|
+
at,
|
|
100
|
+
modelCallId: null,
|
|
101
|
+
parts: [{ type: 'text', text: content }],
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!Array.isArray(content))
|
|
106
|
+
return;
|
|
107
|
+
const toolParts = [];
|
|
108
|
+
const userParts = [];
|
|
109
|
+
for (const block of content) {
|
|
110
|
+
if (!isRecord(block))
|
|
111
|
+
continue;
|
|
112
|
+
if (block.type === 'tool_result' && typeof block.tool_use_id === 'string') {
|
|
113
|
+
toolParts.push({ type: 'tool-result', toolCallId: block.tool_use_id });
|
|
114
|
+
const results = content.filter((b) => isRecord(b) && b.type === 'tool_result').length;
|
|
115
|
+
builder.toolResult(block.tool_use_id, {
|
|
116
|
+
output: block.content ?? null,
|
|
117
|
+
text: textOf(block.content),
|
|
118
|
+
isError: block.is_error === true,
|
|
119
|
+
at,
|
|
120
|
+
messageId: `${uuid}:tool`,
|
|
121
|
+
// The record's structured result belongs to its tool result when it carries one.
|
|
122
|
+
details: results === 1 ? (record.toolUseResult ?? null) : null,
|
|
123
|
+
});
|
|
124
|
+
const result = isRecord(record.toolUseResult) ? record.toolUseResult : null;
|
|
125
|
+
const agentId = result ? str(result.agentId) : null;
|
|
126
|
+
if (agentId)
|
|
127
|
+
builder.child(agentId, block.tool_use_id);
|
|
128
|
+
}
|
|
129
|
+
else if (block.type === 'text' && typeof block.text === 'string') {
|
|
130
|
+
if (INTERRUPTED.test(block.text))
|
|
131
|
+
aborted = { at };
|
|
132
|
+
userParts.push({ type: 'text', text: block.text });
|
|
133
|
+
}
|
|
134
|
+
else if (block.type === 'image' || block.type === 'document') {
|
|
135
|
+
const source = isRecord(block.source) ? block.source : {};
|
|
136
|
+
userParts.push({ type: 'attachment', mediaType: str(source.media_type) ?? String(block.type), ref: str(source.url) ?? `inline:${uuid}` });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (toolParts.length > 0)
|
|
140
|
+
builder.message({ id: `${uuid}:tool`, role: 'tool', actor: 'tool-result', at, modelCallId: null, parts: toolParts });
|
|
141
|
+
if (userParts.length > 0) {
|
|
142
|
+
builder.message({
|
|
143
|
+
id: uuid,
|
|
144
|
+
role: 'user',
|
|
145
|
+
actor: record.isMeta === true ? 'injected' : subagent ? 'subagent-spawn' : 'human',
|
|
146
|
+
at,
|
|
147
|
+
modelCallId: null,
|
|
148
|
+
parts: userParts,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// assistant
|
|
154
|
+
aborted = null;
|
|
155
|
+
const model = str(message.model);
|
|
156
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
157
|
+
if (record.isApiErrorMessage === true || model === '<synthetic>') {
|
|
158
|
+
const text = textOf(content) ?? 'API error';
|
|
159
|
+
const call = builder.modelCall({
|
|
160
|
+
id: uuid,
|
|
161
|
+
at,
|
|
162
|
+
provider: null,
|
|
163
|
+
servedModel: null,
|
|
164
|
+
requestedModel: null,
|
|
165
|
+
usage: null,
|
|
166
|
+
costUsd: null,
|
|
167
|
+
stopReason: str(message.stop_reason),
|
|
168
|
+
error: sessionError(str(record.error) ?? 'api-error', text, at),
|
|
169
|
+
});
|
|
170
|
+
builder.message({ id: uuid, role: 'assistant', actor: 'injected', at, modelCallId: call.id, parts: [{ type: 'text', text }] });
|
|
171
|
+
errors += 1;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const apiId = str(message.id) ?? uuid;
|
|
175
|
+
const parts = [];
|
|
176
|
+
for (const block of content) {
|
|
177
|
+
if (!isRecord(block))
|
|
178
|
+
continue;
|
|
179
|
+
if (block.type === 'text' && typeof block.text === 'string')
|
|
180
|
+
parts.push({ type: 'text', text: block.text });
|
|
181
|
+
else if (block.type === 'thinking' && typeof block.thinking === 'string')
|
|
182
|
+
parts.push({ type: 'reasoning', text: block.thinking, redacted: false });
|
|
183
|
+
else if (block.type === 'redacted_thinking')
|
|
184
|
+
parts.push({ type: 'reasoning', text: '', redacted: true });
|
|
185
|
+
else if ((block.type === 'tool_use' || block.type === 'server_tool_use') && typeof block.id === 'string') {
|
|
186
|
+
parts.push({ type: 'tool-call', toolCallId: block.id });
|
|
187
|
+
builder.toolCall({ id: block.id, name: str(block.name) ?? 'unknown', input: block.input ?? null, inputText: null, at, messageId: apiId });
|
|
188
|
+
}
|
|
189
|
+
else if (typeof block.type === 'string' && block.type.endsWith('_tool_result') && typeof block.tool_use_id === 'string') {
|
|
190
|
+
builder.toolResult(block.tool_use_id, { output: block.content ?? null, text: textOf(block.content), isError: false, at, messageId: apiId });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const existing = builder.getModelCall(apiId);
|
|
194
|
+
if (existing) {
|
|
195
|
+
// A later block of the same response: its usage is the response's final usage so far.
|
|
196
|
+
existing.usage = usageOf(message.usage) ?? existing.usage;
|
|
197
|
+
existing.stopReason = str(message.stop_reason) ?? existing.stopReason;
|
|
198
|
+
if (builder.hasMessage(apiId))
|
|
199
|
+
builder.appendParts(apiId, parts);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
builder.modelCall({
|
|
203
|
+
id: apiId,
|
|
204
|
+
at,
|
|
205
|
+
provider: null,
|
|
206
|
+
servedModel: model,
|
|
207
|
+
requestedModel: null,
|
|
208
|
+
usage: usageOf(message.usage),
|
|
209
|
+
costUsd: null,
|
|
210
|
+
stopReason: str(message.stop_reason),
|
|
211
|
+
error: null,
|
|
212
|
+
});
|
|
213
|
+
builder.message({ id: apiId, role: 'assistant', actor: 'agent', at, modelCallId: apiId, parts });
|
|
214
|
+
};
|
|
215
|
+
const finish = () => {
|
|
216
|
+
builder.source(stats);
|
|
217
|
+
if (sidechainSkipped > 0)
|
|
218
|
+
builder.gap(`${sidechainSkipped} sidechain records in the main transcript belong to subagents and were not read as this session`);
|
|
219
|
+
if (errors > 0 && builder.modelCalls.every((c) => c.error))
|
|
220
|
+
builder.gap('every model call in the session failed');
|
|
221
|
+
builder.inferEnding(aborted ? { at: aborted.at, error: null } : undefined);
|
|
222
|
+
return builder;
|
|
223
|
+
};
|
|
224
|
+
return { builder, observe, finish };
|
|
225
|
+
}
|
|
226
|
+
export async function fold(ref, mode, options, source = fileRecords(ref.path, { strict: options.corruption === 'strict', signal: options.signal })) {
|
|
227
|
+
return runFold(createFold(ref, mode, source.stats), source);
|
|
228
|
+
}
|
|
229
|
+
async function locate(home, opts) {
|
|
230
|
+
const root = join(home, STORE.root);
|
|
231
|
+
const all = await walkFiles(root);
|
|
232
|
+
const main = storeGlob(STORE.files[0], opts.nativeSessionId);
|
|
233
|
+
const nested = storeGlob(STORE.files[1], opts.nativeSessionId);
|
|
234
|
+
const subagentsBySession = new Map();
|
|
235
|
+
for (const rel of all) {
|
|
236
|
+
if (!nested.test(rel))
|
|
237
|
+
continue;
|
|
238
|
+
const [project, id] = rel.split('/');
|
|
239
|
+
const key = `${project}/${id}`;
|
|
240
|
+
subagentsBySession.set(key, [...(subagentsBySession.get(key) ?? []), join(root, rel)]);
|
|
241
|
+
}
|
|
242
|
+
const refs = [];
|
|
243
|
+
for (const rel of all) {
|
|
244
|
+
if (!main.test(rel))
|
|
245
|
+
continue;
|
|
246
|
+
const path = join(root, rel);
|
|
247
|
+
const id = basename(rel, '.jsonl');
|
|
248
|
+
const mtime = await mtimeMs(path);
|
|
249
|
+
if (opts.sinceMs && mtime < opts.sinceMs)
|
|
250
|
+
continue;
|
|
251
|
+
const head = await headRecord(path, (r) => typeof r.cwd === 'string');
|
|
252
|
+
const cwd = head ? str(head.cwd) : null;
|
|
253
|
+
if (opts.cwd && (cwd === null || !cwd.startsWith(opts.cwd)))
|
|
254
|
+
continue;
|
|
255
|
+
const subagents = subagentsBySession.get(`${rel.split('/')[0]}/${id}`) ?? [];
|
|
256
|
+
refs.push({ harness: HARNESS, format: FORMAT, nativeSessionId: id, path, files: [path, ...subagents], home, cwd, mtimeMs: mtime, parentNativeSessionId: null });
|
|
257
|
+
for (const file of subagents) {
|
|
258
|
+
const agentId = basename(file, '.jsonl').replace(/^agent-/u, '');
|
|
259
|
+
refs.push({ harness: HARNESS, format: FORMAT, nativeSessionId: agentId, path: file, files: [file], home, cwd, mtimeMs: await mtimeMs(file), parentNativeSessionId: id });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
263
|
+
}
|
|
264
|
+
export const claudeCodeReader = {
|
|
265
|
+
harness: HARNESS,
|
|
266
|
+
aliases: ['claude', 'claude-code-acp', 'claudish'],
|
|
267
|
+
formats: [FORMAT],
|
|
268
|
+
stores: [STORE],
|
|
269
|
+
globs: { session: STORE.files[0], children: STORE.files[1] },
|
|
270
|
+
evidence: EVIDENCE[FORMAT],
|
|
271
|
+
locate: (home, opts = {}) => locate(home, opts),
|
|
272
|
+
async read(ref, opts = {}) {
|
|
273
|
+
return (await fold(ref, 'full', opts)).build();
|
|
274
|
+
},
|
|
275
|
+
async summarize(ref, opts = {}) {
|
|
276
|
+
return (await fold(ref, 'summary', opts)).summary();
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
/** A reader for one transcript file outside a HOME layout (the CLI, a retained copy). */
|
|
280
|
+
export function claudeCodeRefForFile(path, parentNativeSessionId = null) {
|
|
281
|
+
const name = basename(path, '.jsonl');
|
|
282
|
+
return {
|
|
283
|
+
harness: HARNESS,
|
|
284
|
+
format: FORMAT,
|
|
285
|
+
nativeSessionId: parentNativeSessionId === null ? name : name.replace(/^agent-/u, ''),
|
|
286
|
+
path,
|
|
287
|
+
files: [path],
|
|
288
|
+
home: null,
|
|
289
|
+
cwd: null,
|
|
290
|
+
mtimeMs: 0,
|
|
291
|
+
parentNativeSessionId,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SessionBuilder, type BuildMode } from '../builder.js';
|
|
2
|
+
import type { HarnessSessionReader, ReadOptions, SessionRef } from '../schema.js';
|
|
3
|
+
import { type RecordSource, type SourceStats } from '../source.js';
|
|
4
|
+
import { type SessionFold } from './fold.js';
|
|
5
|
+
export declare function createFold(ref: SessionRef, mode: BuildMode, stats: SourceStats): SessionFold;
|
|
6
|
+
export declare function fold(ref: SessionRef, mode: BuildMode, options: ReadOptions, source?: RecordSource): Promise<SessionBuilder>;
|
|
7
|
+
export declare const codexReader: HarnessSessionReader;
|
|
8
|
+
export declare function codexRefForFile(path: string): SessionRef;
|