@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 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.
@@ -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
+ }