diff --git a/lisp/core-communication.lisp b/lisp/core-communication.lisp index 2ef953c..b713381 100644 --- a/lisp/core-communication.lisp +++ b/lisp/core-communication.lisp @@ -3,7 +3,7 @@ (defvar *actuator-registry* (make-hash-table :test 'equalp) "Global registry mapping target keywords to their physical actuator functions.") -(defun actuator-register (name fn) +(defun register-actuator (name fn) "Registers an actuator function. Actuators receive: (ACTION CONTEXT)." (let ((key (if (keywordp name) name (intern (string-upcase (string name)) :keyword)))) (setf (gethash key *actuator-registry*) fn))) diff --git a/lisp/core-defpackage.lisp b/lisp/core-defpackage.lisp index 90d4cae..0fc589a 100644 --- a/lisp/core-defpackage.lisp +++ b/lisp/core-defpackage.lisp @@ -56,7 +56,7 @@ #:context-get-skill-telemetry #:telemetry-track #:context-assemble-global-awareness - #:loop-process + #:process-signal #:loop-process #:perceive-gate #:probabilistic-gate @@ -64,8 +64,9 @@ #:act-gate #:reason-gate #:dispatch-gate + #:register-pre-reason-handler #:inject-stimulus - #:initialize-actuators + #:actuator-initialize #:dispatch-action #:register-actuator #:load-skill-from-org diff --git a/lisp/core-loop-act.lisp b/lisp/core-loop-act.lisp index c4ec715..e221653 100644 --- a/lisp/core-loop-act.lisp +++ b/lisp/core-loop-act.lisp @@ -96,6 +96,10 @@ (t (format nil "TOOL [~a] RESULT: ~s" tool-name result)))) (format nil "TOOL [~a] RESULT: ~a" tool-name result))) +;; Alias: act-gate → loop-gate-act +(defun act-gate (signal) + (loop-gate-act signal)) + (defun loop-gate-act (signal) "Final stage of the metabolic pipeline: Actuation. For approval-required actions, creates a Flight Plan instead of executing." @@ -106,21 +110,20 @@ For approval-required actions, creates a Flight Plan instead of executing." (source (getf meta :source)) (feedback nil)) ;; HITL: if the approved action requires human approval, - ;; create a Flight Plan and notify the user via their client. + ;; create a Flight Plan (Emacs) and HITL entry (all gateways). (when (and approved (eq (getf approved :level) :approval-required)) (let* ((payload (getf approved :payload)) - (blocked-action (getf payload :action))) - (log-message "ACT: Action requires approval — creating Flight Plan") + (blocked-action (getf payload :action)) + (hitl (hitl-create blocked-action))) + (log-message "ACT: Action requires approval — creating Flight Plan + HITL (~a)" (getf hitl :token)) (dispatcher-flight-plan-create blocked-action) (setf (getf signal :status) :suspended) - ;; Dispatch HITL notification to the user's client via the source actuator (action-dispatch (list :target source - :payload (list :text - "HITL: Action requires your approval. Check Flight Plan and set TODO to APPROVED.")) + :payload (list :text (getf hitl :message))) signal) - (setf approved nil) ;; Don't execute the original action - (setf feedback nil))) ;; Don't loop back — wait for human + (setf approved nil) + (setf feedback nil))) (when approved (let* ((original-type (getf approved :type)) (verified (cognitive-verify approved signal))) diff --git a/lisp/core-loop-perceive.lisp b/lisp/core-loop-perceive.lisp index c932d27..7ff13f9 100644 --- a/lisp/core-loop-perceive.lisp +++ b/lisp/core-loop-perceive.lisp @@ -8,6 +8,19 @@ (defvar *loop-focus-id* nil "The Org ID of the node the user is currently interacting with.") +(defvar *pre-reason-handlers* (make-hash-table :test 'eq) + "Pre-reason handler registry: sensor keyword → handler function.") + +(defun register-pre-reason-handler (sensor fn) + "Registers FN to handle signals with SENSOR in the perceive gate. +FN receives (signal) and returns T if consumed, nil to continue." + (setf (gethash sensor *pre-reason-handlers*) fn)) + +;; Alias for backward compatibility +(defun inject-stimulus (raw-message &key stream (depth 0)) + "Alias for stimulus-inject." + (stimulus-inject raw-message :stream stream :depth depth)) + (defun stimulus-inject (raw-message &key stream (depth 0)) "Inject a raw message into the signal processing pipeline." (let* ((payload (getf raw-message :payload)) @@ -40,12 +53,28 @@ (skip-event () (log-message "SYSTEM RECOVERY: Stimulus dropped.")))))) +;; Alias: perceive-gate → loop-gate-perceive +(defun perceive-gate (signal) + (loop-gate-perceive signal)) + (defun loop-gate-perceive (signal) "Stage 1 of the metabolic pipeline: Normalize sensory input." (let* ((payload (getf signal :payload)) - (type (getf signal :type)) - (meta (getf signal :meta)) - (sensor (getf payload :sensor))) + (type (getf signal :type)) + (meta (getf signal :meta)) + (sensor (getf payload :sensor))) + ;; HITL: intercept approval/denial commands before LLM processing + (when (and (eq sensor :user-input) + (stringp (getf payload :text))) + (let ((text (getf payload :text))) + (when (ignore-errors (hitl-handle-message text (getf meta :source))) + (log-message "GATE [Perceive]: HITL command processed — ~a" text) + (return-from loop-gate-perceive signal)))) + ;; Pre-reason handlers: dispatch custom sensors to registered skill handlers + (let ((handler (gethash sensor *pre-reason-handlers*))) + (when handler + (when (funcall handler signal) + (return-from loop-gate-perceive signal)))) (log-message "GATE [Perceive]: ~a (~a) [Source: ~s]" type (or sensor "no-sensor") (getf meta :source)) @@ -69,7 +98,7 @@ (:approval-required (when (getf payload :approved) (log-message "GATE [Perceive]: Approved Flight Plan re-injected") - (setf (getf signal :approved) t) + (setf (getf signal :approved) t) (setf (getf signal :approved-action) (getf payload :action)))) ;; Default sensor: pass through without requiring user-input processing (otherwise diff --git a/lisp/core-loop-reason.lisp b/lisp/core-loop-reason.lisp index 7e40d0a..4678b5a 100644 --- a/lisp/core-loop-reason.lisp +++ b/lisp/core-loop-reason.lisp @@ -121,6 +121,10 @@ modified action (for approval-required or pass)." :action approval-action)) current-action))) +;; Alias: reason-gate → loop-gate-reason +(defun reason-gate (signal) + (loop-gate-reason signal)) + (defun loop-gate-reason (signal) (let* ((type (proto-get signal :type)) (payload (proto-get signal :payload)) diff --git a/lisp/core-loop.lisp b/lisp/core-loop.lisp index efc5fd3..d40a6dd 100644 --- a/lisp/core-loop.lisp +++ b/lisp/core-loop.lisp @@ -9,6 +9,10 @@ (defvar *heartbeat-thread* nil "Handle to the heartbeat thread.") +;; Alias: process-signal → loop-process (backward compatibility) +(defun process-signal (signal) + (loop-process signal)) + (defun loop-process (signal) "The entry point to the Metabolic Pipeline: Perceive -> Reason -> Act." (let ((current-signal signal)) @@ -111,8 +115,8 @@ (cl-dotenv:load-env env-file))) (load-memory-from-disk) - (initialize-actuators) - (initialize-all-skills) + (actuator-initialize) + (skill-initialize-all) ;; Run proactive doctor before starting services (diagnostics-startup-run) diff --git a/lisp/gateway-manager.lisp b/lisp/gateway-manager.lisp index 89eaae9..ce7297c 100644 --- a/lisp/gateway-manager.lisp +++ b/lisp/gateway-manager.lisp @@ -25,12 +25,13 @@ (chat-id (cdr (assoc :id chat))) (text (cdr (assoc :text message)))) (setf (getf (gethash "telegram" *gateway-configs*) :last-update-id) update-id) - (when (and text chat-id) - (log-message "TELEGRAM: Received message from ~a" chat-id) - (inject-stimulus - (list :type :EVENT - :meta (list :source :telegram :chat-id (format nil "~a" chat-id)) - :payload (list :sensor :user-input :text text))))))) + (when (and text chat-id) + (log-message "TELEGRAM: Received message from ~a" chat-id) + (unless (ignore-errors (hitl-handle-message text :telegram)) + (stimulus-inject + (list :type :EVENT + :meta (list :source :telegram :chat-id (format nil "~a" chat-id)) + :payload (list :sensor :user-input :text text))))))) (error (c) (log-message "TELEGRAM POLL ERROR: ~a" c)))))) (defun telegram-send (action context) @@ -69,12 +70,13 @@ (source (cdr (assoc :source envelope))) (data-message (cdr (assoc :data-message envelope))) (text (cdr (assoc :message data-message)))) - (when (and source text) - (log-message "SIGNAL: Received message from ~a" source) - (inject-stimulus - (list :type :EVENT - :meta (list :source :signal :chat-id source) - :payload (list :sensor :user-input :text text)))))))) + (when (and source text) + (log-message "SIGNAL: Received message from ~a" source) + (unless (ignore-errors (hitl-handle-message text :signal)) + (stimulus-inject + (list :type :EVENT + :meta (list :source :signal :chat-id source) + :payload (list :sensor :user-input :text text)))))))) (error (c) (log-message "SIGNAL POLL ERROR: ~a" c)))))) (defun signal-send (action context) diff --git a/lisp/programming-repl.lisp b/lisp/programming-repl.lisp index 4a1a78d..4428f05 100644 --- a/lisp/programming-repl.lisp +++ b/lisp/programming-repl.lisp @@ -101,6 +101,30 @@ REPL Skill Commands: - Show this message ")) +(defun repl-handle (signal) + "Pre-reason handler for :repl-eval sensor. Evaluates code and +writes the result back through the reply-stream." + (let* ((payload (getf signal :payload)) + (code (getf payload :code)) + (stream (getf (getf signal :meta) :reply-stream)) + (result (multiple-value-bind (val out err) + (repl-eval code) + (if err + (list :status :error :message err) + (list :status :success :value (or val "")))))) + (when stream + (handler-case + (progn + (write-sequence (frame-message result) stream) + (finish-output stream)) + (error (c) + (log-message "REPL-EVAL: Failed to write response: ~a" c)))) + ;; Return T to signal the message was consumed + t)) + +;; Register the handler at load time +(register-pre-reason-handler :repl-eval #'repl-handle) + (defun repl-mandate (context) "Returns REPL-first engineering mandate when context involves code editing." (let ((raw (or (proto-get (proto-get context :payload) :text) ""))) diff --git a/org/core-communication.org b/org/core-communication.org index 3f9c191..112ad28 100644 --- a/org/core-communication.org +++ b/org/core-communication.org @@ -44,7 +44,7 @@ The global registry mapping target keywords (~:cli~, ~:telegram~, ~:signal~, etc (defvar *actuator-registry* (make-hash-table :test 'equalp) "Global registry mapping target keywords to their physical actuator functions.") -(defun actuator-register (name fn) +(defun register-actuator (name fn) "Registers an actuator function. Actuators receive: (ACTION CONTEXT)." (let ((key (if (keywordp name) name (intern (string-upcase (string name)) :keyword)))) (setf (gethash key *actuator-registry*) fn))) diff --git a/org/core-defpackage.org b/org/core-defpackage.org index 9e27af9..3907118 100644 --- a/org/core-defpackage.org +++ b/org/core-defpackage.org @@ -81,7 +81,7 @@ The package definition. All public symbols are exported here. #:context-get-skill-telemetry #:telemetry-track #:context-assemble-global-awareness - #:loop-process + #:process-signal #:loop-process #:perceive-gate #:probabilistic-gate @@ -89,8 +89,9 @@ The package definition. All public symbols are exported here. #:act-gate #:reason-gate #:dispatch-gate + #:register-pre-reason-handler #:inject-stimulus - #:initialize-actuators + #:actuator-initialize #:dispatch-action #:register-actuator #:load-skill-from-org diff --git a/org/core-loop-act.org b/org/core-loop-act.org index 5c3c6ea..7ff3f9d 100644 --- a/org/core-loop-act.org +++ b/org/core-loop-act.org @@ -195,6 +195,10 @@ After dispatch, the gate captures any feedback produced by the actuation (tool o ;; REPL-VERIFIED: 2026-05-03T13:00:00 #+begin_src lisp +;; Alias: act-gate → loop-gate-act +(defun act-gate (signal) + (loop-gate-act signal)) + (defun loop-gate-act (signal) "Final stage of the metabolic pipeline: Actuation. For approval-required actions, creates a Flight Plan instead of executing." diff --git a/org/core-loop-perceive.org b/org/core-loop-perceive.org index e1082c2..724eb4a 100644 --- a/org/core-loop-perceive.org +++ b/org/core-loop-perceive.org @@ -61,6 +61,30 @@ A global interrupt flag that can be set by any signal. When set, the metabolic l (defvar *loop-focus-id* nil "The Org ID of the node the user is currently interacting with.") #+end_src + +** Pre-Reason Handler Registry + +Skills register handlers for custom sensors here. When a signal arrives +with a registered sensor, the handler is called in the perceive gate, +before the signal reaches the LLM. The handler receives the full signal +and returns T if the signal was consumed (don't continue to reason) +or nil if processing should proceed normally. + +;; REPL-VERIFIED: 2026-05-03T13:00:00 +#+begin_src lisp +(defvar *pre-reason-handlers* (make-hash-table :test 'eq) + "Pre-reason handler registry: sensor keyword → handler function.") + +(defun register-pre-reason-handler (sensor fn) + "Registers FN to handle signals with SENSOR in the perceive gate. +FN receives (signal) and returns T if consumed, nil to continue." + (setf (gethash sensor *pre-reason-handlers*) fn)) + +;; Alias for backward compatibility +(defun inject-stimulus (raw-message &key stream (depth 0)) + "Alias for stimulus-inject." + (stimulus-inject raw-message :stream stream :depth depth)) +#+end_src #+end_src ** Stimulus Injection (stimulus-inject) @@ -117,12 +141,28 @@ All signals get tagged with their processing stage (`:status :perceived`) and th ;; REPL-VERIFIED: 2026-05-03T13:00:00 #+begin_src lisp +;; Alias: perceive-gate → loop-gate-perceive +(defun perceive-gate (signal) + (loop-gate-perceive signal)) + (defun loop-gate-perceive (signal) "Stage 1 of the metabolic pipeline: Normalize sensory input." (let* ((payload (getf signal :payload)) - (type (getf signal :type)) - (meta (getf signal :meta)) - (sensor (getf payload :sensor))) + (type (getf signal :type)) + (meta (getf signal :meta)) + (sensor (getf payload :sensor))) + ;; HITL: intercept approval/denial commands before LLM processing + (when (and (eq sensor :user-input) + (stringp (getf payload :text))) + (let ((text (getf payload :text))) + (when (ignore-errors (hitl-handle-message text (getf meta :source))) + (log-message "GATE [Perceive]: HITL command processed — ~a" text) + (return-from loop-gate-perceive signal)))) + ;; Pre-reason handlers: dispatch custom sensors to registered skill handlers + (let ((handler (gethash sensor *pre-reason-handlers*))) + (when handler + (when (funcall handler signal) + (return-from loop-gate-perceive signal)))) (log-message "GATE [Perceive]: ~a (~a) [Source: ~s]" type (or sensor "no-sensor") (getf meta :source)) diff --git a/org/core-loop-reason.org b/org/core-loop-reason.org index 72607d0..ad13868 100644 --- a/org/core-loop-reason.org +++ b/org/core-loop-reason.org @@ -281,6 +281,10 @@ The retry limit prevents infinite loops. If the LLM cannot produce a passable pr ;; REPL-VERIFIED: 2026-05-03T13:00:00 #+begin_src lisp +;; Alias: reason-gate → loop-gate-reason +(defun reason-gate (signal) + (loop-gate-reason signal)) + (defun loop-gate-reason (signal) (let* ((type (proto-get signal :type)) (payload (proto-get signal :payload)) diff --git a/org/core-loop.org b/org/core-loop.org index f415769..e090eb8 100644 --- a/org/core-loop.org +++ b/org/core-loop.org @@ -80,6 +80,10 @@ The function handles four failure modes: ;; REPL-VERIFIED: 2026-05-03T13:00:00 #+begin_src lisp +;; Alias: process-signal → loop-process (backward compatibility) +(defun process-signal (signal) + (loop-process signal)) + (defun loop-process (signal) "The entry point to the Metabolic Pipeline: Perceive -> Reason -> Act." (let ((current-signal signal)) @@ -253,8 +257,8 @@ Boot sequence: (cl-dotenv:load-env env-file))) (load-memory-from-disk) - (initialize-actuators) - (initialize-all-skills) + (actuator-initialize) + (skill-initialize-all) ;; Run proactive doctor before starting services (diagnostics-startup-run) diff --git a/org/gateway-manager.org b/org/gateway-manager.org index 5b6d18a..868e4fb 100644 --- a/org/gateway-manager.org +++ b/org/gateway-manager.org @@ -60,12 +60,13 @@ Registration of available gateway implementations: each platform registers its p (chat-id (cdr (assoc :id chat))) (text (cdr (assoc :text message)))) (setf (getf (gethash "telegram" *gateway-configs*) :last-update-id) update-id) - (when (and text chat-id) - (log-message "TELEGRAM: Received message from ~a" chat-id) - (inject-stimulus - (list :type :EVENT - :meta (list :source :telegram :chat-id (format nil "~a" chat-id)) - :payload (list :sensor :user-input :text text))))))) + (when (and text chat-id) + (log-message "TELEGRAM: Received message from ~a" chat-id) + (unless (ignore-errors (hitl-handle-message text :telegram)) + (stimulus-inject + (list :type :EVENT + :meta (list :source :telegram :chat-id (format nil "~a" chat-id)) + :payload (list :sensor :user-input :text text))))))) (error (c) (log-message "TELEGRAM POLL ERROR: ~a" c)))))) #+end_src @@ -117,12 +118,13 @@ Registration of available gateway implementations: each platform registers its p (source (cdr (assoc :source envelope))) (data-message (cdr (assoc :data-message envelope))) (text (cdr (assoc :message data-message)))) - (when (and source text) - (log-message "SIGNAL: Received message from ~a" source) - (inject-stimulus - (list :type :EVENT - :meta (list :source :signal :chat-id source) - :payload (list :sensor :user-input :text text)))))))) + (when (and source text) + (log-message "SIGNAL: Received message from ~a" source) + (unless (ignore-errors (hitl-handle-message text :signal)) + (stimulus-inject + (list :type :EVENT + :meta (list :source :signal :chat-id source) + :payload (list :sensor :user-input :text text)))))))) (error (c) (log-message "SIGNAL POLL ERROR: ~a" c)))))) #+end_src diff --git a/org/programming-repl.org b/org/programming-repl.org index f04c37c..438ce10 100644 --- a/org/programming-repl.org +++ b/org/programming-repl.org @@ -195,6 +195,44 @@ REPL Skill Commands: (is (not (null error))))) #+end_src +** REPL-EVAL Pre-Reason Handler + +Registers a handler for =:repl-eval= sensor signals. When the daemon +receives a framed message with =:sensor :repl-eval=, this handler +evaluates the Lisp code directly and writes the result back through +the reply-stream, bypassing the LLM pipeline entirely. + +Since this handler is registered via =register-pre-reason-handler=, +the perceive gate calls it before any LLM reasoning occurs. The +handler returns T (consumed), so the signal never reaches Reason. + +;; REPL-VERIFIED: 2026-05-03T13:00:00 +#+begin_src lisp +(defun repl-handle (signal) + "Pre-reason handler for :repl-eval sensor. Evaluates code and +writes the result back through the reply-stream." + (let* ((payload (getf signal :payload)) + (code (getf payload :code)) + (stream (getf (getf signal :meta) :reply-stream)) + (result (multiple-value-bind (val out err) + (repl-eval code) + (if err + (list :status :error :message err) + (list :status :success :value (or val "")))))) + (when stream + (handler-case + (progn + (write-sequence (frame-message result) stream) + (finish-output stream)) + (error (c) + (log-message "REPL-EVAL: Failed to write response: ~a" c)))) + ;; Return T to signal the message was consumed + t)) + +;; Register the handler at load time +(register-pre-reason-handler :repl-eval #'repl-handle) +#+end_src + * Phase E: Lifecycle The REPL skill loads at priority 200 (after diagnostics at 100, before utils-lisp at 400).