@warpmetrics/warp 0.0.18 → 0.0.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warpmetrics/warp",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Measure your agents, not your LLM calls.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -0,0 +1,43 @@
1
+ // Warpmetrics SDK — reserve()
2
+
3
+ import { generateId } from './utils.js';
4
+ import { actRegistry, runRegistry, groupRegistry, outcomeRegistry } from './registry.js';
5
+
6
+ /**
7
+ * Reserve an event ID without queueing.
8
+ *
9
+ * Takes a descriptor (returned by calling an event function without its
10
+ * required target) and assigns a pre-generated ID. The ID is registered
11
+ * as a stub so that ref() can resolve it immediately.
12
+ *
13
+ * @param {{ _descriptor: true, _eventType: string, name: string, opts: Record<string, any> | null }} descriptor
14
+ * @returns {Readonly<{ id: string, _type: string, name: string, opts: Record<string, any> | null }>}
15
+ */
16
+ export function reserve(descriptor) {
17
+ if (!descriptor || !descriptor._descriptor) {
18
+ throw new Error('reserve() requires a descriptor from act(), run(), etc.');
19
+ }
20
+
21
+ const prefixMap = { act: 'act', run: 'run', group: 'grp', outcome: 'oc' };
22
+ const prefix = prefixMap[descriptor._eventType];
23
+ if (!prefix) {
24
+ throw new Error(`reserve() — unknown event type: ${descriptor._eventType}`);
25
+ }
26
+
27
+ const id = generateId(prefix);
28
+
29
+ // Register as stub so ref() can resolve it
30
+ const registryMap = { act: actRegistry, run: runRegistry, group: groupRegistry, outcome: outcomeRegistry };
31
+ const registry = registryMap[descriptor._eventType];
32
+ if (!registry) {
33
+ throw new Error(`reserve() — no registry for event type: ${descriptor._eventType}`);
34
+ }
35
+ registry.set(id, { ...(registry.get(id) || {}), id, stub: true });
36
+
37
+ return Object.freeze({
38
+ id,
39
+ _type: descriptor._eventType,
40
+ name: descriptor.name,
41
+ opts: descriptor.opts,
42
+ });
43
+ }
@@ -166,7 +166,7 @@ export function logRun(data) {
166
166
  label: data.label,
167
167
  opts: data.opts || null,
168
168
  refId: data.refId || null,
169
- timestamp: new Date().toISOString(),
169
+ startedAt: data.startedAt || new Date().toISOString(),
170
170
  });
171
171
  }
172
172
 
@@ -175,7 +175,7 @@ export function logGroup(data) {
175
175
  id: data.id,
176
176
  label: data.label,
177
177
  opts: data.opts || null,
178
- timestamp: new Date().toISOString(),
178
+ startedAt: data.startedAt || new Date().toISOString(),
179
179
  });
180
180
  }
181
181
 
package/src/core/warp.js CHANGED
@@ -32,13 +32,14 @@ function createInterceptor(originalFn, context, provider) {
32
32
 
33
33
  try {
34
34
  const result = await originalFn.apply(context, args);
35
- const latency = Date.now() - start;
35
+ const duration = Date.now() - start;
36
36
 
37
37
  if (stream) {
38
38
  return wrapStream(result, { callId, provider, model, messages, tools, start });
39
39
  }
40
40
 
41
41
  const ext = provider.extract(result);
42
+ const endedAt = new Date().toISOString();
42
43
 
43
44
  responseRegistry.set(result, {
44
45
  id: callId,
@@ -47,8 +48,9 @@ function createInterceptor(originalFn, context, provider) {
47
48
  response: ext.response,
48
49
  tools: tools ? tools.map(t => t.function?.name || t.name).filter(Boolean) : null,
49
50
  toolCalls: ext.toolCalls,
50
- tokens: ext.tokens, latency,
51
- timestamp: new Date().toISOString(),
51
+ tokens: ext.tokens, duration,
52
+ startedAt: new Date(start).toISOString(),
53
+ endedAt,
52
54
  status: 'success',
53
55
  },
54
56
  });
@@ -56,13 +58,15 @@ function createInterceptor(originalFn, context, provider) {
56
58
  return result;
57
59
  } catch (error) {
58
60
  const errorResult = { _warpError: true };
61
+ const duration = Date.now() - start;
59
62
  responseRegistry.set(errorResult, {
60
63
  id: callId,
61
64
  data: {
62
65
  id: callId, provider: provider.name, model, messages,
63
66
  error: error.message,
64
- latency: Date.now() - start,
65
- timestamp: new Date().toISOString(),
67
+ duration,
68
+ startedAt: new Date(start).toISOString(),
69
+ endedAt: new Date().toISOString(),
66
70
  status: 'error',
67
71
  },
68
72
  });
@@ -95,6 +99,7 @@ function wrapStream(stream, ctx) {
95
99
  ? ctx.provider.normalizeUsage(usage)
96
100
  : { prompt: 0, completion: 0, total: 0 };
97
101
 
102
+ const duration = Date.now() - ctx.start;
98
103
  responseRegistry.set(wrapped, {
99
104
  id: ctx.callId,
100
105
  data: {
@@ -102,8 +107,9 @@ function wrapStream(stream, ctx) {
102
107
  response: content,
103
108
  tools: ctx.tools ? ctx.tools.map(t => t.function?.name || t.name).filter(Boolean) : null,
104
109
  tokens,
105
- latency: Date.now() - ctx.start,
106
- timestamp: new Date().toISOString(),
110
+ duration,
111
+ startedAt: new Date(ctx.start).toISOString(),
112
+ endedAt: new Date().toISOString(),
107
113
  status: 'success',
108
114
  },
109
115
  });
package/src/index.d.ts CHANGED
@@ -29,6 +29,22 @@ export interface Act {
29
29
  readonly _type: 'act';
30
30
  }
31
31
 
32
+ /** Descriptor returned by event functions called without a target. */
33
+ export interface Descriptor<T extends string> {
34
+ readonly _descriptor: true;
35
+ readonly _eventType: T;
36
+ readonly name: string;
37
+ readonly opts: Record<string, any> | null;
38
+ }
39
+
40
+ /** Reserved event handle with pre-generated ID. */
41
+ export interface ReservedAct {
42
+ readonly id: string;
43
+ readonly _type: 'act';
44
+ readonly name: string;
45
+ readonly opts: Record<string, any> | null;
46
+ }
47
+
32
48
 
33
49
  /**
34
50
  * Wrap an LLM client to automatically track every API call.
@@ -63,9 +79,11 @@ export interface TraceData {
63
79
  /** Token usage. */
64
80
  tokens?: { prompt?: number; completion?: number; total?: number };
65
81
  /** Duration in milliseconds. */
66
- latency?: number;
67
- /** ISO 8601 timestamp (auto-generated if omitted). */
68
- timestamp?: string;
82
+ duration?: number;
83
+ /** ISO 8601 timestamp of when the call started. */
84
+ startedAt?: string;
85
+ /** ISO 8601 timestamp of when the call ended (auto-generated if omitted). */
86
+ endedAt?: string;
69
87
  /** "success" (default) or "error". */
70
88
  status?: string;
71
89
  /** Error message. */
@@ -98,6 +116,19 @@ export function act(
98
116
  opts?: Record<string, any>,
99
117
  ): Act | undefined;
100
118
 
119
+ /** Create an act descriptor (no outcome yet). Pass to reserve() to get an ID. */
120
+ export function act(name: string, opts?: Record<string, any>): Descriptor<'act'>;
121
+
122
+ /** Complete a reserved act by providing the outcome. */
123
+ export function act(
124
+ target: Outcome | string,
125
+ reserved: ReservedAct,
126
+ opts?: Record<string, any>,
127
+ ): Act | undefined;
128
+
129
+ /** Reserve an event ID without queueing. Returns a handle with pre-generated ID. */
130
+ export function reserve(descriptor: Descriptor<'act'>): ReservedAct;
131
+
101
132
  /** Resolve any trackable target to its string ID. */
102
133
  export function ref(target: Run | Group | Act | Outcome | object | string): string | undefined;
103
134
 
package/src/index.js CHANGED
@@ -9,6 +9,8 @@
9
9
  // trace(target, data) — manually trace a call (non-SDK tools)
10
10
  // outcome(target, name, opts?) — record a result
11
11
  // act(target, name, opts?) — record an action, returns act ref
12
+ // act(name, opts?) — create an act descriptor (no queue)
13
+ // reserve(descriptor) — reserve an event ID without queueing
12
14
  // ref(target) — get tracking ID
13
15
  export { warp } from './core/warp.js';
14
16
  export { run } from './trace/run.js';
@@ -17,5 +19,6 @@ export { call } from './trace/call.js';
17
19
  export { trace } from './trace/trace.js';
18
20
  export { outcome } from './trace/outcome.js';
19
21
  export { act } from './trace/act.js';
22
+ export { reserve } from './core/reserve.js';
20
23
  export { ref } from './trace/ref.js';
21
24
  export { flush } from './core/transport.js';
package/src/trace/act.js CHANGED
@@ -8,12 +8,51 @@ import { logAct, getConfig } from '../core/transport.js';
8
8
  /**
9
9
  * Record an action taken on an outcome (e.g. acting on feedback).
10
10
  *
11
- * @param {{ id: string, _type: 'outcome' } | string} target — Outcome handle from outcome(), or outcome ref string (wm_oc_*)
12
- * @param {string} name — action name ("Improve Section", "Refine Prompt")
11
+ * Three modes:
12
+ *
13
+ * 1. Normal: act(outcome, name, opts?) — queue an act event
14
+ * 2. Descriptor: act(name, opts?) — return a descriptor (no queue)
15
+ * 3. Reserved: act(outcome, reserved, opts?) — complete a reserved act
16
+ *
17
+ * @param {{ id: string, _type: 'outcome' } | string} target
18
+ * @param {string | { id: string, _type: 'act', name: string, opts: any }} nameOrReserved
13
19
  * @param {Record<string, any>} [opts]
14
- * @returns {{ readonly id: string, readonly _type: 'act' } | undefined}
20
+ * @returns {{ readonly id: string, readonly _type: 'act' } | { _descriptor: true, _eventType: 'act', name: string, opts: any } | undefined}
15
21
  */
16
- export function act(target, name, opts) {
22
+ export function act(target, nameOrReserved, opts) {
23
+ // Path A — Descriptor mode: act(name) or act(name, opts)
24
+ // First arg is a string that doesn't look like a wm_ ref
25
+ if (typeof target === 'string' && !target.startsWith('wm_')) {
26
+ return { _descriptor: true, _eventType: 'act', name: target, opts: nameOrReserved || null };
27
+ }
28
+
29
+ // Path B — Complete a reserved act: act(outcome, reserved, extraOpts?)
30
+ if (nameOrReserved && typeof nameOrReserved === 'object' && nameOrReserved._type === 'act' && nameOrReserved.id) {
31
+ const refId = getRef(target);
32
+
33
+ if (!refId) {
34
+ if (getConfig().debug) console.warn('[warpmetrics] act() — target not tracked.');
35
+ return undefined;
36
+ }
37
+
38
+ if (!refId.startsWith('wm_oc_')) {
39
+ if (getConfig().debug) console.warn('[warpmetrics] act() — target must be an outcome (wm_oc_*).');
40
+ return undefined;
41
+ }
42
+
43
+ const id = nameOrReserved.id;
44
+ const mergedOpts = opts
45
+ ? { ...(nameOrReserved.opts || {}), ...opts }
46
+ : (nameOrReserved.opts || null);
47
+
48
+ const existing = actRegistry.get(id) || {};
49
+ actRegistry.set(id, { ...existing, id, refId });
50
+ logAct({ id, refId, name: nameOrReserved.name, opts: mergedOpts });
51
+
52
+ return Object.freeze({ id, _type: 'act' });
53
+ }
54
+
55
+ // Path C — Normal: act(outcome, name, opts?)
17
56
  const refId = getRef(target);
18
57
 
19
58
  if (!refId) {
@@ -29,7 +68,7 @@ export function act(target, name, opts) {
29
68
  const id = generateId('act');
30
69
  actRegistry.set(id, { id, refId });
31
70
 
32
- logAct({ id, refId, name, opts: opts || null });
71
+ logAct({ id, refId, name: nameOrReserved, opts: opts || null });
33
72
 
34
73
  return Object.freeze({ id, _type: 'act' });
35
74
  }
@@ -14,6 +14,7 @@ import { logGroup, logLink, getConfig } from '../core/transport.js';
14
14
  * @returns {{ readonly id: string, readonly _type: 'group' }}
15
15
  */
16
16
  export function group(target, label, opts) {
17
+ const startedAt = new Date().toISOString();
17
18
  const targetId = getRef(target);
18
19
  if (!targetId) {
19
20
  if (getConfig().debug) console.warn('[warpmetrics] group() — target not recognised.');
@@ -29,6 +30,7 @@ export function group(target, label, opts) {
29
30
  label,
30
31
  opts: opts || null,
31
32
  parentId: targetId,
33
+ startedAt,
32
34
  groups: [],
33
35
  calls: [],
34
36
  };
package/src/trace/run.js CHANGED
@@ -14,6 +14,7 @@ import { logRun, getConfig } from '../core/transport.js';
14
14
  * @returns {{ readonly id: string, readonly _type: 'run' }}
15
15
  */
16
16
  export function run(labelOrRef, labelOrOpts, maybeOpts) {
17
+ const startedAt = new Date().toISOString();
17
18
  let refId = null;
18
19
  let label, opts;
19
20
 
@@ -37,6 +38,7 @@ export function run(labelOrRef, labelOrOpts, maybeOpts) {
37
38
  label,
38
39
  opts,
39
40
  refId,
41
+ startedAt,
40
42
  groups: [],
41
43
  calls: [],
42
44
  };
@@ -26,6 +26,10 @@ export function trace(target, data) {
26
26
 
27
27
  const id = generateId('call');
28
28
 
29
+ const endedAt = data.endedAt || data.timestamp || new Date().toISOString();
30
+ const duration = data.duration ?? data.latency ?? null;
31
+ const startedAt = data.startedAt || (duration != null ? new Date(new Date(endedAt).getTime() - duration).toISOString() : endedAt);
32
+
29
33
  const event = {
30
34
  id,
31
35
  provider: data.provider,
@@ -35,8 +39,9 @@ export function trace(target, data) {
35
39
  tools: data.tools || null,
36
40
  toolCalls: data.toolCalls || null,
37
41
  tokens: data.tokens || null,
38
- latency: data.latency ?? null,
39
- timestamp: data.timestamp || new Date().toISOString(),
42
+ duration,
43
+ startedAt,
44
+ endedAt,
40
45
  status: data.status || 'success',
41
46
  };
42
47