From 855157ccc62c2915dfe80e24d0fb6015129dd7e1 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Sat, 11 Apr 2026 14:29:42 -0400 Subject: [PATCH] FEAT: Implement Lisp Repair Syntax Gate --- docs/rca/rca-lisp-repair.org | 33 +++++++++++++++ literate/neurosymbolic.org | 9 +++- org-agent.asd | 3 ++ skills/org-skill-lisp-repair.org | 71 ++++++++++++++++++++++++++++++++ src/lisp-repair.lisp | 50 ++++++++++++++++++++++ src/neuro.lisp | 9 +++- src/package.lisp | 4 ++ tests/lisp-repair-tests.lisp | 28 +++++++++++++ 8 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 docs/rca/rca-lisp-repair.org create mode 100644 skills/org-skill-lisp-repair.org create mode 100644 src/lisp-repair.lisp create mode 100644 tests/lisp-repair-tests.lisp diff --git a/docs/rca/rca-lisp-repair.org b/docs/rca/rca-lisp-repair.org new file mode 100644 index 0000000..787ffaf --- /dev/null +++ b/docs/rca/rca-lisp-repair.org @@ -0,0 +1,33 @@ +#+TITLE: Root Cause Analysis: Implement Lisp Repair Syntax Gate +#+DATE: 2026-04-11 +#+FILETAGS: :rca:lisp:repair:syntax: + +* Executive Summary +Implemented the `org-skill-lisp-repair` to address the issue of silent parser failures in System 1 (LLM) proposals. The system now attempts deterministic fixes (balancing parentheses) and neural fallback before discarding a thought. + +* 1. Issue: Silent Parser Failures +** Symptoms +System 1 proposals containing minor syntax errors (like missing closing parens) were being discarded by the `think` function, leading to "Action rejected" or "Invalid output format" logs and wasted tokens. +** Root Cause +The `think` loop used `ignore-errors` around `read-from-string`, providing no path for recovery or diagnostics for malformed Lisp. +** Resolution +Replaced `ignore-errors` with a `handler-case` that invokes `repair-lisp-syntax`. + +* 2. Design Decision: Two-Tiered Repair +** Tier 1: Deterministic (Fast) +- **Mechanism:** Count unclosed `(` and append `)`. +- **Reasoning:** Most common error from LLMs under token limits or early termination. Cost: 0 tokens. +** Tier 2: Neural (Deep) +- **Mechanism:** specialized `ask-neuro` prompt with the error message and broken code. +- **Reasoning:** Necessary for unbalanced strings or hallucinated syntax that deterministic rules can't fix. + +* 3. PSF Mandate Alignment +** Literate Programming +- Created `org-skill-lisp-repair.org` as a Universal Literate Note. +- Tangle targets established for `src/lisp-repair.lisp`. +** High-Integrity Memory +- Updated `neurosymbolic.org` documentation to reflect the new safety buffer in the `think` function. + +* 4. Permanent Learnings +- **Dynamic Skill Invocation:** Use `member :skill-name *loaded-skills*` and `fboundp` to ensure kernel functions remain decoupled from non-essential skills. +- **Test Package Exporting:** Always remember to export the `fiveam` suite symbol in the `defpackage` of new test files to avoid `SIMPLE-READER-PACKAGE-ERROR`. diff --git a/literate/neurosymbolic.org b/literate/neurosymbolic.org index c25f05f..1a1fe7a 100644 --- a/literate/neurosymbolic.org +++ b/literate/neurosymbolic.org @@ -175,7 +175,14 @@ 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) + (if (and (member :skill-lisp-repair *loaded-skills*) + (fboundp 'repair-lisp-syntax)) + (repair-lisp-syntax cleaned-thought (format nil "~a" c)) + (progn + (kernel-log "SYSTEM 1 ERROR: Invalid output format from LLM.~%") + nil)))))) (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..73bd58c --- /dev/null +++ b/skills/org-skill-lisp-repair.org @@ -0,0 +1,71 @@ +: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* intercepts System 1 (LLM) proposals that fail the formal Lisp `read-from-string` parser due to syntax errors (e.g., hallucinated trailing text, unbalanced parentheses). It acts as a pre-eval System 2 safety buffer. + +* 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." + (let* ((open-parens (count-char #\( code)) + (close-parens (count-char #\) code)) + (diff (- open-parens close-parens))) + (if (> diff 0) + ;; Append missing closing parens + (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)))) + +(defun repair-lisp-syntax (code error-message) + "The entry point called by the neuro-gate when read-from-string fails." + (kernel-log "SYNTAX GATE: Intercepted broken Lisp. Attempting deterministic repair...") + (let ((fast-fix (deterministic-repair code))) + (handler-case + (read-from-string fast-fix) + (error () + (kernel-log "SYNTAX GATE: Deterministic repair failed. Escalating to neural repair...") + (let ((deep-fix (neural-repair code error-message))) + (handler-case + (read-from-string deep-fix) + (error () + (kernel-log "SYNTAX GATE: Neural repair failed.") + nil))))))) +#+end_src + +** Skill Definition +Since this skill is a passive System 2 interceptor (called dynamically by `neuro.lisp`), it doesn't need to trigger on its own via the Event Bus. + +#+begin_src lisp :tangle ../src/lisp-repair.lisp +(defskill :skill-lisp-repair + :priority 90 + :trigger (lambda (ctx) (declare (ignore ctx)) nil) ;; Passive interceptor + :neuro nil + :symbolic nil) +#+end_src diff --git a/src/lisp-repair.lisp b/src/lisp-repair.lisp new file mode 100644 index 0000000..ed0d937 --- /dev/null +++ b/src/lisp-repair.lisp @@ -0,0 +1,50 @@ +(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." + (let* ((open-parens (count-char #\( code)) + (close-parens (count-char #\) code)) + (diff (- open-parens close-parens))) + (if (> diff 0) + ;; Append missing closing parens + (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)))) + +(defun repair-lisp-syntax (code error-message) + "The entry point called by the neuro-gate when read-from-string fails." + (kernel-log "SYNTAX GATE: Intercepted broken Lisp. Attempting deterministic repair...") + (let ((fast-fix (deterministic-repair code))) + (handler-case + (read-from-string fast-fix) + (error () + (kernel-log "SYNTAX GATE: Deterministic repair failed. Escalating to neural repair...") + (let ((deep-fix (neural-repair code error-message))) + (handler-case + (read-from-string deep-fix) + (error () + (kernel-log "SYNTAX GATE: Neural repair failed.") + nil))))))) + +(defskill :skill-lisp-repair + :priority 90 + :trigger (lambda (ctx) (declare (ignore ctx)) nil) ;; Passive interceptor + :neuro nil + :symbolic nil) diff --git a/src/neuro.lisp b/src/neuro.lisp index 0407095..469bc6a 100644 --- a/src/neuro.lisp +++ b/src/neuro.lisp @@ -129,7 +129,14 @@ 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) + (if (and (member :skill-lisp-repair *loaded-skills*) + (fboundp 'repair-lisp-syntax)) + (repair-lisp-syntax cleaned-thought (format nil "~a" c)) + (progn + (kernel-log "SYSTEM 1 ERROR: Invalid output format from LLM.~%") + nil)))))) (kernel-log "SYSTEM 1 Suggestion: ~a~%" cleaned-thought) (when (and suggestion (listp suggestion)) (push suggestion suggestions)))) diff --git a/src/package.lisp b/src/package.lisp index b8e540e..3603ff0 100644 --- a/src/package.lisp +++ b/src/package.lisp @@ -36,6 +36,10 @@ ;; --- Self-Fix Agent --- #:self-fix-apply + ;; --- Lisp Repair Syntax Gate --- + #:repair-lisp-syntax + #:deterministic-repair + ;; --- Context API (Peripheral Vision) --- #:context-query-store #:context-get-active-projects diff --git a/tests/lisp-repair-tests.lisp b/tests/lisp-repair-tests.lisp new file mode 100644 index 0000000..4ce93da --- /dev/null +++ b/tests/lisp-repair-tests.lisp @@ -0,0 +1,28 @@ +(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 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")) + (is (equal "(:type :REQUEST :target :emacs)" + (org-agent:deterministic-repair broken))))) + +(test test-deterministic-repair-deep-balance + "Verify that deterministic-repair balances multiple nested parentheses." + (let ((broken "(list :a (list :b 1")) + (is (equal "(list :a (list :b 1))" + (org-agent:deterministic-repair broken))))) + +(test test-repair-lisp-syntax-entry + "Verify that repair-lisp-syntax successfully repairs and parses broken Lisp." + (let ((broken "(:type :REQUEST :target :tool") + (error-msg "End of file while reading")) + (let ((result (org-agent:repair-lisp-syntax broken error-msg))) + (is (listp result)) + (is (eq :REQUEST (getf result :type))) + (is (eq :tool (getf result :target))))))