@bratsos/workflow-engine 0.6.0 → 0.8.0
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 +93 -0
- package/dist/{chunk-2MWO6UVR.js → chunk-NANAXHS5.js} +2 -2
- package/dist/chunk-NANAXHS5.js.map +1 -0
- package/dist/{chunk-DIADEUGZ.js → chunk-TWYPSP7P.js} +99 -7
- package/dist/chunk-TWYPSP7P.js.map +1 -0
- package/dist/{chunk-HKGZ2WHJ.js → chunk-WWK2SPN7.js} +16 -9
- package/dist/chunk-WWK2SPN7.js.map +1 -0
- package/dist/{chunk-HOGDFLCG.js → chunk-XPWAEYOO.js} +449 -59
- package/dist/chunk-XPWAEYOO.js.map +1 -0
- package/dist/{client-llB6XpHS.d.ts → client-YFKVt4p7.d.ts} +10 -21
- package/dist/client.d.ts +4 -4
- package/dist/client.js +1 -1
- package/dist/conventions/index.d.ts +114 -0
- package/dist/conventions/index.js +95 -0
- package/dist/conventions/index.js.map +1 -0
- package/dist/{events-D_P24UaY.d.ts → events-B3XPPu0c.d.ts} +23 -1
- package/dist/{index-sGgV8JNu.d.ts → index-CL0KmlyW.d.ts} +10 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +5 -5
- package/dist/{interface-DCdddCe0.d.ts → interface-BPz138Hf.d.ts} +110 -2
- package/dist/kernel/index.d.ts +6 -6
- package/dist/kernel/index.js +2 -2
- package/dist/kernel/testing/index.d.ts +3 -3
- package/dist/persistence/index.d.ts +2 -2
- package/dist/persistence/index.js +2 -2
- package/dist/persistence/prisma/index.d.ts +2 -2
- package/dist/persistence/prisma/index.js +2 -2
- package/dist/{plugins-Oyo_iu0l.d.ts → plugins-zT-aIcEZ.d.ts} +63 -4
- package/dist/{ports-ChGnJcn2.d.ts → ports-DUL4hlQr.d.ts} +11 -2
- package/dist/{stage-_7BKqqUG.d.ts → stage-WuK0mfrC.d.ts} +81 -1
- package/dist/testing/index.d.ts +8 -1
- package/dist/testing/index.js +88 -2
- package/dist/testing/index.js.map +1 -1
- package/package.json +6 -1
- package/skills/workflow-engine/SKILL.md +58 -1
- package/skills/workflow-engine/migrations/README.md +275 -0
- package/skills/workflow-engine/migrations/migrate-0.7-to-0.8.md +528 -0
- package/skills/workflow-engine/references/10-annotations.md +479 -0
- package/dist/chunk-2MWO6UVR.js.map +0 -1
- package/dist/chunk-DIADEUGZ.js.map +0 -1
- package/dist/chunk-HKGZ2WHJ.js.map +0 -1
- package/dist/chunk-HOGDFLCG.js.map +0 -1
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
# Migrating from 0.7 to 0.8
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
`0.8.0` introduces **annotations** — a first-class, queryable surface for attaching provenance to workflow runs and stages. It replaces the ad-hoc pattern of stuffing context into `WorkflowRun.metadata`, with a uniform attach/query API inspired by OpenTelemetry semantic conventions. No breaking API changes; the only deprecation is the existing `WorkflowRun.metadata` field, which still works in 0.8.
|
|
6
|
+
|
|
7
|
+
If you don't use `WorkflowRun.metadata` and don't care about provenance recording, you can upgrade with **only the schema migration step** below — everything else is opt-in.
|
|
8
|
+
|
|
9
|
+
## Notes on conventions
|
|
10
|
+
|
|
11
|
+
- **Scalar / array value convention is documented, not enforced.** The `value` column accepts any JSON. Nested objects work but are discouraged for well-known keys (they break `keyPrefix` query semantics). Use the `payload` slot for rich blobs.
|
|
12
|
+
|
|
13
|
+
## Required actions
|
|
14
|
+
|
|
15
|
+
### 1. Schema migration: add `WorkflowAnnotation` table
|
|
16
|
+
|
|
17
|
+
Add the following model to your Prisma schema:
|
|
18
|
+
|
|
19
|
+
```prisma
|
|
20
|
+
model WorkflowAnnotation {
|
|
21
|
+
id String @id @default(cuid())
|
|
22
|
+
createdAt DateTime @default(now())
|
|
23
|
+
|
|
24
|
+
workflowRunId String
|
|
25
|
+
workflowRun WorkflowRun @relation(fields: [workflowRunId], references: [id], onDelete: Cascade)
|
|
26
|
+
|
|
27
|
+
workflowStageRecordId String?
|
|
28
|
+
workflowStage WorkflowStage? @relation(fields: [workflowStageRecordId], references: [id], onDelete: SetNull)
|
|
29
|
+
attempt Int @default(0)
|
|
30
|
+
|
|
31
|
+
scope String
|
|
32
|
+
scopeId String?
|
|
33
|
+
|
|
34
|
+
actorKind String?
|
|
35
|
+
actorId String?
|
|
36
|
+
actorVersion String?
|
|
37
|
+
|
|
38
|
+
key String
|
|
39
|
+
value Json
|
|
40
|
+
payload Json?
|
|
41
|
+
|
|
42
|
+
idempotencyKey String?
|
|
43
|
+
|
|
44
|
+
@@unique([workflowRunId, key, idempotencyKey])
|
|
45
|
+
@@index([workflowRunId, key])
|
|
46
|
+
@@index([workflowRunId, createdAt])
|
|
47
|
+
@@index([workflowRunId, scope])
|
|
48
|
+
@@index([workflowRunId, actorId])
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
You'll also need to add the back-relations to your existing models:
|
|
53
|
+
|
|
54
|
+
```prisma
|
|
55
|
+
model WorkflowRun {
|
|
56
|
+
// ... existing fields ...
|
|
57
|
+
annotations WorkflowAnnotation[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
model WorkflowStage {
|
|
61
|
+
// ... existing fields ...
|
|
62
|
+
annotations WorkflowAnnotation[]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then run your migration:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Postgres / SQLite via Prisma
|
|
70
|
+
pnpm prisma migrate dev --name add_workflow_annotations
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. (Drive-by) Add missing `metadata` column to `AICall` if you copied the README schema before 0.8
|
|
74
|
+
|
|
75
|
+
`0.8` ships a documentation fix: the `AICall` Prisma schema in earlier README versions omitted `metadata Json?`, even though the code wrote to it (since 0.5). If you copy-pasted that schema, your existing `AICall` rows are missing rich metadata (`temperature`, `finishReason`, `durationMs`, tool details, etc.).
|
|
76
|
+
|
|
77
|
+
Add the column:
|
|
78
|
+
|
|
79
|
+
```prisma
|
|
80
|
+
model AICall {
|
|
81
|
+
// ... existing fields ...
|
|
82
|
+
metadata Json?
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then migrate:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pnpm prisma migrate dev --name aicall_add_metadata_column
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Note: this is additive. Existing rows will have `null` metadata; new rows will be populated.
|
|
93
|
+
|
|
94
|
+
## New features
|
|
95
|
+
|
|
96
|
+
### Annotations: attach provenance from stages
|
|
97
|
+
|
|
98
|
+
Inside a stage's `execute()`, use `ctx.annotate()` to record decisions, observations, or anything else you want queryable later:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const classifyUrgency = defineStage({
|
|
102
|
+
id: "classify-urgency",
|
|
103
|
+
// ... schemas ...
|
|
104
|
+
async execute(ctx) {
|
|
105
|
+
const { object: ai } = await aiHelper.generateObject(...);
|
|
106
|
+
|
|
107
|
+
const usedFallback = ai.confidence < 0.6;
|
|
108
|
+
const urgency = usedFallback ? keywordHeuristic(ctx.input.text) : ai.urgency;
|
|
109
|
+
|
|
110
|
+
ctx.annotate({
|
|
111
|
+
actor: { kind: "agent", id: "triage-agent", version: "v3" },
|
|
112
|
+
attributes: {
|
|
113
|
+
"decision.outcome": urgency,
|
|
114
|
+
"decision.rationale": usedFallback
|
|
115
|
+
? "AI confidence < 0.6 — fell back to keyword heuristic"
|
|
116
|
+
: "AI classification accepted",
|
|
117
|
+
"decision.confidence": ai.confidence,
|
|
118
|
+
"decision.alternatives": ["low", "medium", "high"],
|
|
119
|
+
"decision.used_fallback": usedFallback,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return { output: { urgency } };
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Writes are **buffered and flushed atomically with the stage completion transaction** — if the stage fails, annotations recorded before the failure still persist; if a suspend/resume race rolls back the stage, annotations roll back too.
|
|
129
|
+
|
|
130
|
+
### Annotations: attach trigger context at `run.create`
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
await kernel.dispatch({
|
|
134
|
+
type: "run.create",
|
|
135
|
+
workflowId: "ticket-triage",
|
|
136
|
+
input: { ticket },
|
|
137
|
+
annotations: [{
|
|
138
|
+
actor: { kind: "system", id: "zendesk-integration" },
|
|
139
|
+
attributes: {
|
|
140
|
+
"trigger.source": "webhook:zendesk",
|
|
141
|
+
"trigger.parent_run_id": previousRunId,
|
|
142
|
+
"trigger.reason": "auto-triage on ticket create",
|
|
143
|
+
},
|
|
144
|
+
}],
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Annotations: query later
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
// Everything attached to a run
|
|
152
|
+
const all = await kernel.annotations.list(runId);
|
|
153
|
+
|
|
154
|
+
// Just decisions
|
|
155
|
+
const decisions = await kernel.annotations.list(runId, { keyPrefix: "decision." });
|
|
156
|
+
|
|
157
|
+
// Just things a specific agent recorded
|
|
158
|
+
const trail = await kernel.annotations.list(runId, { actorId: "triage-agent" });
|
|
159
|
+
|
|
160
|
+
// Time-ranged
|
|
161
|
+
const recent = await kernel.annotations.list(runId, {
|
|
162
|
+
since: new Date(Date.now() - 3600_000),
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Annotations: typed keys via the conventions module
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { Decision, Trigger } from "@bratsos/workflow-engine/conventions";
|
|
170
|
+
|
|
171
|
+
// Compile-time typo protection + value-type linkage
|
|
172
|
+
ctx.annotate(Decision.outcome.key, "low"); // ✅ value typed as JsonValue
|
|
173
|
+
ctx.annotate(Decision.confidence.key, 0.42); // ✅ value typed as number
|
|
174
|
+
ctx.annotate(Decision.confidence.key, "high"); // ❌ type error — expected number
|
|
175
|
+
|
|
176
|
+
// Custom keys remain free-form
|
|
177
|
+
ctx.annotate("acme.compliance.signoff", "alice@acme.com");
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The conventions module ships well-known keys for `Trigger.*`, `Decision.*`, `Approval.*`, `Revision.*`. See `references/10-annotations.md` for the full list and conventions for naming custom keys.
|
|
181
|
+
|
|
182
|
+
### Annotations: external attach (plugins, post-hoc reviews)
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
await kernel.annotations.attach(workflowRunId, {
|
|
186
|
+
actor: { kind: "user", id: "alice@acme.com" },
|
|
187
|
+
attributes: { "review.disposition": "approved-anyway" },
|
|
188
|
+
idempotencyKey: "review-2026-05-24-alice",
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Annotations: rerun attempt disambiguation
|
|
193
|
+
|
|
194
|
+
`run.rerunFrom` now assigns a fresh `attempt` value to recreated stages. Annotations from the prior run survive (with their original `attempt`) and new annotations carry the new value, so a future agent can disambiguate decisions across rerun generations:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// First run → annotations with attempt=0
|
|
198
|
+
ctx.annotate(Decision.outcome, "low");
|
|
199
|
+
|
|
200
|
+
// After run.rerunFrom and re-execution → annotations with attempt=1
|
|
201
|
+
ctx.annotate(Decision.outcome, "high");
|
|
202
|
+
|
|
203
|
+
// Query just the latest attempt
|
|
204
|
+
await kernel.annotations.list(runId, { attempt: 1 });
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Annotations: opt-in outbox emission
|
|
208
|
+
|
|
209
|
+
When a plugin or external system wants real-time notifications on annotation writes (audit pipelines, SIEM, live dashboards), pass `emitEvent: true`. The engine writes an `annotation:created` outbox event in the same transaction as the annotation row, plugged into the same delivery machinery as `stage:completed` and friends. Off by default.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
ctx.annotate("decision.outcome", "low", { emitEvent: true });
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Migrating from pre-0.8 provenance patterns
|
|
216
|
+
|
|
217
|
+
Annotations replace several ad-hoc patterns that 0.7-and-earlier consumers used to stuff "why" context into the engine. None of these patterns are removed in 0.8 — the migration is opportunistic, not forced. Move callsites as you touch them.
|
|
218
|
+
|
|
219
|
+
### Pattern: `ctx.log("INFO", "...", { decision: "low", confidence: 0.42 })` for decisions
|
|
220
|
+
|
|
221
|
+
**Before**: structured log entries served double duty as "what happened" and "what was decided."
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// Pre-0.8
|
|
225
|
+
await ctx.log("INFO", "classified as low urgency", {
|
|
226
|
+
confidence: 0.42,
|
|
227
|
+
usedFallback: true,
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**After**: keep logs for narrative; move structured facts to annotations.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// 0.8+
|
|
235
|
+
await ctx.log("INFO", "classified as low urgency");
|
|
236
|
+
ctx.annotate({
|
|
237
|
+
attributes: {
|
|
238
|
+
"decision.outcome": "low",
|
|
239
|
+
"decision.confidence": 0.42,
|
|
240
|
+
"decision.used_fallback": true,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Why**: logs are time-ordered narrative and aren't indexed by key. You can't ask "show me every decision in this run" via logs — you'd grep through `message` fields. Annotations are key-addressable and queryable across runs.
|
|
246
|
+
|
|
247
|
+
### Pattern: stuffing trigger context into `WorkflowRun.metadata`
|
|
248
|
+
|
|
249
|
+
**Before**: `metadata` was the only place to record who triggered the run.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// Pre-0.8
|
|
253
|
+
await kernel.dispatch({
|
|
254
|
+
type: "run.create",
|
|
255
|
+
...,
|
|
256
|
+
metadata: {
|
|
257
|
+
triggeredBy: "alice",
|
|
258
|
+
source: "webhook",
|
|
259
|
+
parentRunId: "run_abc",
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**After**: use the `annotations` array.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// 0.8+
|
|
268
|
+
await kernel.dispatch({
|
|
269
|
+
type: "run.create",
|
|
270
|
+
...,
|
|
271
|
+
annotations: [{
|
|
272
|
+
actor: { kind: "user", id: "alice" },
|
|
273
|
+
attributes: {
|
|
274
|
+
"trigger.source": "webhook",
|
|
275
|
+
"trigger.parent_run_id": "run_abc",
|
|
276
|
+
},
|
|
277
|
+
}],
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Why**: the new path lands inside the same transaction as `createRun`, is queryable by key prefix (`trigger.*`), and the legacy `metadata` column is auto-projected as `legacy.metadata.*` until you migrate — no breakage.
|
|
282
|
+
|
|
283
|
+
### Pattern: stuffing decision context into stage `outputData`
|
|
284
|
+
|
|
285
|
+
**Before**: consumers sometimes mixed the stage's "real output" with decision metadata to avoid losing it.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
// Pre-0.8 — output schema bloat
|
|
289
|
+
return {
|
|
290
|
+
output: {
|
|
291
|
+
urgency: "low", // the actual output
|
|
292
|
+
_meta_aiConfidence: 0.42, // hidden provenance
|
|
293
|
+
_meta_usedFallback: true,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**After**: clean output, annotations carry the "why."
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
// 0.8+
|
|
302
|
+
ctx.annotate({
|
|
303
|
+
attributes: {
|
|
304
|
+
"decision.outcome": "low",
|
|
305
|
+
"decision.confidence": 0.42,
|
|
306
|
+
"decision.used_fallback": true,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
return { output: { urgency: "low" } };
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Why**: stage output is the contract between stages. Polluting it with provenance creates schema noise, makes downstream stages care about things they shouldn't, and makes outputs hard to reuse across workflows. Annotations are out-of-band — downstream stages never see them.
|
|
313
|
+
|
|
314
|
+
### Pattern: using outbox `causationId` to trace cross-stage provenance
|
|
315
|
+
|
|
316
|
+
**Before**: consumers correlated outbox events by `causationId` to reconstruct what an agent decided.
|
|
317
|
+
|
|
318
|
+
**After**: keep outbox events for state transitions (`stage:started`/`completed`/`failed`) — they remain the source of truth for *what happened*. Use annotations for *why* it happened. The two layers compose: events tell you a stage completed; annotations tell you what the agent decided during that stage.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// 0.8+ — typical reconstruction query
|
|
322
|
+
const events = await persistence.getUnpublishedOutboxEvents(...); // state transitions
|
|
323
|
+
const annotations = await kernel.annotations.list(runId); // decisions/facts
|
|
324
|
+
// Join in your app code by stageId/scopeId.
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Pattern: `WorkflowArtifact.metadata` for non-output stage data
|
|
328
|
+
|
|
329
|
+
**Before**: consumers sometimes wrote small structured data to `WorkflowArtifact.metadata` to avoid the BlobStore round-trip.
|
|
330
|
+
|
|
331
|
+
**After**: artifacts remain the right place for *content* (large outputs, embeddings, model responses, debug traces). Annotations are for *labels and facts you'll query later*. If the data is small and you want it indexed by key, prefer annotations. If it's a blob you want attached to a specific artifact key, keep using `WorkflowArtifact`.
|
|
332
|
+
|
|
333
|
+
### Pattern: relying on `WorkflowLog.metadata` for queryable structured logging
|
|
334
|
+
|
|
335
|
+
**Before**: `WorkflowLog.metadata` is a Json column on each log entry. Consumers occasionally treated it as a searchable index.
|
|
336
|
+
|
|
337
|
+
**After**: `WorkflowLog.metadata` stays — it's intrinsic to the log entry, useful for debugging context attached to a specific log line. But it's not indexed by key, isn't cross-DB queryable, and shouldn't be used as a provenance store. If you find yourself querying log metadata to ask "what did this agent decide?", that's a sign to move it to annotations.
|
|
338
|
+
|
|
339
|
+
## Deprecations
|
|
340
|
+
|
|
341
|
+
### `WorkflowRun.metadata` is deprecated
|
|
342
|
+
|
|
343
|
+
The `metadata` parameter on `RunCreateCommand` is marked `@deprecated` in 0.8. It still works — and reading `WorkflowRun.metadata` from the database row still works — but new code should use the `annotations` array instead.
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
// Before (still works in 0.8, will be removed in 1.0)
|
|
347
|
+
await kernel.dispatch({
|
|
348
|
+
type: "run.create",
|
|
349
|
+
workflowId: "x",
|
|
350
|
+
input: {...},
|
|
351
|
+
metadata: { triggeredBy: "alice", source: "webhook" },
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// After (recommended)
|
|
355
|
+
await kernel.dispatch({
|
|
356
|
+
type: "run.create",
|
|
357
|
+
workflowId: "x",
|
|
358
|
+
input: {...},
|
|
359
|
+
annotations: [{
|
|
360
|
+
actor: { kind: "user", id: "alice" },
|
|
361
|
+
attributes: { "trigger.source": "webhook" },
|
|
362
|
+
}],
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`kernel.annotations.list(runId)` automatically materializes legacy `metadata` as virtual `legacy.metadata.*` annotations in query results, so you can migrate reads incrementally.
|
|
367
|
+
|
|
368
|
+
#### Optional: eagerly migrate existing rows
|
|
369
|
+
|
|
370
|
+
For most consumers, the lazy read-shim above is sufficient — old rows surface as `legacy.metadata.*` on demand, no data migration required. If you operate at a volume where you'd rather drop the per-`list()` `getRun()` overhead, or you want to eventually remove the `metadata` column, you can copy existing rows into persisted annotations once.
|
|
371
|
+
|
|
372
|
+
There's no shipped utility for this — the right migration depends on what you put in `metadata` and whether you want to also re-key into proper conventions (`trigger.*`, `decision.*`, etc.). Below is a starting point you can adapt:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
// One-time migration. Idempotent — safe to re-run on the same rows.
|
|
376
|
+
async function copyLegacyMetadataToAnnotations(
|
|
377
|
+
kernel: Kernel,
|
|
378
|
+
prisma: PrismaClient,
|
|
379
|
+
batchSize = 100,
|
|
380
|
+
) {
|
|
381
|
+
let migrated = 0;
|
|
382
|
+
let skipped = 0;
|
|
383
|
+
|
|
384
|
+
while (true) {
|
|
385
|
+
const runs = await prisma.workflowRun.findMany({
|
|
386
|
+
where: {
|
|
387
|
+
metadata: { not: null },
|
|
388
|
+
// Skip runs that already have legacy.metadata.* persisted
|
|
389
|
+
annotations: { none: { key: { startsWith: "legacy.metadata." } } },
|
|
390
|
+
},
|
|
391
|
+
take: batchSize,
|
|
392
|
+
select: { id: true, metadata: true },
|
|
393
|
+
});
|
|
394
|
+
if (runs.length === 0) break;
|
|
395
|
+
|
|
396
|
+
for (const run of runs) {
|
|
397
|
+
// Only object-shaped metadata can be projected to keys.
|
|
398
|
+
// Primitives, arrays, and null are skipped.
|
|
399
|
+
if (
|
|
400
|
+
typeof run.metadata !== "object" ||
|
|
401
|
+
run.metadata === null ||
|
|
402
|
+
Array.isArray(run.metadata)
|
|
403
|
+
) {
|
|
404
|
+
skipped++;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const attributes: Record<string, unknown> = {};
|
|
409
|
+
for (const [k, v] of Object.entries(run.metadata)) {
|
|
410
|
+
// OPTIONAL: replace this verbatim copy with your own mapping
|
|
411
|
+
// — e.g., remap `metadata.user` → `trigger.actor.id` instead
|
|
412
|
+
// of `legacy.metadata.user`. The library can't do this for you;
|
|
413
|
+
// it depends on your domain.
|
|
414
|
+
attributes[`legacy.metadata.${k}`] = v;
|
|
415
|
+
}
|
|
416
|
+
if (Object.keys(attributes).length === 0) {
|
|
417
|
+
skipped++;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await kernel.annotations.attach(run.id, {
|
|
422
|
+
attributes,
|
|
423
|
+
idempotencyKey: `legacy-migrate-${run.id}`,
|
|
424
|
+
});
|
|
425
|
+
migrated++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { migrated, skipped };
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Once a run has any persisted `legacy.metadata.*` annotation, the read-side shim stops synthesizing for that run — so this script must produce the complete set of keys per run, not a partial migration. If your `metadata` values are non-object (a string, number, or array), the script can't project them into keys; you'll need to handle those rows by hand or leave them on the column.
|
|
433
|
+
|
|
434
|
+
## Bug fixes
|
|
435
|
+
|
|
436
|
+
### Stage failure path now persists pre-failure side effects
|
|
437
|
+
|
|
438
|
+
Previously, if a stage threw an exception mid-execution, any `ctx.log(...)` calls made before the throw were preserved (fire-and-forget), but there was no mechanism for the stage to record *why* it was failing — the error message went only to `WorkflowStage.errorMessage`.
|
|
439
|
+
|
|
440
|
+
With annotations in 0.8, you can attribute structured failure context that persists in the failure transaction:
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
async execute(ctx) {
|
|
444
|
+
try {
|
|
445
|
+
return await doWork();
|
|
446
|
+
} catch (err) {
|
|
447
|
+
ctx.annotate({
|
|
448
|
+
attributes: {
|
|
449
|
+
"failure.kind": classifyError(err),
|
|
450
|
+
"failure.retryable": isRetryable(err),
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
The annotation is buffered before the throw and flushed in the failure-path transaction (Phase 3b in `job-execute.ts`).
|
|
459
|
+
|
|
460
|
+
### Cancellation tightened across stage Phase 3 transactions
|
|
461
|
+
|
|
462
|
+
Before 0.8, `run.cancel` could commit between the kernel's outer ghost check and a stage's Phase 3 transaction, letting stage updates and outbox events commit against an already-cancelled run. 0.8 adds an in-transaction status guard: if cancel committed before the Phase 3 transaction starts, the transaction throws and rolls back atomically (stage update + annotations + outbox events). Same protection added to the suspended-stage poll path. The race window is meaningfully narrower than before. A residual microsecond-scale window remains under READ COMMITTED isolation when cancel commits during the Phase 3 transaction itself — this is general engine behavior, not annotation-specific, and may be closed in a future release. No code change required.
|
|
463
|
+
|
|
464
|
+
### AICall metadata documentation gap fixed
|
|
465
|
+
|
|
466
|
+
The README's Prisma schema for `AICall` had been missing the `metadata` column since 0.5, even though the AI helper wrote to it. The documentation is now corrected. See the **Required actions** section above for the corresponding schema update.
|
|
467
|
+
|
|
468
|
+
## Code examples — common patterns
|
|
469
|
+
|
|
470
|
+
### Pattern: trigger annotation at the entrypoint of a chain of runs
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
async function rerunFromAlert(originalRunId: string, alertId: string) {
|
|
474
|
+
return kernel.dispatch({
|
|
475
|
+
type: "run.create",
|
|
476
|
+
workflowId: "x",
|
|
477
|
+
input: {...},
|
|
478
|
+
annotations: [{
|
|
479
|
+
actor: { kind: "system", id: "alerting" },
|
|
480
|
+
attributes: {
|
|
481
|
+
"trigger.source": "alert",
|
|
482
|
+
"trigger.parent_run_id": originalRunId,
|
|
483
|
+
"trigger.reason": `Alert ${alertId} detected anomaly`,
|
|
484
|
+
},
|
|
485
|
+
}],
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Walk back the chain later
|
|
490
|
+
async function findChainRoot(runId: string): Promise<string> {
|
|
491
|
+
const trigger = await kernel.annotations.list(runId, { keyPrefix: "trigger." });
|
|
492
|
+
const parent = trigger.find(a => a.key === "trigger.parent_run_id")?.value as string | undefined;
|
|
493
|
+
return parent ? findChainRoot(parent) : runId;
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Pattern: durable decision audit for compliance
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
ctx.annotate({
|
|
501
|
+
actor: { kind: "agent", id: "compliance-screener", version: "v2.1" },
|
|
502
|
+
attributes: {
|
|
503
|
+
"approval.approvers": ["alice", "bob"], // plural → array
|
|
504
|
+
"approval.timestamp": new Date().toISOString(),
|
|
505
|
+
"approval.policy.version": "v3.2",
|
|
506
|
+
},
|
|
507
|
+
payload: {
|
|
508
|
+
fullReviewArtifact: { ... }, // rich blob, not indexed
|
|
509
|
+
},
|
|
510
|
+
idempotencyKey: `approval-${ctx.workflowRunId}-${reviewId}`,
|
|
511
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Pattern: cross-stage trail via the timeline index
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
// Get all annotations from a run in time order
|
|
518
|
+
const timeline = await kernel.annotations.list(runId);
|
|
519
|
+
// Already sorted by createdAt thanks to the (workflowRunId, createdAt) index.
|
|
520
|
+
|
|
521
|
+
for (const a of timeline) {
|
|
522
|
+
console.log(`[${a.createdAt.toISOString()}] ${a.actorKind}:${a.actorId} — ${a.key} = ${JSON.stringify(a.value)}`);
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Reference
|
|
527
|
+
|
|
528
|
+
For the full design rationale, see `docs/RFC-ANNOTATIONS.md` in the package source. The skill reference doc `references/10-annotations.md` covers the API surface in depth.
|