diff --git a/docs/rca/rca-lisp-repair-async.org b/docs/rca/rca-lisp-repair-async.org new file mode 100644 index 0000000..d200af2 --- /dev/null +++ b/docs/rca/rca-lisp-repair-async.org @@ -0,0 +1,33 @@ +#+TITLE: Root Cause Analysis: Asynchronous Lisp Repair Syntax Gate +#+DATE: 2026-04-11 +#+FILETAGS: :rca:lisp:repair:decoupling:architecture:psf: + +* Executive Summary +Reimplemented the `org-skill-lisp-repair` to align with the "Sovereign Boundary" mandate. The previously synchronous, core-blocking repair logic has been replaced with an asynchronous, event-driven architecture using the Reactive Signal Pipeline. + +* 1. Issue: Core Bloat & Synchronous Coupling +** Symptoms +The initial implementation of the Lisp Repair gate placed a `handler-case` and a dynamic function call (`repair-lisp-syntax`) directly inside the core `think` function (`neuro.lisp`). This forced the core to wait for repairs and made it "aware" of specific repair logic. +** Root Cause +Architectural shortcutting. By placing repair logic in the core execution path, we violated the microkernel principle which mandates that the core should be a "dumb" signal processor. +** Resolution +1. **Refactored Core:** `think` now only emits a `:syntax-error` stimulus if parsing fails. It no longer attempts to repair. +2. **Asynchronous Skill:** `skill-lisp-repair` now triggers on the `:syntax-error` event. It performs the repair and returns the corrected action, which is then dispatched by the pipeline. + +* 2. Side-Issue: Nested Signal Payloads +** Symptoms +`TYPE-ERROR` during testing when extracting the broken code from the stimulus. +** Root Cause +Mismatched expectations of signal nesting. The skill expected the code at `(getf context :payload)`, but in the `decide-gate`, `context` is the full signal, and the error details were nested inside the `:candidate` field of that signal. +** Resolution +Updated the symbolic logic to correctly traverse the nested signal structure: `(getf (getf context :candidate) :payload)`. + +* 3. PSF Mandate Alignment +** Sovereign Boundary +The core is now strictly a parser. Repair is an optional, user-space service. +** Reactive Signal Pipeline +Leveraged the pipeline's ability to re-inject `EVENT` signals to flatten the recursion of the repair loop. + +* 4. Permanent Learnings +- **Emit, Don't Call:** In a microkernel, if a non-fatal error occurs, always emit a signal rather than calling a recovery function. This allows the system to remain asynchronous and modular. +- **Signal Inspection:** When writing symbolic gates, always verify the exact shape of the `context` signal being passed by the kernel to avoid nesting errors. diff --git a/literate/neurosymbolic.org b/literate/neurosymbolic.org index c25f05f..5b2257a 100644 --- a/literate/neurosymbolic.org +++ b/literate/neurosymbolic.org @@ -175,7 +175,13 @@ To call a tool, you MUST use: (let ((regs (nth-value 1 (cl-ppcre:scan-to-strings "(?s)```(?:lisp)?\\n?(.*?)\\n?```" raw-thought)))) (if (and regs (> (length regs) 0)) (elt regs 0) raw-thought)) (string-trim '(#\Space #\Newline #\Tab) raw-thought)))) - (suggestion (ignore-errors (read-from-string cleaned-thought)))) + (suggestion (handler-case (read-from-string cleaned-thought) + (error (c) + ;; EMIT ASYNCHRONOUS REPAIR STIMULUS + (list :type :EVENT :payload + (list :sensor :syntax-error + :code cleaned-thought + :error (format nil "~a" c))))))) (kernel-log "SYSTEM 1 Suggestion: ~a~%" cleaned-thought) (when (and suggestion (listp suggestion)) (push suggestion suggestions)))) diff --git a/org-agent.asd b/org-agent.asd index 87d3f5f..a4280ec 100644 --- a/org-agent.asd +++ b/org-agent.asd @@ -16,6 +16,7 @@ (:file "src/symbolic") (:file "src/safety-harness") (:file "src/self-fix") + (:file "src/lisp-repair") (:file "src/core")) :build-operation "program-op" :build-pathname "org-agent-server" @@ -32,6 +33,7 @@ (:file "tests/immune-system-tests") (:file "tests/task-orchestrator-tests") (:file "tests/self-fix-tests") + (:file "tests/lisp-repair-tests") (:file "tests/chaos-qa")) :perform (test-op (o s) (uiop:symbol-call :fiveam :run! (uiop:find-symbol* :oacp-suite :org-agent-tests)) @@ -43,4 +45,5 @@ (uiop:symbol-call :fiveam :run! (uiop:find-symbol* :immune-suite :org-agent-immune-system-tests)) (uiop:symbol-call :fiveam :run! (uiop:find-symbol* :task-orchestrator-suite :org-agent-task-orchestrator-tests)) (uiop:symbol-call :fiveam :run! (uiop:find-symbol* :self-fix-suite :org-agent-self-fix-tests)) + (uiop:symbol-call :fiveam :run! (uiop:find-symbol* :lisp-repair-suite :org-agent-lisp-repair-tests)) (uiop:symbol-call :fiveam :run! (uiop:find-symbol* :chaos-suite :org-agent-chaos-qa)))) diff --git a/skills/org-skill-lisp-repair.org b/skills/org-skill-lisp-repair.org new file mode 100644 index 0000000..71ff9ca --- /dev/null +++ b/skills/org-skill-lisp-repair.org @@ -0,0 +1,75 @@ +:PROPERTIES: +:ID: 1e5a2c30-d3a9-4674-8db7-b08e7e1f44d1 +:CREATED: [2026-04-11 Sat 14:40] +:END: +#+TITLE: SKILL: Lisp Repair Syntax Gate +#+STARTUP: content +#+FILETAGS: :system:repair:syntax:lisp:psf: + +* Overview +The *Lisp Repair Syntax Gate* asynchronously intercepts `:syntax-error` events emitted by the kernel when System 1 (LLM) proposals fail to parse. It performs deterministic or neural repairs and re-injects the corrected action into the pipeline. + +* Implementation + +** Core Repair Logic +#+begin_src lisp :tangle ../src/lisp-repair.lisp +(in-package :org-agent) + +(defun count-char (char string) + (let ((count 0)) + (loop for c across string + when (char= c char) + do (incf count)) + count)) + +(defun deterministic-repair (code) + "Attempts instant fixes on broken Lisp code (e.g. balancing parens)." + (let* ((open-parens (count-char #\( code)) + (close-parens (count-char #\) code)) + (diff (- open-parens close-parens))) + (if (> diff 0) + (concatenate 'string code (make-string diff :initial-element #\))) + code))) + +(defun neural-repair (code error-message) + "Uses System 1 to deeply repair the syntax structure." + (let ((prompt (format nil "The following Lisp code failed to parse. +ERROR: ~a +CODE: ~a +MANDATE: Output EXACTLY ONE valid Common Lisp list. Do not explain. Do not use markdown blocks." + error-message code)) + (system-prompt "You are a Lisp Syntax Repair Actuator. Return only valid, balanced Lisp code.")) + (let ((repaired (ask-neuro prompt :system-prompt system-prompt))) + (string-trim '(#\Space #\Newline #\Tab) repaired)))) +#+end_src + +** Skill Definition +Reacts to syntax error events and transforms them into repaired requests. + +#+begin_src lisp :tangle ../src/lisp-repair.lisp +(defskill :skill-lisp-repair + :priority 90 + :trigger (lambda (ctx) (eq (getf (getf ctx :payload) :sensor) :syntax-error)) + :neuro nil ;; Handled deterministically in symbolic or manually via ask-neuro + :symbolic (lambda (action context) + (declare (ignore action)) + (let* ((payload (getf context :payload)) + (code (getf payload :code)) + (error-msg (getf payload :error))) + (kernel-log "SYNTAX GATE: Reacting to broken Lisp stimulus...") + (let ((fast-fix (deterministic-repair code))) + (handler-case + (let ((repaired (read-from-string fast-fix))) + (kernel-log "SYNTAX GATE: Deterministic repair SUCCESS.") + repaired) + (error () + (kernel-log "SYNTAX GATE: Deterministic repair failed. Escalating...") + (let ((deep-fix (neural-repair code error-msg))) + (handler-case + (let ((repaired (read-from-string deep-fix))) + (kernel-log "SYNTAX GATE: Neural repair SUCCESS.") + repaired) + (error () + (kernel-log "SYNTAX GATE: Neural repair failed.") + (list :type :LOG :payload (list :text "Lisp Repair Failed."))))))))))) +#+end_src diff --git a/src/lisp-repair.lisp b/src/lisp-repair.lisp new file mode 100644 index 0000000..fe42cbf --- /dev/null +++ b/src/lisp-repair.lisp @@ -0,0 +1,55 @@ +(in-package :org-agent) + +(defun count-char (char string) + (let ((count 0)) + (loop for c across string + when (char= c char) + do (incf count)) + count)) + +(defun deterministic-repair (code) + "Attempts instant fixes on broken Lisp code (e.g. balancing parens)." + (let* ((open-parens (count-char #\( code)) + (close-parens (count-char #\) code)) + (diff (- open-parens close-parens))) + (if (> diff 0) + (concatenate 'string code (make-string diff :initial-element #\))) + code))) + +(defun neural-repair (code error-message) + "Uses System 1 to deeply repair the syntax structure." + (let ((prompt (format nil "The following Lisp code failed to parse. +ERROR: ~a +CODE: ~a +MANDATE: Output EXACTLY ONE valid Common Lisp list. Do not explain. Do not use markdown blocks." + error-message code)) + (system-prompt "You are a Lisp Syntax Repair Actuator. Return only valid, balanced Lisp code.")) + (let ((repaired (ask-neuro prompt :system-prompt system-prompt))) + (string-trim '(#\Space #\Newline #\Tab) repaired)))) + +(defskill :skill-lisp-repair + :priority 90 + :trigger (lambda (ctx) (eq (getf (getf ctx :payload) :sensor) :syntax-error)) + :neuro nil + :symbolic (lambda (action context) + (declare (ignore action)) + (let* ((stimulus (getf context :candidate)) + (payload (getf stimulus :payload)) + (code (getf payload :code)) + (error-msg (getf payload :error))) + (kernel-log "SYNTAX GATE: Reacting to broken Lisp stimulus...") + (let ((fast-fix (deterministic-repair code))) + (handler-case + (let ((repaired (read-from-string fast-fix))) + (kernel-log "SYNTAX GATE: Deterministic repair SUCCESS.") + repaired) + (error () + (kernel-log "SYNTAX GATE: Deterministic repair failed. Escalating...") + (let ((deep-fix (neural-repair code error-msg))) + (handler-case + (let ((repaired (read-from-string deep-fix))) + (kernel-log "SYNTAX GATE: Neural repair SUCCESS.") + repaired) + (error () + (kernel-log "SYNTAX GATE: Neural repair failed.") + (list :type :LOG :payload (list :text "Lisp Repair Failed."))))))))))) diff --git a/src/neuro.lisp b/src/neuro.lisp index 0407095..baf3843 100644 --- a/src/neuro.lisp +++ b/src/neuro.lisp @@ -129,7 +129,13 @@ To call a tool, you MUST use: (let ((regs (nth-value 1 (cl-ppcre:scan-to-strings "(?s)```(?:lisp)?\\n?(.*?)\\n?```" raw-thought)))) (if (and regs (> (length regs) 0)) (elt regs 0) raw-thought)) (string-trim '(#\Space #\Newline #\Tab) raw-thought)))) - (suggestion (ignore-errors (read-from-string cleaned-thought)))) + (suggestion (handler-case (read-from-string cleaned-thought) + (error (c) + ;; EMIT ASYNCHRONOUS REPAIR STIMULUS + (list :type :EVENT :payload + (list :sensor :syntax-error + :code cleaned-thought + :error (format nil "~a" c))))))) (kernel-log "SYSTEM 1 Suggestion: ~a~%" cleaned-thought) (when (and suggestion (listp suggestion)) (push suggestion suggestions)))) diff --git a/tests/lisp-repair-tests.lisp b/tests/lisp-repair-tests.lisp new file mode 100644 index 0000000..efc570d --- /dev/null +++ b/tests/lisp-repair-tests.lisp @@ -0,0 +1,30 @@ +(defpackage :org-agent-lisp-repair-tests + (:use :cl :fiveam :org-agent) + (:export #:lisp-repair-suite)) +(in-package :org-agent-lisp-repair-tests) + +(def-suite lisp-repair-suite :description "Tests for Asynchronous Lisp Repair Syntax Gate.") +(in-suite lisp-repair-suite) + +(test test-deterministic-repair-balance + "Verify that deterministic-repair balances parentheses." + (let ((broken "(:type :REQUEST :target :emacs")) + ;; deterministic-repair will be defined in lisp-repair.lisp (user-space) + ;; but for testing we expect it to be available in the org-agent package. + (is (equal "(:type :REQUEST :target :emacs)" + (org-agent::deterministic-repair broken))))) + +(test test-async-repair-flow + "Verify that the pipeline correctly emits and reacts to syntax-error events." + (clrhash org-agent::*object-store*) + (let* ((broken-code "(:type :REQUEST :target :tool") + (error-msg "End of file") + ;; 1. The Stimulus that caused the error + (stimulus `(:type :EVENT :payload (:sensor :syntax-error :code ,broken-code :error ,error-msg))) + ;; 2. Simulate the decide-gate call for skill-lisp-repair + (result (org-agent:decide-gate (list :type :EVENT :candidate stimulus :payload '(:sensor :syntax-error))))) + + (let ((approved (getf result :approved-action))) + ;; The repair skill should have intercepted the EVENT and returned a repaired REQUEST + (is (eq :REQUEST (getf approved :type))) + (is (eq :tool (getf approved :target))))))