vxcontrol/pentagi · error

invalid subtask patch: %w

Error message

invalid subtask patch: %w

What it means

The patch_flow_subtasks tool handler builds a SubtaskPatch from the LLM-provided operations and runs SubtaskPatch.Validate() before applying anything. Validation enforces per-operation-type invariants: add requires title and description; remove/modify/reorder require an id; modify requires at least title or description; and op must be one of add/remove/modify/reorder. Any violation aborts the whole patch with "invalid subtask patch: %w".

Source

Thrown at backend/pkg/tools/flow_manager.go:920

						"Call %s first, then retry",
					action.TaskID, st.ID, st.Title, StopFlowToolName))
			}
		}

		return "", stateGuard(fmt.Errorf(
			"no 'created' subtasks found for task %d; "+
				"all subtasks have been executed or the task has no plan yet. "+
				"Use %s to create a new task instead",
			action.TaskID, SubmitFlowInputToolName))
	}

	patch := SubtaskPatch{
		Operations: action.Operations,
		Message:    action.Message,
	}

	if err := patch.Validate(); err != nil {
		return "", fmt.Errorf("invalid subtask patch: %w", err)
	}

	if len(action.Operations) == 0 {
		return fmt.Sprintf("No operations provided — the subtask plan for task %d is unchanged.", action.TaskID), nil
	}

	if err := t.handler(ctx, action.TaskID, patch); err != nil {
		return "", fmt.Errorf("failed to patch subtasks for task %d: %w", action.TaskID, err)
	}

	// Query the new subtask list so the LLM can correlate the patched entries with their new IDs.
	newPlanned, err := t.db.GetTaskPlannedSubtasks(ctx, action.TaskID)
	if err != nil {
		// Not fatal — operations were applied; just warn and skip the list.
		return fmt.Sprintf(
			"%d operation(s) applied to the subtask plan for task %d. "+
				"Could not retrieve updated subtask list: %s. "+
				"Call %s with detail='planned' and task_id=%d to verify.",

View on GitHub (pinned to ea665308ba)

Solutions

  1. Read the wrapped cause (e.g. "operation 2: modify requires at least title or description") and fix that specific operation in the tool call arguments.
  2. Use only the enum values add, remove, modify, reorder for the op field.
  3. For add, supply both title and description; for remove/modify/reorder, always include the numeric subtask id copied from get_flow_status output.
  4. If no changes are needed, send an empty operations array (handled gracefully) instead of a malformed placeholder operation.

Example fix

// before
{"task_id": 3, "operations": [{"op": "update", "title": "Scan web app"}]}
// after
{"task_id": 3, "operations": [{"op": "add", "title": "Scan web app", "description": "Enumerate and scan the web application for vulnerabilities"}]}
Defensive patterns

Strategy: validation

Validate before calling

validOps := map[string]bool{"add": true, "remove": true, "modify": true, "reorder": true}
for i, op := range operations {
    if !validOps[string(op.Op)] {
        return fmt.Errorf("operation %d: unknown op %q", i, op.Op)
    }
    if op.Op == "add" && (op.Title == "" || op.Description == "") {
        return fmt.Errorf("operation %d: add requires title and description", i)
    }
    if (op.Op == "remove" || op.Op == "modify" || op.Op == "reorder") && op.ID == nil {
        return fmt.Errorf("operation %d: %s requires id", i, op.Op)
    }
    if op.Op == "modify" && op.Title == "" && op.Description == "" {
        return fmt.Errorf("operation %d: modify requires title or description", i)
    }
}

Try / catch

if _, err := toolHandle(ctx, action); err != nil {
    var invalid *fmt.WrapError // or: strings.Contains(err.Error(), "invalid subtask patch")
    if strings.HasPrefix(err.Error(), "invalid subtask patch:") {
        // do NOT retry as-is; log err and regenerate a schema-conformant patch
    }
}

Prevention

When it happens

Trigger: Calling patch_flow_subtasks where operations[i] has: an unknown op value (typo like "update" or "delete" instead of "modify"/"remove"); an add without title or without description; a remove/modify/reorder missing the id field; or a modify that sets neither title nor description. The failing operation index and reason are embedded in the wrapped message.

Common situations: An LLM emits "op": "delete" or "move" instead of the enum names; the model omits id because it assumed positional indexing; add operations produced with empty description because the model only wrote a title; hand-crafted JSON in tests that predates the Validate() rules.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/0de6a8ef5e8ed61f. Report an issue: GitHub.