@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 +117 -0
- package/package.json +1 -1
- package/src/core/query.js +107 -0
- package/src/index.js +1 -0
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
|
@@ -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