@tangle-network/traces 0.23.1 → 1.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/README.md +61 -1078
- package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
- package/dist/ask-2KF4KSPB.js +39 -0
- package/dist/check-YF6ZHPGG.js +322 -0
- package/dist/chunk-2XXABIRL.js +25 -0
- package/dist/chunk-4VKPN432.js +138 -0
- package/dist/chunk-6GOG5KIK.js +53 -0
- package/dist/chunk-7USMM4MA.js +5695 -0
- package/dist/chunk-CG5QHQO4.js +492 -0
- package/dist/chunk-CQ6UYZG6.js +1217 -0
- package/dist/chunk-DU534HAX.js +263 -0
- package/dist/chunk-FC4V25CG.js +503 -0
- package/dist/chunk-GLUDGUGJ.js +232 -0
- package/dist/chunk-GNRXKQEQ.js +120 -0
- package/dist/chunk-K2KH2HHD.js +228 -0
- package/dist/chunk-M3W6TZHQ.js +381 -0
- package/dist/chunk-O52XK7OB.js +19 -0
- package/dist/chunk-PYH3PL3N.js +62 -0
- package/dist/chunk-QLVP37YB.js +782 -0
- package/dist/chunk-QUHUMZ4W.js +759 -0
- package/dist/chunk-REO2JQJY.js +535 -0
- package/dist/chunk-RZL5GAWH.js +60 -0
- package/dist/chunk-TOCNGML5.js +273 -0
- package/dist/chunk-UQSAGXPH.js +208 -0
- package/dist/chunk-VFOLC4OX.js +407 -0
- package/dist/chunk-WF7PTDCZ.js +3921 -0
- package/dist/chunk-YAX72TST.js +259 -0
- package/dist/cli.js +2086 -2050
- package/dist/diff-J4RAIOXO.js +16 -0
- package/dist/improvement-LCCVGGUS.js +25 -0
- package/dist/index.d.ts +1235 -4357
- package/dist/index.js +53 -1204
- package/dist/live-XXL7VZWB.js +14 -0
- package/dist/mcp-EYIDKBEK.js +85 -0
- package/dist/report-E3G2T5TJ.js +338 -0
- package/dist/run-watch-RAOOAXOF.js +649 -0
- package/dist/supervisor-report-RMPLLBGZ.js +29 -0
- package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
- package/dist/upload-3R7GRSKS.js +20 -0
- package/dist/verify-safe-C5ZXZOZ2.js +8 -0
- package/docs/analyze.md +76 -0
- package/docs/ask.md +88 -0
- package/docs/bundle.md +40 -0
- package/docs/check.md +77 -0
- package/docs/convert.md +44 -0
- package/docs/develop.md +32 -0
- package/docs/diff.md +38 -0
- package/docs/facts.md +114 -0
- package/docs/mcp.md +36 -0
- package/docs/migrate-1.0.md +33 -0
- package/docs/parsers.md +66 -0
- package/docs/release-1.0.0.md +147 -0
- package/docs/release-1.0.1.md +4 -0
- package/docs/sdk.md +77 -0
- package/docs/select.md +70 -0
- package/docs/skills.md +46 -0
- package/docs/upload.md +47 -0
- package/docs/watch-jsonl.v1.example.jsonl +5 -0
- package/docs/watch.md +66 -0
- package/examples/README.md +9 -0
- package/examples/custom-analyst.ts +65 -74
- package/install.sh +22 -2
- package/package.json +10 -9
- package/skills/inspect-agent-traces/SKILL.md +47 -110
- package/dist/chunk-7DVZCCZI.js +0 -20983
- package/docs/replay-verify.md +0 -123
- package/docs/stream-findings.svg +0 -39
- package/docs/trace-analysts.md +0 -643
- package/examples/custom-backend.ts +0 -21
- package/examples/external-engines.ts +0 -62
- package/examples/improvement-config.mjs +0 -63
- package/examples/observe-and-alert.ts +0 -19
- package/examples/register-harness.ts +0 -26
- package/skills/build-trace-analyst/SKILL.md +0 -76
- package/skills/build-trace-analyst/agents/openai.yaml +0 -4
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isMetadataAttribute,
|
|
3
|
+
metadataOnly
|
|
4
|
+
} from "./chunk-6GOG5KIK.js";
|
|
5
|
+
import {
|
|
6
|
+
scanSessions
|
|
7
|
+
} from "./chunk-7USMM4MA.js";
|
|
8
|
+
import {
|
|
9
|
+
ATTR,
|
|
10
|
+
INGEST_SOURCE_CLI
|
|
11
|
+
} from "./chunk-UQSAGXPH.js";
|
|
12
|
+
import {
|
|
13
|
+
writeOtlpFile
|
|
14
|
+
} from "./chunk-CQ6UYZG6.js";
|
|
15
|
+
import {
|
|
16
|
+
redactSpans
|
|
17
|
+
} from "./chunk-RZL5GAWH.js";
|
|
18
|
+
import {
|
|
19
|
+
parseIsoToEpochMs
|
|
20
|
+
} from "./chunk-DU534HAX.js";
|
|
21
|
+
|
|
22
|
+
// src/upload.ts
|
|
23
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
24
|
+
import { createRequire } from "module";
|
|
25
|
+
import { hostname } from "os";
|
|
26
|
+
import { basename, join as join2 } from "path";
|
|
27
|
+
import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
|
|
28
|
+
import { assessShareSafety, combineVerdicts, REDACTION_VERSION, redact, shareAllowed } from "@tangle-network/agent-eval/traces";
|
|
29
|
+
|
|
30
|
+
// src/upload-state.ts
|
|
31
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
32
|
+
import { homedir } from "os";
|
|
33
|
+
import { dirname, join } from "path";
|
|
34
|
+
import { contentHash } from "@tangle-network/agent-eval";
|
|
35
|
+
function stateHome() {
|
|
36
|
+
return process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
37
|
+
}
|
|
38
|
+
function stateFile() {
|
|
39
|
+
return join(stateHome(), "traces", "uploaded.json");
|
|
40
|
+
}
|
|
41
|
+
function legacyStateFile() {
|
|
42
|
+
return join(stateHome(), "tangle-traces", "uploaded.json");
|
|
43
|
+
}
|
|
44
|
+
function uploadKey(harness, sessionId) {
|
|
45
|
+
return `${harness}:${sessionId}`;
|
|
46
|
+
}
|
|
47
|
+
function sessionHash(spans) {
|
|
48
|
+
return contentHash(spans).slice(0, 32);
|
|
49
|
+
}
|
|
50
|
+
function outboundHash(events, identity) {
|
|
51
|
+
return contentHash({ events, identity }).slice(0, 32);
|
|
52
|
+
}
|
|
53
|
+
async function loadState() {
|
|
54
|
+
for (const file of [stateFile(), legacyStateFile()]) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
async function saveState(state) {
|
|
63
|
+
const f = stateFile();
|
|
64
|
+
await mkdir(dirname(f), { recursive: true });
|
|
65
|
+
await writeFile(f, `${JSON.stringify(state, null, 2)}
|
|
66
|
+
`, "utf8");
|
|
67
|
+
}
|
|
68
|
+
function alreadyUploaded(state, harness, sessionId, hash) {
|
|
69
|
+
return state[uploadKey(harness, sessionId)]?.hash === hash;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/upload.ts
|
|
73
|
+
var require2 = createRequire(import.meta.url);
|
|
74
|
+
var TRACES_VERSION = require2("../package.json").version;
|
|
75
|
+
async function planUpload(opts = {}) {
|
|
76
|
+
return { ...await planUploadFrom(scanSessions(opts)), sinceMs: opts.sinceMs };
|
|
77
|
+
}
|
|
78
|
+
async function planUploadFrom(sessions) {
|
|
79
|
+
const state = await loadState();
|
|
80
|
+
const items = [];
|
|
81
|
+
for await (const { ref, spans: raw } of sessions) {
|
|
82
|
+
const { spans, report } = redactSpans(raw);
|
|
83
|
+
const hash = sessionHash(spans);
|
|
84
|
+
items.push({
|
|
85
|
+
ref,
|
|
86
|
+
spans,
|
|
87
|
+
redaction: report,
|
|
88
|
+
hash,
|
|
89
|
+
isNew: true
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return { items, state };
|
|
93
|
+
}
|
|
94
|
+
async function gitBranch(cwd) {
|
|
95
|
+
if (!cwd) return void 0;
|
|
96
|
+
try {
|
|
97
|
+
const head = await readFile2(join2(cwd, ".git", "HEAD"), "utf8");
|
|
98
|
+
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
99
|
+
return m?.[1]?.trim();
|
|
100
|
+
} catch {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function sessionMeta(item) {
|
|
105
|
+
const meta = {
|
|
106
|
+
[ATTR.HARNESS]: item.ref.harness,
|
|
107
|
+
[ATTR.SESSION_FILE]: basename(item.ref.path),
|
|
108
|
+
[ATTR.HOST]: hostname(),
|
|
109
|
+
[ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
|
|
110
|
+
[ATTR.REDACTION_VERSION]: REDACTION_VERSION,
|
|
111
|
+
[ATTR.REDACTION_COUNT]: item.redaction.redactionCount
|
|
112
|
+
};
|
|
113
|
+
if (item.ref.cwd) meta[ATTR.CWD] = item.ref.cwd;
|
|
114
|
+
const branch = await gitBranch(item.ref.cwd);
|
|
115
|
+
if (branch) meta[ATTR.GIT_BRANCH] = branch;
|
|
116
|
+
return redact(meta).value;
|
|
117
|
+
}
|
|
118
|
+
var msToNano = (epochMs) => (BigInt(epochMs) * 1000000n).toString();
|
|
119
|
+
function toTraceSpanEvents(spans, rootMeta) {
|
|
120
|
+
return spans.map((s) => {
|
|
121
|
+
const attributes = {};
|
|
122
|
+
for (const [k, v] of Object.entries(s.attributes)) {
|
|
123
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") attributes[k] = v;
|
|
124
|
+
else if (v != null) attributes[k] = JSON.stringify(v);
|
|
125
|
+
}
|
|
126
|
+
attributes[ATTR.SESSION_ID] = s.trace_id;
|
|
127
|
+
attributes[ATTR.INGEST_SOURCE] = INGEST_SOURCE_CLI;
|
|
128
|
+
if (s.parent_span_id === null) Object.assign(attributes, rootMeta);
|
|
129
|
+
const startMs = parseIsoToEpochMs(s.start_time);
|
|
130
|
+
const endMs = Math.max(startMs, parseIsoToEpochMs(s.end_time));
|
|
131
|
+
return {
|
|
132
|
+
traceId: s.trace_id,
|
|
133
|
+
spanId: s.span_id,
|
|
134
|
+
...s.parent_span_id ? { parentSpanId: s.parent_span_id } : {},
|
|
135
|
+
name: s.name,
|
|
136
|
+
startTimeUnixNano: msToNano(startMs),
|
|
137
|
+
endTimeUnixNano: msToNano(endMs),
|
|
138
|
+
attributes,
|
|
139
|
+
...s.status.code === "UNSET" ? {} : { status: { code: s.status.code, ...s.status.message ? { message: s.status.message } : {} } }
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
var PartialUploadError = class extends Error {
|
|
144
|
+
sessionKey;
|
|
145
|
+
accepted;
|
|
146
|
+
expected;
|
|
147
|
+
constructor(sessionKey, accepted, expected) {
|
|
148
|
+
super(`upload: backend accepted ${accepted} of ${expected} spans for ${sessionKey}`);
|
|
149
|
+
this.name = "PartialUploadError";
|
|
150
|
+
this.sessionKey = sessionKey;
|
|
151
|
+
this.accepted = accepted;
|
|
152
|
+
this.expected = expected;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
async function prepareOutbound(plan, opts, uploadedAt) {
|
|
156
|
+
let candidates = plan.items;
|
|
157
|
+
if (opts.stripContent) {
|
|
158
|
+
candidates = candidates.map((item) => ({ ...item, spans: metadataOnly(item.spans).spans }));
|
|
159
|
+
}
|
|
160
|
+
const identity = {
|
|
161
|
+
stripContent: opts.stripContent === true,
|
|
162
|
+
redactor: null,
|
|
163
|
+
redactionVersion: REDACTION_VERSION
|
|
164
|
+
};
|
|
165
|
+
const items = [];
|
|
166
|
+
const refused = [];
|
|
167
|
+
const verdicts = [];
|
|
168
|
+
for (const item of candidates) {
|
|
169
|
+
const key = uploadKey(item.ref.harness, item.ref.sessionId);
|
|
170
|
+
const previous = plan.state[key];
|
|
171
|
+
const meta = await sessionMeta(item);
|
|
172
|
+
if (opts.stripContent) {
|
|
173
|
+
for (const [key2, value] of Object.entries(meta)) if (!isMetadataAttribute(key2, value)) delete meta[key2];
|
|
174
|
+
}
|
|
175
|
+
const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR.UPLOADED_AT]: timestamp });
|
|
176
|
+
let eventTimestamp = previous?.uploadedAt ?? uploadedAt;
|
|
177
|
+
let events = eventsAt(eventTimestamp);
|
|
178
|
+
let hash = outboundHash(events, identity);
|
|
179
|
+
if (alreadyUploaded(plan.state, item.ref.harness, item.ref.sessionId, hash)) continue;
|
|
180
|
+
if (eventTimestamp !== uploadedAt) {
|
|
181
|
+
eventTimestamp = uploadedAt;
|
|
182
|
+
events = eventsAt(eventTimestamp);
|
|
183
|
+
hash = outboundHash(events, identity);
|
|
184
|
+
}
|
|
185
|
+
const verdict = assessShareSafety(events);
|
|
186
|
+
verdicts.push(verdict);
|
|
187
|
+
if (!shareAllowed(verdict)) {
|
|
188
|
+
refused.push({ harness: item.ref.harness, sessionId: item.ref.sessionId, verdict });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
items.push({ item, events, hash });
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
items,
|
|
195
|
+
skipped: plan.items.length - items.length - refused.length,
|
|
196
|
+
refused,
|
|
197
|
+
...verdicts.length > 0 ? { verdict: combineVerdicts("default", verdicts) } : {}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function executeUpload(plan, opts = {}) {
|
|
201
|
+
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
202
|
+
const { items, skipped, refused, verdict } = await prepareOutbound(plan, opts, uploadedAt);
|
|
203
|
+
const redactionCount = items.reduce((n, { item }) => n + item.redaction.redactionCount, 0);
|
|
204
|
+
if (opts.dryRun) {
|
|
205
|
+
const path = opts.otlpOut === void 0 ? void 0 : await writeOtlpFile(items.flatMap(({ item }) => item.spans), opts.otlpOut);
|
|
206
|
+
return {
|
|
207
|
+
uploadedSessions: 0,
|
|
208
|
+
skippedSessions: skipped,
|
|
209
|
+
acceptedSpans: 0,
|
|
210
|
+
redactionCount,
|
|
211
|
+
dryRun: true,
|
|
212
|
+
...path === void 0 ? {} : { otlpPath: path },
|
|
213
|
+
refused,
|
|
214
|
+
...verdict ? { verdict } : {}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (items.length === 0) {
|
|
218
|
+
return {
|
|
219
|
+
uploadedSessions: 0,
|
|
220
|
+
skippedSessions: skipped,
|
|
221
|
+
acceptedSpans: 0,
|
|
222
|
+
redactionCount,
|
|
223
|
+
dryRun: false,
|
|
224
|
+
refused,
|
|
225
|
+
...verdict ? { verdict } : {}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const client = opts.backend ?? hostedClientFromEnv();
|
|
229
|
+
if (!client) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
"upload: no backend. Pass opts.backend, or configure the hosted Tangle Intelligence client via TANGLE_INGEST_URL (or TANGLE_ORCHESTRATOR_URL), TANGLE_INGEST_API_KEY (or TANGLE_API_KEY), and TANGLE_TENANT_ID. Use --dry-run to preview without uploading."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const state = plan.state;
|
|
235
|
+
let acceptedSpans = 0;
|
|
236
|
+
const completed = [];
|
|
237
|
+
for (const outbound of items) {
|
|
238
|
+
const { item, events, hash } = outbound;
|
|
239
|
+
const key = uploadKey(item.ref.harness, item.ref.sessionId);
|
|
240
|
+
const idempotencyKey = `${key}:${hash}`;
|
|
241
|
+
const res = await client.ingestTraces(events, idempotencyKey);
|
|
242
|
+
if (res.accepted !== events.length) throw new PartialUploadError(key, res.accepted, events.length);
|
|
243
|
+
acceptedSpans += res.accepted;
|
|
244
|
+
completed.push({ key, item, hash, spanCount: events.length });
|
|
245
|
+
opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
|
|
246
|
+
}
|
|
247
|
+
for (const { key, item, hash, spanCount } of completed) {
|
|
248
|
+
state[key] = {
|
|
249
|
+
hash,
|
|
250
|
+
uploadedAt,
|
|
251
|
+
harness: item.ref.harness,
|
|
252
|
+
spanCount
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
await saveState(state);
|
|
256
|
+
return {
|
|
257
|
+
uploadedSessions: items.length,
|
|
258
|
+
skippedSessions: skipped,
|
|
259
|
+
acceptedSpans,
|
|
260
|
+
redactionCount,
|
|
261
|
+
dryRun: false,
|
|
262
|
+
refused,
|
|
263
|
+
...verdict ? { verdict } : {}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export {
|
|
268
|
+
planUpload,
|
|
269
|
+
planUploadFrom,
|
|
270
|
+
toTraceSpanEvents,
|
|
271
|
+
PartialUploadError,
|
|
272
|
+
executeUpload
|
|
273
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
span
|
|
3
|
+
} from "./chunk-CQ6UYZG6.js";
|
|
4
|
+
|
|
5
|
+
// src/attributes.ts
|
|
6
|
+
var ATTR = {
|
|
7
|
+
/** = the trace/session id. Wire contract (dedup linkage). */
|
|
8
|
+
SESSION_ID: "tangle.sessionId",
|
|
9
|
+
/** "cli" marks a CLI upload so the platform can drop a re-upload of a session
|
|
10
|
+
* that also streamed live. Wire contract. */
|
|
11
|
+
INGEST_SOURCE: "tangle.ingest_source",
|
|
12
|
+
HARNESS: "tangle.harness",
|
|
13
|
+
CWD: "tangle.cwd",
|
|
14
|
+
/** THE per-session grouping key the spine derives subjects from
|
|
15
|
+
* (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
|
|
16
|
+
* remote (host/owner/repo) when readable, else the cwd path basename. */
|
|
17
|
+
SUBJECT_KEY: "tangle.subject.key",
|
|
18
|
+
/** Normalized git remote (e.g. `github.com/tangle-network/agent-container`). */
|
|
19
|
+
GIT_REPOSITORY: "git.repository",
|
|
20
|
+
/** Current branch at conversion time. */
|
|
21
|
+
GIT_BRANCH_NAME: "git.branch",
|
|
22
|
+
/** HEAD short sha at conversion time. */
|
|
23
|
+
GIT_COMMIT: "git.commit",
|
|
24
|
+
/** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
|
|
25
|
+
REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
|
|
26
|
+
GIT_BRANCH: "tangle.git_branch",
|
|
27
|
+
HOST: "tangle.host",
|
|
28
|
+
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
29
|
+
* which collided with `tangle.ingest_source`.) */
|
|
30
|
+
SESSION_FILE: "tangle.session_file",
|
|
31
|
+
UPLOADED_AT: "tangle.uploaded_at",
|
|
32
|
+
UPLOADER: "tangle.uploader",
|
|
33
|
+
REDACTION_VERSION: "redaction.version",
|
|
34
|
+
REDACTION_COUNT: "redaction.count",
|
|
35
|
+
/** Upload contract: bounded root summary plus location-only receipt child spans.
|
|
36
|
+
* Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
|
|
37
|
+
SESSION_INTEGRITY: "traces.session.integrity",
|
|
38
|
+
CORRUPTION_COUNT: "traces.session.corruption_count",
|
|
39
|
+
CORRUPTION_DIGEST: "traces.session.corruption_digest",
|
|
40
|
+
CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
|
|
41
|
+
CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
|
|
42
|
+
CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
|
|
43
|
+
CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
|
|
44
|
+
CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
|
|
45
|
+
CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
|
|
46
|
+
CORRUPTION_SHA256: "traces.session.corruption.sha256",
|
|
47
|
+
RAW_SOURCE_RETENTION: "traces.session.raw_source_retention",
|
|
48
|
+
/** Container image reference the session executed in (OTel semconv key). */
|
|
49
|
+
CONTAINER_IMAGE: "container.image.name",
|
|
50
|
+
/** Immutable image digest (`sha256:…`) — the replay-grade environment pin. */
|
|
51
|
+
CONTAINER_IMAGE_DIGEST: "container.image.digest",
|
|
52
|
+
/** Sandbox/container instance id that executed the session. */
|
|
53
|
+
SANDBOX_ID: "tangle.sandbox.id",
|
|
54
|
+
/** Recorded execution cwd, verbatim. Unlike `tangle.cwd` it is never
|
|
55
|
+
* repaired against the host filesystem — replay verification needs the
|
|
56
|
+
* in-container path even when no such path exists on this host. */
|
|
57
|
+
ENVIRONMENT_CWD: "tangle.environment.cwd"
|
|
58
|
+
};
|
|
59
|
+
var SESSION_CWD_ATTR = "traces.session.cwd";
|
|
60
|
+
var SESSION_TOTAL_TOKENS_ATTR = "traces.session.total_tokens";
|
|
61
|
+
var SESSION_TOTAL_INPUT_ATTR = "traces.session.total_input_tokens";
|
|
62
|
+
var SESSION_TOTAL_OUTPUT_ATTR = "traces.session.total_output_tokens";
|
|
63
|
+
var SESSION_TOTAL_CACHED_INPUT_ATTR = "traces.session.total_cached_input_tokens";
|
|
64
|
+
var SESSION_TOTAL_CACHE_WRITE_ATTR = "traces.session.total_cache_write_input_tokens";
|
|
65
|
+
var SESSION_TOTAL_REASONING_ATTR = "traces.session.total_reasoning_tokens";
|
|
66
|
+
var INGEST_SOURCE_CLI = "cli";
|
|
67
|
+
function environmentAttributes(environment) {
|
|
68
|
+
if (!environment) return {};
|
|
69
|
+
const attrs = {};
|
|
70
|
+
if (environment.image) attrs[ATTR.CONTAINER_IMAGE] = environment.image;
|
|
71
|
+
if (environment.imageDigest) attrs[ATTR.CONTAINER_IMAGE_DIGEST] = environment.imageDigest;
|
|
72
|
+
if (environment.sandboxId) attrs[ATTR.SANDBOX_ID] = environment.sandboxId;
|
|
73
|
+
if (environment.cwd) attrs[ATTR.ENVIRONMENT_CWD] = environment.cwd;
|
|
74
|
+
if (environment.gitCommit) attrs[ATTR.GIT_COMMIT] = environment.gitCommit;
|
|
75
|
+
return attrs;
|
|
76
|
+
}
|
|
77
|
+
function stampEnvironmentAttrs(spans, environment) {
|
|
78
|
+
const attrs = environmentAttributes(environment);
|
|
79
|
+
const keys = Object.keys(attrs);
|
|
80
|
+
if (keys.length === 0) return;
|
|
81
|
+
for (const span2 of spans) {
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
if (span2.attributes[key] === void 0) span2.attributes[key] = attrs[key];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
var SESSION_ID_ATTRIBUTE_KEYS = [ATTR.SESSION_ID, "session.id", "traces.session.id"];
|
|
88
|
+
function sessionIdFromAttributes(attributes) {
|
|
89
|
+
return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes[key]).find(
|
|
90
|
+
(value) => typeof value === "string" && value.length > 0
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
function indexSessionIdsByTrace(spans) {
|
|
94
|
+
const candidatesByTrace = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const span2 of spans) {
|
|
96
|
+
const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
|
|
97
|
+
for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
|
|
98
|
+
const value = span2.attributes[key];
|
|
99
|
+
if (typeof value === "string" && value.length > 0) candidates.add(value);
|
|
100
|
+
}
|
|
101
|
+
if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
|
|
102
|
+
}
|
|
103
|
+
const sessionByTrace = /* @__PURE__ */ new Map();
|
|
104
|
+
const conflicts = [];
|
|
105
|
+
for (const [traceId, candidates] of candidatesByTrace) {
|
|
106
|
+
const sessionIds = [...candidates].sort();
|
|
107
|
+
if (sessionIds.length === 1) {
|
|
108
|
+
sessionByTrace.set(traceId, sessionIds[0]);
|
|
109
|
+
} else {
|
|
110
|
+
conflicts.push({ traceId, sessionIds });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
|
|
114
|
+
return { sessionByTrace, conflicts };
|
|
115
|
+
}
|
|
116
|
+
function stampSessionIdentity(spans, fallbackSessionId) {
|
|
117
|
+
const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
|
|
118
|
+
const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
|
|
119
|
+
const traceIds = new Set(spans.map((span2) => span2.trace_id));
|
|
120
|
+
const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
|
|
121
|
+
for (const span2 of spans) {
|
|
122
|
+
if (conflictingTraceIds.has(span2.trace_id)) continue;
|
|
123
|
+
const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
|
|
124
|
+
if (!sessionIdFromAttributes(span2.attributes)) {
|
|
125
|
+
span2.attributes[ATTR.SESSION_ID] = sessionId;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/adapters/conversation.ts
|
|
131
|
+
var CONTENT_CAP = 8e3;
|
|
132
|
+
function capText(raw, limit = CONTENT_CAP) {
|
|
133
|
+
const t = raw.trim();
|
|
134
|
+
if (t.length <= limit) return t;
|
|
135
|
+
return `${t.slice(0, limit)}\u2026 [+${t.length - limit} chars]`;
|
|
136
|
+
}
|
|
137
|
+
var ACTOR_ATTR = "tangle.actor";
|
|
138
|
+
function userPromptSpan(o) {
|
|
139
|
+
return span({
|
|
140
|
+
traceId: o.traceId,
|
|
141
|
+
spanId: o.spanId,
|
|
142
|
+
parentSpanId: o.parentSpanId,
|
|
143
|
+
name: "user.prompt",
|
|
144
|
+
kind: "CHAIN",
|
|
145
|
+
startTime: o.startTime,
|
|
146
|
+
service: o.service ?? null,
|
|
147
|
+
agent: o.agent ?? null,
|
|
148
|
+
step: o.step,
|
|
149
|
+
content: o.content,
|
|
150
|
+
contentSource: o.contentSource,
|
|
151
|
+
extra: { [ACTOR_ATTR]: o.actor ?? "human" }
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/adapters/provenance.ts
|
|
156
|
+
var SYNTHESIZED_SPAN_ATTR = "traces.span.synthesized";
|
|
157
|
+
var SYNTHESIZED_SOURCE_ATTR = "traces.span.synthesized_from";
|
|
158
|
+
var INHERITED_SPAN_ATTR = "traces.session.inherited";
|
|
159
|
+
var INHERITED_SOURCE_ATTR = "traces.session.inherited_source";
|
|
160
|
+
var INHERITED_SPAN_COUNT_ATTR = "traces.session.inherited_span_count";
|
|
161
|
+
var INHERITED_SPANS_OMITTED_ATTR = "traces.session.inherited_spans_omitted";
|
|
162
|
+
var SUBAGENT_SPAN_ATTR = "traces.span.subagent";
|
|
163
|
+
var SUBAGENT_TASK_ATTR = "traces.span.subagent_task";
|
|
164
|
+
var SUBAGENT_SPAN_COUNT_ATTR = "traces.session.subagent_span_count";
|
|
165
|
+
var SUBAGENT_SPAWN_ATTR = "traces.agent.spawn";
|
|
166
|
+
var SUBAGENT_SPAWN_TASK_ATTR = "traces.agent.spawn_task";
|
|
167
|
+
function isSynthesizedSpan(attributes) {
|
|
168
|
+
return attributes[SYNTHESIZED_SPAN_ATTR] === true;
|
|
169
|
+
}
|
|
170
|
+
function isInheritedSpan(attributes) {
|
|
171
|
+
return attributes[INHERITED_SPAN_ATTR] === true;
|
|
172
|
+
}
|
|
173
|
+
function isSubagentSpan(attributes) {
|
|
174
|
+
return attributes[SUBAGENT_SPAN_ATTR] === true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export {
|
|
178
|
+
ATTR,
|
|
179
|
+
SESSION_CWD_ATTR,
|
|
180
|
+
SESSION_TOTAL_TOKENS_ATTR,
|
|
181
|
+
SESSION_TOTAL_INPUT_ATTR,
|
|
182
|
+
SESSION_TOTAL_OUTPUT_ATTR,
|
|
183
|
+
SESSION_TOTAL_CACHED_INPUT_ATTR,
|
|
184
|
+
SESSION_TOTAL_CACHE_WRITE_ATTR,
|
|
185
|
+
SESSION_TOTAL_REASONING_ATTR,
|
|
186
|
+
INGEST_SOURCE_CLI,
|
|
187
|
+
stampEnvironmentAttrs,
|
|
188
|
+
sessionIdFromAttributes,
|
|
189
|
+
indexSessionIdsByTrace,
|
|
190
|
+
stampSessionIdentity,
|
|
191
|
+
capText,
|
|
192
|
+
ACTOR_ATTR,
|
|
193
|
+
userPromptSpan,
|
|
194
|
+
SYNTHESIZED_SPAN_ATTR,
|
|
195
|
+
SYNTHESIZED_SOURCE_ATTR,
|
|
196
|
+
INHERITED_SPAN_ATTR,
|
|
197
|
+
INHERITED_SOURCE_ATTR,
|
|
198
|
+
INHERITED_SPAN_COUNT_ATTR,
|
|
199
|
+
INHERITED_SPANS_OMITTED_ATTR,
|
|
200
|
+
SUBAGENT_SPAN_ATTR,
|
|
201
|
+
SUBAGENT_TASK_ATTR,
|
|
202
|
+
SUBAGENT_SPAN_COUNT_ATTR,
|
|
203
|
+
SUBAGENT_SPAWN_ATTR,
|
|
204
|
+
SUBAGENT_SPAWN_TASK_ATTR,
|
|
205
|
+
isSynthesizedSpan,
|
|
206
|
+
isInheritedSpan,
|
|
207
|
+
isSubagentSpan
|
|
208
|
+
};
|