{"record":{"id":"c76b541945075b07","repo":"conductor-oss/conductor","slug":"plan-source-must-include-a-non-empty-tool-field","errorCode":null,"errorMessage":"plan_source must include a non-empty 'tool' field","messagePattern":"plan_source must include a non-empty 'tool' field","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/compiler/MultiAgentCompiler.java","lineNumber":2751,"sourceCode":"        String plannerResult = AgentCompiler.coercedRef(plannerCoerceRef);\n\n        // ── 2b. Optional plan_source: deterministic tool call to read plan ──\n        // If planSource is configured, call the specified tool (e.g. contextbook_read)\n        // to retrieve the plan from an external source. This provides a deterministic\n        // fallback: even if the planner's text output fails extraction, the plan can\n        // be read directly from where the explorer wrote it.\n        //\n        // Validate at compile time that ``planSource.tool`` is a real tool registered\n        // somewhere in the harness — a typo is silently swallowed if we wait until\n        // runtime (the ``optional:true`` task simply doesn't run, extraction falls\n        // through to the no_plan branch). Reject the harness here so the misconfig\n        // surfaces at deploy.\n        String planReaderRef = null;\n        if (config.getPlanSource() != null) {\n            Map<String, Object> planSource = config.getPlanSource();\n            String toolName = (String) planSource.get(\"tool\");\n            if (toolName == null || toolName.isBlank()) {\n                throw new IllegalArgumentException(\n                        \"plan_source must include a non-empty 'tool' field\");\n            }\n            if (!isToolRegisteredInHarness(config, toolName)) {\n                throw new IllegalArgumentException(\n                        \"plan_source.tool '\"\n                                + toolName\n                                + \"' is not registered as a harness-level tool on '\"\n                                + config.getName()\n                                + \"'. The plan_reader task is emitted in the harness's task \"\n                                + \"namespace, so the tool must be declared in tools=[...] on the harness itself \"\n                                + \"(declaring it on a sub-agent does not work).\");\n            }\n            @SuppressWarnings(\"unchecked\")\n            Map<String, Object> toolArgs =\n                    (Map<String, Object>) planSource.getOrDefault(\"args\", Map.of());\n\n            planReaderRef = prefix + \"_plan_reader\";\n            WorkflowTask planReaderTask = new WorkflowTask();","sourceCodeStart":2733,"sourceCodeEnd":2769,"githubUrl":"https://github.com/conductor-oss/conductor/blob/cf7c3e4a8adfb158be778ab1ec525323c363cd3a/agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/compiler/MultiAgentCompiler.java#L2733-L2769","documentation":"Thrown when a PLAN_EXECUTE agent's planSource config (config.getPlanSource()) exists but its 'tool' field is null or blank. The planSource defines a fallback mechanism: if the planner's text output fails plan extraction, a SIMPLE task calls the named tool to read the plan from an external source (e.g., contextbook). Without a valid tool name, the plan_reader task can't be emitted.","triggerScenarios":"An AgentConfig with strategy=PLAN_EXECUTE where planSource is a non-null Map but either lacks a 'tool' key or the value of 'tool' is null/blank. For example planSource={\"args\": {...}} without a \"tool\" entry.","commonSituations":"Configuring planSource with only 'args' but forgetting the 'tool' key, or setting tool to an empty string. Also happens when planSource is constructed from a template that has a placeholder for the tool name that was never filled in.","solutions":["Add a 'tool' key to the planSource map with a non-blank value naming the tool that reads the plan.","Ensure the tool name matches a tool registered at the harness level (see error 31).","If you don't need a plan source, remove the planSource config entirely (null planSource is valid)."],"exampleFix":"// before\nplanSource = {\"args\": {\"doc\": \"plan.md\"}}\n// after\nplanSource = {\"tool\": \"read_skill_file\", \"args\": {\"doc\": \"plan.md\"}}","handlingStrategy":"validation","validationCode":"void validatePlanSource(AgentConfig config) {\n    Map<String, Object> ps = config.getPlanSource();\n    if (ps != null) {\n        String tool = (String) ps.get(\"tool\");\n        if (tool == null || tool.isBlank()) {\n            throw new IllegalArgumentException(\"plan_source must include a non-empty 'tool' field\");\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    compiler.compile(agentConfig);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"plan_source must include\")) {\n        // add \"tool\": \"<toolName>\" to the planSource map\n    }\n    throw e;\n}","preventionTips":["If planSource is set, always include the 'tool' key.","If you don't need a deterministic plan source, omit planSource entirely."],"tags":["plan-execute","plan-source","config-validation","agentspan"],"backgroundTag":null,"analyzedSha":"cf7c3e4a8adfb158be778ab1ec525323c363cd3a","analyzedAt":"2026-08-14T03:33:19.897Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}