@cygnuxlabs/tracekit 0.0.0-stage → 1.2.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 +294 -2
- package/bin/tracekit.mjs +30 -0
- package/dist/v1.d.ts +97 -0
- package/dist/v1.js +535 -0
- package/dist/v2/adapters/claude-agent-sdk.d.ts +45 -0
- package/dist/v2/adapters/claude-agent-sdk.js +142 -0
- package/dist/v2/adapters/gate.d.ts +62 -0
- package/dist/v2/adapters/gate.js +195 -0
- package/dist/v2/adapters/openai-agents.d.ts +85 -0
- package/dist/v2/adapters/openai-agents.js +114 -0
- package/dist/v2/adapters/vercel-ai.d.ts +51 -0
- package/dist/v2/adapters/vercel-ai.js +132 -0
- package/dist/v2/client.d.ts +159 -0
- package/dist/v2/client.js +749 -0
- package/dist/v2/executor.d.ts +49 -0
- package/dist/v2/executor.js +57 -0
- package/dist/v2/jcs.d.ts +5 -0
- package/dist/v2/jcs.js +12 -0
- package/dist/v2/launcher.d.ts +10 -0
- package/dist/v2/launcher.js +50 -0
- package/dist/v2/rpc_schema.d.ts +3 -0
- package/dist/v2/rpc_schema.js +43 -0
- package/dist/v2/validate.d.ts +6 -0
- package/dist/v2/validate.js +70 -0
- package/dist/verify/cli.d.ts +1 -0
- package/dist/verify/cli.js +46 -0
- package/dist/verify/detect.d.ts +41 -0
- package/dist/verify/detect.js +289 -0
- package/dist/verify/index.d.ts +34 -0
- package/dist/verify/index.js +930 -0
- package/dist/verify/jcs.d.ts +19 -0
- package/dist/verify/jcs.js +154 -0
- package/dist/verify/primitives.d.ts +62 -0
- package/dist/verify/primitives.js +298 -0
- package/dist/verify/schema.d.ts +11 -0
- package/dist/verify/schema.js +98 -0
- package/dist/verify/tracekit.event.v2.json +4588 -0
- package/dist/verify/zip.d.ts +17 -0
- package/dist/verify/zip.js +145 -0
- package/package.json +42 -4
package/dist/v1.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cygnuxlabs/tracekit/v1: the v1 SDK, deprecated and removed in 0.5.0; use the native v2 client (`@cygnuxlabs/tracekit`).
|
|
3
|
+
* Tracekit.start warns once per process (DeprecationWarning TRACEKIT_V1; TRACEKIT_NO_DEPRECATION=1 silences it).
|
|
4
|
+
*
|
|
5
|
+
* import { Tracekit, instrumentOpenAI } from "@cygnuxlabs/tracekit/v1";
|
|
6
|
+
* const tk = await Tracekit.start({ agent: "research-bot" });
|
|
7
|
+
* const openai = instrumentOpenAI(new OpenAI(), tk); // every model call: signed request + response
|
|
8
|
+
* const out = await tk.tool("Bash", { command: "ls" }, () => run("ls")); // policy-gated before it runs
|
|
9
|
+
* await tk.end();
|
|
10
|
+
*
|
|
11
|
+
* The SDK drives Tracekit's Python engine through `python -m tracekit.bridge` (stdio, JSON lines), so policy, redaction,
|
|
12
|
+
* the signer client and the event format are exactly those of the Python SDK. It needs the `tracekit` Python package and
|
|
13
|
+
* a configured signer (`tracekit init --dev`). Set TRACEKIT_PYTHON (or `python`) to choose the interpreter.
|
|
14
|
+
*/
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { createInterface } from "node:readline";
|
|
17
|
+
export class TracekitDenied extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "TracekitDenied";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class BridgeClosed extends Error {
|
|
24
|
+
}
|
|
25
|
+
class Bridge {
|
|
26
|
+
proc;
|
|
27
|
+
pending = new Map();
|
|
28
|
+
next = 1;
|
|
29
|
+
ready;
|
|
30
|
+
closed = false;
|
|
31
|
+
constructor(python, env) {
|
|
32
|
+
this.proc = spawn(python, ["-m", "tracekit.bridge"], { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...env } });
|
|
33
|
+
let readyResolve;
|
|
34
|
+
let readyReject;
|
|
35
|
+
this.ready = new Promise((res, rej) => { readyResolve = res; readyReject = rej; });
|
|
36
|
+
let stderr = "";
|
|
37
|
+
this.proc.stderr.on("data", (d) => { stderr = (stderr + d.toString()).slice(-4000); process.stderr.write(d); });
|
|
38
|
+
this.proc.on("error", (e) => readyReject(new Error(`could not start the Tracekit bridge (${python}): ${e.message}`)));
|
|
39
|
+
this.proc.on("exit", (code) => {
|
|
40
|
+
this.closed = true;
|
|
41
|
+
const err = new BridgeClosed(`Tracekit bridge exited (code ${code}). ${stderr.trim().split("\n").slice(-3).join(" ")}`);
|
|
42
|
+
readyReject(err);
|
|
43
|
+
for (const p of this.pending.values())
|
|
44
|
+
p.reject(err);
|
|
45
|
+
this.pending.clear();
|
|
46
|
+
});
|
|
47
|
+
this.proc.stdin.on("error", (e) => {
|
|
48
|
+
this.closed = true;
|
|
49
|
+
const err = new BridgeClosed(`Tracekit bridge stopped reading requests (${e.message})`);
|
|
50
|
+
for (const p of this.pending.values())
|
|
51
|
+
p.reject(err);
|
|
52
|
+
this.pending.clear();
|
|
53
|
+
this.proc.kill();
|
|
54
|
+
});
|
|
55
|
+
createInterface({ input: this.proc.stdout }).on("line", (line) => {
|
|
56
|
+
let msg;
|
|
57
|
+
try {
|
|
58
|
+
msg = JSON.parse(line);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (msg.id === 0 && msg.ready)
|
|
64
|
+
return readyResolve();
|
|
65
|
+
const p = this.pending.get(msg.id);
|
|
66
|
+
if (!p)
|
|
67
|
+
return;
|
|
68
|
+
this.pending.delete(msg.id);
|
|
69
|
+
if (msg.ok)
|
|
70
|
+
p.resolve(msg);
|
|
71
|
+
else
|
|
72
|
+
p.reject(msg.denied ? new TracekitDenied(msg.error) : new Error(`Tracekit: ${msg.error}`));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async call(op, args, timeoutMs) {
|
|
76
|
+
const id = this.next++;
|
|
77
|
+
let timer;
|
|
78
|
+
const timeout = new Promise((_res, rej) => {
|
|
79
|
+
timer = setTimeout(() => {
|
|
80
|
+
this.pending.delete(id);
|
|
81
|
+
rej(new Error(`Tracekit: bridge request ${op} timed out after ${timeoutMs} ms`));
|
|
82
|
+
}, timeoutMs);
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
return await Promise.race([this.send(id, op, args, timeoutMs), timeout]);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async send(id, op, args, timeoutMs) {
|
|
92
|
+
await this.ready;
|
|
93
|
+
if (this.closed || !this.proc.stdin.writable)
|
|
94
|
+
throw new BridgeClosed("Tracekit bridge is closed");
|
|
95
|
+
const p = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
96
|
+
this.proc.stdin.write(JSON.stringify({ id, op, timeout_s: timeoutMs / 1000, ...args }) + "\n");
|
|
97
|
+
return p;
|
|
98
|
+
}
|
|
99
|
+
kill() {
|
|
100
|
+
this.proc.kill();
|
|
101
|
+
}
|
|
102
|
+
async close() {
|
|
103
|
+
if (this.closed)
|
|
104
|
+
return;
|
|
105
|
+
const done = new Promise((res) => this.proc.once("exit", () => res()));
|
|
106
|
+
this.proc.stdin.end();
|
|
107
|
+
await done;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
let deprecationWarned = false;
|
|
111
|
+
export class Tracekit {
|
|
112
|
+
bridge;
|
|
113
|
+
run;
|
|
114
|
+
failMode;
|
|
115
|
+
timeoutMs;
|
|
116
|
+
approvalTimeoutMs;
|
|
117
|
+
sessionId;
|
|
118
|
+
/** Recording failures after a wrapped call had already succeeded: reported on stderr, never thrown. */
|
|
119
|
+
recordFailures = 0;
|
|
120
|
+
warned = new Set();
|
|
121
|
+
constructor(bridge, run, sessionId, failMode, timeoutMs, approvalTimeoutMs) {
|
|
122
|
+
this.bridge = bridge;
|
|
123
|
+
this.run = run;
|
|
124
|
+
this.failMode = failMode;
|
|
125
|
+
this.timeoutMs = timeoutMs;
|
|
126
|
+
this.approvalTimeoutMs = approvalTimeoutMs;
|
|
127
|
+
this.sessionId = sessionId;
|
|
128
|
+
}
|
|
129
|
+
/** Start a run. One Tracekit per run; call end() when the agent is done. */
|
|
130
|
+
static async start(opts = {}) {
|
|
131
|
+
if (!deprecationWarned && !process.env.TRACEKIT_NO_DEPRECATION) {
|
|
132
|
+
deprecationWarned = true;
|
|
133
|
+
process.emitWarning("@cygnuxlabs/tracekit/v1 (the Python bridge) is deprecated and removed in 0.5.0: use the native v2 client, " +
|
|
134
|
+
"import { Client } from \"@cygnuxlabs/tracekit\" (README: Migrating from v1).", { type: "DeprecationWarning", code: "TRACEKIT_V1" });
|
|
135
|
+
}
|
|
136
|
+
const python = opts.python ?? process.env.TRACEKIT_PYTHON ?? "python3";
|
|
137
|
+
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
138
|
+
const bridge = new Bridge(python, opts.env);
|
|
139
|
+
let r;
|
|
140
|
+
try {
|
|
141
|
+
r = await bridge.call("start", { agent: opts.agent ?? "ts-agent", session_id: opts.sessionId, cwd: opts.cwd ?? process.cwd() }, timeoutMs);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
bridge.kill();
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
return new Tracekit(bridge, r.run, r.session_id, r.fail_mode, timeoutMs, opts.approvalTimeoutMs ?? 3_660_000);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* One bridge request. If the bridge has exited, the run's fail mode decides: open returns null (the caller proceeds
|
|
151
|
+
* unrecorded, with a warning), closed refuses with TracekitDenied.
|
|
152
|
+
*/
|
|
153
|
+
async call(op, args, timeoutMs = this.timeoutMs) {
|
|
154
|
+
try {
|
|
155
|
+
return await this.bridge.call(op, args, timeoutMs);
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
if (!(e instanceof BridgeClosed))
|
|
159
|
+
throw e;
|
|
160
|
+
if (this.failMode === "closed")
|
|
161
|
+
throw new TracekitDenied(`${e.message}; fail_mode=closed refuses the call`);
|
|
162
|
+
this.warnOnce("closed", `${e.message}; fail_mode=open, continuing unrecorded`);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A record that gates nothing (model_begin, prompt, say, think): under fail_mode=open a timeout or bridge error lets
|
|
168
|
+
* the caller proceed unrecorded, as the Python SDK does. tool_begin never goes through here: a timeout there can be
|
|
169
|
+
* an unanswered approval hold.
|
|
170
|
+
*/
|
|
171
|
+
async record(op, args) {
|
|
172
|
+
try {
|
|
173
|
+
return await this.call(op, args);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (e instanceof TracekitDenied || this.failMode === "closed")
|
|
177
|
+
throw e;
|
|
178
|
+
this.warnOnce(op, `${op} not recorded (${e?.message ?? e}); fail_mode=open, continuing unrecorded`);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
warnOnce(kind, message) {
|
|
183
|
+
if (this.warned.has(kind))
|
|
184
|
+
return;
|
|
185
|
+
this.warned.add(kind);
|
|
186
|
+
process.stderr.write(`[tracekit] ${message}\n`);
|
|
187
|
+
}
|
|
188
|
+
/** Record after the wrapped call succeeded: a failure here is reported, never thrown over the call's result. */
|
|
189
|
+
async settle(p) {
|
|
190
|
+
try {
|
|
191
|
+
await p;
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
this.recordFailures++;
|
|
195
|
+
process.stderr.write(`[tracekit] could not record a completed call: ${e?.message ?? e}\n`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
prompt(text) { return this.record("prompt", { run: this.run, text }).then(() => undefined); }
|
|
199
|
+
say(text) { return this.record("say", { run: this.run, text }).then(() => undefined); }
|
|
200
|
+
think(text) { return this.record("think", { run: this.run, text }).then(() => undefined); }
|
|
201
|
+
/**
|
|
202
|
+
* Run a tool through the policy gate. The policy decision is made and signed before `fn` runs; a denied (or held and
|
|
203
|
+
* not approved) call throws TracekitDenied and `fn` never runs. The result or error is recorded afterwards.
|
|
204
|
+
* Pass the model's tool-call id as toolUseId to link the execution to the model request that asked for it.
|
|
205
|
+
*/
|
|
206
|
+
async tool(name, args, fn, opts = {}) {
|
|
207
|
+
const c = await this.call("tool_begin", { run: this.run, name, args, tool_use_id: opts.toolUseId }, this.approvalTimeoutMs);
|
|
208
|
+
if (c === null)
|
|
209
|
+
return fn();
|
|
210
|
+
let result;
|
|
211
|
+
try {
|
|
212
|
+
result = await fn();
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
await this.settle(this.call("tool_end", { call: c.call, error: String(e?.stack ?? e) }));
|
|
216
|
+
throw e;
|
|
217
|
+
}
|
|
218
|
+
await this.settle(this.call("tool_end", { call: c.call, result: toJSON(result) }));
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
/** Low level: record a model call yourself. begin() is written before the request is sent. Null: not recorded. */
|
|
222
|
+
async modelBegin(provider, operation, model, request, streamed = false) {
|
|
223
|
+
const r = await this.record("model_begin", { run: this.run, provider, operation, model, request: toJSON(request), streamed });
|
|
224
|
+
return r === null ? null : r.exchange;
|
|
225
|
+
}
|
|
226
|
+
async modelEnd(exchange, end) {
|
|
227
|
+
if (exchange === null)
|
|
228
|
+
return;
|
|
229
|
+
await this.call("model_end", { exchange, ...end, response: toJSON(end.response) });
|
|
230
|
+
}
|
|
231
|
+
async end(reason = "done") {
|
|
232
|
+
try {
|
|
233
|
+
await this.call("end", { run: this.run, reason });
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
await this.bridge.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function toJSON(v) {
|
|
241
|
+
if (v === undefined)
|
|
242
|
+
return null;
|
|
243
|
+
try {
|
|
244
|
+
return JSON.parse(JSON.stringify(v, (_k, x) => (typeof x === "bigint" ? x.toString() : x)));
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return String(v);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// ------------------------------------------------------------------ usage normalisation (same buckets as tracekit/usage.py)
|
|
251
|
+
export function usageFromOpenAI(u) {
|
|
252
|
+
if (!u)
|
|
253
|
+
return null;
|
|
254
|
+
if (u.prompt_tokens != null || u.completion_tokens != null) {
|
|
255
|
+
const cached = u.prompt_tokens_details?.cached_tokens ?? 0;
|
|
256
|
+
return { input_tokens: Math.max(0, (u.prompt_tokens ?? 0) - cached), output_tokens: u.completion_tokens ?? 0,
|
|
257
|
+
cache_read_tokens: cached || null, reasoning_tokens: u.completion_tokens_details?.reasoning_tokens ?? null };
|
|
258
|
+
}
|
|
259
|
+
if (u.input_tokens != null) {
|
|
260
|
+
const cached = u.input_tokens_details?.cached_tokens ?? 0;
|
|
261
|
+
return { input_tokens: Math.max(0, u.input_tokens - cached), output_tokens: u.output_tokens ?? 0, cache_read_tokens: cached || null,
|
|
262
|
+
reasoning_tokens: u.output_tokens_details?.reasoning_tokens ?? null };
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
export function usageFromAnthropic(u) {
|
|
267
|
+
if (!u || (u.input_tokens == null && u.output_tokens == null))
|
|
268
|
+
return null;
|
|
269
|
+
return { input_tokens: u.input_tokens ?? 0, output_tokens: u.output_tokens ?? 0, cache_read_tokens: u.cache_read_input_tokens ?? null,
|
|
270
|
+
cache_write_tokens: u.cache_creation_input_tokens ?? null };
|
|
271
|
+
}
|
|
272
|
+
function mergeUsage(a, b) {
|
|
273
|
+
if (!a)
|
|
274
|
+
return b;
|
|
275
|
+
if (!b)
|
|
276
|
+
return a;
|
|
277
|
+
const out = { ...a };
|
|
278
|
+
for (const [k, v] of Object.entries(b))
|
|
279
|
+
if (v)
|
|
280
|
+
out[k] = v;
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
// ------------------------------------------------------------------ provider instrumentation
|
|
284
|
+
function isAsyncIterable(x) {
|
|
285
|
+
return x != null && typeof x[Symbol.asyncIterator] === "function";
|
|
286
|
+
}
|
|
287
|
+
/** Wrap a stream so the response is recorded when it ends, errors, or is abandoned (return()). */
|
|
288
|
+
function wrapStream(stream, onItem, finish) {
|
|
289
|
+
const origIter = stream[Symbol.asyncIterator].bind(stream);
|
|
290
|
+
let done = false;
|
|
291
|
+
const end = async (err) => { if (!done) {
|
|
292
|
+
done = true;
|
|
293
|
+
await finish(err);
|
|
294
|
+
} };
|
|
295
|
+
stream[Symbol.asyncIterator] = () => {
|
|
296
|
+
const it = origIter();
|
|
297
|
+
return {
|
|
298
|
+
async next() {
|
|
299
|
+
try {
|
|
300
|
+
const r = await it.next();
|
|
301
|
+
if (r.done)
|
|
302
|
+
await end();
|
|
303
|
+
else
|
|
304
|
+
onItem(r.value);
|
|
305
|
+
return r;
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
await end(String(e?.message ?? e));
|
|
309
|
+
throw e;
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
async return(v) { await end("stream abandoned"); return it.return ? it.return(v) : { done: true, value: v }; },
|
|
313
|
+
async throw(e) { await end(String(e)); if (it.throw)
|
|
314
|
+
return it.throw(e); throw e; },
|
|
315
|
+
[Symbol.asyncIterator]() { return this; },
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
return stream;
|
|
319
|
+
}
|
|
320
|
+
function statusOf(e) {
|
|
321
|
+
const s = e?.status ?? e?.statusCode;
|
|
322
|
+
return typeof s === "number" ? s : null;
|
|
323
|
+
}
|
|
324
|
+
/** OpenAI SDK: chat.completions.create and responses.create, streaming included. Returns the same client. */
|
|
325
|
+
export function instrumentOpenAI(client, tk) {
|
|
326
|
+
const wrap = (holder, method, operation) => {
|
|
327
|
+
if (!holder?.[method] || holder[method].__tracekit)
|
|
328
|
+
return;
|
|
329
|
+
const orig = holder[method].bind(holder);
|
|
330
|
+
const wrapped = async (body, options) => {
|
|
331
|
+
const t0 = Date.now();
|
|
332
|
+
const x = await tk.modelBegin("openai", operation, body?.model ?? null, body, !!body?.stream);
|
|
333
|
+
let out;
|
|
334
|
+
try {
|
|
335
|
+
out = await orig(body, options);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
await tk.settle(tk.modelEnd(x, { error: String(e?.message ?? e), status: statusOf(e) }));
|
|
339
|
+
throw e;
|
|
340
|
+
}
|
|
341
|
+
if (body?.stream && isAsyncIterable(out)) {
|
|
342
|
+
const tools = new Map(), byIndex = new Map();
|
|
343
|
+
let model = null, stop = null, usage = null, first = null, text = "";
|
|
344
|
+
return wrapStream(out, (ch) => {
|
|
345
|
+
first ??= Date.now() - t0;
|
|
346
|
+
model ??= ch.model ?? ch.response?.model ?? null;
|
|
347
|
+
usage = mergeUsage(usage, usageFromOpenAI(ch.usage ?? ch.response?.usage));
|
|
348
|
+
for (const c of ch.choices ?? []) {
|
|
349
|
+
if (c.finish_reason)
|
|
350
|
+
stop = c.finish_reason;
|
|
351
|
+
if (typeof c.delta?.content === "string" && text.length < 1_000_000)
|
|
352
|
+
text += c.delta.content;
|
|
353
|
+
for (const tc of c.delta?.tool_calls ?? []) {
|
|
354
|
+
if (tc.id)
|
|
355
|
+
byIndex.set(tc.index ?? 0, tc.id);
|
|
356
|
+
const id = tc.id ?? byIndex.get(tc.index ?? 0);
|
|
357
|
+
if (id && tc.function?.name)
|
|
358
|
+
tools.set(id, tc.function.name);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (ch.type === "response.output_item.added" && ch.item?.type === "function_call")
|
|
362
|
+
tools.set(ch.item.call_id ?? ch.item.id, ch.item.name);
|
|
363
|
+
if (ch.type === "response.completed")
|
|
364
|
+
stop = ch.response?.status ?? stop;
|
|
365
|
+
}, (err) => tk.settle(tk.modelEnd(x, { response: { text }, error: err ?? null, model, stop_reason: stop, usage, first_byte_ms: first,
|
|
366
|
+
tool_uses: [...tools].map(([id, name]) => ({ id, name })) })));
|
|
367
|
+
}
|
|
368
|
+
const tools = [];
|
|
369
|
+
for (const c of out?.choices ?? [])
|
|
370
|
+
for (const tc of c.message?.tool_calls ?? [])
|
|
371
|
+
tools.push({ id: tc.id, name: tc.function?.name ?? tc.type });
|
|
372
|
+
for (const it of out?.output ?? [])
|
|
373
|
+
if (it.type === "function_call")
|
|
374
|
+
tools.push({ id: it.call_id ?? it.id, name: it.name });
|
|
375
|
+
await tk.settle(tk.modelEnd(x, { response: out, model: out?.model ?? null, stop_reason: out?.choices?.[0]?.finish_reason ?? out?.status ?? null,
|
|
376
|
+
usage: usageFromOpenAI(out?.usage), tool_uses: tools, status: 200 }));
|
|
377
|
+
return out;
|
|
378
|
+
};
|
|
379
|
+
wrapped.__tracekit = true;
|
|
380
|
+
holder[method] = wrapped;
|
|
381
|
+
};
|
|
382
|
+
wrap(client.chat?.completions, "create", "chat");
|
|
383
|
+
wrap(client.responses, "create", "responses");
|
|
384
|
+
return client;
|
|
385
|
+
}
|
|
386
|
+
/** Anthropic SDK: messages.create, streaming (stream: true) included. Returns the same client. */
|
|
387
|
+
export function instrumentAnthropic(client, tk) {
|
|
388
|
+
const holder = client.messages;
|
|
389
|
+
if (!holder?.create || holder.create.__tracekit)
|
|
390
|
+
return client;
|
|
391
|
+
const orig = holder.create.bind(holder);
|
|
392
|
+
const wrapped = async (body, options) => {
|
|
393
|
+
const t0 = Date.now();
|
|
394
|
+
const x = await tk.modelBegin("anthropic", "messages", body?.model ?? null, body, !!body?.stream);
|
|
395
|
+
let out;
|
|
396
|
+
try {
|
|
397
|
+
out = await orig(body, options);
|
|
398
|
+
}
|
|
399
|
+
catch (e) {
|
|
400
|
+
await tk.settle(tk.modelEnd(x, { error: String(e?.message ?? e), status: statusOf(e) }));
|
|
401
|
+
throw e;
|
|
402
|
+
}
|
|
403
|
+
if (body?.stream && isAsyncIterable(out)) {
|
|
404
|
+
const tools = [];
|
|
405
|
+
let model = null, stop = null, usage = null, first = null, text = "";
|
|
406
|
+
return wrapStream(out, (ev) => {
|
|
407
|
+
first ??= Date.now() - t0;
|
|
408
|
+
if (ev.type === "message_start") {
|
|
409
|
+
model = ev.message?.model ?? null;
|
|
410
|
+
usage = mergeUsage(usage, usageFromAnthropic(ev.message?.usage));
|
|
411
|
+
}
|
|
412
|
+
if (ev.type === "content_block_start" && ["tool_use", "server_tool_use", "mcp_tool_use"].includes(ev.content_block?.type))
|
|
413
|
+
tools.push({ id: ev.content_block.id, name: ev.content_block.name });
|
|
414
|
+
if (ev.type === "content_block_delta" && typeof ev.delta?.text === "string" && text.length < 1_000_000)
|
|
415
|
+
text += ev.delta.text;
|
|
416
|
+
if (ev.type === "message_delta") {
|
|
417
|
+
stop = ev.delta?.stop_reason ?? stop;
|
|
418
|
+
usage = mergeUsage(usage, usageFromAnthropic(ev.usage));
|
|
419
|
+
}
|
|
420
|
+
}, (err) => tk.settle(tk.modelEnd(x, { response: { text }, error: err ?? null, model, stop_reason: stop, usage, first_byte_ms: first, tool_uses: tools })));
|
|
421
|
+
}
|
|
422
|
+
const tools = (out?.content ?? []).filter((b) => ["tool_use", "server_tool_use", "mcp_tool_use"].includes(b.type))
|
|
423
|
+
.map((b) => ({ id: b.id, name: b.name }));
|
|
424
|
+
await tk.settle(tk.modelEnd(x, { response: out, model: out?.model ?? null, stop_reason: out?.stop_reason ?? null,
|
|
425
|
+
usage: usageFromAnthropic(out?.usage), tool_uses: tools, status: 200 }));
|
|
426
|
+
return out;
|
|
427
|
+
};
|
|
428
|
+
wrapped.__tracekit = true;
|
|
429
|
+
holder.create = wrapped;
|
|
430
|
+
return client;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Vercel AI SDK language-model middleware (use with wrapLanguageModel({ model, middleware: tracekitMiddleware(tk) })).
|
|
434
|
+
* Works with the v4 (promptTokens/toolCalls) and v5 (inputTokens/content parts) result shapes.
|
|
435
|
+
*/
|
|
436
|
+
export function tracekitMiddleware(tk, provider = "ai-sdk") {
|
|
437
|
+
const usageOf = (u) => {
|
|
438
|
+
if (!u)
|
|
439
|
+
return null;
|
|
440
|
+
const inp = u.inputTokens ?? u.promptTokens, out = u.outputTokens ?? u.completionTokens;
|
|
441
|
+
if (inp == null && out == null)
|
|
442
|
+
return null;
|
|
443
|
+
const cached = u.cachedInputTokens ?? 0;
|
|
444
|
+
return { input_tokens: Math.max(0, (inp ?? 0) - cached), output_tokens: out ?? 0, cache_read_tokens: cached || null,
|
|
445
|
+
reasoning_tokens: u.reasoningTokens ?? null };
|
|
446
|
+
};
|
|
447
|
+
const toolsOf = (r) => {
|
|
448
|
+
const t = [];
|
|
449
|
+
for (const c of r?.toolCalls ?? [])
|
|
450
|
+
t.push({ id: c.toolCallId, name: c.toolName });
|
|
451
|
+
for (const c of r?.content ?? [])
|
|
452
|
+
if (c?.type === "tool-call")
|
|
453
|
+
t.push({ id: c.toolCallId, name: c.toolName });
|
|
454
|
+
return t;
|
|
455
|
+
};
|
|
456
|
+
return {
|
|
457
|
+
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
458
|
+
const x = await tk.modelBegin(model?.provider ?? provider, "generate", model?.modelId ?? null, params, false);
|
|
459
|
+
let r;
|
|
460
|
+
try {
|
|
461
|
+
r = await doGenerate();
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
await tk.settle(tk.modelEnd(x, { error: String(e?.message ?? e), status: statusOf(e) }));
|
|
465
|
+
throw e;
|
|
466
|
+
}
|
|
467
|
+
const fr = r?.finishReason;
|
|
468
|
+
await tk.settle(tk.modelEnd(x, { response: { text: r?.text, content: r?.content }, model: model?.modelId ?? null,
|
|
469
|
+
stop_reason: typeof fr === "string" ? fr : fr?.unified ?? null, usage: usageOf(r?.usage), tool_uses: toolsOf(r), status: 200 }));
|
|
470
|
+
return r;
|
|
471
|
+
},
|
|
472
|
+
wrapStream: async ({ doStream, params, model }) => {
|
|
473
|
+
const t0 = Date.now();
|
|
474
|
+
const x = await tk.modelBegin(model?.provider ?? provider, "stream", model?.modelId ?? null, params, true);
|
|
475
|
+
let res;
|
|
476
|
+
try {
|
|
477
|
+
res = await doStream();
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
await tk.settle(tk.modelEnd(x, { error: String(e?.message ?? e), status: statusOf(e) }));
|
|
481
|
+
throw e;
|
|
482
|
+
}
|
|
483
|
+
const tools = [];
|
|
484
|
+
let usage = null, stop = null, first = null, text = "", ended = false;
|
|
485
|
+
const finish = async (err) => {
|
|
486
|
+
if (ended)
|
|
487
|
+
return;
|
|
488
|
+
ended = true;
|
|
489
|
+
await tk.settle(tk.modelEnd(x, { response: { text }, error: err ?? null, model: model?.modelId ?? null, stop_reason: stop, usage, first_byte_ms: first, tool_uses: tools }));
|
|
490
|
+
};
|
|
491
|
+
const transform = new TransformStream({
|
|
492
|
+
transform(part, controller) {
|
|
493
|
+
first ??= Date.now() - t0;
|
|
494
|
+
if (part?.type === "tool-call")
|
|
495
|
+
tools.push({ id: part.toolCallId, name: part.toolName });
|
|
496
|
+
if ((part?.type === "text-delta" || part?.type === "text") && text.length < 1_000_000)
|
|
497
|
+
text += part.textDelta ?? part.delta ?? part.text ?? "";
|
|
498
|
+
if (part?.type === "finish") {
|
|
499
|
+
usage = usageOf(part.usage);
|
|
500
|
+
const fr = part.finishReason;
|
|
501
|
+
stop = typeof fr === "string" ? fr : fr?.unified ?? null;
|
|
502
|
+
}
|
|
503
|
+
if (part?.type === "error")
|
|
504
|
+
void finish(String(part.error));
|
|
505
|
+
controller.enqueue(part);
|
|
506
|
+
},
|
|
507
|
+
async flush() { await finish(); },
|
|
508
|
+
});
|
|
509
|
+
return { ...res, stream: res.stream.pipeThrough(transform) };
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
// ------------------------------------------------------------------ browser agents
|
|
514
|
+
/**
|
|
515
|
+
* Stagehand: wrap page/stagehand methods (act, extract, observe, goto by default) so every browser action is a
|
|
516
|
+
* policy-checked, signed `browser:<method>` step. A denied action throws TracekitDenied and never reaches the browser.
|
|
517
|
+
* Works on any object with those methods (Stagehand v2 `page`, or the v3 `stagehand` instance). Returns the same object.
|
|
518
|
+
*/
|
|
519
|
+
export function instrumentStagehand(page, tk, methods = ["act", "extract", "observe", "goto"]) {
|
|
520
|
+
for (const m of methods) {
|
|
521
|
+
const original = page[m];
|
|
522
|
+
if (typeof original !== "function" || original.__tracekit)
|
|
523
|
+
continue;
|
|
524
|
+
const wrapped = function (...args) {
|
|
525
|
+
const first = args[0];
|
|
526
|
+
const recorded = { input: typeof first === "object" && first !== null ? toJSON(first) : first ?? null };
|
|
527
|
+
if (args.length > 1)
|
|
528
|
+
recorded.options = toJSON(args[1]);
|
|
529
|
+
return tk.tool(`browser:${m}`, recorded, () => original.apply(this ?? page, args));
|
|
530
|
+
};
|
|
531
|
+
wrapped.__tracekit = true;
|
|
532
|
+
page[m] = wrapped;
|
|
533
|
+
}
|
|
534
|
+
return page;
|
|
535
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type RunHandle } from "../client.js";
|
|
2
|
+
export declare const HOOK_TIMEOUT_S = 600;
|
|
3
|
+
export declare const APPROVAL_WAIT_MS = 540000;
|
|
4
|
+
/** The `hooks` option of `query()`. To add hooks of your own, extend the lists. */
|
|
5
|
+
export declare function tracekitHooks(run: RunHandle): {
|
|
6
|
+
PreToolUse: {
|
|
7
|
+
hooks: ((inp: any, toolUseID?: string) => Promise<{
|
|
8
|
+
hookSpecificOutput: {
|
|
9
|
+
hookEventName: string;
|
|
10
|
+
permissionDecision: string;
|
|
11
|
+
permissionDecisionReason: string;
|
|
12
|
+
};
|
|
13
|
+
} | {
|
|
14
|
+
hookSpecificOutput?: undefined;
|
|
15
|
+
} | {
|
|
16
|
+
hookSpecificOutput: {
|
|
17
|
+
hookEventName: string;
|
|
18
|
+
updatedInput: unknown;
|
|
19
|
+
};
|
|
20
|
+
}>)[];
|
|
21
|
+
timeout: number;
|
|
22
|
+
}[];
|
|
23
|
+
PostToolUse: {
|
|
24
|
+
hooks: ((inp: any, toolUseID?: string) => Promise<{}>)[];
|
|
25
|
+
}[];
|
|
26
|
+
PostToolUseFailure: {
|
|
27
|
+
hooks: ((inp: any, toolUseID?: string) => Promise<{}>)[];
|
|
28
|
+
}[];
|
|
29
|
+
SessionEnd: {
|
|
30
|
+
hooks: ((inp: any) => Promise<{}>)[];
|
|
31
|
+
}[];
|
|
32
|
+
};
|
|
33
|
+
type Key = {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
subpath?: string;
|
|
36
|
+
};
|
|
37
|
+
type Store = {
|
|
38
|
+
append(key: Key, entries: unknown[]): Promise<void>;
|
|
39
|
+
load(key: Key): Promise<unknown[] | null>;
|
|
40
|
+
};
|
|
41
|
+
/** A SessionStore that commits each saved transcript to the signer (L1): after every append a `state_write` of the
|
|
42
|
+
* transcript's digest, from the digest it had when this process last saw it. A transcript changed in the store
|
|
43
|
+
* between two writes (seen on `load`, when a session resumes) makes the signer record a `state_tamper` gap. */
|
|
44
|
+
export declare function tracekitSessionStore<S extends Store>(store: S, run: RunHandle): S;
|
|
45
|
+
export {};
|