@usefarol/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -0
- package/dist/index.d.mts +53 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +164 -0
- package/dist/index.mjs +137 -0
- package/package.json +29 -0
- package/src/index.ts +191 -0
- package/tsconfig.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# farol-sdk
|
|
2
|
+
|
|
3
|
+
**AI agent observability for Node.js** — wrap your agent entrypoints with a single `trace()` helper to send runs, token usage, cost, and spans to [Farol](https://usefarol.dev).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install farol-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires **Node.js 18+** (global `fetch`).
|
|
12
|
+
|
|
13
|
+
## API key
|
|
14
|
+
|
|
15
|
+
Create or copy your API key from the Farol app: **[usefarol.dev](https://usefarol.dev)** → sign in → dashboard / [usefarol.dev/app](https://usefarol.dev/app).
|
|
16
|
+
|
|
17
|
+
## Quick start — Anthropic
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
21
|
+
import { trace } from "farol-sdk";
|
|
22
|
+
|
|
23
|
+
const client = new Anthropic();
|
|
24
|
+
|
|
25
|
+
const myAgent = trace(
|
|
26
|
+
async (run, task: string) => {
|
|
27
|
+
run.topic = task;
|
|
28
|
+
|
|
29
|
+
const span = run.startSpan("llm_call", { type: "llm" });
|
|
30
|
+
try {
|
|
31
|
+
const response = await client.messages.create({
|
|
32
|
+
model: "claude-haiku-4-5",
|
|
33
|
+
max_tokens: 1024,
|
|
34
|
+
messages: [{ role: "user", content: task }],
|
|
35
|
+
});
|
|
36
|
+
span.inputTokens = response.usage.input_tokens;
|
|
37
|
+
span.outputTokens = response.usage.output_tokens;
|
|
38
|
+
run.inputTokens += response.usage.input_tokens;
|
|
39
|
+
run.outputTokens += response.usage.output_tokens;
|
|
40
|
+
span.end();
|
|
41
|
+
return response.content[0].type === "text"
|
|
42
|
+
? response.content[0].text
|
|
43
|
+
: "";
|
|
44
|
+
} catch (e) {
|
|
45
|
+
span.end(e instanceof Error ? e : new Error(String(e)));
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
agentName: "my-agent",
|
|
51
|
+
farolKey: process.env.FAROL_KEY!,
|
|
52
|
+
model: "claude-haiku-4-5",
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await myAgent("Summarize this week’s metrics.");
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick start — OpenAI
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import OpenAI from "openai";
|
|
63
|
+
import { trace } from "farol-sdk";
|
|
64
|
+
|
|
65
|
+
const openai = new OpenAI();
|
|
66
|
+
|
|
67
|
+
const myAgent = trace(
|
|
68
|
+
async (run, userMessage: string) => {
|
|
69
|
+
run.topic = userMessage;
|
|
70
|
+
|
|
71
|
+
const span = run.startSpan("chat", { type: "llm" });
|
|
72
|
+
try {
|
|
73
|
+
const response = await openai.chat.completions.create({
|
|
74
|
+
model: "gpt-4o-mini",
|
|
75
|
+
messages: [{ role: "user", content: userMessage }],
|
|
76
|
+
});
|
|
77
|
+
const usage = response.usage;
|
|
78
|
+
if (usage) {
|
|
79
|
+
span.inputTokens = usage.prompt_tokens;
|
|
80
|
+
span.outputTokens = usage.completion_tokens;
|
|
81
|
+
run.inputTokens += usage.prompt_tokens;
|
|
82
|
+
run.outputTokens += usage.completion_tokens;
|
|
83
|
+
}
|
|
84
|
+
span.end();
|
|
85
|
+
return response.choices[0]?.message.content ?? "";
|
|
86
|
+
} catch (e) {
|
|
87
|
+
span.end(e instanceof Error ? e : new Error(String(e)));
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
agentName: "support-bot",
|
|
93
|
+
farolKey: process.env.FAROL_KEY!,
|
|
94
|
+
model: "gpt-4o-mini",
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
await myAgent("Hello!");
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Span tracking (`startSpan` / `end`)
|
|
102
|
+
|
|
103
|
+
Use `run.startSpan(name, { type, metadata })` for tools, retrieval, or extra LLM steps. Call `span.end()` when the step finishes, or `span.end(error)` on failure. Unclosed spans are auto-ended when the run completes.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { trace } from "farol-sdk";
|
|
107
|
+
|
|
108
|
+
const pipeline = trace(
|
|
109
|
+
async (run, query: string) => {
|
|
110
|
+
run.topic = query;
|
|
111
|
+
|
|
112
|
+
const search = run.startSpan("web_search", {
|
|
113
|
+
type: "tool",
|
|
114
|
+
metadata: { engine: "internal" },
|
|
115
|
+
});
|
|
116
|
+
const results = await fakeSearch(query);
|
|
117
|
+
search.end();
|
|
118
|
+
|
|
119
|
+
const llm = run.startSpan("answer", { type: "llm" });
|
|
120
|
+
try {
|
|
121
|
+
const answer = await fakeLlm(query, results);
|
|
122
|
+
llm.inputTokens = 100;
|
|
123
|
+
llm.outputTokens = 50;
|
|
124
|
+
run.inputTokens += 100;
|
|
125
|
+
run.outputTokens += 50;
|
|
126
|
+
llm.end();
|
|
127
|
+
return answer;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
llm.end(e instanceof Error ? e : new Error(String(e)));
|
|
130
|
+
throw e;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{ agentName: "research-agent", farolKey: process.env.FAROL_KEY! },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
async function fakeSearch(q: string) {
|
|
137
|
+
return [`result for ${q}`];
|
|
138
|
+
}
|
|
139
|
+
async function fakeLlm(_q: string, _ctx: string[]) {
|
|
140
|
+
return "done";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await pipeline("What is Farol?");
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Optional: capture prompt/response text
|
|
147
|
+
|
|
148
|
+
Set `captureIo: true` in `trace` options to include `span.input` / `span.output` in the payload (only when you assign them). **Do not enable for sensitive data** without reviewing compliance needs.
|
|
149
|
+
|
|
150
|
+
## Options
|
|
151
|
+
|
|
152
|
+
| Option | Description |
|
|
153
|
+
|--------|-------------|
|
|
154
|
+
| `agentName` | Display name in the Farol dashboard |
|
|
155
|
+
| `farolKey` | API key (`frl_…`) |
|
|
156
|
+
| `farolEndpoint` | Override ingest URL (default: hosted Farol ingest) |
|
|
157
|
+
| `model` | Model label on the run |
|
|
158
|
+
| `costPer1kInputTokens` / `costPer1kOutputTokens` | USD per 1k tokens for cost estimates |
|
|
159
|
+
| `captureIo` | When `true`, include span `input`/`output` if set |
|
|
160
|
+
|
|
161
|
+
## Build (from source)
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
cd farol-sdk-js
|
|
165
|
+
npm install
|
|
166
|
+
npm run build
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Outputs `dist/index.js`, `dist/index.mjs`, and `dist/index.d.ts`.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
interface TraceOptions {
|
|
2
|
+
agentName: string;
|
|
3
|
+
farolKey: string;
|
|
4
|
+
farolEndpoint?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
costPer1kInputTokens?: number;
|
|
7
|
+
costPer1kOutputTokens?: number;
|
|
8
|
+
captureIo?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface SpanOptions {
|
|
11
|
+
type?: "tool" | "llm";
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
declare class Span {
|
|
15
|
+
name: string;
|
|
16
|
+
type: string;
|
|
17
|
+
metadata: Record<string, unknown>;
|
|
18
|
+
inputTokens: number;
|
|
19
|
+
outputTokens: number;
|
|
20
|
+
costUsd: number;
|
|
21
|
+
input?: string;
|
|
22
|
+
output?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
endedAt?: string;
|
|
26
|
+
durationMs?: number;
|
|
27
|
+
private _startTime;
|
|
28
|
+
constructor(name: string, options?: SpanOptions);
|
|
29
|
+
end(error?: Error): void;
|
|
30
|
+
toDict(captureIo: boolean): Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
declare class Run {
|
|
33
|
+
id: string;
|
|
34
|
+
agent: string;
|
|
35
|
+
model: string;
|
|
36
|
+
topic?: string;
|
|
37
|
+
status: string;
|
|
38
|
+
steps: unknown[];
|
|
39
|
+
spans: Span[];
|
|
40
|
+
inputTokens: number;
|
|
41
|
+
outputTokens: number;
|
|
42
|
+
costUsd: number;
|
|
43
|
+
durationMs: number;
|
|
44
|
+
error?: string;
|
|
45
|
+
timestamp: string;
|
|
46
|
+
anomaly: boolean;
|
|
47
|
+
anomalyReason?: string;
|
|
48
|
+
constructor(agentName: string, model: string);
|
|
49
|
+
startSpan(name: string, options?: SpanOptions): Span;
|
|
50
|
+
}
|
|
51
|
+
declare function trace<T extends unknown[], R>(fn: (run: Run, ...args: T) => Promise<R>, options: TraceOptions): (...args: T) => Promise<R>;
|
|
52
|
+
|
|
53
|
+
export { Run, Span, type SpanOptions, type TraceOptions, trace };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
interface TraceOptions {
|
|
2
|
+
agentName: string;
|
|
3
|
+
farolKey: string;
|
|
4
|
+
farolEndpoint?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
costPer1kInputTokens?: number;
|
|
7
|
+
costPer1kOutputTokens?: number;
|
|
8
|
+
captureIo?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface SpanOptions {
|
|
11
|
+
type?: "tool" | "llm";
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
declare class Span {
|
|
15
|
+
name: string;
|
|
16
|
+
type: string;
|
|
17
|
+
metadata: Record<string, unknown>;
|
|
18
|
+
inputTokens: number;
|
|
19
|
+
outputTokens: number;
|
|
20
|
+
costUsd: number;
|
|
21
|
+
input?: string;
|
|
22
|
+
output?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
endedAt?: string;
|
|
26
|
+
durationMs?: number;
|
|
27
|
+
private _startTime;
|
|
28
|
+
constructor(name: string, options?: SpanOptions);
|
|
29
|
+
end(error?: Error): void;
|
|
30
|
+
toDict(captureIo: boolean): Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
declare class Run {
|
|
33
|
+
id: string;
|
|
34
|
+
agent: string;
|
|
35
|
+
model: string;
|
|
36
|
+
topic?: string;
|
|
37
|
+
status: string;
|
|
38
|
+
steps: unknown[];
|
|
39
|
+
spans: Span[];
|
|
40
|
+
inputTokens: number;
|
|
41
|
+
outputTokens: number;
|
|
42
|
+
costUsd: number;
|
|
43
|
+
durationMs: number;
|
|
44
|
+
error?: string;
|
|
45
|
+
timestamp: string;
|
|
46
|
+
anomaly: boolean;
|
|
47
|
+
anomalyReason?: string;
|
|
48
|
+
constructor(agentName: string, model: string);
|
|
49
|
+
startSpan(name: string, options?: SpanOptions): Span;
|
|
50
|
+
}
|
|
51
|
+
declare function trace<T extends unknown[], R>(fn: (run: Run, ...args: T) => Promise<R>, options: TraceOptions): (...args: T) => Promise<R>;
|
|
52
|
+
|
|
53
|
+
export { Run, Span, type SpanOptions, type TraceOptions, trace };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Run: () => Run,
|
|
24
|
+
Span: () => Span,
|
|
25
|
+
trace: () => trace
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var DEFAULT_ENDPOINT = "https://drmyexzztahpudgrfjsk.supabase.co/functions/v1/ingest";
|
|
29
|
+
var Span = class {
|
|
30
|
+
constructor(name, options = {}) {
|
|
31
|
+
this.inputTokens = 0;
|
|
32
|
+
this.outputTokens = 0;
|
|
33
|
+
this.costUsd = 0;
|
|
34
|
+
this.name = name;
|
|
35
|
+
this.type = options.type ?? "tool";
|
|
36
|
+
this.metadata = options.metadata ?? {};
|
|
37
|
+
this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38
|
+
this._startTime = Date.now();
|
|
39
|
+
}
|
|
40
|
+
end(error) {
|
|
41
|
+
this.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
42
|
+
this.durationMs = Date.now() - this._startTime;
|
|
43
|
+
if (error) this.error = error.message;
|
|
44
|
+
}
|
|
45
|
+
toDict(captureIo) {
|
|
46
|
+
const d = {
|
|
47
|
+
name: this.name,
|
|
48
|
+
type: this.type,
|
|
49
|
+
metadata: this.metadata,
|
|
50
|
+
started_at: this.startedAt,
|
|
51
|
+
ended_at: this.endedAt,
|
|
52
|
+
duration_ms: this.durationMs,
|
|
53
|
+
input_tokens: this.inputTokens,
|
|
54
|
+
output_tokens: this.outputTokens,
|
|
55
|
+
cost_usd: this.costUsd,
|
|
56
|
+
error: this.error ?? null
|
|
57
|
+
};
|
|
58
|
+
if (captureIo) {
|
|
59
|
+
d.input = this.input ?? null;
|
|
60
|
+
d.output = this.output ?? null;
|
|
61
|
+
}
|
|
62
|
+
return d;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var Run = class {
|
|
66
|
+
constructor(agentName, model) {
|
|
67
|
+
this.status = "running";
|
|
68
|
+
this.steps = [];
|
|
69
|
+
this.spans = [];
|
|
70
|
+
this.inputTokens = 0;
|
|
71
|
+
this.outputTokens = 0;
|
|
72
|
+
this.costUsd = 0;
|
|
73
|
+
this.durationMs = 0;
|
|
74
|
+
this.anomaly = false;
|
|
75
|
+
this.id = `run_${Date.now()}`;
|
|
76
|
+
this.agent = agentName;
|
|
77
|
+
this.model = model;
|
|
78
|
+
this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
79
|
+
}
|
|
80
|
+
startSpan(name, options = {}) {
|
|
81
|
+
const span = new Span(name, options);
|
|
82
|
+
this.spans.push(span);
|
|
83
|
+
return span;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
function trace(fn, options) {
|
|
87
|
+
const {
|
|
88
|
+
agentName,
|
|
89
|
+
farolKey,
|
|
90
|
+
farolEndpoint = DEFAULT_ENDPOINT,
|
|
91
|
+
model = "unknown",
|
|
92
|
+
costPer1kInputTokens = 25e-5,
|
|
93
|
+
costPer1kOutputTokens = 125e-5,
|
|
94
|
+
captureIo = false
|
|
95
|
+
} = options;
|
|
96
|
+
if (captureIo) {
|
|
97
|
+
console.warn(
|
|
98
|
+
"[Farol] WARNING: captureIo is enabled \u2014 prompts are being stored in your Farol dashboard"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return async (...args) => {
|
|
102
|
+
const run = new Run(agentName, model);
|
|
103
|
+
const startTime = Date.now();
|
|
104
|
+
try {
|
|
105
|
+
const result = await fn(run, ...args);
|
|
106
|
+
run.status = "success";
|
|
107
|
+
return result;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
run.status = "error";
|
|
110
|
+
run.error = err instanceof Error ? err.message : String(err);
|
|
111
|
+
throw err;
|
|
112
|
+
} finally {
|
|
113
|
+
run.durationMs = Date.now() - startTime;
|
|
114
|
+
run.costUsd = parseFloat(
|
|
115
|
+
(run.inputTokens / 1e3 * costPer1kInputTokens + run.outputTokens / 1e3 * costPer1kOutputTokens).toFixed(6)
|
|
116
|
+
);
|
|
117
|
+
for (const span of run.spans) {
|
|
118
|
+
if (!span.endedAt) span.end();
|
|
119
|
+
}
|
|
120
|
+
await sendToFarol(run, farolKey, farolEndpoint, captureIo);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function sendToFarol(run, farolKey, endpoint, captureIo) {
|
|
125
|
+
try {
|
|
126
|
+
const payload = {
|
|
127
|
+
id: run.id,
|
|
128
|
+
agent: run.agent,
|
|
129
|
+
model: run.model,
|
|
130
|
+
topic: run.topic ?? null,
|
|
131
|
+
status: run.status,
|
|
132
|
+
steps: run.steps,
|
|
133
|
+
spans: run.spans.map((s) => s.toDict(captureIo)),
|
|
134
|
+
input_tokens: run.inputTokens,
|
|
135
|
+
output_tokens: run.outputTokens,
|
|
136
|
+
cost_usd: run.costUsd,
|
|
137
|
+
duration_ms: run.durationMs,
|
|
138
|
+
error: run.error ?? null,
|
|
139
|
+
timestamp: run.timestamp,
|
|
140
|
+
anomaly: run.anomaly,
|
|
141
|
+
anomaly_reason: run.anomalyReason ?? null,
|
|
142
|
+
farol_key: farolKey
|
|
143
|
+
};
|
|
144
|
+
const res = await fetch(endpoint, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: { "Content-Type": "application/json" },
|
|
147
|
+
body: JSON.stringify(payload)
|
|
148
|
+
});
|
|
149
|
+
if (res.ok) {
|
|
150
|
+
console.log("[Farol] Synced to Farol");
|
|
151
|
+
} else {
|
|
152
|
+
const body = await res.text();
|
|
153
|
+
console.error(`[Farol] Ingest failed (${res.status}): ${body}`);
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(`[Farol] Ingest request failed: ${err}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
160
|
+
0 && (module.exports = {
|
|
161
|
+
Run,
|
|
162
|
+
Span,
|
|
163
|
+
trace
|
|
164
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULT_ENDPOINT = "https://drmyexzztahpudgrfjsk.supabase.co/functions/v1/ingest";
|
|
3
|
+
var Span = class {
|
|
4
|
+
constructor(name, options = {}) {
|
|
5
|
+
this.inputTokens = 0;
|
|
6
|
+
this.outputTokens = 0;
|
|
7
|
+
this.costUsd = 0;
|
|
8
|
+
this.name = name;
|
|
9
|
+
this.type = options.type ?? "tool";
|
|
10
|
+
this.metadata = options.metadata ?? {};
|
|
11
|
+
this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12
|
+
this._startTime = Date.now();
|
|
13
|
+
}
|
|
14
|
+
end(error) {
|
|
15
|
+
this.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16
|
+
this.durationMs = Date.now() - this._startTime;
|
|
17
|
+
if (error) this.error = error.message;
|
|
18
|
+
}
|
|
19
|
+
toDict(captureIo) {
|
|
20
|
+
const d = {
|
|
21
|
+
name: this.name,
|
|
22
|
+
type: this.type,
|
|
23
|
+
metadata: this.metadata,
|
|
24
|
+
started_at: this.startedAt,
|
|
25
|
+
ended_at: this.endedAt,
|
|
26
|
+
duration_ms: this.durationMs,
|
|
27
|
+
input_tokens: this.inputTokens,
|
|
28
|
+
output_tokens: this.outputTokens,
|
|
29
|
+
cost_usd: this.costUsd,
|
|
30
|
+
error: this.error ?? null
|
|
31
|
+
};
|
|
32
|
+
if (captureIo) {
|
|
33
|
+
d.input = this.input ?? null;
|
|
34
|
+
d.output = this.output ?? null;
|
|
35
|
+
}
|
|
36
|
+
return d;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var Run = class {
|
|
40
|
+
constructor(agentName, model) {
|
|
41
|
+
this.status = "running";
|
|
42
|
+
this.steps = [];
|
|
43
|
+
this.spans = [];
|
|
44
|
+
this.inputTokens = 0;
|
|
45
|
+
this.outputTokens = 0;
|
|
46
|
+
this.costUsd = 0;
|
|
47
|
+
this.durationMs = 0;
|
|
48
|
+
this.anomaly = false;
|
|
49
|
+
this.id = `run_${Date.now()}`;
|
|
50
|
+
this.agent = agentName;
|
|
51
|
+
this.model = model;
|
|
52
|
+
this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
53
|
+
}
|
|
54
|
+
startSpan(name, options = {}) {
|
|
55
|
+
const span = new Span(name, options);
|
|
56
|
+
this.spans.push(span);
|
|
57
|
+
return span;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
function trace(fn, options) {
|
|
61
|
+
const {
|
|
62
|
+
agentName,
|
|
63
|
+
farolKey,
|
|
64
|
+
farolEndpoint = DEFAULT_ENDPOINT,
|
|
65
|
+
model = "unknown",
|
|
66
|
+
costPer1kInputTokens = 25e-5,
|
|
67
|
+
costPer1kOutputTokens = 125e-5,
|
|
68
|
+
captureIo = false
|
|
69
|
+
} = options;
|
|
70
|
+
if (captureIo) {
|
|
71
|
+
console.warn(
|
|
72
|
+
"[Farol] WARNING: captureIo is enabled \u2014 prompts are being stored in your Farol dashboard"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return async (...args) => {
|
|
76
|
+
const run = new Run(agentName, model);
|
|
77
|
+
const startTime = Date.now();
|
|
78
|
+
try {
|
|
79
|
+
const result = await fn(run, ...args);
|
|
80
|
+
run.status = "success";
|
|
81
|
+
return result;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
run.status = "error";
|
|
84
|
+
run.error = err instanceof Error ? err.message : String(err);
|
|
85
|
+
throw err;
|
|
86
|
+
} finally {
|
|
87
|
+
run.durationMs = Date.now() - startTime;
|
|
88
|
+
run.costUsd = parseFloat(
|
|
89
|
+
(run.inputTokens / 1e3 * costPer1kInputTokens + run.outputTokens / 1e3 * costPer1kOutputTokens).toFixed(6)
|
|
90
|
+
);
|
|
91
|
+
for (const span of run.spans) {
|
|
92
|
+
if (!span.endedAt) span.end();
|
|
93
|
+
}
|
|
94
|
+
await sendToFarol(run, farolKey, farolEndpoint, captureIo);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function sendToFarol(run, farolKey, endpoint, captureIo) {
|
|
99
|
+
try {
|
|
100
|
+
const payload = {
|
|
101
|
+
id: run.id,
|
|
102
|
+
agent: run.agent,
|
|
103
|
+
model: run.model,
|
|
104
|
+
topic: run.topic ?? null,
|
|
105
|
+
status: run.status,
|
|
106
|
+
steps: run.steps,
|
|
107
|
+
spans: run.spans.map((s) => s.toDict(captureIo)),
|
|
108
|
+
input_tokens: run.inputTokens,
|
|
109
|
+
output_tokens: run.outputTokens,
|
|
110
|
+
cost_usd: run.costUsd,
|
|
111
|
+
duration_ms: run.durationMs,
|
|
112
|
+
error: run.error ?? null,
|
|
113
|
+
timestamp: run.timestamp,
|
|
114
|
+
anomaly: run.anomaly,
|
|
115
|
+
anomaly_reason: run.anomalyReason ?? null,
|
|
116
|
+
farol_key: farolKey
|
|
117
|
+
};
|
|
118
|
+
const res = await fetch(endpoint, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: { "Content-Type": "application/json" },
|
|
121
|
+
body: JSON.stringify(payload)
|
|
122
|
+
});
|
|
123
|
+
if (res.ok) {
|
|
124
|
+
console.log("[Farol] Synced to Farol");
|
|
125
|
+
} else {
|
|
126
|
+
const body = await res.text();
|
|
127
|
+
console.error(`[Farol] Ingest failed (${res.status}): ${body}`);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`[Farol] Ingest request failed: ${err}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
Run,
|
|
135
|
+
Span,
|
|
136
|
+
trace
|
|
137
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usefarol/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI agent observability SDK for Node.js — plug-and-play monitoring for your agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["ai", "agents", "observability", "monitoring", "llm", "farol"],
|
|
20
|
+
"author": "Farol",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const DEFAULT_ENDPOINT =
|
|
2
|
+
"https://drmyexzztahpudgrfjsk.supabase.co/functions/v1/ingest";
|
|
3
|
+
|
|
4
|
+
export interface TraceOptions {
|
|
5
|
+
agentName: string;
|
|
6
|
+
farolKey: string;
|
|
7
|
+
farolEndpoint?: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
costPer1kInputTokens?: number;
|
|
10
|
+
costPer1kOutputTokens?: number;
|
|
11
|
+
captureIo?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SpanOptions {
|
|
15
|
+
type?: "tool" | "llm";
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Span {
|
|
20
|
+
name: string;
|
|
21
|
+
type: string;
|
|
22
|
+
metadata: Record<string, unknown>;
|
|
23
|
+
inputTokens = 0;
|
|
24
|
+
outputTokens = 0;
|
|
25
|
+
costUsd = 0;
|
|
26
|
+
input?: string;
|
|
27
|
+
output?: string;
|
|
28
|
+
error?: string;
|
|
29
|
+
startedAt: string;
|
|
30
|
+
endedAt?: string;
|
|
31
|
+
durationMs?: number;
|
|
32
|
+
private _startTime: number;
|
|
33
|
+
|
|
34
|
+
constructor(name: string, options: SpanOptions = {}) {
|
|
35
|
+
this.name = name;
|
|
36
|
+
this.type = options.type ?? "tool";
|
|
37
|
+
this.metadata = options.metadata ?? {};
|
|
38
|
+
this.startedAt = new Date().toISOString();
|
|
39
|
+
this._startTime = Date.now();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
end(error?: Error): void {
|
|
43
|
+
this.endedAt = new Date().toISOString();
|
|
44
|
+
this.durationMs = Date.now() - this._startTime;
|
|
45
|
+
if (error) this.error = error.message;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
toDict(captureIo: boolean): Record<string, unknown> {
|
|
49
|
+
const d: Record<string, unknown> = {
|
|
50
|
+
name: this.name,
|
|
51
|
+
type: this.type,
|
|
52
|
+
metadata: this.metadata,
|
|
53
|
+
started_at: this.startedAt,
|
|
54
|
+
ended_at: this.endedAt,
|
|
55
|
+
duration_ms: this.durationMs,
|
|
56
|
+
input_tokens: this.inputTokens,
|
|
57
|
+
output_tokens: this.outputTokens,
|
|
58
|
+
cost_usd: this.costUsd,
|
|
59
|
+
error: this.error ?? null,
|
|
60
|
+
};
|
|
61
|
+
if (captureIo) {
|
|
62
|
+
d.input = this.input ?? null;
|
|
63
|
+
d.output = this.output ?? null;
|
|
64
|
+
}
|
|
65
|
+
return d;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class Run {
|
|
70
|
+
id: string;
|
|
71
|
+
agent: string;
|
|
72
|
+
model: string;
|
|
73
|
+
topic?: string;
|
|
74
|
+
status = "running";
|
|
75
|
+
steps: unknown[] = [];
|
|
76
|
+
spans: Span[] = [];
|
|
77
|
+
inputTokens = 0;
|
|
78
|
+
outputTokens = 0;
|
|
79
|
+
costUsd = 0;
|
|
80
|
+
durationMs = 0;
|
|
81
|
+
error?: string;
|
|
82
|
+
timestamp: string;
|
|
83
|
+
anomaly = false;
|
|
84
|
+
anomalyReason?: string;
|
|
85
|
+
|
|
86
|
+
constructor(agentName: string, model: string) {
|
|
87
|
+
this.id = `run_${Date.now()}`;
|
|
88
|
+
this.agent = agentName;
|
|
89
|
+
this.model = model;
|
|
90
|
+
this.timestamp = new Date().toISOString();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
startSpan(name: string, options: SpanOptions = {}): Span {
|
|
94
|
+
const span = new Span(name, options);
|
|
95
|
+
this.spans.push(span);
|
|
96
|
+
return span;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function trace<T extends unknown[], R>(
|
|
101
|
+
fn: (run: Run, ...args: T) => Promise<R>,
|
|
102
|
+
options: TraceOptions,
|
|
103
|
+
): (...args: T) => Promise<R> {
|
|
104
|
+
const {
|
|
105
|
+
agentName,
|
|
106
|
+
farolKey,
|
|
107
|
+
farolEndpoint = DEFAULT_ENDPOINT,
|
|
108
|
+
model = "unknown",
|
|
109
|
+
costPer1kInputTokens = 0.00025,
|
|
110
|
+
costPer1kOutputTokens = 0.00125,
|
|
111
|
+
captureIo = false,
|
|
112
|
+
} = options;
|
|
113
|
+
|
|
114
|
+
if (captureIo) {
|
|
115
|
+
console.warn(
|
|
116
|
+
"[Farol] WARNING: captureIo is enabled — prompts are being stored in your Farol dashboard",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return async (...args: T): Promise<R> => {
|
|
121
|
+
const run = new Run(agentName, model);
|
|
122
|
+
const startTime = Date.now();
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = await fn(run, ...args);
|
|
126
|
+
run.status = "success";
|
|
127
|
+
return result;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
run.status = "error";
|
|
130
|
+
run.error = err instanceof Error ? err.message : String(err);
|
|
131
|
+
throw err;
|
|
132
|
+
} finally {
|
|
133
|
+
run.durationMs = Date.now() - startTime;
|
|
134
|
+
run.costUsd = parseFloat(
|
|
135
|
+
(
|
|
136
|
+
(run.inputTokens / 1000) * costPer1kInputTokens +
|
|
137
|
+
(run.outputTokens / 1000) * costPer1kOutputTokens
|
|
138
|
+
).toFixed(6),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
for (const span of run.spans) {
|
|
142
|
+
if (!span.endedAt) span.end();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await sendToFarol(run, farolKey, farolEndpoint, captureIo);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function sendToFarol(
|
|
151
|
+
run: Run,
|
|
152
|
+
farolKey: string,
|
|
153
|
+
endpoint: string,
|
|
154
|
+
captureIo: boolean,
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
try {
|
|
157
|
+
const payload = {
|
|
158
|
+
id: run.id,
|
|
159
|
+
agent: run.agent,
|
|
160
|
+
model: run.model,
|
|
161
|
+
topic: run.topic ?? null,
|
|
162
|
+
status: run.status,
|
|
163
|
+
steps: run.steps,
|
|
164
|
+
spans: run.spans.map((s) => s.toDict(captureIo)),
|
|
165
|
+
input_tokens: run.inputTokens,
|
|
166
|
+
output_tokens: run.outputTokens,
|
|
167
|
+
cost_usd: run.costUsd,
|
|
168
|
+
duration_ms: run.durationMs,
|
|
169
|
+
error: run.error ?? null,
|
|
170
|
+
timestamp: run.timestamp,
|
|
171
|
+
anomaly: run.anomaly,
|
|
172
|
+
anomaly_reason: run.anomalyReason ?? null,
|
|
173
|
+
farol_key: farolKey,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const res = await fetch(endpoint, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { "Content-Type": "application/json" },
|
|
179
|
+
body: JSON.stringify(payload),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (res.ok) {
|
|
183
|
+
console.log("[Farol] Synced to Farol");
|
|
184
|
+
} else {
|
|
185
|
+
const body = await res.text();
|
|
186
|
+
console.error(`[Farol] Ingest failed (${res.status}): ${body}`);
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error(`[Farol] Ingest request failed: ${err}`);
|
|
190
|
+
}
|
|
191
|
+
}
|