{"record":{"id":"616815530fd46f0b","repo":"conductor-oss/conductor","slug":"composite-termination-must-have-at-least-one-sub-c","errorCode":null,"errorMessage":"Composite termination must have at least one sub-condition","messagePattern":"Composite termination must have at least one sub-condition","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/compiler/TerminationCompiler.java","lineNumber":413,"sourceCode":"                + \") { exceeded = true; reason = 'Prompt token limit exceeded'; }\"\n                + \"    if (\"\n                + maxCompletion\n                + \" > 0 && (tokenUsed.completion_tokens || 0) > \"\n                + maxCompletion\n                + \") { exceeded = true; reason = 'Completion token limit exceeded'; }\"\n                + \"    return {should_continue: !exceeded, reason: reason};\"\n                + \"  })();\";\n    }\n\n    /**\n     * Build an inline composite sub-condition. Recursively resolves nested composites. Uses a\n     * unique prefix to avoid variable name collisions in deeply nested composites.\n     */\n    private static String buildInlineComposite(\n            TerminationConfig config, String varName, boolean isAnd, int parentIndex) {\n        List<TerminationConfig> conditions = config.getConditions();\n        if (conditions == null || conditions.isEmpty()) {\n            throw new IllegalArgumentException(\n                    \"Composite termination must have at least one sub-condition\");\n        }\n\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"  var \").append(varName).append(\" = (function() {\");\n        sb.append(\"    var results = [];\");\n\n        for (int i = 0; i < conditions.size(); i++) {\n            String nestedVar = \"s\" + parentIndex + \"_\" + i;\n            TerminationConfig sub = conditions.get(i);\n            String nestedBody = buildNestedSubConditionBody(sub, nestedVar, parentIndex, i);\n            sb.append(nestedBody);\n            sb.append(\"    results.push(\").append(nestedVar).append(\");\");\n        }\n\n        if (isAnd) {\n            sb.append(\"    var allTerminate = true;\");\n            sb.append(\"    var reasons = [];\");","sourceCodeStart":395,"sourceCodeEnd":431,"githubUrl":"https://github.com/conductor-oss/conductor/blob/cf7c3e4a8adfb158be778ab1ec525323c363cd3a/agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/compiler/TerminationCompiler.java#L395-L431","documentation":"Thrown by buildInlineComposite() when a nested composite (an 'and'/'or' inside another composite) has a null or empty conditions list. This is the same validation as error 34 but for the inline/nested compilation path. The compiler recurses into nested composites, and each level must have at least one sub-condition.","triggerScenarios":"A composite TerminationConfig that contains a nested composite (type='and' or 'or') whose own conditions list is null or empty. For example, an 'and' containing an 'or' that has no conditions.","commonSituations":"Building deeply nested termination conditions and forgetting to populate an inner composite's conditions, or a nested composite whose conditions were removed during config refactoring but the composite entry was left behind.","solutions":["Find the nested composite (and/or) inside the conditions list that has no sub-conditions.","Populate that nested composite's conditions with at least one TerminationConfig.","If the nested composite is unnecessary, remove it from the parent's conditions list."],"exampleFix":"// before: nested \"or\" has empty conditions\nTerminationConfig.builder()\n    .type(\"and\")\n    .conditions(List.of(\n        TerminationConfig.builder().type(\"max_message\").maxMessages(10).build(),\n        TerminationConfig.builder().type(\"or\").conditions(List.of()).build()))  // empty nested!\n    .build();\n// after\nTerminationConfig.builder()\n    .type(\"and\")\n    .conditions(List.of(\n        TerminationConfig.builder().type(\"max_message\").maxMessages(10).build(),\n        TerminationConfig.builder().type(\"or\").conditions(List.of(\n            TerminationConfig.builder().type(\"text_mention\").text(\"STOP\").build(),\n            TerminationConfig.builder().type(\"token_usage\").maxTotalTokens(8000).build())).build()))\n    .build();","handlingStrategy":"validation","validationCode":"void validateNestedComposites(TerminationConfig root) {\n    validateComposite(root);\n    if (root.getConditions() != null) {\n        for (TerminationConfig sub : root.getConditions()) {\n            if (Set.of(\"and\", \"or\").contains(sub.getType())) {\n                validateNestedComposites(sub);  // recurse\n            }\n        }\n    }\n}","typeGuard":"static boolean allNestedCompositesValid(TerminationConfig config) {\n    if (Set.of(\"and\", \"or\").contains(config.getType())) {\n        if (config.getConditions() == null || config.getConditions().isEmpty()) return false;\n        return config.getConditions().stream().allMatch(NestedValidator::allNestedCompositesValid);\n    }\n    return true;\n}","tryCatchPattern":"try {\n    String script = TerminationCompiler.buildTerminationScript(rootConfig);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"Composite termination must have at least one\")) {\n        // find the empty nested composite and populate it or remove it\n    }\n    throw e;\n}","preventionTips":["Recursively validate all nested composites — not just the top level.","Remove unused empty composites from conditions lists.","Write unit tests that exercise deeply nested termination trees."],"tags":["termination","composite","nested","config-validation","agentspan"],"backgroundTag":null,"analyzedSha":"cf7c3e4a8adfb158be778ab1ec525323c363cd3a","analyzedAt":"2026-08-14T03:33:19.897Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}