Reason/Plan: Choosing Between Competing Drafts
For new readers
This page documents one component of the self-model reference implementation, a working PatLang system that models cognitive processes for study — it does not itself reason or plan in any phenomenal sense. "Reason/Plan" here names a specific, narrow job: turning a percept into a candidate response for downstream selection, run as several independent, disagreeing instances rather than one arbitrated instance.
What Reason/Plan does
Reason/Plan sits between Perception and Action in the pipeline. It reads every percept published on one or more Perception writer-topics, and for each new percept publishes a candidate action to its own writer-topic, for Action to pick up later. For the walking-skeleton milestone the decision itself is deliberately the simplest possible symbolic step — rp_prompt_from_percept passes the percept's own content straight through as the prompt Action should respond to, pulling out a raw field if the percept parsed cleanly or an intent field otherwise, and falling back to the raw percept text if neither is present. The component's own header in components/reason_plan.patlang is explicit that later milestones replace this tick with real evaluation against Representation, Episodic Memory, and Imagination — the shared queue and signals plumbing described below does not change when that happens.
Multiple Drafts, not single-pass verification
Requirements Spec Section 4.1 requires Perception and Reason/Plan to run as multiple independently-computed instances competing for uptake on the shared queue, per Dennett's Multiple Drafts model (cited in full on the architecture page). The requirement is explicit about what this is not: "this is not a redundancy-for-correctness mechanism — contrast PatLang's own three-execution-path parity checking, which checks agreement on a single correct answer." That contrast is worth holding onto carefully, because it is easy to get backwards.
PatLang the language project runs every feature through three separate execution paths — interpreted, natively compiled, and self-hosted-compiled — and treats disagreement between them as a bug: the whole point is that all three must converge on byte-identical output before a feature counts as done. Reason/Plan's multiple instances are built the opposite way round. When two Perception instances publish deliberately conflicting percepts for what a verification scheme would treat as "the same moment," both must be processed into independent candidates — disagreement between drafts is expected and informative, not an error condition. There is no single correct draft for Reason/Plan's instances to converge on, and nothing in the design tries to make them agree.
features/parallel_drafts.feature states the contrast directly in its own header and tests it concretely: a percept claiming "it is definitely raining" published on one Perception writer-topic, and a conflicting percept claiming "it is definitely NOT raining" published on another, must produce two independent candidates, neither suppressed nor merged — one candidate's prompt reflecting the first percept, the other reflecting the second. A second scenario runs the full scaled topology (two Perception instances and two Reason/Plan instances together) and checks that sending "What is 2 plus 2?" to Perception instance 1 makes both reason_plan_1 and reason_plan_2 independently produce a candidate for it, that Action eventually produces at least one reply from whichever candidates arrive, and that every spawned component can be shut down cleanly with a quit signal.
Why Reason/Plan uses a non-consuming read, not a queue claim
The component's header documents a real bug this requirement forced, not a design choice made in the abstract. Reason/Plan reads percepts with lib/topic_observer.patlang's cursor-tracked, non-consuming obs_new_rows — never qh_claim_one/queue_ack, the single-consumer, ack-based claim pattern used elsewhere in this system (for example by Action reading candidates). The header explains why this was confirmed necessary directly: Short Term and every Reason/Plan instance are all independent readers of the same percept topics, and Requirement 4.1 requires every instance to see every percept, not have them partitioned across instances. With two Reason/Plan instances both acking the same topic, whichever won the race marked a message "acked," and it silently vanished from queue_pending for the other. The bug stayed invisible until Short Term became a third independent reader of the same topics and started missing percepts outright — Milestone 3's own test had only asserted "at least one candidate," loose enough to pass under the race by accident. obs_new_rows gives each named observer (reason_plan_1, reason_plan_2, and so on, built from rp_observer_name) its own cursor into the same durable topic, so every reader genuinely sees every row, with no shared "claimed" state to race over.
Instrumentation and lifecycle
Reason/Plan is launched as pat --ir-run components/reason_plan.patlang <port> <instance_id> <perception_instance_ids_comma_separated> — the comma-separated list of Perception instance ids is how a given Reason/Plan instance's rp_source_topics is built, one queue_writer_topic("percept", id) per source. Like every component in this project it composes lib/component_base.patlang and lib/instrumentation.patlang rather than inheriting from a shared base class. A status query is answered by rp_status_text, reporting the last candidate produced through the same instr_snapshot wire format every component in this system shares (Requirements Spec Section 6.1), so an external auditor reads it the same way it reads any other component's status.
See also
Reason/Plan's candidates flow to Action, which reads across every Reason/Plan writer-topic the same way Reason/Plan itself reads across every Perception writer-topic. See the architecture overview for the Dennett citation and the full component inventory, and safety and ethics for the trust-gating that governs how Action treats a candidate once it has one.