@warpmetrics/warp 0.0.46 → 0.0.47

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 CHANGED
@@ -158,6 +158,123 @@ Manually flush pending events. Events are auto-flushed on an interval and on pro
158
158
  await flush();
159
159
  ```
160
160
 
161
+ ## Entity Model
162
+
163
+ Six entity types form an execution DAG. Every entity is created client-side, batched, and sent atomically to `POST /v1/events`.
164
+
165
+ ### Entities
166
+
167
+ | Entity | Prefix | Purpose |
168
+ |--------|--------|---------|
169
+ | **Run** | `wm_run_` | Top-level execution unit. Has a label, opts, and startedAt. |
170
+ | **Group** | `wm_grp_` | Logical phase/step inside a run or group. Nestable. |
171
+ | **Call** | `wm_call_` | Single LLM API call with tokens, cost, duration. Always a leaf node. Created by `call()` (intercepted) or `trace()` (manual) — same entity either way. |
172
+ | **Link** | — | Parent→child edge. Connects runs/groups to their children. No ID of its own. |
173
+ | **Outcome** | `wm_oc_` | Named result recorded on a run, group, or call. |
174
+ | **Act** | `wm_act_` | Action taken on an outcome. Can trigger follow-up runs. |
175
+
176
+ IDs are monotonic ULIDs: `wm_{prefix}_{ulid}`.
177
+
178
+ ### Hierarchy (Links)
179
+
180
+ ```
181
+ Run ──→ Group ──→ Group (nestable)
182
+ │ │
183
+ │ └──→ Call
184
+ └──→ Call
185
+ ```
186
+
187
+ - Links are directional (parent → child)
188
+ - A child has exactly one parent
189
+ - Calls are always leaf nodes
190
+ - Groups can nest arbitrarily
191
+
192
+ ### Outcome → Act → Run chain
193
+
194
+ The mechanism for multi-step workflows:
195
+
196
+ ```
197
+ Run / Group / Call
198
+
199
+ └─ outcome("Needs Review") → Outcome (refId = entity)
200
+
201
+ └─ act("Review") → Act (refId = outcome)
202
+
203
+ └─ run(act) → Run (refId = act)
204
+ ```
205
+
206
+ - An outcome references exactly one entity (run, group, or call) via `refId`
207
+ - An act references exactly one outcome via `refId`
208
+ - A follow-up run references exactly one act via `refId`
209
+ - Multiple outcomes can target the same entity
210
+ - Multiple acts can target the same outcome
211
+ - An act with a follow-up run is "resolved"; without one it's "pending"
212
+
213
+ ### Runner pattern
214
+
215
+ A graph runner processes the entity model by declaring a workflow graph and walking it:
216
+
217
+ ```
218
+ define graph:
219
+ ActName → {
220
+ executor: string | null # null = phase group (auto-transition)
221
+ results: {
222
+ resultType → [{ outcome, on?, next? }]
223
+ }
224
+ }
225
+
226
+ define states:
227
+ OutcomeName → BoardColumn
228
+
229
+ algorithm processRun(run):
230
+ act = findPendingAct(run)
231
+ while act:
232
+ node = graph[act.name]
233
+
234
+ if node.executor == null: # phase group
235
+ group = createGroup(run, node.label)
236
+ result = { type: "created" }
237
+ else: # work act
238
+ result = execute(node.executor, run, act)
239
+
240
+ edges = node.results[result.type] # [{outcome, on?, next?}]
241
+ for edge in edges:
242
+ container = resolveContainer(edge.on, run) # run or phase group
243
+ oc = recordOutcome(container, edge.outcome)
244
+ if edge.next:
245
+ act = emitAct(oc, edge.next, result.nextActOpts)
246
+
247
+ syncBoard(run, states[lastOutcome])
248
+
249
+ if not edge.next:
250
+ break # terminal
251
+ ```
252
+
253
+ Key concepts:
254
+
255
+ - **Phase groups** (`executor: null`): create a group, auto-resolve with `created`, and immediately transition to the next act. No external work.
256
+ - **Work acts** (`executor: string`): call an executor, get a typed result, map it to graph edges.
257
+ - **`findPendingAct`**: walk the run's outcomes and group outcomes (newest first) to find the last act with no follow-up run.
258
+ - **`resolveContainer`**: outcomes target either the top-level run (for board tracking) or a phase group (for scoped tracking). A single result can produce outcomes on both.
259
+ - **Board sync**: map the last outcome name to a board column via the `states` map.
260
+
261
+ ### Event batch format
262
+
263
+ All entities are sent atomically in a single `POST /v1/events` payload:
264
+
265
+ ```json
266
+ {
267
+ "runs": [{ "id", "label", "opts", "refId", "startedAt" }],
268
+ "groups": [{ "id", "label", "opts", "startedAt" }],
269
+ "calls": [{ "id", "provider", "model", "messages", "response", "tokens", "duration", ... }],
270
+ "links": [{ "parentId", "childId", "type", "timestamp" }],
271
+ "outcomes": [{ "id", "refId", "name", "opts", "timestamp" }],
272
+ "acts": [{ "id", "refId", "name", "opts", "timestamp" }]
273
+ }
274
+ ```
275
+
276
+ The body is base64-encoded: `{ "d": "<base64>" }`.
277
+
161
278
  ## Supported providers
162
279
 
163
280
  - **OpenAI** — `client.chat.completions.create()` and `client.responses.create()`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warpmetrics/warp",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "description": "Measure your agents, not your LLM calls.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -0,0 +1,107 @@
1
+ // Warpmetrics SDK — Query API
2
+ // Read path: query runs and retrieve run details.
3
+
4
+ import { getConfig } from './transport.js';
5
+
6
+ function headers() {
7
+ const { apiKey } = getConfig();
8
+ return {
9
+ 'Content-Type': 'application/json',
10
+ ...(apiKey && { Authorization: `Bearer ${apiKey}` }),
11
+ };
12
+ }
13
+
14
+ function projectHeader() {
15
+ const { projectId } = getConfig();
16
+ if (projectId) return { 'X-Project-Id': projectId };
17
+ return {};
18
+ }
19
+
20
+ /**
21
+ * Query runs with filters, sorting, and latest outcome resolution.
22
+ *
23
+ * @param {object} params
24
+ * @param {object} [params.filter] - Filter conditions (opts.*, latest.*, column)
25
+ * @param {object} [params.sort] - Sort order (field: "asc"|"desc")
26
+ * @param {boolean} [params.latest] - Include latest outcome per run
27
+ * @param {number} [params.limit] - Max runs to return (default 50, max 200)
28
+ * @param {string} [params.since] - ISO 8601 timestamp lower bound
29
+ * @returns {Promise<{ runs: object[], total: number, hasMore: boolean }>}
30
+ */
31
+ export async function query(params) {
32
+ const { baseUrl } = getConfig();
33
+ const res = await fetch(`${baseUrl}/v1/query`, {
34
+ method: 'POST',
35
+ headers: { ...headers(), ...projectHeader() },
36
+ body: JSON.stringify(params),
37
+ });
38
+ if (!res.ok) {
39
+ const err = await res.json().catch(() => ({ error: { message: res.statusText } }));
40
+ throw new Error(`warp.query failed: ${err.error?.message || res.status}`);
41
+ }
42
+ const json = await res.json();
43
+ return json.data || json;
44
+ }
45
+
46
+ /**
47
+ * Get a single run by ID with full history.
48
+ *
49
+ * @param {string} runId - The run ID (wm_run_*)
50
+ * @returns {Promise<object>} - Full run data with outcomes, acts, groups, calls, links
51
+ */
52
+ export async function get(runId) {
53
+ const { baseUrl } = getConfig();
54
+ const res = await fetch(`${baseUrl}/v1/runs/${runId}?fields=full`, {
55
+ headers: { ...headers(), ...projectHeader() },
56
+ });
57
+ if (!res.ok) {
58
+ const err = await res.json().catch(() => ({ error: { message: res.statusText } }));
59
+ throw new Error(`warp.get failed: ${err.error?.message || res.status}`);
60
+ }
61
+ const json = await res.json();
62
+ return json.data || json;
63
+ }
64
+
65
+ /**
66
+ * Get all pending runs targeted at a specific graph, sorted by priority.
67
+ *
68
+ * @param {string} graphName - The graph name to query inbox for
69
+ * @returns {Promise<{ runs: object[], total: number, hasMore: boolean }>}
70
+ */
71
+ export function inbox(graphName) {
72
+ return query({
73
+ filter: { 'opts.to': graphName, 'latest.status': 'pending' },
74
+ sort: { 'opts.priority': 'desc', created_at: 'asc' },
75
+ latest: true,
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Check if an effect has been committed (for idempotency).
81
+ *
82
+ * @param {string} effectKey - The effect key to check
83
+ * @returns {Promise<object|null>} - The existing run or null
84
+ */
85
+ export async function exists(effectKey) {
86
+ const result = await query({
87
+ filter: { 'opts.type': `effect.${effectKey}` },
88
+ latest: true,
89
+ limit: 1,
90
+ });
91
+ return result.runs.length > 0 ? result.runs[0] : null;
92
+ }
93
+
94
+ /**
95
+ * Get latest checkpoint for a run (for resume).
96
+ *
97
+ * @param {string} runId - The parent run ID
98
+ * @returns {Promise<object|null>} - The checkpoint outcome or null
99
+ */
100
+ export async function checkpoint(runId) {
101
+ const result = await query({
102
+ filter: { 'opts.type': `${runId}.checkpoint` },
103
+ latest: true,
104
+ limit: 1,
105
+ });
106
+ return result.runs.length > 0 ? result.runs[0].latestOutcome : null;
107
+ }
package/src/index.js CHANGED
@@ -22,3 +22,4 @@ export { act } from './trace/act.js';
22
22
  export { reserve } from './core/reserve.js';
23
23
  export { ref } from './trace/ref.js';
24
24
  export { flush } from './core/transport.js';
25
+ export { query, get, inbox, exists, checkpoint } from './core/query.js';