@bpmnkit/engine 0.1.29 → 0.1.31

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
@@ -9,7 +9,7 @@
9
9
  [![ai-assisted](https://img.shields.io/badge/AI--assisted-claude-8b5cf6?style=flat-square)](https://github.com/bpmnkit/monorepo)
10
10
  [![experimental](https://img.shields.io/badge/status-experimental-f59e0b?style=flat-square)](https://github.com/bpmnkit/monorepo)
11
11
 
12
- [Website](https://bpmnkit.com) · [Documentation](https://docs.bpmnkit.com) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/engine/CHANGELOG.md)
12
+ [Website](https://bpmnkit.com) · [Documentation](https://bpmnkit.com/docs) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/engine/CHANGELOG.md)
13
13
  </div>
14
14
 
15
15
  ---
@@ -117,10 +117,12 @@ const instance = engine.start("my-process", {}, {
117
117
  | [`@bpmnkit/plugins`](https://www.npmjs.com/package/@bpmnkit/plugins) | 22 composable canvas plugins |
118
118
  | [`@bpmnkit/api`](https://www.npmjs.com/package/@bpmnkit/api) | Camunda 8 REST API TypeScript client |
119
119
  | [`@bpmnkit/ascii`](https://www.npmjs.com/package/@bpmnkit/ascii) | Render BPMN diagrams as Unicode ASCII art |
120
+ | [`@bpmnkit/docspack`](https://www.npmjs.com/package/@bpmnkit/docspack) | BPMN Kit docs as an offline docspack package for AI agents |
120
121
  | [`@bpmnkit/ui`](https://www.npmjs.com/package/@bpmnkit/ui) | Shared design tokens and UI components |
121
122
  | [`@bpmnkit/profiles`](https://www.npmjs.com/package/@bpmnkit/profiles) | Shared auth, profile storage, and client factories for CLI & proxy |
122
123
  | [`@bpmnkit/operate`](https://www.npmjs.com/package/@bpmnkit/operate) | Monitoring & operations frontend for Camunda clusters |
123
124
  | [`@bpmnkit/connector-gen`](https://www.npmjs.com/package/@bpmnkit/connector-gen) | Generate connector templates from OpenAPI specs |
125
+ | [`@bpmnkit/connectors`](https://www.npmjs.com/package/@bpmnkit/connectors) | Camunda 8 OOTB connector catalog and deterministic template application |
124
126
  | [`@bpmnkit/cli`](https://www.npmjs.com/package/@bpmnkit/cli) | Camunda 8 command-line interface (casen) |
125
127
  | [`@bpmnkit/proxy`](https://www.npmjs.com/package/@bpmnkit/proxy) | Local AI bridge and Camunda API proxy server |
126
128
  | [`@bpmnkit/patterns`](https://www.npmjs.com/package/@bpmnkit/patterns) | Domain process patterns for BPMNKit AIKit |
package/dist/dmn.js CHANGED
@@ -9,8 +9,14 @@ export function evaluateDecision(decision, vars) {
9
9
  return null;
10
10
  const hitPolicy = table.hitPolicy ?? "UNIQUE";
11
11
  const matchedOutputs = [];
12
+ // Input expressions do not depend on the rule, so evaluate each column once
13
+ // instead of once per rule.
14
+ const inputValues = table.inputs.map((col) => {
15
+ const inputExpr = col.inputExpression.text ?? "";
16
+ return inputExpr.trim() === "" ? null : evalExpression(inputExpr, vars);
17
+ });
12
18
  for (const rule of table.rules) {
13
- if (ruleMatches(table.inputs, rule.inputEntries, vars)) {
19
+ if (ruleMatches(table.inputs, inputValues, rule.inputEntries, vars)) {
14
20
  const output = {};
15
21
  for (let i = 0; i < table.outputs.length; i++) {
16
22
  const col = table.outputs[i];
@@ -27,7 +33,7 @@ export function evaluateDecision(decision, vars) {
27
33
  }
28
34
  return buildResult(hitPolicy, table.aggregation, matchedOutputs, table.outputs.length);
29
35
  }
30
- function ruleMatches(inputs, inputEntries, vars) {
36
+ function ruleMatches(inputs, inputValues, inputEntries, vars) {
31
37
  const feelVars = vars;
32
38
  for (let i = 0; i < inputs.length; i++) {
33
39
  const col = inputs[i];
@@ -36,8 +42,7 @@ function ruleMatches(inputs, inputEntries, vars) {
36
42
  continue;
37
43
  if (entry.text.trim() === "")
38
44
  continue; // empty = "any"
39
- const inputExpr = col.inputExpression.text ?? "";
40
- const inputValue = inputExpr.trim() === "" ? null : evalExpression(inputExpr, vars);
45
+ const inputValue = inputValues[i] ?? null;
41
46
  const parsed = parseUnaryTests(entry.text);
42
47
  if (parsed.ast === null)
43
48
  continue;
@@ -9,6 +9,10 @@ export declare class ProcessInstance {
9
9
  private _error;
10
10
  /** tokenId → Token */
11
11
  private readonly allTokens;
12
+ /** elementId → tokens currently sitting on that element. */
13
+ private readonly tokensByElement;
14
+ /** Zeebe extensions parsed once per element for the life of this instance. */
15
+ private readonly zeebeExtCache;
12
16
  /** Scope stack: rootScopeId + any active sub-process scopes */
13
17
  private readonly scopes;
14
18
  /** Message correlation: messageName → resolve callback */
@@ -46,6 +50,9 @@ export declare class ProcessInstance {
46
50
  deliverMessage(messageName: string): void;
47
51
  private buildScopeCtx;
48
52
  private createToken;
53
+ /** The first token currently on `elementId`, if any. */
54
+ private tokenOn;
55
+ private zeebeExt;
49
56
  private removeToken;
50
57
  /** elementId → set of incomingFlowIds received */
51
58
  private readonly joins;
package/dist/instance.js CHANGED
@@ -13,6 +13,10 @@ export class ProcessInstance {
13
13
  _error;
14
14
  /** tokenId → Token */
15
15
  allTokens = new Map();
16
+ /** elementId → tokens currently sitting on that element. */
17
+ tokensByElement = new Map();
18
+ /** Zeebe extensions parsed once per element for the life of this instance. */
19
+ zeebeExtCache = new WeakMap();
16
20
  /** Scope stack: rootScopeId + any active sub-process scopes */
17
21
  scopes = new Map();
18
22
  /** Message correlation: messageName → resolve callback */
@@ -78,6 +82,7 @@ export class ProcessInstance {
78
82
  this._state = "terminated";
79
83
  this.cancelAllTimers();
80
84
  this.allTokens.clear();
85
+ this.tokensByElement.clear();
81
86
  for (const ctx of this.scopes.values())
82
87
  ctx.tokens.clear();
83
88
  }
@@ -141,11 +146,39 @@ export class ProcessInstance {
141
146
  createToken(elementId, scopeId) {
142
147
  const token = { id: generateId("tok"), elementId, scopeId };
143
148
  this.allTokens.set(token.id, token);
149
+ const onElement = this.tokensByElement.get(elementId);
150
+ if (onElement)
151
+ onElement.add(token);
152
+ else
153
+ this.tokensByElement.set(elementId, new Set([token]));
144
154
  this.scopes.get(scopeId)?.tokens.add(token.id);
145
155
  return token;
146
156
  }
157
+ /** The first token currently on `elementId`, if any. */
158
+ tokenOn(elementId) {
159
+ const onElement = this.tokensByElement.get(elementId);
160
+ if (onElement === undefined)
161
+ return undefined;
162
+ for (const token of onElement)
163
+ return token;
164
+ return undefined;
165
+ }
166
+ zeebeExt(el) {
167
+ let ext = this.zeebeExtCache.get(el);
168
+ if (ext === undefined) {
169
+ ext = parseZeebeExt(el.extensionElements);
170
+ this.zeebeExtCache.set(el, ext);
171
+ }
172
+ return ext;
173
+ }
147
174
  removeToken(token) {
148
175
  this.allTokens.delete(token.id);
176
+ const onElement = this.tokensByElement.get(token.elementId);
177
+ if (onElement !== undefined) {
178
+ onElement.delete(token);
179
+ if (onElement.size === 0)
180
+ this.tokensByElement.delete(token.elementId);
181
+ }
149
182
  this.scopes.get(token.scopeId)?.tokens.delete(token.id);
150
183
  this.timerCancels.get(token.id)?.();
151
184
  this.timerCancels.delete(token.id);
@@ -186,7 +219,7 @@ export class ProcessInstance {
186
219
  }
187
220
  this.emit({ type: "element:entering", elementId, elementName: el.name, elementType: el.type });
188
221
  // Apply ioMapping inputs
189
- const ext = parseZeebeExt(el.extensionElements);
222
+ const ext = this.zeebeExt(el);
190
223
  if (ext.ioMapping) {
191
224
  for (const inp of ext.ioMapping.inputs) {
192
225
  let val;
@@ -204,12 +237,16 @@ export class ProcessInstance {
204
237
  val = resolved;
205
238
  }
206
239
  }
207
- else {
240
+ else if (inp.source.trimStart().startsWith("=")) {
208
241
  val = this.evalFeel(inp.source, scopeId, {
209
242
  elementId: el.id,
210
243
  property: `input:${inp.target}`,
211
244
  });
212
245
  }
246
+ else {
247
+ // A zeebe:input source without a leading "=" is a literal value, not FEEL.
248
+ val = inp.source;
249
+ }
213
250
  this.variables.setLocal(scopeId, inp.target, val);
214
251
  this.emit({ type: "variable:set", name: inp.target, value: val, scopeId });
215
252
  }
@@ -262,8 +299,21 @@ export class ProcessInstance {
262
299
  case "transaction":
263
300
  await this.handleSubProcess(token, el, ctx);
264
301
  break;
302
+ case "adHocSubProcess":
303
+ if (ext.taskDefinition) {
304
+ // Job-worker implementation (e.g. the AI Agent Sub-process connector) —
305
+ // dispatch as a job so scenarios can mock it like any other task.
306
+ // The tools nested inside are not individually executed; see the
307
+ // scenario runner docs for scope.
308
+ await this.handleJobTask(token, el, ext, ctx);
309
+ }
310
+ else {
311
+ // BPMN-native ad-hoc sub-process — auto-complete (tools not executed).
312
+ await this.complete(token, ctx);
313
+ }
314
+ break;
265
315
  default:
266
- // eventSubProcess, adHocSubProcess, callActivity, etc. — auto-complete
316
+ // eventSubProcess, callActivity, etc. — auto-complete
267
317
  await this.complete(token, ctx);
268
318
  break;
269
319
  }
@@ -273,12 +323,11 @@ export class ProcessInstance {
273
323
  const eventDef = el.eventDefinitions[0];
274
324
  if (eventDef?.type === "terminate") {
275
325
  this.cancelAllTimers();
276
- for (const [id] of this.allTokens) {
277
- const tok = this.allTokens.get(id);
278
- if (tok !== undefined)
279
- this.scopes.get(tok.scopeId)?.tokens.delete(id);
326
+ for (const tok of this.allTokens.values()) {
327
+ this.scopes.get(tok.scopeId)?.tokens.delete(tok.id);
280
328
  }
281
329
  this.allTokens.clear();
330
+ this.tokensByElement.clear();
282
331
  this.emit({
283
332
  type: "element:leaving",
284
333
  elementId: el.id,
@@ -414,7 +463,7 @@ export class ProcessInstance {
414
463
  });
415
464
  return;
416
465
  }
417
- const result = evaluateDecision(decision, this.variables.getAll(scopeId));
466
+ const result = evaluateDecision(decision, this.variables.snapshot(scopeId));
418
467
  this.variables.set(scopeId, cd.resultVariable, result);
419
468
  this.emit({ type: "variable:set", name: cd.resultVariable, value: result, scopeId });
420
469
  }
@@ -536,12 +585,9 @@ export class ProcessInstance {
536
585
  const cancel = scheduleTimer(timerDef, () => {
537
586
  this.boundaryTimerCancels.delete(elementId);
538
587
  // Remove the parent token
539
- for (const [, tok] of this.allTokens) {
540
- if (tok.elementId === elementId) {
541
- this.removeToken(tok);
542
- break;
543
- }
544
- }
588
+ const parentToken = this.tokenOn(elementId);
589
+ if (parentToken !== undefined)
590
+ this.removeToken(parentToken);
545
591
  if (be.cancelActivity !== false) {
546
592
  void this.activate(be.id, scopeId, undefined);
547
593
  }
@@ -562,12 +608,9 @@ export class ProcessInstance {
562
608
  continue;
563
609
  if (errDef.errorRef !== undefined && errDef.errorRef !== errorCode)
564
610
  continue;
565
- for (const [, tok] of this.allTokens) {
566
- if (tok.elementId === attachedTo) {
567
- this.removeToken(tok);
568
- break;
569
- }
570
- }
611
+ const hostToken = this.tokenOn(attachedTo);
612
+ if (hostToken !== undefined)
613
+ this.removeToken(hostToken);
571
614
  void this.activate(be.id, ctx.scopeId, undefined);
572
615
  return;
573
616
  }
@@ -591,7 +634,7 @@ export class ProcessInstance {
591
634
  const el = ctx.elements.get(token.elementId);
592
635
  if (el === undefined)
593
636
  return;
594
- const ext = parseZeebeExt(el.extensionElements);
637
+ const ext = this.zeebeExt(el);
595
638
  // Apply ioMapping outputs
596
639
  if (ext.ioMapping) {
597
640
  for (const out of ext.ioMapping.outputs) {
@@ -659,14 +702,15 @@ export class ProcessInstance {
659
702
  }
660
703
  // ── FEEL helpers ───────────────────────────────────────────────────────────
661
704
  evalFeel(expr, scopeId, emitCtx) {
662
- const vars = this.variables.getAll(scopeId);
705
+ const vars = this.variables.snapshot(scopeId);
663
706
  // Strip Camunda FEEL prefix ("= expr") — the leading "=" is a type indicator, not part of the expression.
664
707
  const normalized = expr.trim().replace(/^=\s*/, "");
665
708
  const parsed = parseExpression(normalized);
666
709
  if (parsed.ast === null)
667
710
  return undefined;
668
711
  const result = evaluate(parsed.ast, { vars: vars });
669
- if (emitCtx !== undefined) {
712
+ // The event carries a copy of every variable; skip building it when nobody listens.
713
+ if (emitCtx !== undefined && this.listeners.length > 0) {
670
714
  this.emit({
671
715
  type: "feel:evaluated",
672
716
  elementId: emitCtx.elementId,
@@ -6,6 +6,11 @@
6
6
  export declare class VariableStore {
7
7
  private readonly scopes;
8
8
  private readonly parents;
9
+ /**
10
+ * Merged root → scope views, built lazily and then kept current by every
11
+ * write, so expression evaluation never re-merges the scope chain.
12
+ */
13
+ private readonly snapshots;
9
14
  createScope(id: string, parentId?: string): void;
10
15
  removeScope(id: string): void;
11
16
  /** Walk up the chain and return the value, or undefined if not found. */
@@ -17,8 +22,16 @@ export declare class VariableStore {
17
22
  set(scopeId: string, name: string, value: unknown): void;
18
23
  /** Set a variable in this scope only, regardless of parent state. */
19
24
  setLocal(scopeId: string, name: string, value: unknown): void;
20
- /** Return all variables merged from root → this scope (child wins). */
25
+ /** Return a fresh copy of all variables merged from root → this scope (child wins). */
21
26
  getAll(scopeId: string): Record<string, unknown>;
27
+ /**
28
+ * All variables merged from root → this scope, as a shared object that
29
+ * later writes update in place. Callers must treat it as read-only and not
30
+ * hold it across a write.
31
+ */
32
+ snapshot(scopeId: string): Record<string, unknown>;
33
+ /** Store the value and patch every cached view that sees this scope's copy of `name`. */
34
+ private write;
22
35
  private hasOwn;
23
36
  private ancestorHas;
24
37
  }
package/dist/variables.js CHANGED
@@ -6,6 +6,11 @@
6
6
  export class VariableStore {
7
7
  scopes = new Map();
8
8
  parents = new Map();
9
+ /**
10
+ * Merged root → scope views, built lazily and then kept current by every
11
+ * write, so expression evaluation never re-merges the scope chain.
12
+ */
13
+ snapshots = new Map();
9
14
  createScope(id, parentId) {
10
15
  this.scopes.set(id, new Map());
11
16
  if (parentId !== undefined) {
@@ -15,6 +20,7 @@ export class VariableStore {
15
20
  removeScope(id) {
16
21
  this.scopes.delete(id);
17
22
  this.parents.delete(id);
23
+ this.snapshots.delete(id);
18
24
  }
19
25
  /** Walk up the chain and return the value, or undefined if not found. */
20
26
  get(scopeId, name) {
@@ -34,7 +40,7 @@ export class VariableStore {
34
40
  */
35
41
  set(scopeId, name, value) {
36
42
  if (this.hasOwn(scopeId, name)) {
37
- this.scopes.get(scopeId)?.set(name, value);
43
+ this.write(scopeId, name, value);
38
44
  return;
39
45
  }
40
46
  const parentId = this.parents.get(scopeId);
@@ -42,25 +48,55 @@ export class VariableStore {
42
48
  this.set(parentId, name, value);
43
49
  return;
44
50
  }
45
- this.scopes.get(scopeId)?.set(name, value);
51
+ this.write(scopeId, name, value);
46
52
  }
47
53
  /** Set a variable in this scope only, regardless of parent state. */
48
54
  setLocal(scopeId, name, value) {
49
- this.scopes.get(scopeId)?.set(name, value);
55
+ this.write(scopeId, name, value);
50
56
  }
51
- /** Return all variables merged from root → this scope (child wins). */
57
+ /** Return a fresh copy of all variables merged from root → this scope (child wins). */
52
58
  getAll(scopeId) {
59
+ return { ...this.snapshot(scopeId) };
60
+ }
61
+ /**
62
+ * All variables merged from root → this scope, as a shared object that
63
+ * later writes update in place. Callers must treat it as read-only and not
64
+ * hold it across a write.
65
+ */
66
+ snapshot(scopeId) {
67
+ const cached = this.snapshots.get(scopeId);
68
+ if (cached !== undefined)
69
+ return cached;
53
70
  const parentId = this.parents.get(scopeId);
54
- const parentVars = parentId !== undefined ? this.getAll(parentId) : {};
71
+ const parentVars = parentId !== undefined ? this.snapshot(parentId) : {};
55
72
  const scope = this.scopes.get(scopeId);
56
73
  if (scope === undefined)
57
74
  return parentVars;
58
75
  const result = { ...parentVars };
59
- for (const [k, v] of scope) {
76
+ for (const [k, v] of scope)
60
77
  result[k] = v;
61
- }
78
+ this.snapshots.set(scopeId, result);
62
79
  return result;
63
80
  }
81
+ /** Store the value and patch every cached view that sees this scope's copy of `name`. */
82
+ write(scopeId, name, value) {
83
+ const scope = this.scopes.get(scopeId);
84
+ if (scope === undefined)
85
+ return;
86
+ scope.set(name, value);
87
+ for (const [viewId, view] of this.snapshots) {
88
+ // Walk from the view's scope up to the writer; a scope in between that
89
+ // owns the name shadows the write for this view.
90
+ let current = viewId;
91
+ while (current !== undefined && current !== scopeId) {
92
+ if (this.scopes.get(current)?.has(name))
93
+ break;
94
+ current = this.parents.get(current);
95
+ }
96
+ if (current === scopeId)
97
+ view[name] = value;
98
+ }
99
+ }
64
100
  hasOwn(scopeId, name) {
65
101
  return this.scopes.get(scopeId)?.has(name) ?? false;
66
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/engine",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -21,11 +21,11 @@
21
21
  "dist/**/*.d.ts"
22
22
  ],
23
23
  "dependencies": {
24
- "@bpmnkit/core": "0.1.1",
25
- "@bpmnkit/feel": "0.0.19"
24
+ "@bpmnkit/core": "0.2.0",
25
+ "@bpmnkit/feel": "0.0.20"
26
26
  },
27
27
  "optionalDependencies": {
28
- "@bpmnkit/reebe-wasm": "0.1.5"
28
+ "@bpmnkit/reebe-wasm": "0.1.6"
29
29
  },
30
30
  "description": "Lightweight BPMN 2.0 process execution engine for browsers and Node.js — zero dependencies",
31
31
  "keywords": [