feat: shell execution correctness + artifact_field_equals condition

- Add  field to ProcessResultArtifact ("success"/"failed")
- Materialize ToolResult.Failure as a failed ProcessResult artifact
  in the orchestrator, with artifact lifecycle events emitted
- Non-recoverable ToolResult.Failure now fails the stage ("FATAL:")
  instead of being silently fed back as LLM context
- Fix enterStage to return Continue on non-retryable failures,
  enabling back-edge transitions (fix/retry loops)
- Add ArtifactFieldEquals transition condition with flat and
  nested dot-notation field access + FieldOperator enum
  (eq, neq, gt, gte, lt, lte)
- Wire artifact_field_equals through ConditionFactory and
  TomlWorkflowLoader (condition_field, condition_operator)
- Cache ProcessResult artifact content for evaluation context
- Add 16 tests covering all condition operators and error cases
- DefaultTransitionResolver catches condition evaluation errors
  and returns Blocked instead of crashing
This commit is contained in:
2026-05-26 17:28:51 +04:00
parent 9734eec63c
commit 3b24c7e91a
11 changed files with 304 additions and 10 deletions
@@ -5,8 +5,10 @@ import com.correx.core.transitions.conditions.AllOf
import com.correx.core.transitions.conditions.AlwaysTrue
import com.correx.core.transitions.conditions.AnyOf
import com.correx.core.transitions.conditions.ArtifactAbsent
import com.correx.core.transitions.conditions.ArtifactFieldEquals
import com.correx.core.transitions.conditions.ArtifactPresent
import com.correx.core.transitions.conditions.ArtifactValidated
import com.correx.core.transitions.conditions.FieldOperator
import com.correx.core.transitions.conditions.Not
import com.correx.core.transitions.conditions.VariableEquals
import com.correx.core.transitions.graph.TransitionCondition
@@ -17,6 +19,7 @@ fun ConditionSpec.toCondition(): TransitionCondition = when (type) {
"artifact_absent" -> buildArtifactAbsent()
"artifact_validated" -> buildArtifactValidated()
"variable_equals" -> buildVariableEquals()
"artifact_field_equals" -> buildArtifactFieldEquals()
"all_of" -> AllOf(conditions.map { it.toCondition() })
"any_of" -> AnyOf(conditions.map { it.toCondition() })
"not" -> Not((condition ?: error("condition required for not")).toCondition())
@@ -37,3 +40,11 @@ private fun ConditionSpec.buildVariableEquals() =
key ?: error("key required for variable_equals"),
value ?: error("value required for variable_equals"),
)
private fun ConditionSpec.buildArtifactFieldEquals() =
ArtifactFieldEquals(
artifactId = ArtifactId(artifactId ?: error("artifact_id required for artifact_field_equals")),
field = field ?: error("field required for artifact_field_equals"),
value = value ?: error("value required for artifact_field_equals"),
operator = operator?.let { FieldOperator.fromString(it) } ?: FieldOperator.EQ,
)
@@ -5,6 +5,8 @@ data class ConditionSpec(
val artifactId: String? = null,
val key: String? = null,
val value: String? = null,
val field: String? = null,
val operator: String? = null,
val conditions: List<ConditionSpec> = emptyList(),
val condition: ConditionSpec? = null,
)
@@ -49,6 +49,8 @@ private data class TransitionSection(
@param:JsonProperty("condition_artifact_id") val conditionArtifactId: String? = null,
@param:JsonProperty("condition_key") val conditionKey: String? = null,
@param:JsonProperty("condition_value") val conditionValue: String? = null,
@param:JsonProperty("condition_field") val conditionField: String? = null,
@param:JsonProperty("condition_operator") val conditionOperator: String? = null,
)
private const val TERMINAL = "done"
@@ -112,6 +114,8 @@ class TomlWorkflowLoader(
artifactId = t.conditionArtifactId,
key = t.conditionKey,
value = t.conditionValue,
field = t.conditionField,
operator = t.conditionOperator,
).toCondition(),
)
}.toSet()