mixle.task.task_decomposition module

Task decomposition as structure learning (CARD L4): does the loop’s five altitudes.

A task is a joint over (inputs, proposed intermediates, output). “Decomposition” is not a heuristic split – it is DEPENDENCY-FOREST DISCOVERY over that joint, reusing the exact machinery mixle.inference.structure already built for heterogeneous records: dependency_gain() scores whether an edge (parent field -> child field) is worth its BIC complexity in nats, the same model-based, family-agnostic test used for category->real, count->binary, or any other pair. A task’s “is this decomposition good” question is the SAME question that module already answers for record fields – routing output through a candidate intermediate is exactly a dependency edge, and MDL gain (this module’s acceptance metric) IS the sum of dependency_gain() along the discovered edges, by construction: a good decomposition is one whose edges pay for their own complexity in compressed nats.

Three pieces of existing machinery are integrated here, not rebuilt:

  • mixle.inference.structure (dependency_gain / regression_gain / fit_linear_gaussian_edge) – scores and fits every edge (input -> intermediate, intermediate -> output) this module considers.

  • mixle.task.plan_model / mixle.task.outcome_decomposer – a “decomposition” (which intermediates were used, in what order) is exactly a “plan” (which tool types were used, in what order): PlanModel fits a distribution over decompositions the same way it fits one over tool sequences, and DecompositionProposer refits on high-outcome decompositions the same round-based way train_outcome_decomposer() refits a plan model on successful traces – so as (decomposition, outcome) pairs get logged from real task instances, FUTURE proposals shift toward what actually worked.

  • mixle.task.design_prior (record_accepted_recipe / rank_design_families) – a decomposed vs. monolithic recipe is recorded under a family tag on the existing design ledger, so which approach has actually won persists across tasks the same way a structural-family prior does elsewhere.

    forest = discover_decomposition(examples, candidate_intermediates) forest.chosen # [“m1”, “m2”], not [] (monolithic) – for a genuinely

    # decomposable task

    forest.mdl_gain # nats: positive means the decomposition compresses proposer = record_decomposition_outcome(proposer, forest.chosen, outcome) # trains the proposer

class DecompositionProposer(plan_model, log=<factory>)[source]

Bases: object

An outcome-trained proposer over decompositions: plan_model scores/samples which intermediates (in what order) to route a task’s output through, and shifts toward higher-outcome decompositions as they get logged – train_outcome_decomposer()’s refit-on-successes loop, applied to decomposition proposals instead of tool-call plans.

Parameters:
class DependencyForest(chosen, edge_gains, mdl_gain, edges=<factory>)[source]

Bases: object

A discovered decomposition of output: the ordered list of parent fields it was routed through (chosen, e.g. ["m1", "m2"]; empty means monolithic – no candidate intermediate or input cleared min_gain), each step’s own gain, and the total mdl_gain – the description-length gain (nats) of this decomposition over solving output directly from the raw inputs. Positive mdl_gain means the decomposition COMPRESSES; by construction it is the sum of the chosen edges’ own dependency_gain()/ regression_gain() scores.

Parameters:
predict(inputs, candidate_intermediates)[source]

Sum of each chosen edge’s prediction from its own parent field – the decomposed model’s point estimate, used to compare predictive accuracy against the monolithic baseline.

Parameters:
Return type:

float

class TaskExample(inputs, output)[source]

Bases: object

One observed instance of a task: named inputs and the realized output. The joint this module reasons over is (inputs, proposed_intermediates, output)proposed_intermediates are not stored here, they are RECOMPUTED per candidate by discover_decomposition() (a candidate intermediate is a function of inputs, not a fixed observed field).

Parameters:
discover_decomposition(task_examples, candidate_intermediates, *, max_parents=4, min_gain=0.0, max_its=30, seed=0)[source]

Discover which candidate intermediates output should be routed through, by greedy forward selection scored with regression_gain() / dependency_gain() – the SAME model-based description-length test learn_structure() uses for record fields, applied here per step against the current RESIDUAL so multiple intermediates (output = f(g(a), h(b))) can each earn their own edge, not just the single best one (a plain DependencyTreeDistribution forest allows one parent per field; a task’s output routinely needs several).

Every raw input is itself a candidate parent, so a task with NO real decomposable structure correctly comes back with chosen == [] (monolithic: the raw inputs already explain output as well as anything) rather than inventing intermediates that do not pay for themselves.

Parameters:
Return type:

DependencyForest

fit_decomposition(task_examples, decomposition, candidate_intermediates, *, max_its=30, seed=0)[source]

Fit a SPECIFIC, given decomposition (in order) rather than discovering one – every named field is forced in, in order, scored and residualized the same way discover_decomposition()’s forward selection does. Lets a caller both score (DependencyForest.mdl_gain) and predict with (DependencyForest.predict()) a decomposition it did not necessarily search for – e.g. a deliberately-worse candidate, for the MDL-gain/outcome correlation check.

Parameters:
Return type:

DependencyForest

init_decomposition_proposer(seed_decompositions)[source]

Fit the round-0 (imitation) proposer on a seed corpus of decompositions – e.g. every chosen a few discover_decomposition() calls returned on early task instances.

Parameters:

seed_decompositions (Sequence[Sequence[str]])

Return type:

DecompositionProposer

log_decomposition_recipe(design, mdl_gain, *, family)[source]

Record one decomposition attempt’s MDL gain into the existing design ledger under family ("decomposed" / MONOLITHIC) – a thin wrapper over record_accepted_recipe() so rank_design_families() and best_family() answer “has decomposing this kind of task actually paid off” from real history, the same what-worked prior every other structural family search uses.

Parameters:
  • design (DesignModel)

  • mdl_gain (float)

  • family (str)

Return type:

None

mdl_score(task_examples, decomposition, candidate_intermediates, *, max_its=30, seed=0)[source]

The MDL gain (nats) of routing output through a SPECIFIC, given decomposition – a thin accessor over fit_decomposition() for callers that only want the score (e.g. ranking several candidate decompositions for the MDL-gain/outcome correlation check).

Parameters:
Return type:

float

monolithic_predict(train, test)[source]

OLS fit of output on the raw inputs (every field jointly, closed form) – the “solve as one black box” baseline discover_decomposition() is compared against. Matched compute against the decomposed model: both are single closed-form linear solves over the same n examples.

Parameters:
Return type:

list[float]

record_decomposition_outcome(proposer, decomposition, outcome, *, success_quantile=0.6, min_log=4)[source]

Log one (decomposition, outcome) pair from a REAL task instance, and once at least min_log outcomes are on file, refit plan_model on the decompositions scoring at or above this round’s own success_quantile – literally train_outcome_decomposer()’s keep-the-successes-and-refit step, so future sample() calls favor what actually worked, not just what the seed corpus imitated.

Parameters:
Return type:

DecompositionProposer