You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(core): parallel branch yields with consensus-aware merge (#60)
- Introduce yields(String...) on parallel branches for structured domain output via the inject→execute→extract pipeline.
- Rewrite ConsensusEvaluator to use structured BranchResult data, fixing bugs in heuristic-based vote extraction.
- Extract AgentLifecycleRunner to unify agent execution across StandardNodeExecutor and ParallelNodeExecutor, restoring missing listener events on branches.
Resolve: #59
Signed-off-by: Aleksandr Suvorov <asuvorov@hensu.io>
-`ApprovalTransition` - Routes on the `approved` boolean engine variable (written via `writes("approved")`); falls through if absent or non-boolean
160
+
-`ApprovalTransition` - Routes on the `approved` boolean engine variable (injected by `ApprovalVariableInjector`); falls through if absent or non-boolean
158
161
-`AlwaysTransition` - Unconditional
159
162
160
163
### State Schema
@@ -374,6 +377,11 @@ CRUD operations, UPSERT semantics, FK constraints, tenant isolation, and seriali
374
377
-`hensu-core/.../workflow/state/WorkflowStateSchema.java` - Typed state variable schema (optional per-workflow declaration)
375
378
-`hensu-core/.../workflow/state/StateVariableDeclaration.java` - Single variable declaration (name, type, isInput)
376
379
-`hensu-core/.../workflow/state/VarType.java` - Variable type enum (STRING, NUMBER, BOOLEAN, LIST_STRING)
380
+
-`hensu-core/.../execution/EngineVariables.java` - SSOT for engine-managed variable names (score, approved, recommendation)
381
+
-`hensu-core/.../execution/SynchronizedListenerDecorator.java` - Thread-safe listener wrapper for parallel branches
|`PlanSnapshot`|`HensuSnapshot.planSnapshot`| Nested record inside `HensuSnapshot`|
243
245
|`PlanSnapshot.PlannedStepSnapshot`|`PlanSnapshot.steps()`| Nested record inside `PlanSnapshot`|
244
246
|`PlanSnapshot.StepResultSnapshot`|`PlanSnapshot.results()`| Nested record inside `PlanSnapshot`|
245
247
248
+
**Parallel execution types** (manually deserialized in `NodeDeserializer`, but serialized via default Jackson `BeanSerializer` in `WorkflowSerializer.toJson()`):
|`ScoreCondition`|`ScoreTransition` condition list | Nested record inside transition rules |
259
+
|`DoubleRange`|`ScoreCondition.range()`| Nested record inside `ScoreCondition`|
260
+
261
+
> **Why not `treeToValue`?** These types are simple records with primitive/string/enum fields.
262
+
> Manual `JsonNode` extraction in `NodeDeserializer` avoids reflection during deserialization.
263
+
> Registration is needed only for the **serialization** path (`WorkflowSerializer.toJson()`),
264
+
> where Jackson's default `BeanSerializer` reads component accessors reflectively.
265
+
266
+
> **`Branch.yields`** is a `List<String>`. `NodeDeserializer` extracts it by iterating the JSON
267
+
> array node manually — no `treeToValue` or `convertValue` needed for `List<String>`.
268
+
246
269
Unlike the `treeToValue` pattern, these records do **not** need a custom deserializer or mixin. Registration alone is sufficient because Jackson's built-in record support handles the canonical constructor mapping.
1.**`@JsonPOJOBuilder` mixin targets** — Jackson instantiates the builder via its private no-arg constructor, calls each setter, then calls `build()`. GraalVM cannot trace these calls through the generic mixin machinery.
1274
1274
1275
1275
2.**`treeToValue` delegation** — When a custom deserializer calls `mapper.treeToValue(node, SomeClass.class)`, Jackson uses POJO reflection for `SomeClass`. Simple records (primitives, strings, enums only) should be **fixed** by switching to manual `JsonNode` extraction instead. Register only types where manual extraction is impractical (e.g., nested `Duration` fields).
1276
1276
1277
-
3.**Record types embedded in builder classes** — When a `record` is a field inside a mixin-registered builder type, Jackson reaches it via its canonical constructor and component accessors. GraalVM cannot trace those calls statically. Register the record and every nested record transitively. No mixin or custom deserializer is needed — registration alone is sufficient.
1277
+
3.**Manual deser, default Jackson ser** — Types deserialized manually in `NodeDeserializer` (direct `JsonNode` extraction) but serialized via Jackson's default `BeanSerializer` in `WorkflowSerializer.toJson()`. The deserialization path needs no reflection, but the serialization path reads record component accessors reflectively. Current types: `Branch`, `ConsensusConfig`, `ConsensusStrategy`, `ConsensusResult`, `ConsensusResult.Vote`, `ConsensusResult.VoteType`, `ScoreCondition`, `DoubleRange`.
1278
1278
1279
-
**When to add vs. fix:** if the class is a simple record with no `Duration`/nested-complex fields, fix the deserializer. If it contains `Duration` or deeply nested types, add it here. For records embedded in builder types, always register them. See [hensu-serialization Developer Guide](developer-guide-serialization.md#the-treetovalue-rule) for the full rule.
1279
+
4.**Simple immutable types with custom deser but default ser** — `WorkflowStateSchema` and `StateVariableDeclaration` use a custom deserializer (`WorkflowStateSchemaDeserializer`) that extracts fields manually. However, Jackson's default serializer reads `getVariables()` reflectively, so both classes must be registered.
1280
+
1281
+
5.**Record types embedded in builder classes** — When a `record` is a field inside a mixin-registered builder type, Jackson reaches it via its canonical constructor and component accessors. GraalVM cannot trace those calls statically. Register the record and every nested record transitively. No mixin or custom deserializer is needed — registration alone is sufficient. Current types: `ReviewConfig`, `HensuSnapshot`, `PlanSnapshot` hierarchy.
1282
+
1283
+
**When to add vs. fix:** if the class is a simple record with no `Duration`/nested-complex fields, fix the deserializer. If it contains `Duration` or deeply nested types, add it here. For records embedded in builder types, always register them. For types in pattern 3, registration is needed only because the serialization path uses default Jackson. See [hensu-serialization Developer Guide](developer-guide-serialization.md#the-treetovalue-rule) for the full rule.
0 commit comments