@volcengine/tls-observer-claude-code 0.0.1
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/.claude-plugin/plugin.json +11 -0
- package/LICENCE +13 -0
- package/README.md +91 -0
- package/dist/config.d.ts +25 -0
- package/dist/exporter.d.ts +10 -0
- package/dist/hooks/compact.d.ts +4 -0
- package/dist/hooks/dispatch.d.ts +3 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/stop.d.ts +5 -0
- package/dist/hooks/subagent-stop.d.ts +3 -0
- package/dist/hooks/tool-use.d.ts +4 -0
- package/dist/hooks/trace-flow.d.ts +32 -0
- package/dist/hooks/user-prompt-submit.d.ts +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1982 -0
- package/dist/state.d.ts +6 -0
- package/dist/stdin.d.ts +1 -0
- package/dist/tls-producer.d.ts +12 -0
- package/dist/trace.d.ts +23 -0
- package/dist/transcript.d.ts +15 -0
- package/dist/types.d.ts +223 -0
- package/hooks/hooks.json +121 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1982 @@
|
|
|
1
|
+
import { homedir, hostname, networkInterfaces, userInfo } from "node:os";
|
|
2
|
+
import { tlsOpenapi } from "@volcengine/openapi";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import * as __rspack_external_node_fs_promises_153e37e0 from "node:fs/promises";
|
|
5
|
+
import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
|
|
6
|
+
import * as __rspack_external_node_path_c5b9b54f from "node:path";
|
|
7
|
+
function firstEnv(...names) {
|
|
8
|
+
for (const name of names){
|
|
9
|
+
const value = process.env[name];
|
|
10
|
+
if ('string' == typeof value && '' !== value.trim()) return value.trim();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function envFlag(name, defaultValue = false) {
|
|
14
|
+
const value = process.env[name];
|
|
15
|
+
if (void 0 === value || '' === value) return defaultValue;
|
|
16
|
+
return /^(1|true|yes|y|on)$/i.test(value);
|
|
17
|
+
}
|
|
18
|
+
function envDisabled(name) {
|
|
19
|
+
return /^(0|false|no|n|off)$/i.test(process.env[name] ?? '');
|
|
20
|
+
}
|
|
21
|
+
function envPositiveInteger(name, fallback) {
|
|
22
|
+
const value = Number.parseInt(process.env[name] ?? '', 10);
|
|
23
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
24
|
+
}
|
|
25
|
+
function defaultDataRoot() {
|
|
26
|
+
return __rspack_external_node_path_c5b9b54f.join(homedir(), '.claude', 'plugins', 'data', 'tls-observer-claude-code');
|
|
27
|
+
}
|
|
28
|
+
function defaultEnvFile() {
|
|
29
|
+
return __rspack_external_node_path_c5b9b54f.join(homedir(), '.claude', 'tls-observer-claude-code.env');
|
|
30
|
+
}
|
|
31
|
+
function parseEnvValue(raw) {
|
|
32
|
+
const value = raw.trim();
|
|
33
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1).replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function loadEnvFile(filePath) {
|
|
37
|
+
const target = filePath || firstEnv('CLAUDE_CODE_TLS_ENV_FILE') || defaultEnvFile();
|
|
38
|
+
let text;
|
|
39
|
+
try {
|
|
40
|
+
text = __rspack_external_node_fs_5ea92f0c.readFileSync(target, 'utf8');
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
for (const rawLine of text.split(/\r?\n/)){
|
|
45
|
+
const line = rawLine.trim();
|
|
46
|
+
if (!line || line.startsWith('#')) continue;
|
|
47
|
+
const normalized = line.startsWith('export ') ? line.slice(7).trim() : line;
|
|
48
|
+
const match = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
49
|
+
if (!match) continue;
|
|
50
|
+
const [, key, rawValue] = match;
|
|
51
|
+
process.env[key] ??= parseEnvValue(rawValue);
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
function loadConfig() {
|
|
56
|
+
loadEnvFile();
|
|
57
|
+
const dataRoot = firstEnv('CLAUDE_CODE_TLS_DATA_ROOT', 'CLAUDE_PLUGIN_DATA') || defaultDataRoot();
|
|
58
|
+
const serviceName = firstEnv('CLAUDE_CODE_OTEL_SERVICE_NAME') || "claude-code-transcript-hook";
|
|
59
|
+
const exportRetries = Math.min(envPositiveInteger('CLAUDE_CODE_TLS_EXPORT_TIMEOUT_RETRIES', 1), 3);
|
|
60
|
+
return {
|
|
61
|
+
enabled: !envDisabled('CLAUDE_CODE_TLS_EXPORT'),
|
|
62
|
+
dataRoot,
|
|
63
|
+
stateFilePath: firstEnv('CLAUDE_CODE_TLS_STATE_FILE') || __rspack_external_node_path_c5b9b54f.join(dataRoot, 'state', 'claude-code-state.json'),
|
|
64
|
+
sendLogsDir: __rspack_external_node_path_c5b9b54f.join(dataRoot, 'logs', 'send'),
|
|
65
|
+
artifactsDir: __rspack_external_node_path_c5b9b54f.join(dataRoot, 'artifacts'),
|
|
66
|
+
diagnosticLogMaxBytes: envPositiveInteger('CLAUDE_CODE_TLS_DIAGNOSTIC_LOG_MAX_BYTES', 5242880),
|
|
67
|
+
captureContent: !envDisabled('CLAUDE_CODE_TLS_CAPTURE_CONTENT'),
|
|
68
|
+
debugArtifacts: envFlag('CLAUDE_CODE_TLS_DEBUG_ARTIFACTS', false),
|
|
69
|
+
serviceName,
|
|
70
|
+
serviceInstanceId: `${serviceName}@${hostname()}:${process.pid}`,
|
|
71
|
+
tlsEndpoint: firstEnv('CLAUDE_CODE_TLS_ENDPOINT', 'TRACE_TLS_ENDPOINT', 'TLS_ENDPOINT'),
|
|
72
|
+
otelEndpoint: firstEnv('CLAUDE_CODE_TLS_OTEL_ENDPOINT', 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'),
|
|
73
|
+
region: firstEnv('CLAUDE_CODE_TLS_REGION', 'TLS_REGION'),
|
|
74
|
+
traceTopicId: firstEnv('CLAUDE_CODE_TLS_TRACE_TOPIC_ID', 'CLAUDE_CODE_TLS_TOPIC_ID', 'TLS_TRACE_TOPIC_ID'),
|
|
75
|
+
apiKey: firstEnv('CLAUDE_CODE_TLS_API_KEY', 'TLS_API_KEY'),
|
|
76
|
+
ak: firstEnv('CLAUDE_CODE_TLS_AK', 'TLS_AK'),
|
|
77
|
+
sk: firstEnv('CLAUDE_CODE_TLS_SK', 'TLS_SK'),
|
|
78
|
+
authMode: 'resource' === firstEnv('CLAUDE_CODE_TLS_AUTH_MODE', 'TLS_AUTH_MODE') ? 'resource' : 'header',
|
|
79
|
+
exportTimeoutMs: envPositiveInteger('CLAUDE_CODE_TLS_EXPORT_TIMEOUT_MS', 20000),
|
|
80
|
+
exportTimeoutRetries: exportRetries
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function localIdentityMetadata() {
|
|
84
|
+
return {
|
|
85
|
+
'user.name': userInfo().username
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function safeJsonStringify(value, fallback = '{}') {
|
|
89
|
+
const seen = new WeakSet();
|
|
90
|
+
try {
|
|
91
|
+
return JSON.stringify(value, (_key, item)=>{
|
|
92
|
+
if (item && 'object' == typeof item) {
|
|
93
|
+
if (seen.has(item)) return '[Circular]';
|
|
94
|
+
seen.add(item);
|
|
95
|
+
}
|
|
96
|
+
return item;
|
|
97
|
+
}) ?? fallback;
|
|
98
|
+
} catch {
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function compactObject(value) {
|
|
103
|
+
return Object.fromEntries(Object.entries(value).filter(([, item])=>null != item && '' !== item));
|
|
104
|
+
}
|
|
105
|
+
const TLS_ANONYMOUS_IDENTITY_HEADER = 'x-tls-anonymous-identity';
|
|
106
|
+
const NO_RETRY_STATUS_CODES = [
|
|
107
|
+
400,
|
|
108
|
+
401,
|
|
109
|
+
403,
|
|
110
|
+
404
|
|
111
|
+
];
|
|
112
|
+
const producerEntries = new Map();
|
|
113
|
+
function firstExternalIpv4() {
|
|
114
|
+
for (const items of Object.values(networkInterfaces()))for (const item of items ?? [])if ('IPv4' === item.family && !item.internal && item.address) return item.address;
|
|
115
|
+
return '127.0.0.1';
|
|
116
|
+
}
|
|
117
|
+
function normalizeProducerEndpoint(endpoint) {
|
|
118
|
+
const raw = endpoint.trim();
|
|
119
|
+
if (!raw) return raw;
|
|
120
|
+
try {
|
|
121
|
+
const url = new URL(/^https?:\/\//.test(raw) ? raw : `https://${raw}`);
|
|
122
|
+
return url.hostname || raw;
|
|
123
|
+
} catch {
|
|
124
|
+
return raw.replace(/^https?:\/\//, '').replace(/\/.*$/, '').replace(/:\d+$/, '');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function producerKey(config) {
|
|
128
|
+
return JSON.stringify({
|
|
129
|
+
endpoint: normalizeProducerEndpoint(config.endpoint),
|
|
130
|
+
region: config.region,
|
|
131
|
+
apiKey: config.apiKey || '',
|
|
132
|
+
ak: config.ak || '',
|
|
133
|
+
sk: config.sk || ''
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function timeoutError(label, timeoutMs) {
|
|
137
|
+
return new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
138
|
+
}
|
|
139
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
140
|
+
const effectiveTimeoutMs = Number.isFinite(timeoutMs) && Number(timeoutMs) > 0 ? Math.trunc(Number(timeoutMs)) : 20000;
|
|
141
|
+
return new Promise((resolve, reject)=>{
|
|
142
|
+
const timer = setTimeout(()=>reject(timeoutError(label, effectiveTimeoutMs)), effectiveTimeoutMs);
|
|
143
|
+
promise.then((value)=>{
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
resolve(value);
|
|
146
|
+
}, (error)=>{
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
reject(error);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function getProducer(config) {
|
|
153
|
+
const key = producerKey(config);
|
|
154
|
+
const existing = producerEntries.get(key);
|
|
155
|
+
if (existing) return existing;
|
|
156
|
+
const producerConfig = tlsOpenapi.Producer.getDefaultProducerConfig();
|
|
157
|
+
producerConfig.Endpoint = normalizeProducerEndpoint(config.endpoint);
|
|
158
|
+
producerConfig.Region = config.region;
|
|
159
|
+
producerConfig.NoRetryStatusCodeList = NO_RETRY_STATUS_CODES;
|
|
160
|
+
if (config.apiKey) {
|
|
161
|
+
producerConfig.AccessKeyID = '';
|
|
162
|
+
producerConfig.AccessKeySecret = '';
|
|
163
|
+
producerConfig.Headers = {
|
|
164
|
+
[TLS_ANONYMOUS_IDENTITY_HEADER]: config.apiKey
|
|
165
|
+
};
|
|
166
|
+
} else {
|
|
167
|
+
producerConfig.AccessKeyID = config.ak || '';
|
|
168
|
+
producerConfig.AccessKeySecret = config.sk || '';
|
|
169
|
+
}
|
|
170
|
+
const producer = new tlsOpenapi.Producer(producerConfig);
|
|
171
|
+
producer.Start();
|
|
172
|
+
const entry = {
|
|
173
|
+
producer,
|
|
174
|
+
source: firstExternalIpv4(),
|
|
175
|
+
timeoutMs: config.timeoutMs ?? 20000
|
|
176
|
+
};
|
|
177
|
+
producerEntries.set(key, entry);
|
|
178
|
+
return entry;
|
|
179
|
+
}
|
|
180
|
+
async function sendTlsLogRecords(config, records, fileName) {
|
|
181
|
+
if (0 === records.length) return;
|
|
182
|
+
const entry = getProducer(config);
|
|
183
|
+
const logGroup = {
|
|
184
|
+
Logs: records.map((record)=>({
|
|
185
|
+
Time: Date.now(),
|
|
186
|
+
Contents: Object.entries(record).map(([Key, Value])=>({
|
|
187
|
+
Key,
|
|
188
|
+
Value
|
|
189
|
+
}))
|
|
190
|
+
})),
|
|
191
|
+
Source: entry.source,
|
|
192
|
+
LogTags: [],
|
|
193
|
+
FileName: fileName
|
|
194
|
+
};
|
|
195
|
+
await withTimeout(new Promise((resolve, reject)=>{
|
|
196
|
+
let settled = false;
|
|
197
|
+
const settle = (fn, value)=>{
|
|
198
|
+
if (settled) return;
|
|
199
|
+
settled = true;
|
|
200
|
+
fn(value);
|
|
201
|
+
};
|
|
202
|
+
const resolveUnknown = resolve;
|
|
203
|
+
const sendPromise = entry.producer.SendLogs('', config.traceTopicId, entry.source, fileName, logGroup, {
|
|
204
|
+
Success: ()=>settle(resolveUnknown),
|
|
205
|
+
Fail: (result)=>{
|
|
206
|
+
const attempts = result?.Attempts ?? [];
|
|
207
|
+
const lastAttempt = attempts[attempts.length - 1] ?? {};
|
|
208
|
+
settle(reject, new Error(`TLS SendLogs failed topicId=${config.traceTopicId} requestId=${String(lastAttempt.RequestId ?? '')} errorCode=${String(lastAttempt.ErrorCode ?? '')} error=${JSON.stringify(lastAttempt)}`));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
Promise.resolve(sendPromise).catch((error)=>settle(reject, error));
|
|
212
|
+
}), config.timeoutMs, 'TLS SendLogs');
|
|
213
|
+
}
|
|
214
|
+
async function closeTraceLogProducers() {
|
|
215
|
+
const entries = [
|
|
216
|
+
...producerEntries.values()
|
|
217
|
+
];
|
|
218
|
+
producerEntries.clear();
|
|
219
|
+
await Promise.allSettled(entries.map((entry)=>{
|
|
220
|
+
const closeTimeoutMs = Math.min(Math.max(entry.timeoutMs || 20000, 1000), 5000);
|
|
221
|
+
return withTimeout(Promise.resolve(entry.producer.Close()), closeTimeoutMs, 'TLS Producer Close');
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
var package_namespaceObject = {
|
|
225
|
+
rE: "0.0.1"
|
|
226
|
+
};
|
|
227
|
+
function toOtlpAnyValue(value) {
|
|
228
|
+
if ('string' == typeof value) return {
|
|
229
|
+
stringValue: value
|
|
230
|
+
};
|
|
231
|
+
if ('boolean' == typeof value) return {
|
|
232
|
+
boolValue: value
|
|
233
|
+
};
|
|
234
|
+
if ('number' == typeof value) return Number.isInteger(value) ? {
|
|
235
|
+
intValue: String(value)
|
|
236
|
+
} : {
|
|
237
|
+
doubleValue: value
|
|
238
|
+
};
|
|
239
|
+
if (Array.isArray(value)) return {
|
|
240
|
+
arrayValue: {
|
|
241
|
+
values: value.map((item)=>toOtlpAnyValue(item))
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
if (value && 'object' == typeof value) return {
|
|
245
|
+
kvlistValue: {
|
|
246
|
+
values: Object.entries(value).filter(([, item])=>void 0 !== item).map(([key, item])=>({
|
|
247
|
+
key,
|
|
248
|
+
value: toOtlpAnyValue(item)
|
|
249
|
+
}))
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
stringValue: String(value ?? '')
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function toOtlpAttributes(attributes) {
|
|
257
|
+
return Object.entries(attributes).filter(([, value])=>null != value).map(([key, value])=>({
|
|
258
|
+
key,
|
|
259
|
+
value: toOtlpAnyValue(value)
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
function otlpValueToJs(value) {
|
|
263
|
+
if (void 0 !== value.stringValue) return value.stringValue;
|
|
264
|
+
if (void 0 !== value.intValue) return Number(value.intValue);
|
|
265
|
+
if (void 0 !== value.doubleValue) return value.doubleValue;
|
|
266
|
+
if (void 0 !== value.boolValue) return value.boolValue;
|
|
267
|
+
if (value.arrayValue && 'object' == typeof value.arrayValue) {
|
|
268
|
+
const arr = value.arrayValue;
|
|
269
|
+
return (arr.values || []).map(otlpValueToJs);
|
|
270
|
+
}
|
|
271
|
+
if (value.kvlistValue && 'object' == typeof value.kvlistValue) {
|
|
272
|
+
const kv = value.kvlistValue;
|
|
273
|
+
const result = {};
|
|
274
|
+
for (const item of kv.values || [])result[item.key] = otlpValueToJs(item.value);
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
function otlpAttributesToMap(attributes) {
|
|
280
|
+
return attributes.reduce((acc, attr)=>{
|
|
281
|
+
acc[attr.key] = otlpValueToJs(attr.value);
|
|
282
|
+
return acc;
|
|
283
|
+
}, {});
|
|
284
|
+
}
|
|
285
|
+
function toUnixNanoString(ms) {
|
|
286
|
+
return String(BigInt(Math.trunc(Number.isFinite(ms) ? ms : Date.now())) * BigInt(1000000));
|
|
287
|
+
}
|
|
288
|
+
function spanKindToName(kind) {
|
|
289
|
+
const kindMap = {
|
|
290
|
+
0: 'unspecified',
|
|
291
|
+
1: 'internal',
|
|
292
|
+
2: 'server',
|
|
293
|
+
3: 'client',
|
|
294
|
+
4: 'producer',
|
|
295
|
+
5: 'consumer'
|
|
296
|
+
};
|
|
297
|
+
if ('number' == typeof kind) return kindMap[kind] || 'internal';
|
|
298
|
+
const upper = String(kind).toUpperCase();
|
|
299
|
+
if ('SERVER' === upper) return 'server';
|
|
300
|
+
if ('CLIENT' === upper) return 'client';
|
|
301
|
+
return 'internal';
|
|
302
|
+
}
|
|
303
|
+
function statusCodeToName(code) {
|
|
304
|
+
const statusMap = {
|
|
305
|
+
0: 'UNSET',
|
|
306
|
+
1: 'OK',
|
|
307
|
+
2: 'ERROR'
|
|
308
|
+
};
|
|
309
|
+
if (null == code) return 'UNSET';
|
|
310
|
+
const num = Number(code);
|
|
311
|
+
return statusMap[num] ?? 'UNSET';
|
|
312
|
+
}
|
|
313
|
+
function inferSpanTypeFromName(name, explicitType) {
|
|
314
|
+
if ('string' == typeof explicitType && explicitType) return explicitType;
|
|
315
|
+
if ('agent.turn' === name || 'codex_turn' === name) return 'message';
|
|
316
|
+
if ('llm.request' === name) return 'model';
|
|
317
|
+
if ('tool.call' === name) return 'tool';
|
|
318
|
+
}
|
|
319
|
+
function otlpSpanToTlsEnvelope(otlpSpan, resource, scope, options) {
|
|
320
|
+
const status = otlpSpan.status;
|
|
321
|
+
const statusCodeNum = Number(status?.code);
|
|
322
|
+
const isError = 2 === statusCodeNum;
|
|
323
|
+
const attributes = otlpAttributesToMap(otlpSpan.attributes || []);
|
|
324
|
+
const spanType = inferSpanTypeFromName(otlpSpan.name, otlpSpan.type);
|
|
325
|
+
const isTool = 'tool' === spanType;
|
|
326
|
+
if (isTool) attributes['tool.error'] = isError;
|
|
327
|
+
else delete attributes['tool.error'];
|
|
328
|
+
if (isError) {
|
|
329
|
+
attributes['error.type'] = attributes['error.type'] ?? 'error';
|
|
330
|
+
attributes['error.message'] = String(status?.message || '');
|
|
331
|
+
} else {
|
|
332
|
+
attributes['error.type'] = '';
|
|
333
|
+
attributes['error.message'] = '';
|
|
334
|
+
}
|
|
335
|
+
if (options?.filterAttributes) {
|
|
336
|
+
for (const key of Object.keys(attributes))if (!options.filterAttributes(key)) delete attributes[key];
|
|
337
|
+
}
|
|
338
|
+
const resourceAttributes = otlpAttributesToMap(resource.attributes || []);
|
|
339
|
+
const events = (otlpSpan.events || []).map((event)=>({
|
|
340
|
+
timeUnixNano: event.timeUnixNano,
|
|
341
|
+
name: event.name,
|
|
342
|
+
attributes: otlpAttributesToMap(event.attributes || [])
|
|
343
|
+
}));
|
|
344
|
+
const links = (otlpSpan.links || []).map((link)=>({
|
|
345
|
+
traceId: link.traceId,
|
|
346
|
+
spanId: link.spanId,
|
|
347
|
+
traceState: link.traceState,
|
|
348
|
+
attributes: otlpAttributesToMap(link.attributes || [])
|
|
349
|
+
}));
|
|
350
|
+
const startNano = BigInt(String(otlpSpan.startTimeUnixNano));
|
|
351
|
+
const endNano = BigInt(String(otlpSpan.endTimeUnixNano));
|
|
352
|
+
const us = BigInt(1000);
|
|
353
|
+
const durationUs = (endNano - startNano) / us;
|
|
354
|
+
return {
|
|
355
|
+
TraceID: String(otlpSpan.traceId),
|
|
356
|
+
SpanID: String(otlpSpan.spanId),
|
|
357
|
+
ParentSpanID: String(otlpSpan.parentSpanId || ''),
|
|
358
|
+
Name: String(otlpSpan.name),
|
|
359
|
+
Kind: spanKindToName(otlpSpan.kind),
|
|
360
|
+
Start: String(startNano / us),
|
|
361
|
+
End: String(endNano / us),
|
|
362
|
+
Duration: String(durationUs),
|
|
363
|
+
StatusCode: statusCodeToName(statusCodeNum),
|
|
364
|
+
StatusDescription: String(status?.message || ''),
|
|
365
|
+
Attributes: safeJsonStringify(attributes),
|
|
366
|
+
Resource: safeJsonStringify(resourceAttributes),
|
|
367
|
+
ServiceName: String(resourceAttributes['service.name'] || ''),
|
|
368
|
+
Host: String(resourceAttributes['host.name'] || ''),
|
|
369
|
+
OTLPName: String(scope.name),
|
|
370
|
+
OTLPVersion: String(scope.version),
|
|
371
|
+
Events: safeJsonStringify(events),
|
|
372
|
+
Links: safeJsonStringify(links),
|
|
373
|
+
TraceState: String(otlpSpan.traceState || '')
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const TLS_AUTH_RESOURCE_KEYS = {
|
|
377
|
+
traceTopicId: 'tls.otel.tracetopic',
|
|
378
|
+
apiKey: 'tls.otel.apikey',
|
|
379
|
+
ak: 'tls.otel.ak',
|
|
380
|
+
sk: 'tls.otel.sk',
|
|
381
|
+
region: 'tls.otel.region'
|
|
382
|
+
};
|
|
383
|
+
const SCHEMA_FIELD_ORDER = [
|
|
384
|
+
'span.name',
|
|
385
|
+
'span.kind',
|
|
386
|
+
'tls.app.type',
|
|
387
|
+
'session.id',
|
|
388
|
+
'span.status.code',
|
|
389
|
+
'start_time_unix_us',
|
|
390
|
+
'end_time_unix_us',
|
|
391
|
+
'span.duration_us',
|
|
392
|
+
'service.name',
|
|
393
|
+
'service.instance.id',
|
|
394
|
+
'host.name',
|
|
395
|
+
'observed_time_unix_ms',
|
|
396
|
+
'telemetry.sdk.name',
|
|
397
|
+
'telemetry.sdk.language',
|
|
398
|
+
'telemetry.sdk.version',
|
|
399
|
+
'otel.scope.name',
|
|
400
|
+
'otel.scope.version',
|
|
401
|
+
'event.name',
|
|
402
|
+
'user.role',
|
|
403
|
+
'gen_ai.operation.name',
|
|
404
|
+
'gen_ai.provider.name',
|
|
405
|
+
'gen_ai.request.model',
|
|
406
|
+
'gen_ai.response.model',
|
|
407
|
+
'gen_ai.request.duration_ms',
|
|
408
|
+
'gen_ai.input.messages',
|
|
409
|
+
'gen_ai.output.messages',
|
|
410
|
+
'gen_ai.usage.input_tokens',
|
|
411
|
+
'gen_ai.usage.output_tokens',
|
|
412
|
+
'gen_ai.usage.reasoning.output_tokens',
|
|
413
|
+
'gen_ai.usage.cache_read.input_tokens',
|
|
414
|
+
'gen_ai.usage.cache_creation.input_tokens',
|
|
415
|
+
'gen_ai.usage.total_tokens',
|
|
416
|
+
'gen_ai.tool.name',
|
|
417
|
+
'gen_ai.tool.call.id',
|
|
418
|
+
'gen_ai.tool.call.arguments',
|
|
419
|
+
'gen_ai.tool.call.result',
|
|
420
|
+
'error.type',
|
|
421
|
+
'error.message'
|
|
422
|
+
];
|
|
423
|
+
function serializeForAttr(value) {
|
|
424
|
+
const serialized = 'string' == typeof value ? value : safeJsonStringify(value);
|
|
425
|
+
return String(serialized ?? '').slice(0, 65536);
|
|
426
|
+
}
|
|
427
|
+
function spanKind(span) {
|
|
428
|
+
if ('SERVER' === span.span_kind) return 2;
|
|
429
|
+
if ('CLIENT' === span.span_kind) return 3;
|
|
430
|
+
return 1;
|
|
431
|
+
}
|
|
432
|
+
function spanKindName(span) {
|
|
433
|
+
if ('SERVER' === span.span_kind) return 'server';
|
|
434
|
+
if ('CLIENT' === span.span_kind) return 'client';
|
|
435
|
+
return 'internal';
|
|
436
|
+
}
|
|
437
|
+
function unixMsToUs(ms) {
|
|
438
|
+
return String(1000 * Math.trunc(Number.isFinite(ms) ? ms : Date.now()));
|
|
439
|
+
}
|
|
440
|
+
function hasError(span) {
|
|
441
|
+
if (span.status?.code === 'ERROR') return true;
|
|
442
|
+
return Boolean(span.attributes['error.type']);
|
|
443
|
+
}
|
|
444
|
+
function buildResourceAttributes(trace, config) {
|
|
445
|
+
const attributes = {
|
|
446
|
+
...trace.resource
|
|
447
|
+
};
|
|
448
|
+
if ('resource' === config.authMode) {
|
|
449
|
+
attributes[TLS_AUTH_RESOURCE_KEYS.traceTopicId] = config.traceTopicId;
|
|
450
|
+
attributes[TLS_AUTH_RESOURCE_KEYS.region] = config.region;
|
|
451
|
+
if (config.apiKey) attributes[TLS_AUTH_RESOURCE_KEYS.apiKey] = config.apiKey;
|
|
452
|
+
else {
|
|
453
|
+
attributes[TLS_AUTH_RESOURCE_KEYS.ak] = config.ak;
|
|
454
|
+
attributes[TLS_AUTH_RESOURCE_KEYS.sk] = config.sk;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return attributes;
|
|
458
|
+
}
|
|
459
|
+
function cleanSpanAttributesForOtlp(span) {
|
|
460
|
+
const attributes = {
|
|
461
|
+
...span.attributes
|
|
462
|
+
};
|
|
463
|
+
for (const key of [
|
|
464
|
+
'span.name',
|
|
465
|
+
'span.kind',
|
|
466
|
+
'span.status.code',
|
|
467
|
+
'service.name',
|
|
468
|
+
'service.instance.id',
|
|
469
|
+
'host.name',
|
|
470
|
+
'telemetry.sdk.name',
|
|
471
|
+
'telemetry.sdk.language',
|
|
472
|
+
'telemetry.sdk.version'
|
|
473
|
+
])delete attributes[key];
|
|
474
|
+
return attributes;
|
|
475
|
+
}
|
|
476
|
+
function buildOtlpPayload(trace, config) {
|
|
477
|
+
return {
|
|
478
|
+
resourceSpans: [
|
|
479
|
+
{
|
|
480
|
+
schemaUrl: trace.schema_url,
|
|
481
|
+
resource: {
|
|
482
|
+
attributes: toOtlpAttributes(buildResourceAttributes(trace, config))
|
|
483
|
+
},
|
|
484
|
+
scopeSpans: [
|
|
485
|
+
{
|
|
486
|
+
schemaUrl: trace.schema_url,
|
|
487
|
+
scope: {
|
|
488
|
+
name: 'tls-observer-claude-code',
|
|
489
|
+
version: package_namespaceObject.rE
|
|
490
|
+
},
|
|
491
|
+
spans: trace.spans.map((span)=>({
|
|
492
|
+
traceId: span.trace_id,
|
|
493
|
+
spanId: span.span_id,
|
|
494
|
+
...span.parent_span_id ? {
|
|
495
|
+
parentSpanId: span.parent_span_id
|
|
496
|
+
} : {},
|
|
497
|
+
name: span.name,
|
|
498
|
+
kind: spanKind(span),
|
|
499
|
+
startTimeUnixNano: toUnixNanoString(span.start_time_unix_ms),
|
|
500
|
+
endTimeUnixNano: toUnixNanoString(span.end_time_unix_ms),
|
|
501
|
+
attributes: toOtlpAttributes(cleanSpanAttributesForOtlp(span)),
|
|
502
|
+
status: hasError(span) ? {
|
|
503
|
+
code: 2,
|
|
504
|
+
message: span.status?.message || 'error'
|
|
505
|
+
} : {
|
|
506
|
+
code: span.status?.code === 'OK' ? 1 : 0
|
|
507
|
+
}
|
|
508
|
+
}))
|
|
509
|
+
}
|
|
510
|
+
]
|
|
511
|
+
}
|
|
512
|
+
]
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function requireExportConfig(config) {
|
|
516
|
+
if (!config.enabled) return {
|
|
517
|
+
ok: false,
|
|
518
|
+
reason: 'disabled'
|
|
519
|
+
};
|
|
520
|
+
const missing = [];
|
|
521
|
+
const endpoint = config.tlsEndpoint || config.otelEndpoint;
|
|
522
|
+
if (!endpoint) missing.push('CLAUDE_CODE_TLS_ENDPOINT or CLAUDE_CODE_TLS_OTEL_ENDPOINT');
|
|
523
|
+
if (!config.region) missing.push('CLAUDE_CODE_TLS_REGION');
|
|
524
|
+
if (!config.traceTopicId) missing.push('CLAUDE_CODE_TLS_TRACE_TOPIC_ID');
|
|
525
|
+
if (!config.apiKey && (!config.ak || !config.sk)) missing.push('CLAUDE_CODE_TLS_API_KEY or CLAUDE_CODE_TLS_AK/CLAUDE_CODE_TLS_SK');
|
|
526
|
+
if (config.apiKey && (config.ak || config.sk)) return {
|
|
527
|
+
ok: false,
|
|
528
|
+
reason: 'invalid_auth',
|
|
529
|
+
missing: [
|
|
530
|
+
'use apiKey or ak/sk, not both'
|
|
531
|
+
]
|
|
532
|
+
};
|
|
533
|
+
if (missing.length > 0) return {
|
|
534
|
+
ok: false,
|
|
535
|
+
reason: 'missing_config',
|
|
536
|
+
missing
|
|
537
|
+
};
|
|
538
|
+
return {
|
|
539
|
+
ok: true,
|
|
540
|
+
url: endpoint
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function exporter_producerConfig(config) {
|
|
544
|
+
return {
|
|
545
|
+
endpoint: config.tlsEndpoint || config.otelEndpoint || '',
|
|
546
|
+
region: config.region || '',
|
|
547
|
+
traceTopicId: config.traceTopicId || '',
|
|
548
|
+
apiKey: config.apiKey,
|
|
549
|
+
ak: config.ak,
|
|
550
|
+
sk: config.sk,
|
|
551
|
+
timeoutMs: config.exportTimeoutMs
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
async function exportTraceToTls(trace, config) {
|
|
555
|
+
const required = requireExportConfig(config);
|
|
556
|
+
if (!required.ok) return {
|
|
557
|
+
status: 'skipped',
|
|
558
|
+
reason: required.reason,
|
|
559
|
+
message: required.missing?.join(', '),
|
|
560
|
+
trace_id: trace.trace_id,
|
|
561
|
+
span_count: trace.spans.length
|
|
562
|
+
};
|
|
563
|
+
const otlpPayload = buildOtlpPayload(trace, config);
|
|
564
|
+
const resourceSpan = otlpPayload.resourceSpans[0];
|
|
565
|
+
const resource = resourceSpan.resource;
|
|
566
|
+
const scopeSpan = resourceSpan.scopeSpans[0];
|
|
567
|
+
const scope = scopeSpan.scope;
|
|
568
|
+
const otlpSpans = scopeSpan.spans;
|
|
569
|
+
const records = otlpSpans.map((otlpSpan)=>otlpSpanToTlsEnvelope(otlpSpan, resource, scope));
|
|
570
|
+
const totalPayloadBytes = records.reduce((sum, record)=>sum + Buffer.byteLength(JSON.stringify(record), 'utf8'), 0);
|
|
571
|
+
const startedAt = Date.now();
|
|
572
|
+
const diagnostics = {
|
|
573
|
+
endpoint_host: normalizeProducerEndpoint(required.url),
|
|
574
|
+
endpoint_path: 'SendLogs'
|
|
575
|
+
};
|
|
576
|
+
try {
|
|
577
|
+
await sendTlsLogRecords(exporter_producerConfig(config), records, 'claude-code-trace');
|
|
578
|
+
} catch (error) {
|
|
579
|
+
return {
|
|
580
|
+
status: 'failed',
|
|
581
|
+
reason: 'producer_error',
|
|
582
|
+
message: error instanceof Error ? error.message : String(error),
|
|
583
|
+
trace_id: trace.trace_id,
|
|
584
|
+
span_count: trace.spans.length,
|
|
585
|
+
payload_bytes: totalPayloadBytes,
|
|
586
|
+
attempts: 1,
|
|
587
|
+
duration_ms: Date.now() - startedAt,
|
|
588
|
+
...diagnostics
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
status: 'success',
|
|
593
|
+
trace_id: trace.trace_id,
|
|
594
|
+
span_count: trace.spans.length,
|
|
595
|
+
payload_bytes: totalPayloadBytes,
|
|
596
|
+
attempts: 1,
|
|
597
|
+
duration_ms: Date.now() - startedAt,
|
|
598
|
+
...diagnostics
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function stableFieldValue(value) {
|
|
602
|
+
if (null == value) return '';
|
|
603
|
+
if ('string' == typeof value || 'number' == typeof value || 'boolean' == typeof value) return value;
|
|
604
|
+
return serializeForAttr(value);
|
|
605
|
+
}
|
|
606
|
+
function inferUserRole(span) {
|
|
607
|
+
const input = span.input;
|
|
608
|
+
const role = input?.messages?.find((message)=>'string' == typeof message?.role)?.role;
|
|
609
|
+
return role || '';
|
|
610
|
+
}
|
|
611
|
+
function inferErrorMessage(span) {
|
|
612
|
+
if ('string' == typeof span.status?.message) return span.status.message;
|
|
613
|
+
const output = span.output;
|
|
614
|
+
if ('string' == typeof output?.error) return output.error;
|
|
615
|
+
return '';
|
|
616
|
+
}
|
|
617
|
+
function buildSchemaAlignedRecord(trace, span, observedTimeUnixMs = Date.now()) {
|
|
618
|
+
const attributes = span.attributes ?? {};
|
|
619
|
+
const resource = trace.resource ?? {};
|
|
620
|
+
const isModel = 'model' === span.type;
|
|
621
|
+
const isError = hasError(span);
|
|
622
|
+
const record = {
|
|
623
|
+
'span.name': span.name,
|
|
624
|
+
'span.kind': spanKindName(span),
|
|
625
|
+
'tls.app.type': attributes['tls.app.type'] ?? '',
|
|
626
|
+
'session.id': attributes['session.id'] ?? attributes['gen_ai.conversation.id'] ?? '',
|
|
627
|
+
'span.status.code': isError ? 'ERROR' : 'UNSET',
|
|
628
|
+
start_time_unix_us: unixMsToUs(span.start_time_unix_ms),
|
|
629
|
+
end_time_unix_us: unixMsToUs(span.end_time_unix_ms),
|
|
630
|
+
'span.duration_us': String(1000 * Math.max(0, Math.trunc(span.duration_ms))),
|
|
631
|
+
'service.name': resource['service.name'] ?? '',
|
|
632
|
+
'service.instance.id': resource['service.instance.id'] ?? '',
|
|
633
|
+
'host.name': resource['host.name'] ?? '',
|
|
634
|
+
observed_time_unix_ms: observedTimeUnixMs,
|
|
635
|
+
'telemetry.sdk.name': resource['telemetry.sdk.name'] ?? 'tls-observer-claude-code',
|
|
636
|
+
'telemetry.sdk.language': resource['telemetry.sdk.language'] ?? 'nodejs',
|
|
637
|
+
'telemetry.sdk.version': resource['telemetry.sdk.version'] ?? '',
|
|
638
|
+
'otel.scope.name': 'tls-observer-claude-code',
|
|
639
|
+
'otel.scope.version': package_namespaceObject.rE,
|
|
640
|
+
'event.name': span.name,
|
|
641
|
+
'user.role': inferUserRole(span),
|
|
642
|
+
'gen_ai.operation.name': attributes['gen_ai.operation.name'] ?? '',
|
|
643
|
+
'gen_ai.provider.name': attributes['gen_ai.provider.name'] ?? '',
|
|
644
|
+
'gen_ai.request.model': attributes['gen_ai.request.model'] ?? '',
|
|
645
|
+
'gen_ai.response.model': attributes['gen_ai.response.model'] ?? '',
|
|
646
|
+
'gen_ai.request.duration_ms': isModel ? span.duration_ms : '',
|
|
647
|
+
'gen_ai.input.messages': attributes['gen_ai.input.messages'] ?? '',
|
|
648
|
+
'gen_ai.output.messages': attributes['gen_ai.output.messages'] ?? '',
|
|
649
|
+
'gen_ai.usage.input_tokens': attributes['gen_ai.usage.input_tokens'] ?? '',
|
|
650
|
+
'gen_ai.usage.output_tokens': attributes['gen_ai.usage.output_tokens'] ?? '',
|
|
651
|
+
'gen_ai.usage.reasoning.output_tokens': attributes['gen_ai.usage.reasoning.output_tokens'] ?? '',
|
|
652
|
+
'gen_ai.usage.cache_read.input_tokens': attributes['gen_ai.usage.cache_read.input_tokens'] ?? '',
|
|
653
|
+
'gen_ai.usage.cache_creation.input_tokens': attributes['gen_ai.usage.cache_creation.input_tokens'] ?? '',
|
|
654
|
+
'gen_ai.usage.total_tokens': attributes['gen_ai.usage.total_tokens'] ?? '',
|
|
655
|
+
'gen_ai.tool.name': attributes['gen_ai.tool.name'] ?? '',
|
|
656
|
+
'gen_ai.tool.call.id': attributes['gen_ai.tool.call.id'] ?? '',
|
|
657
|
+
'gen_ai.tool.call.arguments': attributes['gen_ai.tool.call.arguments'] ?? '',
|
|
658
|
+
'gen_ai.tool.call.result': attributes['gen_ai.tool.call.result'] ?? '',
|
|
659
|
+
'error.type': isError ? attributes['error.type'] ?? 'error' : '',
|
|
660
|
+
'error.message': isError ? attributes['error.message'] || inferErrorMessage(span) || '' : ''
|
|
661
|
+
};
|
|
662
|
+
return Object.fromEntries(SCHEMA_FIELD_ORDER.map((key)=>[
|
|
663
|
+
key,
|
|
664
|
+
stableFieldValue(record[key])
|
|
665
|
+
]));
|
|
666
|
+
}
|
|
667
|
+
function buildSchemaAlignedRecords(trace, observedTimeUnixMs = Date.now()) {
|
|
668
|
+
return trace.spans.map((span)=>buildSchemaAlignedRecord(trace, span, observedTimeUnixMs));
|
|
669
|
+
}
|
|
670
|
+
function exportSummary(trace, result) {
|
|
671
|
+
return compactObject({
|
|
672
|
+
generated_at: new Date().toISOString(),
|
|
673
|
+
trace_id: trace.trace_id,
|
|
674
|
+
span_count: trace.spans.length,
|
|
675
|
+
export_status: result.status,
|
|
676
|
+
export_reason: result.reason,
|
|
677
|
+
export_message: result.message,
|
|
678
|
+
endpoint_host: result.endpoint_host,
|
|
679
|
+
endpoint_path: result.endpoint_path,
|
|
680
|
+
payload_bytes: result.payload_bytes,
|
|
681
|
+
duration_ms: result.duration_ms
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const LOCK_TIMEOUT_MS = 5000;
|
|
685
|
+
const LOCK_RETRY_MS = 20;
|
|
686
|
+
const SESSION_MAX_AGE_MS = 86400000;
|
|
687
|
+
function sleep(ms) {
|
|
688
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
689
|
+
}
|
|
690
|
+
function lockPath(stateFilePath) {
|
|
691
|
+
return `${stateFilePath}.lock`;
|
|
692
|
+
}
|
|
693
|
+
async function acquireLock(stateFilePath) {
|
|
694
|
+
const lock = lockPath(stateFilePath);
|
|
695
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
696
|
+
__rspack_external_node_fs_5ea92f0c.mkdirSync(__rspack_external_node_path_c5b9b54f.dirname(stateFilePath), {
|
|
697
|
+
recursive: true
|
|
698
|
+
});
|
|
699
|
+
while(Date.now() < deadline)try {
|
|
700
|
+
const fd = __rspack_external_node_fs_5ea92f0c.openSync(lock, 'wx');
|
|
701
|
+
__rspack_external_node_fs_5ea92f0c.closeSync(fd);
|
|
702
|
+
return;
|
|
703
|
+
} catch {
|
|
704
|
+
await sleep(LOCK_RETRY_MS);
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
__rspack_external_node_fs_5ea92f0c.unlinkSync(lock);
|
|
708
|
+
} catch {}
|
|
709
|
+
const fd = __rspack_external_node_fs_5ea92f0c.openSync(lock, 'wx');
|
|
710
|
+
__rspack_external_node_fs_5ea92f0c.closeSync(fd);
|
|
711
|
+
}
|
|
712
|
+
function releaseLock(stateFilePath) {
|
|
713
|
+
try {
|
|
714
|
+
__rspack_external_node_fs_5ea92f0c.unlinkSync(lockPath(stateFilePath));
|
|
715
|
+
} catch {}
|
|
716
|
+
}
|
|
717
|
+
function loadState(stateFilePath) {
|
|
718
|
+
try {
|
|
719
|
+
return JSON.parse(__rspack_external_node_fs_5ea92f0c.readFileSync(stateFilePath, 'utf8'));
|
|
720
|
+
} catch {
|
|
721
|
+
return {};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function getSessionState(state, sessionId) {
|
|
725
|
+
return state[sessionId] ?? {
|
|
726
|
+
last_line: -1,
|
|
727
|
+
last_offset: 0,
|
|
728
|
+
turn_count: 0,
|
|
729
|
+
updated: '',
|
|
730
|
+
tool_start_times: {},
|
|
731
|
+
tool_observations: {},
|
|
732
|
+
pending_subagent_traces: [],
|
|
733
|
+
subagent_turn_count: 0
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
async function atomicUpdateState(stateFilePath, fn) {
|
|
737
|
+
await acquireLock(stateFilePath);
|
|
738
|
+
try {
|
|
739
|
+
const next = pruneOldSessions(fn(loadState(stateFilePath)));
|
|
740
|
+
__rspack_external_node_fs_5ea92f0c.mkdirSync(__rspack_external_node_path_c5b9b54f.dirname(stateFilePath), {
|
|
741
|
+
recursive: true
|
|
742
|
+
});
|
|
743
|
+
__rspack_external_node_fs_5ea92f0c.writeFileSync(stateFilePath, JSON.stringify(next, null, 2));
|
|
744
|
+
} finally{
|
|
745
|
+
releaseLock(stateFilePath);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function pruneOldSessions(state, now = Date.now()) {
|
|
749
|
+
const cutoff = now - SESSION_MAX_AGE_MS;
|
|
750
|
+
const pruned = {};
|
|
751
|
+
for (const [sessionId, session] of Object.entries(state)){
|
|
752
|
+
const updatedMs = session.updated ? new Date(session.updated).getTime() : 0;
|
|
753
|
+
if (updatedMs >= cutoff || session.current_turn_id) pruned[sessionId] = session;
|
|
754
|
+
}
|
|
755
|
+
return pruned;
|
|
756
|
+
}
|
|
757
|
+
function putToolObservation(session, observation) {
|
|
758
|
+
return {
|
|
759
|
+
...session,
|
|
760
|
+
updated: new Date().toISOString(),
|
|
761
|
+
tool_observations: {
|
|
762
|
+
...session.tool_observations,
|
|
763
|
+
[observation.tool_use_id]: {
|
|
764
|
+
...session.tool_observations?.[observation.tool_use_id],
|
|
765
|
+
...observation
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const SCHEMA_URL = 'https://opentelemetry.io/schemas/1.41.0';
|
|
771
|
+
const MAX_ATTRIBUTE_TEXT = 65536;
|
|
772
|
+
const TLS_APP_TYPE = 'claude-code';
|
|
773
|
+
const ROOT_SPAN_NAME = 'agent.turn';
|
|
774
|
+
const LLM_SPAN_NAME = 'llm.request';
|
|
775
|
+
const TOOL_SPAN_NAME = 'tool.call';
|
|
776
|
+
function hashHex(value, length) {
|
|
777
|
+
const hex = createHash('sha256').update(value).digest('hex').slice(0, length);
|
|
778
|
+
if (!/^0+$/.test(hex)) return hex;
|
|
779
|
+
return '1'.padStart(length, '0');
|
|
780
|
+
}
|
|
781
|
+
function toTraceId(value) {
|
|
782
|
+
const compact = value.replace(/-/g, '').toLowerCase();
|
|
783
|
+
if (/^[0-9a-f]{32}$/.test(compact) && compact !== '0'.repeat(32)) return compact;
|
|
784
|
+
return hashHex(value, 32);
|
|
785
|
+
}
|
|
786
|
+
function toSpanId(...parts) {
|
|
787
|
+
return hashHex(parts.map((part)=>String(part ?? '')).join(':'), 16);
|
|
788
|
+
}
|
|
789
|
+
function parseMs(value, fallback) {
|
|
790
|
+
if ('number' == typeof value && Number.isFinite(value)) return Math.trunc(value);
|
|
791
|
+
const parsed = Date.parse(String(value ?? ''));
|
|
792
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
793
|
+
}
|
|
794
|
+
function orderedTiming(startTime, endTime) {
|
|
795
|
+
const fallback = Date.now();
|
|
796
|
+
const startMs = parseMs(startTime, fallback);
|
|
797
|
+
const rawEndMs = parseMs(endTime, startMs);
|
|
798
|
+
if (rawEndMs < startMs) return {
|
|
799
|
+
startMs,
|
|
800
|
+
endMs: startMs
|
|
801
|
+
};
|
|
802
|
+
return {
|
|
803
|
+
startMs,
|
|
804
|
+
endMs: rawEndMs
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function stringifyStructuredAttributes(attributes) {
|
|
808
|
+
return Object.fromEntries(Object.entries(attributes).map(([key, value])=>[
|
|
809
|
+
key,
|
|
810
|
+
null !== value && 'object' == typeof value ? clipString(safeJsonStringify(value)) : value
|
|
811
|
+
]));
|
|
812
|
+
}
|
|
813
|
+
function traceSpan(options) {
|
|
814
|
+
const { startMs, endMs } = orderedTiming(options.startTime, options.endTime);
|
|
815
|
+
return {
|
|
816
|
+
trace_id: options.traceId,
|
|
817
|
+
span_id: options.spanId,
|
|
818
|
+
parent_span_id: options.parentSpanId ?? null,
|
|
819
|
+
name: options.name,
|
|
820
|
+
type: options.type,
|
|
821
|
+
span_kind: options.spanKind ?? ('entry' === options.type ? 'SERVER' : 'model' === options.type ? 'CLIENT' : 'INTERNAL'),
|
|
822
|
+
start_time: new Date(startMs).toISOString(),
|
|
823
|
+
end_time: new Date(endMs).toISOString(),
|
|
824
|
+
start_time_unix_ms: startMs,
|
|
825
|
+
end_time_unix_ms: endMs,
|
|
826
|
+
duration_ms: endMs - startMs,
|
|
827
|
+
attributes: compactObject(stringifyStructuredAttributes({
|
|
828
|
+
...options.attributes ?? {},
|
|
829
|
+
'tls.app.type': TLS_APP_TYPE
|
|
830
|
+
})),
|
|
831
|
+
input: options.input,
|
|
832
|
+
output: options.output,
|
|
833
|
+
status: options.status
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function clipString(value) {
|
|
837
|
+
return value.length > MAX_ATTRIBUTE_TEXT ? `${value.slice(0, MAX_ATTRIBUTE_TEXT)}...[truncated]` : value;
|
|
838
|
+
}
|
|
839
|
+
function normalizeValue(value, captureContent) {
|
|
840
|
+
if (!captureContent) return;
|
|
841
|
+
if ('string' == typeof value) return clipString(value);
|
|
842
|
+
if (null == value) return value;
|
|
843
|
+
const text = safeJsonStringify(value);
|
|
844
|
+
return text.length > MAX_ATTRIBUTE_TEXT ? `${text.slice(0, MAX_ATTRIBUTE_TEXT)}...[truncated]` : value;
|
|
845
|
+
}
|
|
846
|
+
function contentBlockToPart(block, captureContent) {
|
|
847
|
+
if ('text' === block.type) return {
|
|
848
|
+
type: 'text',
|
|
849
|
+
content: normalizeValue(block.text, captureContent) ?? ''
|
|
850
|
+
};
|
|
851
|
+
if ('thinking' === block.type) return {
|
|
852
|
+
type: 'reasoning',
|
|
853
|
+
content: normalizeValue(block.thinking, captureContent) ?? ''
|
|
854
|
+
};
|
|
855
|
+
return {
|
|
856
|
+
type: 'tool_call',
|
|
857
|
+
id: block.id,
|
|
858
|
+
name: block.name,
|
|
859
|
+
arguments: normalizeValue(block.input, captureContent)
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function messageContentToParts(content, captureContent) {
|
|
863
|
+
if ('string' == typeof content) return [
|
|
864
|
+
{
|
|
865
|
+
type: 'text',
|
|
866
|
+
content: normalizeValue(content, captureContent) ?? ''
|
|
867
|
+
}
|
|
868
|
+
];
|
|
869
|
+
return content.map((item)=>normalizeValue(item, captureContent));
|
|
870
|
+
}
|
|
871
|
+
function assistantOutputMessages(call, captureContent) {
|
|
872
|
+
return [
|
|
873
|
+
{
|
|
874
|
+
role: 'assistant',
|
|
875
|
+
parts: call.content.map((block)=>contentBlockToPart(block, captureContent))
|
|
876
|
+
}
|
|
877
|
+
];
|
|
878
|
+
}
|
|
879
|
+
function messagesAttribute(messages, captureContent) {
|
|
880
|
+
if (!captureContent || !messages) return;
|
|
881
|
+
return clipString(safeJsonStringify(messages));
|
|
882
|
+
}
|
|
883
|
+
function toolResultMessage(toolCall, captureContent) {
|
|
884
|
+
return {
|
|
885
|
+
role: 'tool',
|
|
886
|
+
parts: [
|
|
887
|
+
{
|
|
888
|
+
type: 'tool_call_response',
|
|
889
|
+
id: toolCall.tool_use.id,
|
|
890
|
+
result: normalizeValue(toolCall.result?.content, captureContent)
|
|
891
|
+
}
|
|
892
|
+
]
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
function usageAttributes(usage) {
|
|
896
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
897
|
+
const cacheCreation = usage.cache_creation_input_tokens ?? 0;
|
|
898
|
+
const inputTokens = usage.input_tokens ?? 0;
|
|
899
|
+
const outputTokens = usage.output_tokens ?? 0;
|
|
900
|
+
return compactObject({
|
|
901
|
+
'gen_ai.usage.input_tokens': inputTokens || void 0,
|
|
902
|
+
'gen_ai.usage.output_tokens': outputTokens || void 0,
|
|
903
|
+
'gen_ai.usage.total_tokens': inputTokens || outputTokens ? inputTokens + outputTokens : void 0,
|
|
904
|
+
'gen_ai.usage.reasoning.output_tokens': usage.reasoning_output_tokens || void 0,
|
|
905
|
+
'gen_ai.usage.cache_read.input_tokens': cacheRead || void 0,
|
|
906
|
+
'gen_ai.usage.cache_creation.input_tokens': cacheCreation || void 0
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function sumNumericAttribute(records, key) {
|
|
910
|
+
return records.reduce((sum, record)=>{
|
|
911
|
+
const value = record[key];
|
|
912
|
+
return 'number' == typeof value && Number.isFinite(value) ? sum + value : sum;
|
|
913
|
+
}, 0);
|
|
914
|
+
}
|
|
915
|
+
function rootUsageAttributes(calls) {
|
|
916
|
+
const usageRecords = calls.map((call)=>usageAttributes(call.usage));
|
|
917
|
+
const totalTokens = sumNumericAttribute(usageRecords, 'gen_ai.usage.total_tokens');
|
|
918
|
+
const inputTokens = sumNumericAttribute(usageRecords, 'gen_ai.usage.input_tokens');
|
|
919
|
+
const outputTokens = sumNumericAttribute(usageRecords, 'gen_ai.usage.output_tokens');
|
|
920
|
+
const reasoningOutputTokens = sumNumericAttribute(usageRecords, 'gen_ai.usage.reasoning.output_tokens');
|
|
921
|
+
const cacheReadInputTokens = sumNumericAttribute(usageRecords, 'gen_ai.usage.cache_read.input_tokens');
|
|
922
|
+
return compactObject({
|
|
923
|
+
'gen_ai.usage.total_tokens': totalTokens || void 0,
|
|
924
|
+
'gen_ai.usage.input_tokens': inputTokens || void 0,
|
|
925
|
+
'gen_ai.usage.output_tokens': outputTokens || void 0,
|
|
926
|
+
'gen_ai.usage.reasoning.output_tokens': reasoningOutputTokens || void 0,
|
|
927
|
+
'gen_ai.usage.cache_read.input_tokens': cacheReadInputTokens || void 0
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
function toolObservationForCall(toolCall, state) {
|
|
931
|
+
return state.tool_observations?.[toolCall.tool_use.id];
|
|
932
|
+
}
|
|
933
|
+
function toolTiming(toolCall, observation, fallbackStart, fallbackEnd) {
|
|
934
|
+
return {
|
|
935
|
+
start: observation?.start_time_ms ?? fallbackStart,
|
|
936
|
+
end: observation?.end_time_ms ?? toolCall.result?.timestamp ?? fallbackEnd
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function commonSessionAttributes(input) {
|
|
940
|
+
return compactObject({
|
|
941
|
+
'session.id': input.session_id,
|
|
942
|
+
'gen_ai.conversation.id': input.session_id
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
function buildResource(config) {
|
|
946
|
+
return compactObject({
|
|
947
|
+
'service.name': config.serviceName,
|
|
948
|
+
'service.instance.id': config.serviceInstanceId,
|
|
949
|
+
'host.name': hostname(),
|
|
950
|
+
'telemetry.sdk.language': 'nodejs',
|
|
951
|
+
'telemetry.sdk.name': 'tls-observer-claude-code',
|
|
952
|
+
'telemetry.sdk.version': '0.1.0',
|
|
953
|
+
...localIdentityMetadata()
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
function buildTraceFromTurn(options) {
|
|
957
|
+
const { input, turn, turnIndex, sessionState, config, extraAttributes } = options;
|
|
958
|
+
const traceSeed = [
|
|
959
|
+
input.session_id ?? 'claude',
|
|
960
|
+
turn.promptId ?? sessionState.current_turn_id ?? turnIndex,
|
|
961
|
+
turn.userTimestamp,
|
|
962
|
+
options.traceSeedSuffix
|
|
963
|
+
].filter((part)=>void 0 !== part && '' !== part).join(':');
|
|
964
|
+
const traceId = toTraceId(traceSeed);
|
|
965
|
+
const spans = [];
|
|
966
|
+
const userStartMs = parseMs(turn.userTimestamp, Date.now());
|
|
967
|
+
const turnStartMs = Math.min(userStartMs, parseMs(sessionState.current_turn_start_ms, userStartMs), ...turn.llmCalls.map((call)=>parseMs(call.startTime, userStartMs)));
|
|
968
|
+
const activityEnds = [
|
|
969
|
+
...turn.llmCalls.map((call)=>parseMs(call.endTime, turnStartMs)),
|
|
970
|
+
...turn.llmCalls.flatMap((call)=>call.toolCalls.map((tool)=>parseMs(tool.result?.timestamp, turnStartMs)))
|
|
971
|
+
].filter(Number.isFinite);
|
|
972
|
+
const turnEndMs = Math.max(turnStartMs, ...activityEnds, Date.now());
|
|
973
|
+
const turnSpanId = toSpanId(traceId, 'turn', turnIndex);
|
|
974
|
+
const sessionAttrs = compactObject({
|
|
975
|
+
...commonSessionAttributes(input),
|
|
976
|
+
...extraAttributes ?? {}
|
|
977
|
+
});
|
|
978
|
+
const userMessages = [
|
|
979
|
+
{
|
|
980
|
+
role: 'user',
|
|
981
|
+
parts: messageContentToParts(turn.userContent, config.captureContent)
|
|
982
|
+
}
|
|
983
|
+
];
|
|
984
|
+
const lastAssistant = turn.llmCalls.at(-1);
|
|
985
|
+
const requestModel = lastAssistant?.model;
|
|
986
|
+
const responseModel = lastAssistant?.model;
|
|
987
|
+
const outputMessages = lastAssistant ? assistantOutputMessages(lastAssistant, config.captureContent) : void 0;
|
|
988
|
+
spans.push(traceSpan({
|
|
989
|
+
traceId,
|
|
990
|
+
spanId: turnSpanId,
|
|
991
|
+
name: ROOT_SPAN_NAME,
|
|
992
|
+
type: 'entry',
|
|
993
|
+
startTime: turnStartMs,
|
|
994
|
+
endTime: turnEndMs,
|
|
995
|
+
attributes: {
|
|
996
|
+
...sessionAttrs,
|
|
997
|
+
'gen_ai.operation.name': 'invoke_agent',
|
|
998
|
+
'gen_ai.provider.name': 'anthropic',
|
|
999
|
+
'gen_ai.request.model': requestModel,
|
|
1000
|
+
'gen_ai.response.model': responseModel,
|
|
1001
|
+
'gen_ai.input.messages': messagesAttribute(userMessages, config.captureContent),
|
|
1002
|
+
'gen_ai.output.messages': messagesAttribute(outputMessages, config.captureContent),
|
|
1003
|
+
...rootUsageAttributes(turn.llmCalls)
|
|
1004
|
+
},
|
|
1005
|
+
input: config.captureContent ? {
|
|
1006
|
+
messages: userMessages
|
|
1007
|
+
} : void 0,
|
|
1008
|
+
output: config.captureContent && outputMessages ? {
|
|
1009
|
+
messages: outputMessages
|
|
1010
|
+
} : void 0,
|
|
1011
|
+
status: turn.isComplete ? {
|
|
1012
|
+
code: 'OK'
|
|
1013
|
+
} : {
|
|
1014
|
+
code: 'ERROR',
|
|
1015
|
+
message: 'interrupted'
|
|
1016
|
+
}
|
|
1017
|
+
}));
|
|
1018
|
+
const accumulatedMessages = [
|
|
1019
|
+
...userMessages
|
|
1020
|
+
];
|
|
1021
|
+
for (const [llmIndex, call] of turn.llmCalls.entries()){
|
|
1022
|
+
const output = assistantOutputMessages(call, config.captureContent);
|
|
1023
|
+
const inputMessages = [
|
|
1024
|
+
...accumulatedMessages
|
|
1025
|
+
];
|
|
1026
|
+
const llmSpanId = toSpanId(traceId, 'llm', llmIndex, call.startTime, call.endTime);
|
|
1027
|
+
spans.push(traceSpan({
|
|
1028
|
+
traceId,
|
|
1029
|
+
spanId: llmSpanId,
|
|
1030
|
+
parentSpanId: turnSpanId,
|
|
1031
|
+
name: LLM_SPAN_NAME,
|
|
1032
|
+
type: 'model',
|
|
1033
|
+
spanKind: 'CLIENT',
|
|
1034
|
+
startTime: call.startTime,
|
|
1035
|
+
endTime: call.endTime,
|
|
1036
|
+
attributes: {
|
|
1037
|
+
...sessionAttrs,
|
|
1038
|
+
'gen_ai.operation.name': 'chat',
|
|
1039
|
+
'gen_ai.provider.name': 'anthropic',
|
|
1040
|
+
'gen_ai.request.model': call.model,
|
|
1041
|
+
'gen_ai.response.model': call.model,
|
|
1042
|
+
'gen_ai.response.finish_reasons': call.stopReason ? [
|
|
1043
|
+
call.stopReason
|
|
1044
|
+
] : void 0,
|
|
1045
|
+
'gen_ai.input.messages': messagesAttribute(inputMessages, config.captureContent),
|
|
1046
|
+
'gen_ai.output.messages': messagesAttribute(output, config.captureContent),
|
|
1047
|
+
...usageAttributes(call.usage)
|
|
1048
|
+
},
|
|
1049
|
+
input: config.captureContent ? {
|
|
1050
|
+
messages: inputMessages
|
|
1051
|
+
} : void 0,
|
|
1052
|
+
output: config.captureContent ? {
|
|
1053
|
+
messages: output
|
|
1054
|
+
} : void 0
|
|
1055
|
+
}));
|
|
1056
|
+
accumulatedMessages.push(...output);
|
|
1057
|
+
for (const toolCall of call.toolCalls){
|
|
1058
|
+
const observation = toolObservationForCall(toolCall, sessionState);
|
|
1059
|
+
const timing = toolTiming(toolCall, observation, call.endTime, toolCall.result?.timestamp ?? call.endTime);
|
|
1060
|
+
const isError = observation?.status === 'error' || toolCall.result?.is_error;
|
|
1061
|
+
const toolOutput = observation?.tool_response ?? toolCall.result?.content;
|
|
1062
|
+
const toolInput = observation?.tool_input ?? toolCall.tool_use.input;
|
|
1063
|
+
const toolSpanId = toSpanId(traceId, 'tool', toolCall.tool_use.id);
|
|
1064
|
+
spans.push(traceSpan({
|
|
1065
|
+
traceId,
|
|
1066
|
+
spanId: toolSpanId,
|
|
1067
|
+
parentSpanId: turnSpanId,
|
|
1068
|
+
name: TOOL_SPAN_NAME,
|
|
1069
|
+
type: 'tool',
|
|
1070
|
+
spanKind: 'INTERNAL',
|
|
1071
|
+
startTime: timing.start,
|
|
1072
|
+
endTime: timing.end,
|
|
1073
|
+
attributes: {
|
|
1074
|
+
...sessionAttrs,
|
|
1075
|
+
'gen_ai.operation.name': 'execute_tool',
|
|
1076
|
+
'gen_ai.tool.name': toolCall.tool_use.name,
|
|
1077
|
+
'gen_ai.tool.call.id': toolCall.tool_use.id,
|
|
1078
|
+
'gen_ai.tool.call.arguments': normalizeValue(toolInput, config.captureContent),
|
|
1079
|
+
'gen_ai.tool.call.result': normalizeValue(toolOutput, config.captureContent),
|
|
1080
|
+
'error.type': isError ? 'tool_error' : void 0,
|
|
1081
|
+
'error.message': isError ? normalizeValue(toolOutput, config.captureContent) : void 0
|
|
1082
|
+
},
|
|
1083
|
+
input: normalizeValue(toolInput, config.captureContent),
|
|
1084
|
+
output: normalizeValue(toolOutput, config.captureContent),
|
|
1085
|
+
status: isError ? {
|
|
1086
|
+
code: 'ERROR',
|
|
1087
|
+
message: 'tool_error'
|
|
1088
|
+
} : {
|
|
1089
|
+
code: 'OK'
|
|
1090
|
+
}
|
|
1091
|
+
}));
|
|
1092
|
+
accumulatedMessages.push(toolResultMessage(toolCall, config.captureContent));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return {
|
|
1096
|
+
schema_url: SCHEMA_URL,
|
|
1097
|
+
source: options.source ?? "claude_stop_hook_transcript",
|
|
1098
|
+
trace_id: traceId,
|
|
1099
|
+
resource: buildResource(config),
|
|
1100
|
+
spans: spans.sort((left, right)=>left.start_time_unix_ms - right.start_time_unix_ms || left.end_time_unix_ms - right.end_time_unix_ms)
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function buildCompactTrace(options) {
|
|
1104
|
+
const { input, sessionState, config } = options;
|
|
1105
|
+
const endMs = Date.now();
|
|
1106
|
+
const startMs = sessionState.compaction_start_time_ms ?? endMs;
|
|
1107
|
+
const traceId = toTraceId(`${input.session_id ?? 'claude'}:compact:${startMs}:${endMs}`);
|
|
1108
|
+
const spanId = toSpanId(traceId, 'compact');
|
|
1109
|
+
const trigger = input.trigger ?? sessionState.compaction_trigger;
|
|
1110
|
+
const summary = normalizeValue(input.compact_summary, config.captureContent);
|
|
1111
|
+
const customInstructions = normalizeValue(sessionState.compaction_custom_instructions, config.captureContent);
|
|
1112
|
+
return {
|
|
1113
|
+
schema_url: SCHEMA_URL,
|
|
1114
|
+
source: 'claude_post_compact_hook',
|
|
1115
|
+
trace_id: traceId,
|
|
1116
|
+
resource: buildResource(config),
|
|
1117
|
+
spans: [
|
|
1118
|
+
traceSpan({
|
|
1119
|
+
traceId,
|
|
1120
|
+
spanId,
|
|
1121
|
+
name: 'agent.compact',
|
|
1122
|
+
type: 'event',
|
|
1123
|
+
spanKind: 'INTERNAL',
|
|
1124
|
+
startTime: startMs,
|
|
1125
|
+
endTime: endMs,
|
|
1126
|
+
attributes: {
|
|
1127
|
+
'session.id': input.session_id,
|
|
1128
|
+
'gen_ai.conversation.id': input.session_id,
|
|
1129
|
+
'gen_ai.operation.name': 'context_compaction',
|
|
1130
|
+
'event.name': 'context.compaction',
|
|
1131
|
+
'gen_ai.output.messages': messagesAttribute([
|
|
1132
|
+
{
|
|
1133
|
+
role: 'assistant',
|
|
1134
|
+
parts: [
|
|
1135
|
+
{
|
|
1136
|
+
type: 'text',
|
|
1137
|
+
content: summary ?? ''
|
|
1138
|
+
}
|
|
1139
|
+
]
|
|
1140
|
+
}
|
|
1141
|
+
], config.captureContent),
|
|
1142
|
+
'gen_ai.request.duration_ms': Math.max(0, endMs - startMs)
|
|
1143
|
+
},
|
|
1144
|
+
input: config.captureContent ? {
|
|
1145
|
+
trigger,
|
|
1146
|
+
custom_instructions: customInstructions
|
|
1147
|
+
} : void 0,
|
|
1148
|
+
output: config.captureContent ? {
|
|
1149
|
+
compact_summary: summary
|
|
1150
|
+
} : void 0,
|
|
1151
|
+
status: {
|
|
1152
|
+
code: 'OK'
|
|
1153
|
+
}
|
|
1154
|
+
})
|
|
1155
|
+
]
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
function buildStopFailureTrace(options) {
|
|
1159
|
+
const { input, config } = options;
|
|
1160
|
+
const now = Date.now();
|
|
1161
|
+
const traceId = toTraceId(`${input.session_id ?? 'claude'}:stop-failure:${now}`);
|
|
1162
|
+
const spanId = toSpanId(traceId, 'stop-failure');
|
|
1163
|
+
const errorMessage = input.error || 'Claude Code turn failed';
|
|
1164
|
+
return {
|
|
1165
|
+
schema_url: SCHEMA_URL,
|
|
1166
|
+
source: 'claude_stop_failure_hook',
|
|
1167
|
+
trace_id: traceId,
|
|
1168
|
+
resource: buildResource(config),
|
|
1169
|
+
spans: [
|
|
1170
|
+
traceSpan({
|
|
1171
|
+
traceId,
|
|
1172
|
+
spanId,
|
|
1173
|
+
name: ROOT_SPAN_NAME,
|
|
1174
|
+
type: 'event',
|
|
1175
|
+
startTime: now,
|
|
1176
|
+
endTime: now,
|
|
1177
|
+
attributes: {
|
|
1178
|
+
'session.id': input.session_id,
|
|
1179
|
+
'gen_ai.operation.name': 'invoke_agent',
|
|
1180
|
+
'gen_ai.provider.name': 'anthropic',
|
|
1181
|
+
'gen_ai.conversation.id': input.session_id,
|
|
1182
|
+
'error.type': input.error_type || 'stop_failure',
|
|
1183
|
+
'error.message': errorMessage
|
|
1184
|
+
},
|
|
1185
|
+
output: {
|
|
1186
|
+
error: errorMessage
|
|
1187
|
+
},
|
|
1188
|
+
status: {
|
|
1189
|
+
code: 'ERROR',
|
|
1190
|
+
message: errorMessage
|
|
1191
|
+
}
|
|
1192
|
+
})
|
|
1193
|
+
]
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
async function writeTextFileAtomic(filePath, text, options = {}) {
|
|
1197
|
+
await __rspack_external_node_fs_promises_153e37e0.mkdir(__rspack_external_node_path_c5b9b54f.dirname(filePath), {
|
|
1198
|
+
recursive: true
|
|
1199
|
+
});
|
|
1200
|
+
const tempPath = __rspack_external_node_path_c5b9b54f.join(__rspack_external_node_path_c5b9b54f.dirname(filePath), `.${__rspack_external_node_path_c5b9b54f.basename(filePath)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
|
|
1201
|
+
let renamed = false;
|
|
1202
|
+
try {
|
|
1203
|
+
await __rspack_external_node_fs_promises_153e37e0.writeFile(tempPath, text, {
|
|
1204
|
+
encoding: 'utf8',
|
|
1205
|
+
...void 0 === options.mode ? {} : {
|
|
1206
|
+
mode: options.mode
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
await __rspack_external_node_fs_promises_153e37e0.rename(tempPath, filePath);
|
|
1210
|
+
renamed = true;
|
|
1211
|
+
if (void 0 !== options.mode) await __rspack_external_node_fs_promises_153e37e0.chmod(filePath, options.mode);
|
|
1212
|
+
} finally{
|
|
1213
|
+
if (!renamed) await __rspack_external_node_fs_promises_153e37e0.rm(tempPath, {
|
|
1214
|
+
force: true
|
|
1215
|
+
}).catch(()=>void 0);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
async function writeJsonFileAtomic(filePath, value, options = {}) {
|
|
1219
|
+
await writeTextFileAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`, options);
|
|
1220
|
+
}
|
|
1221
|
+
async function appendJsonlFile(filePath, values) {
|
|
1222
|
+
const entries = Array.isArray(values) ? values : [
|
|
1223
|
+
values
|
|
1224
|
+
];
|
|
1225
|
+
if (0 === entries.length) return;
|
|
1226
|
+
await __rspack_external_node_fs_promises_153e37e0.mkdir(__rspack_external_node_path_c5b9b54f.dirname(filePath), {
|
|
1227
|
+
recursive: true
|
|
1228
|
+
});
|
|
1229
|
+
await __rspack_external_node_fs_promises_153e37e0.appendFile(filePath, `${entries.map((entry)=>JSON.stringify(entry)).join('\n')}\n`, 'utf8');
|
|
1230
|
+
}
|
|
1231
|
+
async function trimJsonlFileByBytes(filePath, maxBytes) {
|
|
1232
|
+
if (maxBytes <= 0) return void await __rspack_external_node_fs_promises_153e37e0.rm(filePath, {
|
|
1233
|
+
force: true
|
|
1234
|
+
}).catch(()=>void 0);
|
|
1235
|
+
let stat;
|
|
1236
|
+
try {
|
|
1237
|
+
stat = await __rspack_external_node_fs_promises_153e37e0.stat(filePath);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
if ('ENOENT' === error.code) return;
|
|
1240
|
+
throw error;
|
|
1241
|
+
}
|
|
1242
|
+
if (stat.size <= maxBytes) return;
|
|
1243
|
+
const lines = (await __rspack_external_node_fs_promises_153e37e0.readFile(filePath, 'utf8')).split(/\r?\n/).filter(Boolean);
|
|
1244
|
+
const kept = [];
|
|
1245
|
+
let keptBytes = 0;
|
|
1246
|
+
for(let index = lines.length - 1; index >= 0; index -= 1){
|
|
1247
|
+
const line = lines[index];
|
|
1248
|
+
const lineBytes = Buffer.byteLength(`${line}\n`, 'utf8');
|
|
1249
|
+
if (kept.length > 0 && keptBytes + lineBytes > maxBytes) break;
|
|
1250
|
+
kept.push(line);
|
|
1251
|
+
keptBytes += lineBytes;
|
|
1252
|
+
if (keptBytes >= maxBytes) break;
|
|
1253
|
+
}
|
|
1254
|
+
await writeTextFileAtomic(filePath, kept.length ? `${kept.reverse().join('\n')}\n` : '');
|
|
1255
|
+
}
|
|
1256
|
+
async function appendBoundedJsonlFile(filePath, values, maxBytes) {
|
|
1257
|
+
await appendJsonlFile(filePath, values);
|
|
1258
|
+
await trimJsonlFileByBytes(filePath, maxBytes);
|
|
1259
|
+
}
|
|
1260
|
+
function readTranscript(filePath, afterLine = -1, afterOffset = 0) {
|
|
1261
|
+
let size;
|
|
1262
|
+
try {
|
|
1263
|
+
size = __rspack_external_node_fs_5ea92f0c.statSync(filePath).size;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return {
|
|
1266
|
+
messages: [],
|
|
1267
|
+
lastLine: afterLine,
|
|
1268
|
+
lastOffset: Math.max(0, afterOffset)
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
const startOffset = Number.isFinite(afterOffset) && afterOffset > 0 && afterOffset <= size ? Math.trunc(afterOffset) : 0;
|
|
1272
|
+
const usingOffset = startOffset > 0;
|
|
1273
|
+
const fd = __rspack_external_node_fs_5ea92f0c.openSync(filePath, 'r');
|
|
1274
|
+
try {
|
|
1275
|
+
const chunkSize = 2097152;
|
|
1276
|
+
const buf = Buffer.alloc(chunkSize);
|
|
1277
|
+
const messages = [];
|
|
1278
|
+
let lastLine = afterLine;
|
|
1279
|
+
let lastOffset = startOffset;
|
|
1280
|
+
let lineIndex = usingOffset ? afterLine : -1;
|
|
1281
|
+
let partial = '';
|
|
1282
|
+
let readPos = startOffset;
|
|
1283
|
+
let cursorOffset = startOffset;
|
|
1284
|
+
let bytesRead = 0;
|
|
1285
|
+
while((bytesRead = __rspack_external_node_fs_5ea92f0c.readSync(fd, buf, 0, chunkSize, readPos)) > 0){
|
|
1286
|
+
readPos += bytesRead;
|
|
1287
|
+
const chunk = partial + buf.toString('utf8', 0, bytesRead);
|
|
1288
|
+
const lines = chunk.split('\n');
|
|
1289
|
+
partial = lines.pop() ?? '';
|
|
1290
|
+
for (const line of lines){
|
|
1291
|
+
const rawWithNewline = `${line}\n`;
|
|
1292
|
+
cursorOffset += Buffer.byteLength(rawWithNewline, 'utf8');
|
|
1293
|
+
const trimmed = line.trim();
|
|
1294
|
+
if (trimmed) {
|
|
1295
|
+
lineIndex += 1;
|
|
1296
|
+
lastLine = lineIndex;
|
|
1297
|
+
lastOffset = cursorOffset;
|
|
1298
|
+
if (usingOffset || !(lineIndex <= afterLine)) try {
|
|
1299
|
+
messages.push(JSON.parse(trimmed));
|
|
1300
|
+
} catch {}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (partial.trim()) {
|
|
1305
|
+
cursorOffset += Buffer.byteLength(partial, 'utf8');
|
|
1306
|
+
lineIndex += 1;
|
|
1307
|
+
lastLine = lineIndex;
|
|
1308
|
+
lastOffset = cursorOffset;
|
|
1309
|
+
if (usingOffset || lineIndex > afterLine) try {
|
|
1310
|
+
messages.push(JSON.parse(partial.trim()));
|
|
1311
|
+
} catch {}
|
|
1312
|
+
}
|
|
1313
|
+
return {
|
|
1314
|
+
messages,
|
|
1315
|
+
lastLine,
|
|
1316
|
+
lastOffset
|
|
1317
|
+
};
|
|
1318
|
+
} finally{
|
|
1319
|
+
__rspack_external_node_fs_5ea92f0c.closeSync(fd);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function getTranscriptEndPosition(filePath) {
|
|
1323
|
+
try {
|
|
1324
|
+
return {
|
|
1325
|
+
lastLine: -1,
|
|
1326
|
+
lastOffset: __rspack_external_node_fs_5ea92f0c.statSync(filePath).size
|
|
1327
|
+
};
|
|
1328
|
+
} catch {
|
|
1329
|
+
return {
|
|
1330
|
+
lastLine: -1,
|
|
1331
|
+
lastOffset: 0
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function isHumanMessage(msg) {
|
|
1336
|
+
if ('user' !== msg.type) return false;
|
|
1337
|
+
if ('string' == typeof msg.message.content) return true;
|
|
1338
|
+
if (Array.isArray(msg.message.content)) return !msg.message.content.some((block)=>block?.type === 'tool_result');
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
function isToolResult(msg) {
|
|
1342
|
+
return 'user' === msg.type && Array.isArray(msg.message.content) && msg.message.content.some((block)=>block?.type === 'tool_result');
|
|
1343
|
+
}
|
|
1344
|
+
function isAssistantMessage(msg) {
|
|
1345
|
+
return 'assistant' === msg.type;
|
|
1346
|
+
}
|
|
1347
|
+
function stripModelDateSuffix(model) {
|
|
1348
|
+
return model.replace(/-\d{8}$/, '');
|
|
1349
|
+
}
|
|
1350
|
+
function mergeAdjacentTextBlocks(blocks) {
|
|
1351
|
+
const result = [];
|
|
1352
|
+
let textBuffer = null;
|
|
1353
|
+
for (const block of blocks){
|
|
1354
|
+
if ('text' === block.type) {
|
|
1355
|
+
textBuffer = (textBuffer ?? '') + block.text;
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
if (null !== textBuffer) {
|
|
1359
|
+
result.push({
|
|
1360
|
+
type: 'text',
|
|
1361
|
+
text: textBuffer
|
|
1362
|
+
});
|
|
1363
|
+
textBuffer = null;
|
|
1364
|
+
}
|
|
1365
|
+
result.push(block);
|
|
1366
|
+
}
|
|
1367
|
+
if (null !== textBuffer) result.push({
|
|
1368
|
+
type: 'text',
|
|
1369
|
+
text: textBuffer
|
|
1370
|
+
});
|
|
1371
|
+
return result;
|
|
1372
|
+
}
|
|
1373
|
+
function mergeAssistantChunks(chunks) {
|
|
1374
|
+
const first = chunks[0];
|
|
1375
|
+
const last = chunks[chunks.length - 1];
|
|
1376
|
+
const content = mergeAdjacentTextBlocks(chunks.flatMap((chunk)=>chunk.message.content ?? []));
|
|
1377
|
+
return {
|
|
1378
|
+
content,
|
|
1379
|
+
model: stripModelDateSuffix(first.message.model || 'claude'),
|
|
1380
|
+
usage: last.message.usage ?? {},
|
|
1381
|
+
startTime: first.timestamp || new Date().toISOString(),
|
|
1382
|
+
endTime: last.timestamp || first.timestamp || new Date().toISOString(),
|
|
1383
|
+
stopReason: last.message.stop_reason
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function toolResultContent(content) {
|
|
1387
|
+
if ('string' == typeof content) return content;
|
|
1388
|
+
return content.filter((item)=>item?.type === 'text' && 'string' == typeof item.text).map((item)=>item.text).join('');
|
|
1389
|
+
}
|
|
1390
|
+
function findToolResult(toolUseId, toolResults) {
|
|
1391
|
+
for (const msg of toolResults)for (const block of msg.message.content)if ('tool_result' === block.type && block.tool_use_id === toolUseId) return {
|
|
1392
|
+
content: toolResultContent(block.content),
|
|
1393
|
+
timestamp: msg.timestamp || new Date().toISOString(),
|
|
1394
|
+
agentId: msg.toolUseResult?.agentId,
|
|
1395
|
+
is_error: block.is_error
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function groupIntoTurns(messages) {
|
|
1399
|
+
const turns = [];
|
|
1400
|
+
let currentPromptId;
|
|
1401
|
+
let currentUser = null;
|
|
1402
|
+
let assistantChunks = new Map();
|
|
1403
|
+
let assistantOrder = [];
|
|
1404
|
+
let toolResults = [];
|
|
1405
|
+
let hasStopReasonEndTurn = false;
|
|
1406
|
+
function resetForUser(msg) {
|
|
1407
|
+
currentUser = msg;
|
|
1408
|
+
currentPromptId = msg.promptId;
|
|
1409
|
+
assistantChunks = new Map();
|
|
1410
|
+
assistantOrder = [];
|
|
1411
|
+
toolResults = [];
|
|
1412
|
+
hasStopReasonEndTurn = false;
|
|
1413
|
+
}
|
|
1414
|
+
function finalizeTurn(forceIncomplete = false) {
|
|
1415
|
+
if (!currentUser || 0 === assistantChunks.size) return;
|
|
1416
|
+
const assistantMessages = Array.from(assistantChunks.values()).flat();
|
|
1417
|
+
const hasStopReasonField = assistantMessages.some((msg)=>void 0 !== msg.message.stop_reason);
|
|
1418
|
+
const isComplete = hasStopReasonEndTurn || !forceIncomplete && !hasStopReasonField;
|
|
1419
|
+
const llmCalls = [];
|
|
1420
|
+
for (const messageId of assistantOrder){
|
|
1421
|
+
const chunks = assistantChunks.get(messageId);
|
|
1422
|
+
if (!chunks?.length) continue;
|
|
1423
|
+
const merged = mergeAssistantChunks(chunks);
|
|
1424
|
+
const toolUses = merged.content.filter((block)=>'tool_use' === block.type);
|
|
1425
|
+
llmCalls.push({
|
|
1426
|
+
content: merged.content,
|
|
1427
|
+
model: merged.model,
|
|
1428
|
+
usage: merged.usage,
|
|
1429
|
+
startTime: merged.startTime,
|
|
1430
|
+
endTime: merged.endTime,
|
|
1431
|
+
stopReason: merged.stopReason,
|
|
1432
|
+
toolCalls: toolUses.map((toolUse)=>{
|
|
1433
|
+
const result = findToolResult(toolUse.id, toolResults);
|
|
1434
|
+
return {
|
|
1435
|
+
tool_use: toolUse,
|
|
1436
|
+
result: result ? {
|
|
1437
|
+
content: result.content,
|
|
1438
|
+
timestamp: result.timestamp,
|
|
1439
|
+
is_error: result.is_error
|
|
1440
|
+
} : void 0,
|
|
1441
|
+
agentId: result?.agentId
|
|
1442
|
+
};
|
|
1443
|
+
})
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
turns.push({
|
|
1447
|
+
userContent: currentUser.message.content,
|
|
1448
|
+
userTimestamp: currentUser.timestamp || new Date().toISOString(),
|
|
1449
|
+
promptId: currentUser.promptId,
|
|
1450
|
+
llmCalls,
|
|
1451
|
+
isComplete
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
for (const msg of messages){
|
|
1455
|
+
if (isHumanMessage(msg)) {
|
|
1456
|
+
const isNewTurn = null === currentUser || void 0 !== msg.promptId && msg.promptId !== currentPromptId || void 0 === msg.promptId;
|
|
1457
|
+
if (isNewTurn) {
|
|
1458
|
+
finalizeTurn();
|
|
1459
|
+
resetForUser(msg);
|
|
1460
|
+
}
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (currentUser) {
|
|
1464
|
+
if (isAssistantMessage(msg)) {
|
|
1465
|
+
const messageId = msg.message.id || `${msg.timestamp || 'assistant'}:${assistantOrder.length}`;
|
|
1466
|
+
if (!assistantChunks.has(messageId)) {
|
|
1467
|
+
assistantChunks.set(messageId, []);
|
|
1468
|
+
assistantOrder.push(messageId);
|
|
1469
|
+
}
|
|
1470
|
+
assistantChunks.get(messageId)?.push(msg);
|
|
1471
|
+
if ('end_turn' === msg.message.stop_reason) hasStopReasonEndTurn = true;
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
if (isToolResult(msg)) toolResults.push(msg);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
finalizeTurn(true);
|
|
1478
|
+
return turns;
|
|
1479
|
+
}
|
|
1480
|
+
function expandHome(filePath) {
|
|
1481
|
+
if (!filePath) return;
|
|
1482
|
+
if ('~' === filePath) return homedir();
|
|
1483
|
+
if (filePath.startsWith('~/')) return __rspack_external_node_path_c5b9b54f.join(homedir(), filePath.slice(2));
|
|
1484
|
+
return filePath;
|
|
1485
|
+
}
|
|
1486
|
+
function sanitizeFileName(value) {
|
|
1487
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 160) || 'trace';
|
|
1488
|
+
}
|
|
1489
|
+
async function writeSendLog(config, record) {
|
|
1490
|
+
await writeJsonFileAtomic(__rspack_external_node_path_c5b9b54f.join(config.sendLogsDir, 'latest.send-log.json'), record);
|
|
1491
|
+
await appendBoundedJsonlFile(__rspack_external_node_path_c5b9b54f.join(config.dataRoot, 'logs', 'diagnostics', 'send-log.jsonl'), record, config.diagnosticLogMaxBytes);
|
|
1492
|
+
}
|
|
1493
|
+
async function writeDebugArtifacts(config, trace) {
|
|
1494
|
+
if (!config.debugArtifacts) return {};
|
|
1495
|
+
const baseName = sanitizeFileName(`${trace.trace_id}.${Date.now()}`);
|
|
1496
|
+
const tracePath = __rspack_external_node_path_c5b9b54f.join(config.artifactsDir, 'otel-traces', `${baseName}.trace.json`);
|
|
1497
|
+
const payloadPath = __rspack_external_node_path_c5b9b54f.join(config.artifactsDir, 'otlp-payloads', `${baseName}.otlp.json`);
|
|
1498
|
+
const schemaPath = __rspack_external_node_path_c5b9b54f.join(config.artifactsDir, 'schema-aligned', `${baseName}.schema-aligned.json`);
|
|
1499
|
+
await writeJsonFileAtomic(tracePath, {
|
|
1500
|
+
generated_at: new Date().toISOString(),
|
|
1501
|
+
trace
|
|
1502
|
+
});
|
|
1503
|
+
await writeJsonFileAtomic(payloadPath, buildOtlpPayload(trace, config));
|
|
1504
|
+
await writeJsonFileAtomic(schemaPath, {
|
|
1505
|
+
generated_at: new Date().toISOString(),
|
|
1506
|
+
records: buildSchemaAlignedRecords(trace)
|
|
1507
|
+
});
|
|
1508
|
+
return {
|
|
1509
|
+
trace_path: tracePath,
|
|
1510
|
+
otlp_payload_path: payloadPath,
|
|
1511
|
+
schema_aligned_path: schemaPath
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
async function exportTraceAndLog(options) {
|
|
1515
|
+
const { config, trace, hookEventName } = options;
|
|
1516
|
+
const artifacts = await writeDebugArtifacts(config, trace);
|
|
1517
|
+
const result = await exportTraceToTls(trace, config);
|
|
1518
|
+
await writeSendLog(config, {
|
|
1519
|
+
...exportSummary(trace, result),
|
|
1520
|
+
hook_event_name: hookEventName,
|
|
1521
|
+
capture_content: config.captureContent,
|
|
1522
|
+
debug_artifacts: config.debugArtifacts,
|
|
1523
|
+
...artifacts
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
function synthesizeMissingFinalAssistant(turns, input, session) {
|
|
1527
|
+
if (!turns.length || !input.last_assistant_message) return;
|
|
1528
|
+
const lastTurn = turns[turns.length - 1];
|
|
1529
|
+
const lastLlm = lastTurn.llmCalls.at(-1);
|
|
1530
|
+
if (!lastLlm || 0 === lastLlm.toolCalls.length) return;
|
|
1531
|
+
const lastToolEnd = Math.max(...lastLlm.toolCalls.map((tool)=>session.tool_observations?.[tool.tool_use.id]?.end_time_ms).filter((value)=>Number.isFinite(value)), Date.parse(lastLlm.endTime));
|
|
1532
|
+
lastTurn.llmCalls.push({
|
|
1533
|
+
content: [
|
|
1534
|
+
{
|
|
1535
|
+
type: 'text',
|
|
1536
|
+
text: input.last_assistant_message
|
|
1537
|
+
}
|
|
1538
|
+
],
|
|
1539
|
+
model: lastLlm.model,
|
|
1540
|
+
usage: {},
|
|
1541
|
+
startTime: new Date(lastToolEnd).toISOString(),
|
|
1542
|
+
endTime: new Date().toISOString(),
|
|
1543
|
+
toolCalls: [],
|
|
1544
|
+
synthetic: true
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
async function exportTranscriptTurns(options) {
|
|
1548
|
+
const { messages, lastLine, lastOffset } = readTranscript(options.transcriptPath, options.afterLine, options.afterOffset ?? 0);
|
|
1549
|
+
const turns = groupIntoTurns(messages);
|
|
1550
|
+
if (options.synthesizeFinalAssistant) synthesizeMissingFinalAssistant(turns, options.input, options.session);
|
|
1551
|
+
let tracedTurns = 0;
|
|
1552
|
+
for (const turn of turns){
|
|
1553
|
+
const trace = buildTraceFromTurn({
|
|
1554
|
+
input: options.input,
|
|
1555
|
+
turn,
|
|
1556
|
+
turnIndex: (options.turnIndexBase ?? options.session.turn_count) + tracedTurns + 1,
|
|
1557
|
+
sessionState: options.session,
|
|
1558
|
+
config: options.config,
|
|
1559
|
+
source: options.source,
|
|
1560
|
+
traceSeedSuffix: options.traceSeedSuffix,
|
|
1561
|
+
extraAttributes: options.extraAttributes
|
|
1562
|
+
});
|
|
1563
|
+
await exportTraceAndLog({
|
|
1564
|
+
config: options.config,
|
|
1565
|
+
trace,
|
|
1566
|
+
hookEventName: options.hookEventName
|
|
1567
|
+
});
|
|
1568
|
+
tracedTurns += 1;
|
|
1569
|
+
}
|
|
1570
|
+
return {
|
|
1571
|
+
lastLine,
|
|
1572
|
+
lastOffset,
|
|
1573
|
+
tracedTurns
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
async function exportPendingSubagents(options) {
|
|
1577
|
+
const pending = options.session.pending_subagent_traces ?? [];
|
|
1578
|
+
let tracedTurns = 0;
|
|
1579
|
+
for (const [index, subagent] of pending.entries()){
|
|
1580
|
+
const transcriptPath = expandHome(subagent.agent_transcript_path);
|
|
1581
|
+
if (!transcriptPath) continue;
|
|
1582
|
+
const input = {
|
|
1583
|
+
hook_event_name: 'Stop',
|
|
1584
|
+
session_id: options.input.session_id,
|
|
1585
|
+
transcript_path: transcriptPath
|
|
1586
|
+
};
|
|
1587
|
+
const sessionState = {
|
|
1588
|
+
...options.session,
|
|
1589
|
+
current_turn_id: `${options.session.current_turn_id ?? options.input.session_id ?? 'claude'}:subagent:${subagent.agent_id ?? index}`
|
|
1590
|
+
};
|
|
1591
|
+
const result = await exportTranscriptTurns({
|
|
1592
|
+
input,
|
|
1593
|
+
config: options.config,
|
|
1594
|
+
transcriptPath,
|
|
1595
|
+
afterLine: -1,
|
|
1596
|
+
afterOffset: 0,
|
|
1597
|
+
session: sessionState,
|
|
1598
|
+
hookEventName: options.input.hook_event_name ?? 'SubagentStop',
|
|
1599
|
+
source: "claude_subagent_stop_transcript",
|
|
1600
|
+
traceSeedSuffix: `subagent:${subagent.agent_id ?? index}`,
|
|
1601
|
+
turnIndexBase: options.session.subagent_turn_count ?? 0,
|
|
1602
|
+
extraAttributes: {
|
|
1603
|
+
'gen_ai.agent.id': subagent.agent_id,
|
|
1604
|
+
'gen_ai.agent.name': subagent.agent_type
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
tracedTurns += result.tracedTurns;
|
|
1608
|
+
}
|
|
1609
|
+
return tracedTurns;
|
|
1610
|
+
}
|
|
1611
|
+
async function handlePreCompact(input, config) {
|
|
1612
|
+
if (!input.session_id) return;
|
|
1613
|
+
const sessionId = input.session_id;
|
|
1614
|
+
const startMs = Date.now();
|
|
1615
|
+
await atomicUpdateState(config.stateFilePath, (state)=>{
|
|
1616
|
+
const session = getSessionState(state, sessionId);
|
|
1617
|
+
return {
|
|
1618
|
+
...state,
|
|
1619
|
+
[sessionId]: {
|
|
1620
|
+
...session,
|
|
1621
|
+
compaction_start_time_ms: startMs,
|
|
1622
|
+
compaction_trigger: input.trigger,
|
|
1623
|
+
compaction_custom_instructions: input.custom_instructions,
|
|
1624
|
+
updated: new Date().toISOString()
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
async function handlePostCompact(input, config) {
|
|
1630
|
+
if (!input.session_id) return;
|
|
1631
|
+
const sessionId = input.session_id;
|
|
1632
|
+
const session = getSessionState(loadState(config.stateFilePath), sessionId);
|
|
1633
|
+
const trace = buildCompactTrace({
|
|
1634
|
+
input,
|
|
1635
|
+
sessionState: session,
|
|
1636
|
+
config
|
|
1637
|
+
});
|
|
1638
|
+
await exportTraceAndLog({
|
|
1639
|
+
config,
|
|
1640
|
+
trace,
|
|
1641
|
+
hookEventName: 'PostCompact'
|
|
1642
|
+
});
|
|
1643
|
+
await atomicUpdateState(config.stateFilePath, (state)=>{
|
|
1644
|
+
const latestSession = getSessionState(state, sessionId);
|
|
1645
|
+
return {
|
|
1646
|
+
...state,
|
|
1647
|
+
[sessionId]: {
|
|
1648
|
+
...latestSession,
|
|
1649
|
+
compaction_start_time_ms: void 0,
|
|
1650
|
+
compaction_trigger: void 0,
|
|
1651
|
+
compaction_custom_instructions: void 0,
|
|
1652
|
+
updated: new Date().toISOString()
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
const TRANSCRIPT_FLUSH_DELAY_MS = 200;
|
|
1658
|
+
function clearedActiveSession(session, updates = {}) {
|
|
1659
|
+
return {
|
|
1660
|
+
...session,
|
|
1661
|
+
...updates,
|
|
1662
|
+
current_turn_id: void 0,
|
|
1663
|
+
current_turn_start_ms: void 0,
|
|
1664
|
+
current_prompt: void 0,
|
|
1665
|
+
tool_start_times: {},
|
|
1666
|
+
tool_observations: {},
|
|
1667
|
+
pending_subagent_traces: [],
|
|
1668
|
+
updated: new Date().toISOString()
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
async function waitForTranscriptFlush() {
|
|
1672
|
+
await new Promise((resolve)=>setTimeout(resolve, TRANSCRIPT_FLUSH_DELAY_MS));
|
|
1673
|
+
}
|
|
1674
|
+
async function handleStop(input, config) {
|
|
1675
|
+
if (!input.session_id || input.stop_hook_active) return;
|
|
1676
|
+
const sessionId = input.session_id;
|
|
1677
|
+
const transcriptPath = expandHome(input.transcript_path);
|
|
1678
|
+
if (!transcriptPath) return;
|
|
1679
|
+
await waitForTranscriptFlush();
|
|
1680
|
+
const session = getSessionState(loadState(config.stateFilePath), sessionId);
|
|
1681
|
+
const mainResult = await exportTranscriptTurns({
|
|
1682
|
+
input,
|
|
1683
|
+
config,
|
|
1684
|
+
transcriptPath,
|
|
1685
|
+
afterLine: session.last_line,
|
|
1686
|
+
afterOffset: session.last_offset ?? 0,
|
|
1687
|
+
session,
|
|
1688
|
+
hookEventName: 'Stop',
|
|
1689
|
+
synthesizeFinalAssistant: true
|
|
1690
|
+
});
|
|
1691
|
+
const subagentTurns = await exportPendingSubagents({
|
|
1692
|
+
input,
|
|
1693
|
+
config,
|
|
1694
|
+
session
|
|
1695
|
+
});
|
|
1696
|
+
await atomicUpdateState(config.stateFilePath, (state)=>{
|
|
1697
|
+
const latestSession = getSessionState(state, sessionId);
|
|
1698
|
+
const updates = 0 === mainResult.tracedTurns && 0 === subagentTurns ? {} : {
|
|
1699
|
+
last_line: mainResult.lastLine,
|
|
1700
|
+
last_offset: mainResult.lastOffset,
|
|
1701
|
+
turn_count: latestSession.turn_count + mainResult.tracedTurns,
|
|
1702
|
+
subagent_turn_count: (latestSession.subagent_turn_count ?? 0) + subagentTurns
|
|
1703
|
+
};
|
|
1704
|
+
return {
|
|
1705
|
+
...state,
|
|
1706
|
+
[sessionId]: clearedActiveSession(latestSession, updates)
|
|
1707
|
+
};
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
async function handleStopFailure(input, config) {
|
|
1711
|
+
const trace = buildStopFailureTrace({
|
|
1712
|
+
input,
|
|
1713
|
+
config
|
|
1714
|
+
});
|
|
1715
|
+
await exportTraceAndLog({
|
|
1716
|
+
config,
|
|
1717
|
+
trace,
|
|
1718
|
+
hookEventName: 'StopFailure'
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
async function handleSessionEnd(input, config) {
|
|
1722
|
+
if (!input.session_id) return;
|
|
1723
|
+
const sessionId = input.session_id;
|
|
1724
|
+
const transcriptPath = expandHome(input.transcript_path);
|
|
1725
|
+
const session = getSessionState(loadState(config.stateFilePath), sessionId);
|
|
1726
|
+
if (!session.current_turn_id && !session.pending_subagent_traces?.length) return;
|
|
1727
|
+
await waitForTranscriptFlush();
|
|
1728
|
+
let lastLine = session.last_line;
|
|
1729
|
+
let lastOffset = session.last_offset ?? 0;
|
|
1730
|
+
let tracedTurns = 0;
|
|
1731
|
+
if (transcriptPath && session.current_turn_id) {
|
|
1732
|
+
const stopInput = {
|
|
1733
|
+
hook_event_name: 'Stop',
|
|
1734
|
+
session_id: sessionId,
|
|
1735
|
+
transcript_path: transcriptPath
|
|
1736
|
+
};
|
|
1737
|
+
const result = await exportTranscriptTurns({
|
|
1738
|
+
input: stopInput,
|
|
1739
|
+
config,
|
|
1740
|
+
transcriptPath,
|
|
1741
|
+
afterLine: session.last_line,
|
|
1742
|
+
afterOffset: session.last_offset ?? 0,
|
|
1743
|
+
session,
|
|
1744
|
+
hookEventName: 'SessionEnd',
|
|
1745
|
+
source: "claude_session_end_transcript",
|
|
1746
|
+
traceSeedSuffix: `session-end:${input.reason ?? 'unknown'}`
|
|
1747
|
+
});
|
|
1748
|
+
lastLine = result.lastLine;
|
|
1749
|
+
lastOffset = result.lastOffset;
|
|
1750
|
+
tracedTurns = result.tracedTurns;
|
|
1751
|
+
}
|
|
1752
|
+
const subagentTurns = await exportPendingSubagents({
|
|
1753
|
+
input,
|
|
1754
|
+
config,
|
|
1755
|
+
session
|
|
1756
|
+
});
|
|
1757
|
+
await atomicUpdateState(config.stateFilePath, (state)=>{
|
|
1758
|
+
const latestSession = getSessionState(state, sessionId);
|
|
1759
|
+
return {
|
|
1760
|
+
...state,
|
|
1761
|
+
[sessionId]: clearedActiveSession(latestSession, {
|
|
1762
|
+
last_line: lastLine,
|
|
1763
|
+
last_offset: lastOffset,
|
|
1764
|
+
turn_count: latestSession.turn_count + tracedTurns,
|
|
1765
|
+
subagent_turn_count: (latestSession.subagent_turn_count ?? 0) + subagentTurns
|
|
1766
|
+
})
|
|
1767
|
+
};
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
async function handleSubagentStop(input, config) {
|
|
1771
|
+
if (!input.session_id) return;
|
|
1772
|
+
const sessionId = input.session_id;
|
|
1773
|
+
const agentTranscriptPath = expandHome(input.agent_transcript_path);
|
|
1774
|
+
if (!agentTranscriptPath) return;
|
|
1775
|
+
await atomicUpdateState(config.stateFilePath, (state)=>{
|
|
1776
|
+
const session = getSessionState(state, sessionId);
|
|
1777
|
+
const pending = session.pending_subagent_traces ?? [];
|
|
1778
|
+
const nextSubagent = {
|
|
1779
|
+
agent_id: input.agent_id,
|
|
1780
|
+
agent_type: input.agent_type,
|
|
1781
|
+
agent_transcript_path: agentTranscriptPath,
|
|
1782
|
+
session_id: sessionId
|
|
1783
|
+
};
|
|
1784
|
+
const alreadyQueued = pending.some((item)=>item.agent_transcript_path === nextSubagent.agent_transcript_path && item.agent_id === nextSubagent.agent_id);
|
|
1785
|
+
return {
|
|
1786
|
+
...state,
|
|
1787
|
+
[sessionId]: {
|
|
1788
|
+
...session,
|
|
1789
|
+
pending_subagent_traces: alreadyQueued ? pending : [
|
|
1790
|
+
...pending,
|
|
1791
|
+
nextSubagent
|
|
1792
|
+
],
|
|
1793
|
+
updated: new Date().toISOString()
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
async function handlePreToolUse(input, config) {
|
|
1799
|
+
if (!input.session_id || !input.tool_use_id) return;
|
|
1800
|
+
const sessionId = input.session_id;
|
|
1801
|
+
const toolUseId = input.tool_use_id;
|
|
1802
|
+
const startMs = Date.now();
|
|
1803
|
+
await atomicUpdateState(config.stateFilePath, (state)=>{
|
|
1804
|
+
const session = getSessionState(state, sessionId);
|
|
1805
|
+
return {
|
|
1806
|
+
...state,
|
|
1807
|
+
[sessionId]: {
|
|
1808
|
+
...session,
|
|
1809
|
+
updated: new Date().toISOString(),
|
|
1810
|
+
tool_start_times: {
|
|
1811
|
+
...session.tool_start_times,
|
|
1812
|
+
[toolUseId]: startMs
|
|
1813
|
+
},
|
|
1814
|
+
tool_observations: {
|
|
1815
|
+
...session.tool_observations,
|
|
1816
|
+
[toolUseId]: {
|
|
1817
|
+
...session.tool_observations?.[toolUseId],
|
|
1818
|
+
tool_use_id: toolUseId,
|
|
1819
|
+
tool_name: input.tool_name,
|
|
1820
|
+
tool_input: input.tool_input,
|
|
1821
|
+
start_time_ms: startMs
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
async function handlePostToolUse(input, config) {
|
|
1829
|
+
if (!input.session_id || !input.tool_use_id) return;
|
|
1830
|
+
const sessionId = input.session_id;
|
|
1831
|
+
const toolUseId = input.tool_use_id;
|
|
1832
|
+
const endMs = Date.now();
|
|
1833
|
+
await atomicUpdateState(config.stateFilePath, (state)=>{
|
|
1834
|
+
const session = getSessionState(state, sessionId);
|
|
1835
|
+
return {
|
|
1836
|
+
...state,
|
|
1837
|
+
[sessionId]: putToolObservation(session, {
|
|
1838
|
+
tool_use_id: toolUseId,
|
|
1839
|
+
tool_name: input.tool_name,
|
|
1840
|
+
tool_input: input.tool_input,
|
|
1841
|
+
tool_response: input.tool_response,
|
|
1842
|
+
start_time_ms: session.tool_start_times?.[toolUseId] ?? endMs,
|
|
1843
|
+
end_time_ms: endMs,
|
|
1844
|
+
status: 'PostToolUseFailure' === input.hook_event_name ? 'error' : 'ok'
|
|
1845
|
+
})
|
|
1846
|
+
};
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
async function handleUserPromptSubmit(input, config) {
|
|
1850
|
+
if (!input.session_id) return;
|
|
1851
|
+
const sessionId = input.session_id;
|
|
1852
|
+
const startMs = Date.now();
|
|
1853
|
+
const transcriptPath = expandHome(input.transcript_path);
|
|
1854
|
+
let interruptedLastLine;
|
|
1855
|
+
let interruptedLastOffset;
|
|
1856
|
+
let interruptedTurns = 0;
|
|
1857
|
+
let interruptedSubagentTurns = 0;
|
|
1858
|
+
if (transcriptPath) {
|
|
1859
|
+
const session = getSessionState(loadState(config.stateFilePath), sessionId);
|
|
1860
|
+
if (session.current_turn_id) {
|
|
1861
|
+
const stopInput = {
|
|
1862
|
+
hook_event_name: 'Stop',
|
|
1863
|
+
session_id: sessionId,
|
|
1864
|
+
transcript_path: transcriptPath
|
|
1865
|
+
};
|
|
1866
|
+
const result = await exportTranscriptTurns({
|
|
1867
|
+
input: stopInput,
|
|
1868
|
+
config,
|
|
1869
|
+
transcriptPath,
|
|
1870
|
+
afterLine: session.last_line,
|
|
1871
|
+
afterOffset: session.last_offset ?? 0,
|
|
1872
|
+
session,
|
|
1873
|
+
hookEventName: 'UserPromptSubmit',
|
|
1874
|
+
source: "claude_interrupted_turn_transcript",
|
|
1875
|
+
traceSeedSuffix: 'interrupted-before-new-prompt'
|
|
1876
|
+
});
|
|
1877
|
+
interruptedLastLine = result.lastLine;
|
|
1878
|
+
interruptedLastOffset = result.lastOffset;
|
|
1879
|
+
interruptedTurns = result.tracedTurns;
|
|
1880
|
+
interruptedSubagentTurns = await exportPendingSubagents({
|
|
1881
|
+
input: stopInput,
|
|
1882
|
+
config,
|
|
1883
|
+
session
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
const endPosition = transcriptPath ? getTranscriptEndPosition(transcriptPath) : {
|
|
1888
|
+
lastLine: -1,
|
|
1889
|
+
lastOffset: 0
|
|
1890
|
+
};
|
|
1891
|
+
await atomicUpdateState(config.stateFilePath, (state)=>{
|
|
1892
|
+
const session = getSessionState(state, sessionId);
|
|
1893
|
+
return {
|
|
1894
|
+
...state,
|
|
1895
|
+
[sessionId]: {
|
|
1896
|
+
...session,
|
|
1897
|
+
last_line: interruptedLastLine ?? (session.last_line >= 0 ? session.last_line : endPosition.lastLine),
|
|
1898
|
+
last_offset: interruptedLastOffset ?? (session.last_offset && session.last_offset > 0 ? session.last_offset : endPosition.lastOffset),
|
|
1899
|
+
turn_count: session.turn_count + interruptedTurns,
|
|
1900
|
+
subagent_turn_count: (session.subagent_turn_count ?? 0) + interruptedSubagentTurns,
|
|
1901
|
+
current_turn_id: `${sessionId}:${startMs}`,
|
|
1902
|
+
current_turn_start_ms: startMs,
|
|
1903
|
+
current_prompt: input.prompt,
|
|
1904
|
+
tool_start_times: {},
|
|
1905
|
+
tool_observations: {},
|
|
1906
|
+
pending_subagent_traces: [],
|
|
1907
|
+
updated: new Date().toISOString()
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
async function dispatchHook(input, config) {
|
|
1913
|
+
switch(input.hook_event_name){
|
|
1914
|
+
case 'UserPromptSubmit':
|
|
1915
|
+
await handleUserPromptSubmit(input, config);
|
|
1916
|
+
return;
|
|
1917
|
+
case 'PreToolUse':
|
|
1918
|
+
await handlePreToolUse(input, config);
|
|
1919
|
+
return;
|
|
1920
|
+
case 'PostToolUse':
|
|
1921
|
+
case 'PostToolUseFailure':
|
|
1922
|
+
await handlePostToolUse(input, config);
|
|
1923
|
+
return;
|
|
1924
|
+
case 'SubagentStop':
|
|
1925
|
+
await handleSubagentStop(input, config);
|
|
1926
|
+
return;
|
|
1927
|
+
case 'PreCompact':
|
|
1928
|
+
await handlePreCompact(input, config);
|
|
1929
|
+
return;
|
|
1930
|
+
case 'PostCompact':
|
|
1931
|
+
await handlePostCompact(input, config);
|
|
1932
|
+
return;
|
|
1933
|
+
case 'Stop':
|
|
1934
|
+
await handleStop(input, config);
|
|
1935
|
+
return;
|
|
1936
|
+
case 'StopFailure':
|
|
1937
|
+
await handleStopFailure(input, config);
|
|
1938
|
+
return;
|
|
1939
|
+
case 'SessionEnd':
|
|
1940
|
+
await handleSessionEnd(input, config);
|
|
1941
|
+
return;
|
|
1942
|
+
default:
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
async function readStdinJson() {
|
|
1947
|
+
const text = await new Promise((resolve, reject)=>{
|
|
1948
|
+
let input = '';
|
|
1949
|
+
process.stdin.setEncoding('utf8');
|
|
1950
|
+
process.stdin.on('data', (chunk)=>{
|
|
1951
|
+
input += chunk;
|
|
1952
|
+
});
|
|
1953
|
+
process.stdin.on('end', ()=>resolve(input));
|
|
1954
|
+
process.stdin.on('error', reject);
|
|
1955
|
+
});
|
|
1956
|
+
if (!text.trim()) return {};
|
|
1957
|
+
return JSON.parse(text);
|
|
1958
|
+
}
|
|
1959
|
+
async function main() {
|
|
1960
|
+
try {
|
|
1961
|
+
const input = await readStdinJson();
|
|
1962
|
+
const config = loadConfig();
|
|
1963
|
+
await __rspack_external_node_fs_promises_153e37e0.mkdir(config.dataRoot, {
|
|
1964
|
+
recursive: true
|
|
1965
|
+
});
|
|
1966
|
+
await dispatchHook(input, config);
|
|
1967
|
+
} finally{
|
|
1968
|
+
await closeTraceLogProducers();
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
main().catch(async (error)=>{
|
|
1972
|
+
try {
|
|
1973
|
+
const config = loadConfig();
|
|
1974
|
+
await writeSendLog(config, {
|
|
1975
|
+
generated_at: new Date().toISOString(),
|
|
1976
|
+
export_status: 'failed',
|
|
1977
|
+
reason: 'hook_error',
|
|
1978
|
+
message: error instanceof Error ? error.stack || error.message : String(error)
|
|
1979
|
+
});
|
|
1980
|
+
} catch {}
|
|
1981
|
+
process.exit(0);
|
|
1982
|
+
});
|