kindred-tracer-node 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/dist/context.d.ts +18 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +26 -0
- package/dist/exporter.d.ts +14 -0
- package/dist/exporter.d.ts.map +1 -0
- package/dist/exporter.js +228 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/interceptor.d.ts +9 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/interceptor.js +280 -0
- package/dist/parser.d.ts +21 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +47 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# kindred-tracer-node
|
|
2
|
+
|
|
3
|
+
Kindred Tracer SDK for Node.js - Auto-instrumentation for AI agents.
|
|
4
|
+
|
|
5
|
+
This package automatically intercepts HTTP/HTTPS requests from your AI agent, categorizes them as LLM calls or tool executions, and exports logs to the Kindred log-search system.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install kindred-tracer-node
|
|
11
|
+
# or
|
|
12
|
+
pnpm add kindred-tracer-node
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Basic Usage
|
|
18
|
+
|
|
19
|
+
Just call `kindredTracer()` once at startup, and all HTTP requests will be automatically intercepted and logged:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { kindredTracer } from 'kindred-tracer-node';
|
|
23
|
+
|
|
24
|
+
// At startup - initialize the tracer
|
|
25
|
+
kindredTracer();
|
|
26
|
+
|
|
27
|
+
// Your agent code here - no wrapping needed!
|
|
28
|
+
// All HTTP requests will be automatically logged
|
|
29
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
body: JSON.stringify({ /* ... */ })
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
Set the following environment variables:
|
|
38
|
+
|
|
39
|
+
- `KINDRED_API_KEY` (required) - Your Kindred API key for authentication
|
|
40
|
+
- `KINDRED_API_URL` (optional) - Base URL for Kindred API, defaults to `https://api.usekindred.dev`
|
|
41
|
+
- `KINDRED_SESSION_ID` (optional) - Session identifier. If not set, a UUID will be auto-generated
|
|
42
|
+
- `KINDRED_AGENT_ID` (optional) - Agent identifier
|
|
43
|
+
- `KINDRED_RUN_ID` (optional) - Run identifier
|
|
44
|
+
|
|
45
|
+
You can also pass these values directly to `kindredTracer()`:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { kindredTracer } from 'kindred-tracer-node';
|
|
49
|
+
|
|
50
|
+
// Initialize with explicit values
|
|
51
|
+
kindredTracer('session-123', 'agent-456', 'run-789');
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## How It Works
|
|
55
|
+
|
|
56
|
+
1. **Simple Initialization**: Call `kindredTracer()` once at startup to set up global context and enable interception.
|
|
57
|
+
|
|
58
|
+
2. **Auto-instrumentation**: The tracer automatically patches Node.js's `https.request` and `http.request` when initialized.
|
|
59
|
+
|
|
60
|
+
3. **Global Context**: Uses a global context that applies to all HTTP requests after initialization.
|
|
61
|
+
|
|
62
|
+
4. **Request Detection**:
|
|
63
|
+
- **LLM Calls**: Detected by hostname (e.g., `api.openai.com`, `api.anthropic.com`) → logged as `role: "agent"`
|
|
64
|
+
- **Tool Calls**: Any other hostname → logged as `role: "tool"`
|
|
65
|
+
|
|
66
|
+
5. **Streaming Support**: Handles streaming responses correctly - chunks are passed through immediately (zero latency) while being buffered for logging.
|
|
67
|
+
|
|
68
|
+
6. **Non-blocking Export**: Logs are batched and exported asynchronously to avoid slowing down your agent.
|
|
69
|
+
|
|
70
|
+
## Log Format
|
|
71
|
+
|
|
72
|
+
Logs are automatically formatted and sent to `${KINDRED_API_URL}/api/logs/ingest` with the following structure:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
{
|
|
76
|
+
session_id: string;
|
|
77
|
+
timestamp: string; // ISO 8601
|
|
78
|
+
role: "user" | "agent" | "tool" | "system";
|
|
79
|
+
content: string;
|
|
80
|
+
agent_id?: string;
|
|
81
|
+
run_id?: string;
|
|
82
|
+
meta?: {
|
|
83
|
+
type: "llm_generation" | "tool_execution";
|
|
84
|
+
request_id: string;
|
|
85
|
+
host: string;
|
|
86
|
+
method: string;
|
|
87
|
+
path: string;
|
|
88
|
+
request_headers: Record<string, unknown>;
|
|
89
|
+
request_body: string | null;
|
|
90
|
+
response_status: number;
|
|
91
|
+
response_headers: Record<string, unknown>;
|
|
92
|
+
response_body: string | null;
|
|
93
|
+
duration_ms: number;
|
|
94
|
+
tool_calls?: Array<{...}>; // Extracted from OpenAI responses
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Flushing Logs
|
|
100
|
+
|
|
101
|
+
Before shutting down your application, you can flush any pending logs:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { flushLogs } from 'kindred-tracer-node';
|
|
105
|
+
|
|
106
|
+
// On shutdown
|
|
107
|
+
await flushLogs();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Security
|
|
111
|
+
|
|
112
|
+
The tracer automatically sanitizes sensitive headers before logging:
|
|
113
|
+
- `Authorization`
|
|
114
|
+
- `x-api-key`
|
|
115
|
+
- `api-key`
|
|
116
|
+
- `x-auth-token`
|
|
117
|
+
- `cookie`
|
|
118
|
+
|
|
119
|
+
## Supported LLM Providers
|
|
120
|
+
|
|
121
|
+
The tracer automatically detects requests to:
|
|
122
|
+
- OpenAI (`api.openai.com`)
|
|
123
|
+
- Anthropic (`api.anthropic.com`)
|
|
124
|
+
- Google Gemini (`generativelanguage.googleapis.com`)
|
|
125
|
+
- Cohere (`api.cohere.com`)
|
|
126
|
+
- Mistral (`api.mistral.ai`)
|
|
127
|
+
|
|
128
|
+
## Example
|
|
129
|
+
|
|
130
|
+
Here's a complete example:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { kindredTracer, flushLogs } from 'kindred-tracer-node';
|
|
134
|
+
|
|
135
|
+
// Set your API key
|
|
136
|
+
process.env.KINDRED_API_KEY = 'your-api-key-here';
|
|
137
|
+
|
|
138
|
+
// Initialize the tracer (reads sessionId from KINDRED_SESSION_ID env var, or auto-generates)
|
|
139
|
+
kindredTracer();
|
|
140
|
+
|
|
141
|
+
// Your agent code - all HTTP requests are automatically logged
|
|
142
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
model: 'gpt-4',
|
|
147
|
+
messages: [{ role: 'user', content: 'Hello!' }]
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Before shutdown, flush any pending logs
|
|
152
|
+
await flushLogs();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context management using global context for simple initialization pattern
|
|
3
|
+
*/
|
|
4
|
+
export interface TraceContext {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
agentId?: string;
|
|
7
|
+
runId?: string;
|
|
8
|
+
traceId: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Get the current trace context
|
|
12
|
+
*/
|
|
13
|
+
export declare function getContext(): TraceContext | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Set the global trace context
|
|
16
|
+
*/
|
|
17
|
+
export declare function setGlobalContext(sessionId: string, agentId?: string, runId?: string, traceId?: string): void;
|
|
18
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD;;GAEG;AACH,wBAAgB,UAAU,IAAI,YAAY,GAAG,SAAS,CAErD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,GACf,IAAI,CAON"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Context management using global context for simple initialization pattern
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getContext = getContext;
|
|
7
|
+
exports.setGlobalContext = setGlobalContext;
|
|
8
|
+
// Global context storage
|
|
9
|
+
let globalContext = undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Get the current trace context
|
|
12
|
+
*/
|
|
13
|
+
function getContext() {
|
|
14
|
+
return globalContext;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Set the global trace context
|
|
18
|
+
*/
|
|
19
|
+
function setGlobalContext(sessionId, agentId, runId, traceId) {
|
|
20
|
+
globalContext = {
|
|
21
|
+
sessionId,
|
|
22
|
+
agentId,
|
|
23
|
+
runId,
|
|
24
|
+
traceId: traceId || sessionId, // Use sessionId as traceId if not provided
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking log exporter to Kindred API
|
|
3
|
+
*/
|
|
4
|
+
import type { LogEntry } from "./types";
|
|
5
|
+
/**
|
|
6
|
+
* Add a log entry to the buffer
|
|
7
|
+
*/
|
|
8
|
+
export declare function addLog(log: LogEntry): void;
|
|
9
|
+
/**
|
|
10
|
+
* Flush any remaining logs (call on shutdown)
|
|
11
|
+
* Waits for all pending logs to be sent
|
|
12
|
+
*/
|
|
13
|
+
export declare function flush(): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=exporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAcxC;;GAEG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAgB1C;AA2KD;;;GAGG;AACH,wBAAsB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CA6C3C"}
|
package/dist/exporter.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Non-blocking log exporter to Kindred API
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.addLog = addLog;
|
|
10
|
+
exports.flush = flush;
|
|
11
|
+
const https_1 = __importDefault(require("https"));
|
|
12
|
+
const http_1 = __importDefault(require("http"));
|
|
13
|
+
const url_1 = require("url");
|
|
14
|
+
const API_URL = process.env.KINDRED_API_URL || "https://api.usekindred.dev";
|
|
15
|
+
const API_KEY = process.env.KINDRED_API_KEY;
|
|
16
|
+
// In-memory buffer for batching logs
|
|
17
|
+
const logBuffer = [];
|
|
18
|
+
const BATCH_SIZE = 5; // Send logs in smaller batches to avoid payload size limits
|
|
19
|
+
const FLUSH_INTERVAL_MS = 5000; // Flush every 5 seconds
|
|
20
|
+
const MAX_PAYLOAD_SIZE = 100 * 1024; // 100KB max payload size
|
|
21
|
+
let flushTimer = null;
|
|
22
|
+
/**
|
|
23
|
+
* Add a log entry to the buffer
|
|
24
|
+
*/
|
|
25
|
+
function addLog(log) {
|
|
26
|
+
logBuffer.push(log);
|
|
27
|
+
// Flush if buffer is full
|
|
28
|
+
if (logBuffer.length >= BATCH_SIZE) {
|
|
29
|
+
flushLogs().catch((error) => {
|
|
30
|
+
console.error("[kindred-tracer] Failed to flush logs:", error);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else if (!flushTimer) {
|
|
34
|
+
// Start flush timer if not already running
|
|
35
|
+
flushTimer = setTimeout(() => {
|
|
36
|
+
flushLogs().catch((error) => {
|
|
37
|
+
console.error("[kindred-tracer] Failed to flush logs:", error);
|
|
38
|
+
});
|
|
39
|
+
}, FLUSH_INTERVAL_MS);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Truncate large strings in log metadata to prevent payload size issues
|
|
44
|
+
*/
|
|
45
|
+
function truncateLogEntry(log) {
|
|
46
|
+
const MAX_STRING_LENGTH = 10000; // 10KB max per string field
|
|
47
|
+
if (!log.meta) {
|
|
48
|
+
return log;
|
|
49
|
+
}
|
|
50
|
+
const truncatedMeta = { ...log.meta };
|
|
51
|
+
// Truncate large strings in metadata
|
|
52
|
+
const truncateString = (str) => {
|
|
53
|
+
if (!str || typeof str !== "string")
|
|
54
|
+
return str || null;
|
|
55
|
+
if (str.length <= MAX_STRING_LENGTH)
|
|
56
|
+
return str;
|
|
57
|
+
return str.substring(0, MAX_STRING_LENGTH) + `... [truncated ${str.length - MAX_STRING_LENGTH} chars]`;
|
|
58
|
+
};
|
|
59
|
+
if (typeof truncatedMeta.request_body === "string") {
|
|
60
|
+
truncatedMeta.request_body = truncateString(truncatedMeta.request_body);
|
|
61
|
+
}
|
|
62
|
+
if (typeof truncatedMeta.response_body === "string") {
|
|
63
|
+
truncatedMeta.response_body = truncateString(truncatedMeta.response_body);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...log,
|
|
67
|
+
meta: truncatedMeta,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Flush logs to the API (non-blocking)
|
|
72
|
+
*/
|
|
73
|
+
function flushLogs() {
|
|
74
|
+
if (logBuffer.length === 0) {
|
|
75
|
+
if (flushTimer) {
|
|
76
|
+
clearTimeout(flushTimer);
|
|
77
|
+
flushTimer = null;
|
|
78
|
+
}
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
}
|
|
81
|
+
// Copy and clear buffer
|
|
82
|
+
const logsToSend = [...logBuffer];
|
|
83
|
+
logBuffer.length = 0;
|
|
84
|
+
if (flushTimer) {
|
|
85
|
+
clearTimeout(flushTimer);
|
|
86
|
+
flushTimer = null;
|
|
87
|
+
}
|
|
88
|
+
// Truncate large payloads
|
|
89
|
+
const truncatedLogs = logsToSend.map(truncateLogEntry);
|
|
90
|
+
// Send and return promise so caller can wait
|
|
91
|
+
return sendLogs(truncatedLogs).catch((error) => {
|
|
92
|
+
// Log error but don't throw - we don't want to break the agent
|
|
93
|
+
console.error("[kindred-tracer] Failed to send logs:", error);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Send logs to Kindred API
|
|
98
|
+
*/
|
|
99
|
+
async function sendLogs(logs) {
|
|
100
|
+
if (!API_KEY) {
|
|
101
|
+
console.warn("[kindred-tracer] KINDRED_API_KEY not set, skipping log export");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (logs.length === 0) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const ingestUrl = new url_1.URL(`${API_URL}/api/logs/ingest`);
|
|
108
|
+
const isHttps = ingestUrl.protocol === "https:";
|
|
109
|
+
const client = isHttps ? https_1.default : http_1.default;
|
|
110
|
+
// Try to stringify, but split into smaller batches if too large
|
|
111
|
+
let postData;
|
|
112
|
+
let logsToSend = logs;
|
|
113
|
+
try {
|
|
114
|
+
postData = JSON.stringify({ logs });
|
|
115
|
+
// If payload is too large, split into smaller batches
|
|
116
|
+
if (postData.length > MAX_PAYLOAD_SIZE && logs.length > 1) {
|
|
117
|
+
const batchSize = Math.max(1, Math.floor(logs.length / 2));
|
|
118
|
+
const firstBatch = logs.slice(0, batchSize);
|
|
119
|
+
const secondBatch = logs.slice(batchSize);
|
|
120
|
+
// Send first batch
|
|
121
|
+
await sendLogs(firstBatch);
|
|
122
|
+
// Send second batch
|
|
123
|
+
await sendLogs(secondBatch);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
// If stringify fails (too large), try sending fewer logs
|
|
129
|
+
if (logs.length > 1) {
|
|
130
|
+
const half = Math.floor(logs.length / 2);
|
|
131
|
+
await sendLogs(logs.slice(0, half));
|
|
132
|
+
await sendLogs(logs.slice(half));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// If even one log is too large, skip it
|
|
136
|
+
console.error("[kindred-tracer] Log entry too large to serialize, skipping");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const options = {
|
|
140
|
+
hostname: ingestUrl.hostname,
|
|
141
|
+
port: ingestUrl.port || (isHttps ? 443 : 80),
|
|
142
|
+
path: ingestUrl.pathname,
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
"Content-Length": Buffer.byteLength(postData),
|
|
147
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
148
|
+
},
|
|
149
|
+
timeout: 10000, // 10 second timeout
|
|
150
|
+
};
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const req = client.request(options, (res) => {
|
|
153
|
+
let responseData = "";
|
|
154
|
+
res.on("data", (chunk) => {
|
|
155
|
+
responseData += chunk;
|
|
156
|
+
});
|
|
157
|
+
res.on("end", () => {
|
|
158
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
159
|
+
if (process.env.KINDRED_TRACER_VERBOSE) {
|
|
160
|
+
console.log(`[kindred-tracer] Successfully sent ${logs.length} log(s) to API`);
|
|
161
|
+
}
|
|
162
|
+
resolve();
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const errorMsg = `API returned ${res.statusCode}: ${responseData || "unknown error"}`;
|
|
166
|
+
console.error(`[kindred-tracer] ${errorMsg}`);
|
|
167
|
+
reject(new Error(errorMsg));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
res.on("error", (error) => {
|
|
171
|
+
reject(error);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
req.on("error", (error) => {
|
|
175
|
+
reject(error);
|
|
176
|
+
});
|
|
177
|
+
req.on("timeout", () => {
|
|
178
|
+
req.destroy();
|
|
179
|
+
reject(new Error("Request timeout"));
|
|
180
|
+
});
|
|
181
|
+
req.write(postData);
|
|
182
|
+
req.end();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Flush any remaining logs (call on shutdown)
|
|
187
|
+
* Waits for all pending logs to be sent
|
|
188
|
+
*/
|
|
189
|
+
async function flush() {
|
|
190
|
+
// Clear any pending timer
|
|
191
|
+
if (flushTimer) {
|
|
192
|
+
clearTimeout(flushTimer);
|
|
193
|
+
flushTimer = null;
|
|
194
|
+
}
|
|
195
|
+
// Send all logs currently in buffer (don't wait for new ones)
|
|
196
|
+
if (logBuffer.length === 0) {
|
|
197
|
+
if (process.env.KINDRED_TRACER_VERBOSE) {
|
|
198
|
+
console.log("[kindred-tracer] No logs to flush");
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Copy buffer and clear it immediately
|
|
203
|
+
const logsToSend = [...logBuffer];
|
|
204
|
+
logBuffer.length = 0;
|
|
205
|
+
if (process.env.KINDRED_TRACER_VERBOSE) {
|
|
206
|
+
console.log(`[kindred-tracer] Flushing ${logsToSend.length} log(s)`);
|
|
207
|
+
}
|
|
208
|
+
if (logsToSend.length > 0) {
|
|
209
|
+
// Truncate and send with timeout
|
|
210
|
+
const truncatedLogs = logsToSend.map(truncateLogEntry);
|
|
211
|
+
try {
|
|
212
|
+
// Add a timeout wrapper to prevent hanging
|
|
213
|
+
await Promise.race([
|
|
214
|
+
sendLogs(truncatedLogs),
|
|
215
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Flush timeout after 15 seconds")), 15000)),
|
|
216
|
+
]);
|
|
217
|
+
if (process.env.KINDRED_TRACER_VERBOSE) {
|
|
218
|
+
console.log("[kindred-tracer] Flush completed successfully");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.error("[kindred-tracer] Failed to send logs during flush:", error.message || error);
|
|
223
|
+
// Don't throw - we want flush to complete even if send fails
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Give a moment for the HTTP request to complete
|
|
227
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
228
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kindred Tracer SDK for Node.js
|
|
3
|
+
* Auto-instrumentation for AI agents
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Initialize the Kindred tracer.
|
|
7
|
+
*
|
|
8
|
+
* Reads sessionId, agentId, and runId from environment variables or parameters.
|
|
9
|
+
* Auto-generates sessionId if not provided.
|
|
10
|
+
*
|
|
11
|
+
* @param sessionId - Session identifier. If not provided, reads from KINDRED_SESSION_ID env var.
|
|
12
|
+
* If still not set, auto-generates a UUID.
|
|
13
|
+
* @param agentId - Agent identifier. If not provided, reads from KINDRED_AGENT_ID env var.
|
|
14
|
+
* @param runId - Run identifier. If not provided, reads from KINDRED_RUN_ID env var.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { kindredTracer } from 'kindred-tracer-node';
|
|
19
|
+
*
|
|
20
|
+
* // At startup - reads from environment variables
|
|
21
|
+
* kindredTracer();
|
|
22
|
+
*
|
|
23
|
+
* // Or specify explicitly
|
|
24
|
+
* kindredTracer('session-123', 'agent-456');
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function kindredTracer(sessionId?: string, agentId?: string, runId?: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Flush any pending logs to the API.
|
|
30
|
+
* Call this before shutting down your application.
|
|
31
|
+
*/
|
|
32
|
+
export declare function flushLogs(): Promise<void>;
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAC3B,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAeN;AAED;;;GAGG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/C"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Kindred Tracer SDK for Node.js
|
|
4
|
+
* Auto-instrumentation for AI agents
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.kindredTracer = kindredTracer;
|
|
8
|
+
exports.flushLogs = flushLogs;
|
|
9
|
+
const uuid_1 = require("uuid");
|
|
10
|
+
const context_1 = require("./context");
|
|
11
|
+
const interceptor_1 = require("./interceptor");
|
|
12
|
+
const exporter_1 = require("./exporter");
|
|
13
|
+
// Track if interceptor has been initialized
|
|
14
|
+
let isInterceptorInitialized = false;
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the Kindred tracer.
|
|
17
|
+
*
|
|
18
|
+
* Reads sessionId, agentId, and runId from environment variables or parameters.
|
|
19
|
+
* Auto-generates sessionId if not provided.
|
|
20
|
+
*
|
|
21
|
+
* @param sessionId - Session identifier. If not provided, reads from KINDRED_SESSION_ID env var.
|
|
22
|
+
* If still not set, auto-generates a UUID.
|
|
23
|
+
* @param agentId - Agent identifier. If not provided, reads from KINDRED_AGENT_ID env var.
|
|
24
|
+
* @param runId - Run identifier. If not provided, reads from KINDRED_RUN_ID env var.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { kindredTracer } from 'kindred-tracer-node';
|
|
29
|
+
*
|
|
30
|
+
* // At startup - reads from environment variables
|
|
31
|
+
* kindredTracer();
|
|
32
|
+
*
|
|
33
|
+
* // Or specify explicitly
|
|
34
|
+
* kindredTracer('session-123', 'agent-456');
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function kindredTracer(sessionId, agentId, runId) {
|
|
38
|
+
// Read from parameters or environment variables
|
|
39
|
+
const finalSessionId = sessionId || process.env.KINDRED_SESSION_ID || (0, uuid_1.v4)();
|
|
40
|
+
const finalAgentId = agentId || process.env.KINDRED_AGENT_ID;
|
|
41
|
+
const finalRunId = runId || process.env.KINDRED_RUN_ID;
|
|
42
|
+
// Set global context
|
|
43
|
+
(0, context_1.setGlobalContext)(finalSessionId, finalAgentId, finalRunId);
|
|
44
|
+
// Initialize interceptor if not already done
|
|
45
|
+
if (!isInterceptorInitialized) {
|
|
46
|
+
(0, interceptor_1.initializeInterceptor)();
|
|
47
|
+
isInterceptorInitialized = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Flush any pending logs to the API.
|
|
52
|
+
* Call this before shutting down your application.
|
|
53
|
+
*/
|
|
54
|
+
async function flushLogs() {
|
|
55
|
+
return (0, exporter_1.flush)();
|
|
56
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP/HTTPS request interceptor
|
|
3
|
+
* Handles streaming responses correctly: passes through chunks immediately while buffering for logging
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Initialize the HTTP interceptor
|
|
7
|
+
*/
|
|
8
|
+
export declare function initializeInterceptor(): void;
|
|
9
|
+
//# sourceMappingURL=interceptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4EH;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAsB5C"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HTTP/HTTPS request interceptor
|
|
4
|
+
* Handles streaming responses correctly: passes through chunks immediately while buffering for logging
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.initializeInterceptor = initializeInterceptor;
|
|
11
|
+
const https_1 = __importDefault(require("https"));
|
|
12
|
+
const http_1 = __importDefault(require("http"));
|
|
13
|
+
const url_1 = require("url");
|
|
14
|
+
const context_1 = require("./context");
|
|
15
|
+
const exporter_1 = require("./exporter");
|
|
16
|
+
const parser_1 = require("./parser");
|
|
17
|
+
const types_1 = require("./types");
|
|
18
|
+
const uuid_1 = require("uuid");
|
|
19
|
+
// Store original request functions
|
|
20
|
+
const originalHttpsRequest = https_1.default.request;
|
|
21
|
+
const originalHttpRequest = http_1.default.request;
|
|
22
|
+
// Track if we've already patched
|
|
23
|
+
let isPatched = false;
|
|
24
|
+
/**
|
|
25
|
+
* Sanitize headers by removing sensitive information
|
|
26
|
+
*/
|
|
27
|
+
function sanitizeHeaders(headers) {
|
|
28
|
+
const sanitized = { ...headers };
|
|
29
|
+
const sensitiveKeys = [
|
|
30
|
+
"authorization",
|
|
31
|
+
"x-api-key",
|
|
32
|
+
"api-key",
|
|
33
|
+
"x-auth-token",
|
|
34
|
+
"cookie",
|
|
35
|
+
];
|
|
36
|
+
for (const key of sensitiveKeys) {
|
|
37
|
+
const lowerKey = key.toLowerCase();
|
|
38
|
+
for (const headerKey of Object.keys(sanitized)) {
|
|
39
|
+
if (headerKey.toLowerCase() === lowerKey) {
|
|
40
|
+
delete sanitized[headerKey];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return sanitized;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Extract request body from options or data
|
|
48
|
+
*/
|
|
49
|
+
async function extractRequestBody(options, data) {
|
|
50
|
+
if (data !== undefined) {
|
|
51
|
+
if (typeof data === "string") {
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
if (Buffer.isBuffer(data)) {
|
|
55
|
+
return data.toString("utf-8");
|
|
56
|
+
}
|
|
57
|
+
return JSON.stringify(data);
|
|
58
|
+
}
|
|
59
|
+
// If body is in options, try to extract it
|
|
60
|
+
if ("body" in options && options.body !== undefined) {
|
|
61
|
+
if (typeof options.body === "string") {
|
|
62
|
+
return options.body;
|
|
63
|
+
}
|
|
64
|
+
if (Buffer.isBuffer(options.body)) {
|
|
65
|
+
return options.body.toString("utf-8");
|
|
66
|
+
}
|
|
67
|
+
return JSON.stringify(options.body);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Initialize the HTTP interceptor
|
|
73
|
+
*/
|
|
74
|
+
function initializeInterceptor() {
|
|
75
|
+
if (isPatched) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
isPatched = true;
|
|
79
|
+
// Patch https.request
|
|
80
|
+
https_1.default.request = function (options, callback) {
|
|
81
|
+
return interceptRequest(originalHttpsRequest, options, callback, true);
|
|
82
|
+
};
|
|
83
|
+
// Patch http.request
|
|
84
|
+
http_1.default.request = function (options, callback) {
|
|
85
|
+
return interceptRequest(originalHttpRequest, options, callback, false);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Intercept HTTP/HTTPS requests
|
|
90
|
+
*/
|
|
91
|
+
function interceptRequest(originalRequest, options, callback, isHttps = true) {
|
|
92
|
+
const context = (0, context_1.getContext)();
|
|
93
|
+
// If no context, just pass through
|
|
94
|
+
if (!context) {
|
|
95
|
+
if (typeof options === "string" || options instanceof url_1.URL) {
|
|
96
|
+
return originalRequest(options, callback);
|
|
97
|
+
}
|
|
98
|
+
return originalRequest(options, callback);
|
|
99
|
+
}
|
|
100
|
+
// Normalize options
|
|
101
|
+
let normalizedOptions;
|
|
102
|
+
if (typeof options === "string" || options instanceof url_1.URL) {
|
|
103
|
+
const url = new url_1.URL(options.toString());
|
|
104
|
+
normalizedOptions = {
|
|
105
|
+
hostname: url.hostname,
|
|
106
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
107
|
+
path: url.pathname + url.search,
|
|
108
|
+
protocol: url.protocol,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
normalizedOptions = { ...options };
|
|
113
|
+
}
|
|
114
|
+
const hostname = normalizedOptions.hostname ||
|
|
115
|
+
(normalizedOptions.host ? normalizedOptions.host.split(":")[0] : null);
|
|
116
|
+
if (!hostname) {
|
|
117
|
+
// Can't determine host, pass through
|
|
118
|
+
return originalRequest(normalizedOptions, callback);
|
|
119
|
+
}
|
|
120
|
+
// Determine if this is an LLM call or tool call
|
|
121
|
+
const isLLM = (0, types_1.isLLMHost)(hostname);
|
|
122
|
+
const role = isLLM ? "agent" : "tool";
|
|
123
|
+
// Extract request data
|
|
124
|
+
const requestId = (0, uuid_1.v4)();
|
|
125
|
+
const startTime = Date.now();
|
|
126
|
+
let requestBody = null;
|
|
127
|
+
const requestHeaders = sanitizeHeaders(normalizedOptions.headers || {});
|
|
128
|
+
// Create the request
|
|
129
|
+
const req = originalRequest(normalizedOptions, (res) => {
|
|
130
|
+
// Buffer for response body (for logging) - accumulates chunks
|
|
131
|
+
const responseChunks = [];
|
|
132
|
+
// Intercept data events to buffer while passing through immediately
|
|
133
|
+
// This ensures zero latency - chunks go to user immediately
|
|
134
|
+
const originalEmit = res.emit.bind(res);
|
|
135
|
+
res.emit = function (event, ...args) {
|
|
136
|
+
// Buffer data chunks for logging
|
|
137
|
+
if (event === "data" && args[0] instanceof Buffer) {
|
|
138
|
+
responseChunks.push(args[0]);
|
|
139
|
+
}
|
|
140
|
+
// Always emit the event (pass through) - this is critical for zero latency
|
|
141
|
+
return originalEmit(event, ...args);
|
|
142
|
+
};
|
|
143
|
+
// When response ends, log it
|
|
144
|
+
res.once("end", () => {
|
|
145
|
+
// Reconstruct response body from buffered chunks
|
|
146
|
+
// Truncate to prevent huge payloads (max 50KB per response)
|
|
147
|
+
const MAX_RESPONSE_BODY_SIZE = 50 * 1024; // 50KB
|
|
148
|
+
let responseBody = null;
|
|
149
|
+
if (responseChunks.length > 0) {
|
|
150
|
+
const fullBody = Buffer.concat(responseChunks).toString("utf-8");
|
|
151
|
+
if (fullBody.length > MAX_RESPONSE_BODY_SIZE) {
|
|
152
|
+
responseBody =
|
|
153
|
+
fullBody.substring(0, MAX_RESPONSE_BODY_SIZE) +
|
|
154
|
+
`... [truncated ${fullBody.length - MAX_RESPONSE_BODY_SIZE} chars]`;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
responseBody = fullBody;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Parse response if it's JSON
|
|
161
|
+
let parsedResponse = null;
|
|
162
|
+
let toolCalls = null;
|
|
163
|
+
if (responseBody) {
|
|
164
|
+
try {
|
|
165
|
+
parsedResponse = JSON.parse(responseBody);
|
|
166
|
+
if (isLLM) {
|
|
167
|
+
const parsed = (0, parser_1.parseOpenAIResponse)(parsedResponse);
|
|
168
|
+
if (parsed.toolCalls) {
|
|
169
|
+
toolCalls = parsed.toolCalls;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Not JSON, ignore
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const endTime = Date.now();
|
|
178
|
+
const duration = endTime - startTime;
|
|
179
|
+
// Build log entry
|
|
180
|
+
const logEntry = {
|
|
181
|
+
session_id: context.sessionId,
|
|
182
|
+
timestamp: new Date(startTime).toISOString(),
|
|
183
|
+
role,
|
|
184
|
+
content: isLLM
|
|
185
|
+
? `LLM request to ${hostname}${normalizedOptions.path || ""}`
|
|
186
|
+
: `Tool request to ${hostname}${normalizedOptions.path || ""}`,
|
|
187
|
+
agent_id: context.agentId,
|
|
188
|
+
run_id: context.runId,
|
|
189
|
+
meta: {
|
|
190
|
+
type: isLLM ? "llm_generation" : "tool_execution",
|
|
191
|
+
request_id: requestId,
|
|
192
|
+
host: hostname,
|
|
193
|
+
method: normalizedOptions.method || "GET",
|
|
194
|
+
path: normalizedOptions.path || "",
|
|
195
|
+
request_headers: requestHeaders,
|
|
196
|
+
request_body: requestBody,
|
|
197
|
+
response_status: res.statusCode,
|
|
198
|
+
response_headers: sanitizeHeaders(res.headers || {}),
|
|
199
|
+
response_body: responseBody,
|
|
200
|
+
duration_ms: duration,
|
|
201
|
+
...(toolCalls ? { tool_calls: toolCalls } : {}),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
// Export log (non-blocking)
|
|
205
|
+
(0, exporter_1.addLog)(logEntry);
|
|
206
|
+
});
|
|
207
|
+
// Call original callback if provided
|
|
208
|
+
if (callback) {
|
|
209
|
+
callback(res);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
// Intercept request body if data is written
|
|
213
|
+
// Accumulate request body chunks (with size limit)
|
|
214
|
+
const requestChunks = [];
|
|
215
|
+
const MAX_REQUEST_BODY_SIZE = 50 * 1024; // 50KB
|
|
216
|
+
let requestBodySize = 0;
|
|
217
|
+
const originalWrite = req.write.bind(req);
|
|
218
|
+
const originalEnd = req.end.bind(req);
|
|
219
|
+
req.write = function (chunk, encoding, cb) {
|
|
220
|
+
// Buffer request body chunks (up to size limit)
|
|
221
|
+
if (requestBodySize < MAX_REQUEST_BODY_SIZE) {
|
|
222
|
+
let chunkBuffer;
|
|
223
|
+
if (typeof chunk === "string") {
|
|
224
|
+
const enc = typeof encoding === "string" ? encoding : undefined;
|
|
225
|
+
chunkBuffer = Buffer.from(chunk, enc);
|
|
226
|
+
}
|
|
227
|
+
else if (Buffer.isBuffer(chunk)) {
|
|
228
|
+
chunkBuffer = chunk;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
return originalWrite(chunk, encoding, cb);
|
|
232
|
+
}
|
|
233
|
+
if (requestBodySize + chunkBuffer.length <= MAX_REQUEST_BODY_SIZE) {
|
|
234
|
+
requestChunks.push(chunkBuffer);
|
|
235
|
+
requestBodySize += chunkBuffer.length;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Truncate at limit
|
|
239
|
+
const remaining = MAX_REQUEST_BODY_SIZE - requestBodySize;
|
|
240
|
+
requestChunks.push(chunkBuffer.subarray(0, remaining));
|
|
241
|
+
requestBodySize = MAX_REQUEST_BODY_SIZE;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return originalWrite(chunk, encoding, cb);
|
|
245
|
+
};
|
|
246
|
+
req.end = function (chunk, encoding, cb) {
|
|
247
|
+
// Buffer final chunk if present and under limit
|
|
248
|
+
if (chunk !== undefined && requestBodySize < MAX_REQUEST_BODY_SIZE) {
|
|
249
|
+
let chunkBuffer;
|
|
250
|
+
if (typeof chunk === "string") {
|
|
251
|
+
const enc = typeof encoding === "string" ? encoding : undefined;
|
|
252
|
+
chunkBuffer = Buffer.from(chunk, enc);
|
|
253
|
+
}
|
|
254
|
+
else if (Buffer.isBuffer(chunk)) {
|
|
255
|
+
chunkBuffer = chunk;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
return originalEnd(chunk, encoding, cb);
|
|
259
|
+
}
|
|
260
|
+
if (requestBodySize + chunkBuffer.length <= MAX_REQUEST_BODY_SIZE) {
|
|
261
|
+
requestChunks.push(chunkBuffer);
|
|
262
|
+
requestBodySize += chunkBuffer.length;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const remaining = MAX_REQUEST_BODY_SIZE - requestBodySize;
|
|
266
|
+
requestChunks.push(chunkBuffer.subarray(0, remaining));
|
|
267
|
+
requestBodySize = MAX_REQUEST_BODY_SIZE;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Reconstruct request body (may be truncated)
|
|
271
|
+
if (requestChunks.length > 0) {
|
|
272
|
+
requestBody = Buffer.concat(requestChunks).toString("utf-8");
|
|
273
|
+
if (requestBodySize >= MAX_REQUEST_BODY_SIZE) {
|
|
274
|
+
requestBody += "... [truncated]";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return originalEnd(chunk, encoding, cb);
|
|
278
|
+
};
|
|
279
|
+
return req;
|
|
280
|
+
}
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response parser for extracting structured data from LLM responses
|
|
3
|
+
*/
|
|
4
|
+
export interface ToolCall {
|
|
5
|
+
id?: string;
|
|
6
|
+
type: string;
|
|
7
|
+
function: {
|
|
8
|
+
name: string;
|
|
9
|
+
arguments: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface ParsedResponse {
|
|
13
|
+
toolCalls?: ToolCall[];
|
|
14
|
+
content?: string;
|
|
15
|
+
role?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse OpenAI JSON response and extract tool calls
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseOpenAIResponse(body: unknown): ParsedResponse;
|
|
21
|
+
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,QAAQ;IACvB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CA4CjE"}
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Response parser for extracting structured data from LLM responses
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseOpenAIResponse = parseOpenAIResponse;
|
|
7
|
+
/**
|
|
8
|
+
* Parse OpenAI JSON response and extract tool calls
|
|
9
|
+
*/
|
|
10
|
+
function parseOpenAIResponse(body) {
|
|
11
|
+
const result = {};
|
|
12
|
+
if (typeof body !== "object" || body === null) {
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
const obj = body;
|
|
16
|
+
// Handle chat completion response
|
|
17
|
+
if (Array.isArray(obj.choices)) {
|
|
18
|
+
const firstChoice = obj.choices[0];
|
|
19
|
+
if (firstChoice?.message) {
|
|
20
|
+
const message = firstChoice.message;
|
|
21
|
+
// Extract tool calls
|
|
22
|
+
if (Array.isArray(message.tool_calls)) {
|
|
23
|
+
result.toolCalls = message.tool_calls.map((tc) => {
|
|
24
|
+
const toolCall = tc;
|
|
25
|
+
const fn = toolCall.function;
|
|
26
|
+
return {
|
|
27
|
+
id: typeof toolCall.id === "string" ? toolCall.id : undefined,
|
|
28
|
+
type: typeof toolCall.type === "string" ? toolCall.type : "function",
|
|
29
|
+
function: {
|
|
30
|
+
name: typeof fn?.name === "string" ? fn.name : "",
|
|
31
|
+
arguments: typeof fn?.arguments === "string" ? fn.arguments : "{}",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Extract content
|
|
37
|
+
if (typeof message.content === "string") {
|
|
38
|
+
result.content = message.content;
|
|
39
|
+
}
|
|
40
|
+
// Extract role
|
|
41
|
+
if (typeof message.role === "string") {
|
|
42
|
+
result.role = message.role;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for kindred-tracer
|
|
3
|
+
*/
|
|
4
|
+
export interface LogEntry {
|
|
5
|
+
session_id: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
role: "user" | "agent" | "tool" | "system";
|
|
8
|
+
content: string;
|
|
9
|
+
agent_id?: string;
|
|
10
|
+
run_id?: string;
|
|
11
|
+
meta?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* LLM provider hosts
|
|
15
|
+
*/
|
|
16
|
+
export declare const LLM_HOSTS: Set<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Check if a host is an LLM provider
|
|
19
|
+
*/
|
|
20
|
+
export declare function isLLMHost(host: string): boolean;
|
|
21
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,aAMpB,CAAC;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/C"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Type definitions for kindred-tracer
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LLM_HOSTS = void 0;
|
|
7
|
+
exports.isLLMHost = isLLMHost;
|
|
8
|
+
/**
|
|
9
|
+
* LLM provider hosts
|
|
10
|
+
*/
|
|
11
|
+
exports.LLM_HOSTS = new Set([
|
|
12
|
+
"api.openai.com",
|
|
13
|
+
"api.anthropic.com",
|
|
14
|
+
"generativelanguage.googleapis.com", // Google Gemini
|
|
15
|
+
"api.cohere.com",
|
|
16
|
+
"api.mistral.ai",
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Check if a host is an LLM provider
|
|
20
|
+
*/
|
|
21
|
+
function isLLMHost(host) {
|
|
22
|
+
return exports.LLM_HOSTS.has(host.toLowerCase());
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kindred-tracer-node",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Kindred Tracer SDK for Node.js - Auto-instrumentation for AI agents",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/aryanmundre/Kindred.git",
|
|
10
|
+
"directory": "packages/kindred-tracer/node"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/aryanmundre/Kindred#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/aryanmundre/Kindred/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "node -e \"require('fs').rmSync('dist', {recursive: true, force: true}); require('fs').rmSync('tsconfig.tsbuildinfo', {force: true});\"",
|
|
21
|
+
"prebuild": "pnpm run clean",
|
|
22
|
+
"build": "tsc --build",
|
|
23
|
+
"prepublishOnly": "pnpm run build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"uuid": "^9.0.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.10.6",
|
|
30
|
+
"@types/uuid": "^9.0.7",
|
|
31
|
+
"typescript": "^5.3.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": ["kindred", "tracer", "instrumentation", "ai", "agent"],
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|