@meterbility/agent 0.3.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/LICENSE +33 -0
- package/README.md +11 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.js +415 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Meterbility authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
----------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
Note: This MIT license covers everything in this repository EXCEPT the
|
|
26
|
+
contents of the /ee directory (when present), which are licensed under the
|
|
27
|
+
Elastic License 2.0 (ELv2) — see /ee/LICENSE.
|
|
28
|
+
|
|
29
|
+
The /ee directory is reserved for Enterprise Edition modules: multi-tenant
|
|
30
|
+
fleet orchestration, SSO/RBAC, audit logs, and long-retention features.
|
|
31
|
+
|
|
32
|
+
Nothing under /ee today; the directory is created as a forward-compatible
|
|
33
|
+
marker so the licensing boundary is visible before any /ee code lands.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @meterbility/agent
|
|
2
|
+
|
|
3
|
+
Part of [Meterbility](https://github.com/HoneycombHairDevelopers/Meterbility) — the debugger for AI agents. Capture every run, inspect every decision, pause and inject live, fork from any step.
|
|
4
|
+
|
|
5
|
+
TypeScript SDK for tracing custom agents: `MeterbilityTracer`, `traceAnthropic`, and Live Probe (pause / inject / resume).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @meterbility/agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
See the [Meterbility documentation](https://github.com/HoneycombHairDevelopers/Meterbility#readme) for the full guide. MIT licensed.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { Store } from '@meterbility/collector';
|
|
2
|
+
import { ContextComponent, Action, Outcome, TokenUsage, Step } from '@meterbility/shared';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public SDK types for custom TS agents. The shape is deliberately small
|
|
6
|
+
* and host-agnostic — anywhere you call a model and run tools, you can
|
|
7
|
+
* emit Steps. The collector handles the rest.
|
|
8
|
+
*/
|
|
9
|
+
interface TracerOptions {
|
|
10
|
+
/** Project — usually a path or stable identifier for the codebase. */
|
|
11
|
+
project: string;
|
|
12
|
+
/** Logical agent identity within the project. */
|
|
13
|
+
agent: string;
|
|
14
|
+
/** Free-form title for the run. Often the user's first message. */
|
|
15
|
+
runTitle?: string;
|
|
16
|
+
/** Free-form tags. */
|
|
17
|
+
tags?: string[];
|
|
18
|
+
/** Override METERBILITY_HOME for this tracer. */
|
|
19
|
+
meterHome?: string;
|
|
20
|
+
/** Override the source_runtime label. Defaults to "sdk-ts". */
|
|
21
|
+
sourceRuntime?: "sdk-ts" | "sdk-py" | "claude-code" | "codex-cli" | "cursor" | "fork";
|
|
22
|
+
/** Source session id (e.g. the host runtime's session uuid). */
|
|
23
|
+
sourceSessionId?: string;
|
|
24
|
+
/** Working directory associated with the run. */
|
|
25
|
+
cwd?: string;
|
|
26
|
+
/** Git branch associated with the run. */
|
|
27
|
+
gitBranch?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Enable the Live Probe hook. When true, the SDK checks
|
|
30
|
+
* `$METERBILITY_HOME/probe/<run_id>.json` before each model call and:
|
|
31
|
+
* - blocks until an operator-initiated pause is released
|
|
32
|
+
* - prepends any queued inject message to the next user turn
|
|
33
|
+
*
|
|
34
|
+
* Defaults to false — zero overhead when the operator isn't using
|
|
35
|
+
* the probe. Turn this on when you want `meter probe` / the web
|
|
36
|
+
* probe panel to be able to graceful-pause this run.
|
|
37
|
+
*/
|
|
38
|
+
probeEnabled?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* How often (ms) to poll the probe file while paused. Defaults to
|
|
41
|
+
* 250ms — fast enough to feel responsive in the operator UI, slow
|
|
42
|
+
* enough that idle pauses don't churn the filesystem.
|
|
43
|
+
*/
|
|
44
|
+
probePollIntervalMs?: number;
|
|
45
|
+
}
|
|
46
|
+
interface StartStepOptions {
|
|
47
|
+
/** Model identity, e.g. "claude-opus-4-7". */
|
|
48
|
+
model: string;
|
|
49
|
+
/** Optional system prompt bytes — added to the context snapshot. */
|
|
50
|
+
systemPrompt?: string;
|
|
51
|
+
/** Optional tool definitions — added to the context snapshot. */
|
|
52
|
+
toolDefinitions?: unknown;
|
|
53
|
+
/** Optional retrieved docs — added to the context snapshot. */
|
|
54
|
+
retrievedDocs?: Array<{
|
|
55
|
+
source: string;
|
|
56
|
+
content: string;
|
|
57
|
+
}>;
|
|
58
|
+
/** Conversation history components (user/assistant/tool messages). */
|
|
59
|
+
history?: Array<{
|
|
60
|
+
role: "user" | "assistant" | "tool";
|
|
61
|
+
content: string;
|
|
62
|
+
}>;
|
|
63
|
+
/** Additional context components (advanced). */
|
|
64
|
+
extraComponents?: ContextComponent[];
|
|
65
|
+
/** Optional tags to apply to this step. */
|
|
66
|
+
tags?: string[];
|
|
67
|
+
}
|
|
68
|
+
interface RecordDecisionOptions {
|
|
69
|
+
/** Raw model output (typed unknown — caller passes whatever the SDK returned). */
|
|
70
|
+
decision: unknown;
|
|
71
|
+
/** Action representation. Use {@link helpers} or build manually. */
|
|
72
|
+
action: Action;
|
|
73
|
+
}
|
|
74
|
+
interface RecordOutcomeOptions {
|
|
75
|
+
outcome: Outcome;
|
|
76
|
+
}
|
|
77
|
+
interface RecordTokensOptions {
|
|
78
|
+
tokens: TokenUsage;
|
|
79
|
+
/** Wall-clock ms from start_step to end. Optional — tracer computes if omitted. */
|
|
80
|
+
latency_ms?: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Quick helpers for common Action shapes — keeps caller code tidy.
|
|
84
|
+
*/
|
|
85
|
+
declare const helpers: {
|
|
86
|
+
toolCall(name: string, input: unknown, id?: string): Action;
|
|
87
|
+
message(text: string): Action;
|
|
88
|
+
thinkingOnly(): Action;
|
|
89
|
+
subAgent(name: string): Action;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One Step builder, returned by tracer.startStep().
|
|
94
|
+
*
|
|
95
|
+
* The capture surface is imperative on purpose: most agent frameworks
|
|
96
|
+
* already have an event-loop-shaped flow ("call model, get reply, run
|
|
97
|
+
* tool, get result"), and forcing them into a different shape costs more
|
|
98
|
+
* than it earns.
|
|
99
|
+
*
|
|
100
|
+
* Internally a MeterbilityStep buffers up the context snapshot, decision,
|
|
101
|
+
* action, outcome, and tokens; persists once `end()` is called. That
|
|
102
|
+
* keeps async I/O off the agent's critical path until the step is over.
|
|
103
|
+
*/
|
|
104
|
+
interface StepInit {
|
|
105
|
+
tracer: MeterbilityTracer;
|
|
106
|
+
sequence: number;
|
|
107
|
+
parent_step_id?: string;
|
|
108
|
+
startedAtMs: number;
|
|
109
|
+
options: StartStepOptions;
|
|
110
|
+
}
|
|
111
|
+
declare class MeterbilityStep {
|
|
112
|
+
readonly step_id: string;
|
|
113
|
+
readonly sequence: number;
|
|
114
|
+
readonly parent_step_id?: string;
|
|
115
|
+
private tracer;
|
|
116
|
+
private model;
|
|
117
|
+
private startedAtMs;
|
|
118
|
+
private startedAtIso;
|
|
119
|
+
private context;
|
|
120
|
+
private decisionContent?;
|
|
121
|
+
private action;
|
|
122
|
+
private outcome;
|
|
123
|
+
private tokens;
|
|
124
|
+
private explicitLatency?;
|
|
125
|
+
private tags;
|
|
126
|
+
private ended;
|
|
127
|
+
private startOptions;
|
|
128
|
+
constructor(init: StepInit);
|
|
129
|
+
recordDecision(opts: RecordDecisionOptions): this;
|
|
130
|
+
recordAction(action: Action): this;
|
|
131
|
+
recordToolCall(name: string, input: unknown, id?: string): this;
|
|
132
|
+
recordMessage(text: string): this;
|
|
133
|
+
recordOutcome(opts: RecordOutcomeOptions): this;
|
|
134
|
+
recordToolResult(content: unknown, opts?: {
|
|
135
|
+
isError?: boolean;
|
|
136
|
+
summary?: string;
|
|
137
|
+
}): this;
|
|
138
|
+
recordTokens(opts: RecordTokensOptions): this;
|
|
139
|
+
tag(tag: string): this;
|
|
140
|
+
/**
|
|
141
|
+
* Persist the step. Idempotent on re-call. Returns the Step row.
|
|
142
|
+
*/
|
|
143
|
+
end(): Promise<Step>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* SDK-side Probe hook — Track B / Turn 8 chunk 2.
|
|
148
|
+
*
|
|
149
|
+
* Runs before every model call when `tracer.probeEnabled` is true.
|
|
150
|
+
* Implements the graceful-pause + inject contract from SPEC §7.1:
|
|
151
|
+
*
|
|
152
|
+
* 1. If an operator has requested a pause, the SDK acknowledges
|
|
153
|
+
* (confirmPaused) and blocks until the operator resumes. The
|
|
154
|
+
* CURRENT model call (if any) is never interrupted — the check
|
|
155
|
+
* happens at the TOP of the call wrapper, so any in-flight stream
|
|
156
|
+
* completes naturally before we yield.
|
|
157
|
+
*
|
|
158
|
+
* 2. After the pause check (or if no pause was active), the SDK
|
|
159
|
+
* consumes any pending inject message and appends it to the
|
|
160
|
+
* request's `messages` array as a new user turn. This is how the
|
|
161
|
+
* operator's "hey, the failing test is a stale fixture" nudge
|
|
162
|
+
* reaches the model.
|
|
163
|
+
*
|
|
164
|
+
* When `probeEnabled` is false on the tracer, none of this runs and
|
|
165
|
+
* the cost is one boolean check per call.
|
|
166
|
+
*
|
|
167
|
+
* ## Testability seams
|
|
168
|
+
*
|
|
169
|
+
* - `sleep(ms)`: how long to wait between probe-state polls. Tests
|
|
170
|
+
* inject a synchronous-ish stub that flips state on first call.
|
|
171
|
+
* Production uses a real `setTimeout` wrapper.
|
|
172
|
+
* - `now()`: clock for timestamping pause acknowledgements. Tests pass
|
|
173
|
+
* a counter; production uses `Date.now`.
|
|
174
|
+
*
|
|
175
|
+
* The hook is intentionally pure-functional on its arguments — it
|
|
176
|
+
* never reaches into the tracer instance for state, only for config.
|
|
177
|
+
*/
|
|
178
|
+
interface ProbeRuntime {
|
|
179
|
+
/** How long to wait between probe-state polls while paused. */
|
|
180
|
+
pollIntervalMs: number;
|
|
181
|
+
/** Sleep implementation (injectable for tests). */
|
|
182
|
+
sleep: (ms: number) => Promise<void>;
|
|
183
|
+
/** Clock (injectable for tests). */
|
|
184
|
+
now: () => number;
|
|
185
|
+
}
|
|
186
|
+
declare const DEFAULT_PROBE_RUNTIME: ProbeRuntime;
|
|
187
|
+
/**
|
|
188
|
+
* The shape `applyProbeToRequest` needs from any "request with a
|
|
189
|
+
* messages array." Kept structural so it works for the Anthropic
|
|
190
|
+
* request shape AND any future provider request shape — we never
|
|
191
|
+
* import a specific SDK's types into this hook.
|
|
192
|
+
*/
|
|
193
|
+
interface ProbeMutableRequest {
|
|
194
|
+
messages: Array<{
|
|
195
|
+
role: "user" | "assistant";
|
|
196
|
+
content: string | Array<unknown>;
|
|
197
|
+
}>;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Run the probe protocol against this call.
|
|
201
|
+
*
|
|
202
|
+
* 1. Read current probe state.
|
|
203
|
+
* 2. If `pause_requested`, confirm and poll until `running`.
|
|
204
|
+
* 3. After unblocking, consume any pending inject and append it to
|
|
205
|
+
* `req.messages` as a user turn.
|
|
206
|
+
*
|
|
207
|
+
* Returns the (possibly modified) request. Caller passes the result on
|
|
208
|
+
* to the underlying SDK call.
|
|
209
|
+
*/
|
|
210
|
+
declare function applyProbeToRequest<R extends ProbeMutableRequest>(runId: string, req: R, runtime?: ProbeRuntime): Promise<R>;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Public SDK entry point.
|
|
214
|
+
*
|
|
215
|
+
* Lifecycle:
|
|
216
|
+
*
|
|
217
|
+
* const tracer = new MeterbilityTracer({ project: "my-app", agent: "support" });
|
|
218
|
+
* const step = tracer.startStep({ model: "claude-opus-4-7", history });
|
|
219
|
+
* // ...call model, record outcome, record tokens...
|
|
220
|
+
* await step.end();
|
|
221
|
+
*
|
|
222
|
+
* One tracer instance = one Run. Create a new tracer per agent invocation.
|
|
223
|
+
* The tracer is intentionally synchronous to create — capture should never
|
|
224
|
+
* block the agent's hot path — and persists asynchronously via promises
|
|
225
|
+
* returned from step.end().
|
|
226
|
+
*/
|
|
227
|
+
declare class MeterbilityTracer {
|
|
228
|
+
readonly run_id: string;
|
|
229
|
+
readonly project_id: string;
|
|
230
|
+
readonly agent_id: string;
|
|
231
|
+
readonly store: Store;
|
|
232
|
+
/**
|
|
233
|
+
* Whether `traceAnthropic` (or any other model-call wrapper) should
|
|
234
|
+
* run the Live Probe hook before each model call. Mirrors
|
|
235
|
+
* `TracerOptions.probeEnabled`; kept readonly so call sites can
|
|
236
|
+
* branch on `tracer.probeEnabled` without a getter.
|
|
237
|
+
*/
|
|
238
|
+
readonly probeEnabled: boolean;
|
|
239
|
+
/**
|
|
240
|
+
* Runtime used by the probe hook — poll interval + injectable
|
|
241
|
+
* sleep/now seams for tests. Defaults to {@link DEFAULT_PROBE_RUNTIME}.
|
|
242
|
+
* Tests construct a custom one and assign it post-construction.
|
|
243
|
+
*/
|
|
244
|
+
probeRuntime: ProbeRuntime;
|
|
245
|
+
private stepCount;
|
|
246
|
+
private prevStepId?;
|
|
247
|
+
private ended;
|
|
248
|
+
private startedAt;
|
|
249
|
+
private status;
|
|
250
|
+
constructor(opts: TracerOptions);
|
|
251
|
+
/**
|
|
252
|
+
* Begin one Step. Returns a {@link MeterbilityStep} the caller fills in
|
|
253
|
+
* imperatively: recordDecision, recordOutcome, recordTokens, end.
|
|
254
|
+
*/
|
|
255
|
+
startStep(opts: StartStepOptions): MeterbilityStep;
|
|
256
|
+
/**
|
|
257
|
+
* Mark the run finished. Status is inferred from the last step's
|
|
258
|
+
* outcome unless overridden. Caller must invoke before exiting so the
|
|
259
|
+
* run row gets sealed and totals recomputed.
|
|
260
|
+
*/
|
|
261
|
+
end(overrides?: {
|
|
262
|
+
status?: "ok" | "error" | "abandoned";
|
|
263
|
+
}): Promise<void>;
|
|
264
|
+
/** Internal: notify tracer of a terminal status from a step. */
|
|
265
|
+
_stepCompleted(stepStatus: "ok" | "error" | "in_progress" | "abandoned"): void;
|
|
266
|
+
/** Internal: bump totals after a step inserts. */
|
|
267
|
+
_refreshTotals(): void;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Convenience wrapper for the common "I'm calling the Anthropic SDK"
|
|
272
|
+
* case. Captures one Step per messages.create() invocation, mapping the
|
|
273
|
+
* SDK's request/response into Meterbility's data model.
|
|
274
|
+
*
|
|
275
|
+
* Usage:
|
|
276
|
+
*
|
|
277
|
+
* import Anthropic from "@anthropic-ai/sdk";
|
|
278
|
+
* const client = new Anthropic();
|
|
279
|
+
* const tracer = new MeterbilityTracer({ project, agent });
|
|
280
|
+
* const traced = traceAnthropic(tracer, (req) => client.messages.create(req));
|
|
281
|
+
*
|
|
282
|
+
* const resp = await traced({
|
|
283
|
+
* model: "claude-opus-4-7",
|
|
284
|
+
* max_tokens: 1024,
|
|
285
|
+
* system: "you are helpful",
|
|
286
|
+
* messages: [{ role: "user", content: "hello" }],
|
|
287
|
+
* });
|
|
288
|
+
*
|
|
289
|
+
* Returns the SDK response untouched. One captured Step per call.
|
|
290
|
+
*/
|
|
291
|
+
declare function traceAnthropic<Req extends AnthropicMessagesRequest, Resp extends AnthropicMessagesResponse>(tracer: MeterbilityTracer, call: (req: Req) => Promise<Resp>): (req: Req) => Promise<Resp>;
|
|
292
|
+
interface AnthropicMessagesRequest {
|
|
293
|
+
model: string;
|
|
294
|
+
max_tokens: number;
|
|
295
|
+
system?: string | Array<{
|
|
296
|
+
type: string;
|
|
297
|
+
text: string;
|
|
298
|
+
}>;
|
|
299
|
+
messages: Array<{
|
|
300
|
+
role: "user" | "assistant";
|
|
301
|
+
content: string | Array<{
|
|
302
|
+
type: string;
|
|
303
|
+
text?: string;
|
|
304
|
+
} | Record<string, unknown>>;
|
|
305
|
+
}>;
|
|
306
|
+
tools?: unknown;
|
|
307
|
+
}
|
|
308
|
+
interface AnthropicMessagesResponse {
|
|
309
|
+
model: string;
|
|
310
|
+
usage?: {
|
|
311
|
+
input_tokens?: number;
|
|
312
|
+
output_tokens?: number;
|
|
313
|
+
cache_read_input_tokens?: number;
|
|
314
|
+
cache_creation_input_tokens?: number;
|
|
315
|
+
cache_creation?: {
|
|
316
|
+
ephemeral_5m_input_tokens?: number;
|
|
317
|
+
ephemeral_1h_input_tokens?: number;
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
content?: Array<AnthropicTextBlock | AnthropicToolUseBlock | {
|
|
321
|
+
type: string;
|
|
322
|
+
}>;
|
|
323
|
+
}
|
|
324
|
+
interface AnthropicTextBlock {
|
|
325
|
+
type: "text";
|
|
326
|
+
text: string;
|
|
327
|
+
}
|
|
328
|
+
interface AnthropicToolUseBlock {
|
|
329
|
+
type: "tool_use";
|
|
330
|
+
id: string;
|
|
331
|
+
name: string;
|
|
332
|
+
input: Record<string, unknown>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export { DEFAULT_PROBE_RUNTIME, MeterbilityStep, MeterbilityTracer, type ProbeMutableRequest, type ProbeRuntime, type RecordDecisionOptions, type RecordOutcomeOptions, type RecordTokensOptions, type StartStepOptions, type StepInit, type TracerOptions, applyProbeToRequest, helpers, traceAnthropic };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// src/tracer.ts
|
|
2
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3
|
+
import { Store } from "@meterbility/collector";
|
|
4
|
+
import {
|
|
5
|
+
insertRun,
|
|
6
|
+
setRunStatus,
|
|
7
|
+
updateRunTotals,
|
|
8
|
+
upsertAgent,
|
|
9
|
+
upsertProjectByCwd
|
|
10
|
+
} from "@meterbility/collector";
|
|
11
|
+
import { clearProbe } from "@meterbility/shared";
|
|
12
|
+
|
|
13
|
+
// src/step.ts
|
|
14
|
+
import { randomUUID } from "crypto";
|
|
15
|
+
import { hashJson } from "@meterbility/shared";
|
|
16
|
+
import { costCents } from "@meterbility/spec";
|
|
17
|
+
import { insertStep, recordContextSnapshot } from "@meterbility/collector";
|
|
18
|
+
var MeterbilityStep = class {
|
|
19
|
+
step_id = `stp_${randomUUID()}`;
|
|
20
|
+
sequence;
|
|
21
|
+
parent_step_id;
|
|
22
|
+
tracer;
|
|
23
|
+
model;
|
|
24
|
+
startedAtMs;
|
|
25
|
+
startedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
26
|
+
context;
|
|
27
|
+
decisionContent;
|
|
28
|
+
action = { kind: "none" };
|
|
29
|
+
outcome = { status: "pending" };
|
|
30
|
+
tokens = {
|
|
31
|
+
input: 0,
|
|
32
|
+
output: 0,
|
|
33
|
+
cached_read: 0,
|
|
34
|
+
cache_creation: 0
|
|
35
|
+
};
|
|
36
|
+
explicitLatency;
|
|
37
|
+
tags = [];
|
|
38
|
+
ended = false;
|
|
39
|
+
startOptions;
|
|
40
|
+
constructor(init) {
|
|
41
|
+
this.tracer = init.tracer;
|
|
42
|
+
this.sequence = init.sequence;
|
|
43
|
+
this.parent_step_id = init.parent_step_id;
|
|
44
|
+
this.startedAtMs = init.startedAtMs;
|
|
45
|
+
this.model = init.options.model;
|
|
46
|
+
this.startOptions = init.options;
|
|
47
|
+
this.context = [];
|
|
48
|
+
if (init.options.tags) this.tags.push(...init.options.tags);
|
|
49
|
+
}
|
|
50
|
+
recordDecision(opts) {
|
|
51
|
+
this.decisionContent = opts.decision;
|
|
52
|
+
this.action = opts.action;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
recordAction(action) {
|
|
56
|
+
this.action = action;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
recordToolCall(name, input, id) {
|
|
60
|
+
this.action = {
|
|
61
|
+
kind: "tool_call",
|
|
62
|
+
tool_name: name,
|
|
63
|
+
tool_use_id: id,
|
|
64
|
+
tool_input: input
|
|
65
|
+
};
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
recordMessage(text) {
|
|
69
|
+
this.action = { kind: "message", text };
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
recordOutcome(opts) {
|
|
73
|
+
this.outcome = opts.outcome;
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
recordToolResult(content, opts) {
|
|
77
|
+
this.outcome = {
|
|
78
|
+
status: opts?.isError ? "error" : "ok",
|
|
79
|
+
is_error: opts?.isError === true,
|
|
80
|
+
summary: opts?.summary
|
|
81
|
+
};
|
|
82
|
+
this._pendingToolResult = content;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
recordTokens(opts) {
|
|
86
|
+
this.tokens = opts.tokens;
|
|
87
|
+
if (opts.latency_ms !== void 0) this.explicitLatency = opts.latency_ms;
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
tag(tag) {
|
|
91
|
+
if (!this.tags.includes(tag)) this.tags.push(tag);
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Persist the step. Idempotent on re-call. Returns the Step row.
|
|
96
|
+
*/
|
|
97
|
+
async end() {
|
|
98
|
+
if (this.ended) {
|
|
99
|
+
throw new Error("MeterbilityStep.end() called twice");
|
|
100
|
+
}
|
|
101
|
+
this.ended = true;
|
|
102
|
+
this.context = await buildContextComponents(
|
|
103
|
+
this.tracer.store,
|
|
104
|
+
this.startOptions
|
|
105
|
+
);
|
|
106
|
+
const snapshot = {
|
|
107
|
+
id: hashJson(this.context),
|
|
108
|
+
components: this.context
|
|
109
|
+
};
|
|
110
|
+
const snapBlobRef = await this.tracer.store.blobs.putJson(snapshot);
|
|
111
|
+
recordContextSnapshot(
|
|
112
|
+
this.tracer.store,
|
|
113
|
+
snapshot.id,
|
|
114
|
+
snapBlobRef,
|
|
115
|
+
snapshot.components.length
|
|
116
|
+
);
|
|
117
|
+
const decisionRef = await this.tracer.store.blobs.putJson(
|
|
118
|
+
this.decisionContent ?? null
|
|
119
|
+
);
|
|
120
|
+
const pending = this._pendingToolResult;
|
|
121
|
+
if (pending !== void 0) {
|
|
122
|
+
const ref = await this.tracer.store.blobs.putJson(pending);
|
|
123
|
+
this.outcome = { ...this.outcome, tool_result_ref: ref };
|
|
124
|
+
}
|
|
125
|
+
const latency_ms = this.explicitLatency !== void 0 ? this.explicitLatency : Date.now() - this.startedAtMs;
|
|
126
|
+
const { cost_cents, approx } = costCents(this.model, {
|
|
127
|
+
input: this.tokens.input,
|
|
128
|
+
output: this.tokens.output,
|
|
129
|
+
cached_read: this.tokens.cached_read,
|
|
130
|
+
cache_creation: this.tokens.cache_creation,
|
|
131
|
+
cache_creation_1h: this.tokens.cache_creation_1h
|
|
132
|
+
});
|
|
133
|
+
if (approx && !this.tags.includes("cost:approx")) {
|
|
134
|
+
this.tags.push("cost:approx");
|
|
135
|
+
}
|
|
136
|
+
const status = this.outcome.status === "error" ? "error" : this.outcome.status === "pending" ? "in_progress" : "ok";
|
|
137
|
+
const step = {
|
|
138
|
+
step_id: this.step_id,
|
|
139
|
+
run_id: this.tracer.run_id,
|
|
140
|
+
parent_step_id: this.parent_step_id,
|
|
141
|
+
sequence: this.sequence,
|
|
142
|
+
timestamp: this.startedAtIso,
|
|
143
|
+
model: this.model,
|
|
144
|
+
context_snapshot_id: snapshot.id,
|
|
145
|
+
decision_ref: decisionRef,
|
|
146
|
+
action: this.action,
|
|
147
|
+
outcome: this.outcome,
|
|
148
|
+
tokens: this.tokens,
|
|
149
|
+
latency_ms,
|
|
150
|
+
cost_cents,
|
|
151
|
+
tags: this.tags,
|
|
152
|
+
status
|
|
153
|
+
};
|
|
154
|
+
insertStep(this.tracer.store, step);
|
|
155
|
+
this.tracer._stepCompleted(status);
|
|
156
|
+
this.tracer._refreshTotals();
|
|
157
|
+
return step;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
async function buildContextComponents(store, opts) {
|
|
161
|
+
const components = [];
|
|
162
|
+
if (opts.systemPrompt !== void 0) {
|
|
163
|
+
const content_ref = await store.blobs.putString(opts.systemPrompt);
|
|
164
|
+
components.push({ type: "system_prompt", content_ref });
|
|
165
|
+
}
|
|
166
|
+
if (opts.toolDefinitions !== void 0) {
|
|
167
|
+
const content_ref = await store.blobs.putJson(opts.toolDefinitions);
|
|
168
|
+
components.push({ type: "tool_definitions", content_ref });
|
|
169
|
+
}
|
|
170
|
+
if (opts.history && opts.history.length > 0) {
|
|
171
|
+
const messages = [];
|
|
172
|
+
for (const m of opts.history) {
|
|
173
|
+
messages.push({
|
|
174
|
+
role: m.role,
|
|
175
|
+
content_ref: await store.blobs.putString(m.content)
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
components.push({ type: "conversation_history", messages });
|
|
179
|
+
}
|
|
180
|
+
if (opts.retrievedDocs && opts.retrievedDocs.length > 0) {
|
|
181
|
+
const docs = [];
|
|
182
|
+
for (const d of opts.retrievedDocs) {
|
|
183
|
+
docs.push({
|
|
184
|
+
source: d.source,
|
|
185
|
+
content_ref: await store.blobs.putString(d.content)
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
components.push({ type: "retrieved_documents", docs });
|
|
189
|
+
}
|
|
190
|
+
if (opts.extraComponents) components.push(...opts.extraComponents);
|
|
191
|
+
return components;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/probe.ts
|
|
195
|
+
import {
|
|
196
|
+
confirmPaused,
|
|
197
|
+
consumeInject,
|
|
198
|
+
readState
|
|
199
|
+
} from "@meterbility/shared";
|
|
200
|
+
var DEFAULT_PROBE_RUNTIME = {
|
|
201
|
+
pollIntervalMs: 250,
|
|
202
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
203
|
+
now: () => Date.now()
|
|
204
|
+
};
|
|
205
|
+
async function applyProbeToRequest(runId, req, runtime = DEFAULT_PROBE_RUNTIME) {
|
|
206
|
+
let state = readState(runId, runtime.now);
|
|
207
|
+
if (state.state === "pause_requested") {
|
|
208
|
+
state = confirmPaused(runId, runtime.now);
|
|
209
|
+
while (state.state !== "running") {
|
|
210
|
+
await runtime.sleep(runtime.pollIntervalMs);
|
|
211
|
+
state = readState(runId, runtime.now);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const injected = consumeInject(runId, runtime.now);
|
|
215
|
+
if (injected !== null) {
|
|
216
|
+
return {
|
|
217
|
+
...req,
|
|
218
|
+
messages: [...req.messages, { role: "user", content: injected }]
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return req;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/tracer.ts
|
|
225
|
+
var MeterbilityTracer = class {
|
|
226
|
+
run_id;
|
|
227
|
+
project_id;
|
|
228
|
+
agent_id;
|
|
229
|
+
store;
|
|
230
|
+
/**
|
|
231
|
+
* Whether `traceAnthropic` (or any other model-call wrapper) should
|
|
232
|
+
* run the Live Probe hook before each model call. Mirrors
|
|
233
|
+
* `TracerOptions.probeEnabled`; kept readonly so call sites can
|
|
234
|
+
* branch on `tracer.probeEnabled` without a getter.
|
|
235
|
+
*/
|
|
236
|
+
probeEnabled;
|
|
237
|
+
/**
|
|
238
|
+
* Runtime used by the probe hook — poll interval + injectable
|
|
239
|
+
* sleep/now seams for tests. Defaults to {@link DEFAULT_PROBE_RUNTIME}.
|
|
240
|
+
* Tests construct a custom one and assign it post-construction.
|
|
241
|
+
*/
|
|
242
|
+
probeRuntime;
|
|
243
|
+
stepCount = 0;
|
|
244
|
+
prevStepId;
|
|
245
|
+
ended = false;
|
|
246
|
+
startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
247
|
+
status = "in_progress";
|
|
248
|
+
constructor(opts) {
|
|
249
|
+
if (opts.meterHome) process.env.METERBILITY_HOME = opts.meterHome;
|
|
250
|
+
this.store = Store.open();
|
|
251
|
+
const project = upsertProjectByCwd(
|
|
252
|
+
this.store,
|
|
253
|
+
opts.cwd ?? opts.project,
|
|
254
|
+
opts.project
|
|
255
|
+
);
|
|
256
|
+
this.project_id = project.project_id;
|
|
257
|
+
const agent = upsertAgent(this.store, project.project_id, opts.agent);
|
|
258
|
+
this.agent_id = agent.agent_id;
|
|
259
|
+
this.run_id = `run_${randomUUID2()}`;
|
|
260
|
+
const run = {
|
|
261
|
+
run_id: this.run_id,
|
|
262
|
+
agent_id: this.agent_id,
|
|
263
|
+
project_id: this.project_id,
|
|
264
|
+
source_session_id: opts.sourceSessionId,
|
|
265
|
+
source_runtime: opts.sourceRuntime ?? "sdk-ts",
|
|
266
|
+
title: opts.runTitle,
|
|
267
|
+
status: "in_progress",
|
|
268
|
+
started_at: this.startedAt,
|
|
269
|
+
git_branch: opts.gitBranch,
|
|
270
|
+
cwd: opts.cwd ?? opts.project,
|
|
271
|
+
tokens_total_input: 0,
|
|
272
|
+
tokens_total_output: 0,
|
|
273
|
+
tokens_total_cached: 0,
|
|
274
|
+
cost_cents: 0,
|
|
275
|
+
step_count: 0,
|
|
276
|
+
tags: opts.tags ?? []
|
|
277
|
+
};
|
|
278
|
+
insertRun(this.store, run);
|
|
279
|
+
this.probeEnabled = opts.probeEnabled ?? false;
|
|
280
|
+
this.probeRuntime = {
|
|
281
|
+
...DEFAULT_PROBE_RUNTIME,
|
|
282
|
+
pollIntervalMs: opts.probePollIntervalMs ?? DEFAULT_PROBE_RUNTIME.pollIntervalMs
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Begin one Step. Returns a {@link MeterbilityStep} the caller fills in
|
|
287
|
+
* imperatively: recordDecision, recordOutcome, recordTokens, end.
|
|
288
|
+
*/
|
|
289
|
+
startStep(opts) {
|
|
290
|
+
const sequence = this.stepCount;
|
|
291
|
+
const step = new MeterbilityStep({
|
|
292
|
+
tracer: this,
|
|
293
|
+
sequence,
|
|
294
|
+
parent_step_id: this.prevStepId,
|
|
295
|
+
startedAtMs: Date.now(),
|
|
296
|
+
options: opts
|
|
297
|
+
});
|
|
298
|
+
this.stepCount += 1;
|
|
299
|
+
this.prevStepId = step.step_id;
|
|
300
|
+
return step;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Mark the run finished. Status is inferred from the last step's
|
|
304
|
+
* outcome unless overridden. Caller must invoke before exiting so the
|
|
305
|
+
* run row gets sealed and totals recomputed.
|
|
306
|
+
*/
|
|
307
|
+
async end(overrides) {
|
|
308
|
+
if (this.ended) return;
|
|
309
|
+
this.ended = true;
|
|
310
|
+
const status = overrides?.status ?? this.status;
|
|
311
|
+
setRunStatus(this.store, this.run_id, status, (/* @__PURE__ */ new Date()).toISOString());
|
|
312
|
+
updateRunTotals(this.store, this.run_id);
|
|
313
|
+
clearProbe(this.run_id);
|
|
314
|
+
this.store.close();
|
|
315
|
+
}
|
|
316
|
+
/** Internal: notify tracer of a terminal status from a step. */
|
|
317
|
+
_stepCompleted(stepStatus) {
|
|
318
|
+
if (stepStatus === "error") this.status = "error";
|
|
319
|
+
else if (stepStatus === "ok" && this.status !== "error")
|
|
320
|
+
this.status = "ok";
|
|
321
|
+
}
|
|
322
|
+
/** Internal: bump totals after a step inserts. */
|
|
323
|
+
_refreshTotals() {
|
|
324
|
+
updateRunTotals(this.store, this.run_id);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// src/anthropic.ts
|
|
329
|
+
function traceAnthropic(tracer, call) {
|
|
330
|
+
return async (reqIn) => {
|
|
331
|
+
const req = tracer.probeEnabled ? await applyProbeToRequest(tracer.run_id, reqIn, tracer.probeRuntime) : reqIn;
|
|
332
|
+
const history = [];
|
|
333
|
+
for (const m of req.messages ?? []) {
|
|
334
|
+
const text2 = typeof m.content === "string" ? m.content : (m.content ?? []).filter(
|
|
335
|
+
(b) => b.type === "text"
|
|
336
|
+
).map((b) => b.text).join("\n");
|
|
337
|
+
history.push({
|
|
338
|
+
role: m.role === "assistant" ? "assistant" : "user",
|
|
339
|
+
content: text2
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const step = tracer.startStep({
|
|
343
|
+
model: req.model,
|
|
344
|
+
systemPrompt: typeof req.system === "string" ? req.system : void 0,
|
|
345
|
+
toolDefinitions: req.tools,
|
|
346
|
+
history
|
|
347
|
+
});
|
|
348
|
+
const t0 = Date.now();
|
|
349
|
+
let resp;
|
|
350
|
+
try {
|
|
351
|
+
resp = await call(req);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
step.recordAction({ kind: "none" }).recordOutcome({
|
|
354
|
+
outcome: {
|
|
355
|
+
status: "error",
|
|
356
|
+
is_error: true,
|
|
357
|
+
summary: err.message?.slice(0, 200)
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
await step.end();
|
|
361
|
+
throw err;
|
|
362
|
+
}
|
|
363
|
+
const t1 = Date.now();
|
|
364
|
+
const blocks = resp.content ?? [];
|
|
365
|
+
const toolUse = blocks.find(
|
|
366
|
+
(b) => b.type === "tool_use"
|
|
367
|
+
);
|
|
368
|
+
const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
369
|
+
const action = toolUse ? {
|
|
370
|
+
kind: "tool_call",
|
|
371
|
+
tool_name: toolUse.name,
|
|
372
|
+
tool_use_id: toolUse.id,
|
|
373
|
+
tool_input: toolUse.input
|
|
374
|
+
} : text ? { kind: "message", text } : { kind: "thinking_only" };
|
|
375
|
+
const cc = resp.usage?.cache_creation;
|
|
376
|
+
const tokens5m = cc ? cc.ephemeral_5m_input_tokens ?? 0 : resp.usage?.cache_creation_input_tokens ?? 0;
|
|
377
|
+
const tokens1h = cc?.ephemeral_1h_input_tokens ?? 0;
|
|
378
|
+
step.recordDecision({ decision: resp.content, action }).recordTokens({
|
|
379
|
+
tokens: {
|
|
380
|
+
input: resp.usage?.input_tokens ?? 0,
|
|
381
|
+
output: resp.usage?.output_tokens ?? 0,
|
|
382
|
+
cached_read: resp.usage?.cache_read_input_tokens ?? 0,
|
|
383
|
+
cache_creation: tokens5m,
|
|
384
|
+
cache_creation_1h: tokens1h
|
|
385
|
+
},
|
|
386
|
+
latency_ms: t1 - t0
|
|
387
|
+
}).recordOutcome({ outcome: { status: "ok" } });
|
|
388
|
+
await step.end();
|
|
389
|
+
return resp;
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/types.ts
|
|
394
|
+
var helpers = {
|
|
395
|
+
toolCall(name, input, id) {
|
|
396
|
+
return { kind: "tool_call", tool_name: name, tool_use_id: id, tool_input: input };
|
|
397
|
+
},
|
|
398
|
+
message(text) {
|
|
399
|
+
return { kind: "message", text };
|
|
400
|
+
},
|
|
401
|
+
thinkingOnly() {
|
|
402
|
+
return { kind: "thinking_only" };
|
|
403
|
+
},
|
|
404
|
+
subAgent(name) {
|
|
405
|
+
return { kind: "sub_agent_dispatch", sub_agent: name };
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
export {
|
|
409
|
+
DEFAULT_PROBE_RUNTIME,
|
|
410
|
+
MeterbilityStep,
|
|
411
|
+
MeterbilityTracer,
|
|
412
|
+
applyProbeToRequest,
|
|
413
|
+
helpers,
|
|
414
|
+
traceAnthropic
|
|
415
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meterbility/agent",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@meterbility/shared": "^0.3.0",
|
|
15
|
+
"@meterbility/spec": "^0.3.0",
|
|
16
|
+
"@meterbility/collector": "^0.3.0"
|
|
17
|
+
},
|
|
18
|
+
"description": "TypeScript SDK for tracing custom agents with Meterbility, including Live Probe",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/HoneycombHairDevelopers/Meterbility.git",
|
|
22
|
+
"directory": "packages/agent"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/HoneycombHairDevelopers/Meterbility#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/HoneycombHairDevelopers/Meterbility/issues"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.6"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"types": "dist/index.d.ts",
|
|
38
|
+
"tsup": {
|
|
39
|
+
"entry": [
|
|
40
|
+
"src/index.ts"
|
|
41
|
+
],
|
|
42
|
+
"format": [
|
|
43
|
+
"esm"
|
|
44
|
+
],
|
|
45
|
+
"dts": true,
|
|
46
|
+
"clean": true
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup"
|
|
50
|
+
}
|
|
51
|
+
}
|