#+PROPERTY: header-args:lisp :tangle (concat (identity (getenv "INSTALL_DIR")) "/harness/act.lisp")" ) #+TITLE: Stage 3: Act (act.lisp) #+AUTHOR: Amr #+FILETAGS: :harness:act: #+STARTUP: content * Stage 3: Act (act.lisp) ** Architectural Intent: The Last Mile The Act stage is where cognition meets reality. After the Probabilistic engine proposes and the Deterministic engine verifies, Act executes the approved action. The key insight of the Act stage is that *execution is the point of no return*. Once a command is sent to the shell or a file is written, side effects have occurred. Therefore, Act implements a "last-mile" safety check - even after skills have verified the action, there's a final validation before dispatch. ** Why Separate Actuators? The actuator pattern decouples /what to do/ from /how to do it/: - The reasoning engine generates action plists like `(:TYPE :REQUEST :TARGET :SHELL :PAYLOAD ...)` - The actuator interprets the target and executes appropriately - Adding a new actuator (Telegram, Matrix, etc.) doesn't require changing the reasoning code This follows the Open/Closed principle: open for extension, closed for modification. ** The Feedback Loop Act is unique in the pipeline because it can generate new signals. When a tool executes and returns data, that data becomes a new signal that feeds back into Perceive → Reason → Act. Example feedback chain: 1. User asks "What files changed today?" 2. Reason generates shell command action 3. Act executes shell, gets file list 4. Act returns file list as feedback signal 5. Reason processes file list, generates human-readable response 6. Act displays response * Package Context #+begin_src lisp (in-package :opencortex) #+end_src * Actuator Configuration ** Actuator Registry Variables #+begin_src lisp (defvar *default-actuator* :cli "The actuator used when no explicit target is specified. Override with DEFAULT_ACTUATOR environment variable. (defvar *silent-actuators* '(:cli :system-message :emacs) "List of actuators that don't generate tool-output feedback. These typically have their own feedback mechanisms (CLI prints directly, etc.) #+end_src ** initialize-actuators: System Bootstrap #+begin_src lisp (defun initialize-actuators () "Load actuator configuration from environment and register core actuators. Environment variables: - DEFAULT_ACTUATOR: Keyword for default target (:cli, :shell, etc.) - SILENT_ACTUATORS: Comma-separated list of actuators that skip feedback Registers three core actuators: 1. :system - Internal commands (eval, create-skill, message) 2. :tool - Cognitive tool execution 3. :tui - Terminal UI output via reply stream" ;; Load environment configuration (let ((def (getenv "DEFAULT_ACTUATOR) (silent (getenv "SILENT_ACTUATORS)) ;; Set default actuator (when def (setf *default-actuator* (intern (string-upcase def) "KEYWORD)) ;; Parse silent actuators list (when silent (setf *silent-actuators* (mapcar (lambda (s) (intern (string-upcase (string-trim '(#\Space) s)) "KEYWORD) (str:split "," silent))))) ;; Register core harness actuators (register-actuator :system #'execute-system-action) (register-actuator :tool #'execute-tool-action) ;; TUI actuator: sends response back through the reply stream (register-actuator :tui (lambda (action context) (let* ((meta (getf context :meta)) (stream (getf meta :reply-stream))) (when (and stream (open-stream-p stream)) (format stream "~a" (frame-message action)) (finish-output stream)))))) #+end_src * Action Dispatching ** dispatch-action: The Router #+begin_src lisp (defun dispatch-action (action context) "Route an approved action to its registered actuator. ACTION is a plist with structure: (:TYPE :REQUEST :TARGET :shell :PAYLOAD (...)) CONTEXT is the signal being processed (for metadata access) The target is resolved in order of priority: 1. Explicit :target in the action 2. :source from the original signal's metadata 3. *default-actuator* configuration variable Returns the actuator's result (may be a feedback signal or NIL)." (let ((payload (proto-get action :payload))) ;; Heartbeats don't generate actuation (when (eq (proto-get payload :sensor) :heartbeat) (return-from dispatch-action nil)) (when (and action (listp action)) (let* ((meta (proto-get context :meta)) (source (proto-get meta :source)) (raw-target (or (ignore-errors (getf action :TARGET)) (ignore-errors (getf action :target)) source *default-actuator*)) (target (intern (string-upcase (string raw-target)) :keyword)) (actuator-fn (gethash target *actuator-registry*))) ;; Preserve metadata in outbound action (when (and meta (null (getf action :meta))) (setf (getf action :meta) meta)) ;; Execute or log error (if actuator-fn (funcall actuator-fn action context) (harness-log "ACT ERROR: No actuator registered for '~s' (requested by ~s)" target raw-target)))))) #+end_src * Actuator Implementations ** execute-system-action: Internal Commands #+begin_src lisp (defun execute-system-action (action context) "Execute internal harness commands. This actuator handles meta-commands that affect the harness itself, rather than external side effects. Commands include: - :eval - Evaluate arbitrary Lisp code (DANGEROUS, validate first!) - :create-skill - Write a new skill org file and reload - :message - Log a message to the harness log These commands bypass the normal actuator system since they operate on the harness internals rather than external systems." (declare (ignore context)) (let* ((payload (ignore-errors (getf action :payload))) (cmd (ignore-errors (getf payload :action)))) (case cmd ;; Evaluate Lisp code - guarded by lisp-utils skill (:eval (let ((code (getf payload :code))) (eval (read-from-string code)))) ;; Create and load a new skill from content (:create-skill (let* ((filename (getf payload :filename)) (content (getf payload :content)) (skills-dir (merge-pathnames "" (asdf:system-source-directory :opencortex))) (full-path (merge-pathnames filename skills-dir))) (with-open-file (out full-path :direction :output :if-exists :supersede) (write-string content out)) (load-skill-from-org full-path))) ;; Log an informational message (:message (harness-log "ACT [System]: ~a" (getf payload :text))) ;; Unknown command (t (harness-log "ACT ERROR [System]: Unknown command '~s'" cmd))))) #+end_src ** execute-tool-action: Cognitive Tool Execution #+begin_src lisp (defun execute-tool-action (action context) "Execute a registered cognitive tool. Tools are registered functions with: - A guard function (optional, for safety checks) - A body function (the actual implementation) - Metadata (description, parameter specs) This actuator: 1. Looks up the tool by name 2. Runs the guard function (if present) 3. Executes the body function with parsed arguments 4. Returns a feedback signal with the result The feedback mechanism allows tool results to trigger further reasoning." (let* ((payload (getf action :payload)) (tool-name (getf payload :tool)) (tool-args (getf payload :args)) (depth (getf context :depth 0)) (meta (getf context :meta)) (source (getf meta :source)) (tool (gethash (string-downcase (string tool-name)) *cognitive-tools*))) (if tool (handler-case ;; Parse arguments (handle both flat and nested plists) (let* ((clean-args (if (and (listp tool-args) (listp (car tool-args))) (car tool-args) tool-args)) (result (funcall (cognitive-tool-body tool) clean-args))) ;; Format result for source (when source (dispatch-action (list :TYPE :REQUEST :TARGET source :PAYLOAD (list :ACTION :MESSAGE :TEXT (format-tool-result tool-name result))) context)) ;; Return feedback signal for potential further processing (list :TYPE :EVENT :DEPTH (1+ depth) :META meta :PAYLOAD (list :SENSOR :tool-output :RESULT result :TOOL tool-name))) ;; Tool execution error (error (c) (list :TYPE :EVENT :DEPTH (1+ depth) :META meta :PAYLOAD (list :SENSOR :tool-error :TOOL tool-name :MESSAGE (format nil "~a" c))))) ;; Tool not found (list :TYPE :EVENT :DEPTH (1+ depth) :META meta :PAYLOAD (list :SENSOR :tool-error :MESSAGE (format nil "Tool '~a' not found" tool-name)))))) #+end_src ** format-tool-result: Human-Readable Output #+begin_src lisp (defun format-tool-result (tool-name result) "Format a tool result for human-readable display. Tools return either: - A plist: (:status :success :content \"...\ or (:status :error :message \"...\ - A raw value (string, number, etc.) This function normalizes both formats into a consistent string presentation." (if (listp result) (let ((status (getf result :status)) (content (getf result :content)) (msg (getf result :message))) (cond ((and (eq status :success) content) (format nil "~a" content)) ((and (eq status :error) msg) (format nil "ERROR [~a]: ~a" tool-name msg)) (t (format nil "TOOL [~a] RESULT: ~s" tool-name result)))) (format nil "TOOL [~a] RESULT: ~a" tool-name result))) #+end_src * The Act Gate ** act-gate: Final Pipeline Stage #+begin_src lisp (defun act-gate (signal) "Final stage of the metabolic pipeline: Actuation. This stage has three responsibilities: 1. Last-mile safety check: Run deterministic gates one more time before execution (handles race conditions, concurrent modifications) 2. Actuation: Dispatch the approved action to its target actuator 3. Feedback generation: If the action produced results, create a feedback signal that feeds back into the pipeline Modifies the signal: - :approved-action - May be modified by last-mile verification - :status - Set to :acted Returns a feedback signal if the action produced results, otherwise NIL." (let* ((approved (getf signal :approved-action)) (type (getf signal :type)) (meta (getf signal :meta)) (source (getf meta :source)) (feedback nil) (context signal)) ;; Step 1: Last-mile deterministic verification ;; This catches any issues that arose between reasoning and acting (when approved (let* ((original-type (getf approved :type)) (verified (deterministic-verify approved signal))) ;; Check if deterministic verification blocked the action (if (and (listp verified) (member (getf verified :type) '(:LOG :EVENT :log :event)) (not (member original-type '(:LOG :EVENT :log :event)))) ;; Action was blocked by verification (progn (harness-log "ACT BLOCKED: Action failed last-mile deterministic check. (setf (getf signal :approved-action) nil) (setf approved nil) (setf feedback verified)) ;; Action passed verification (progn (setf (getf signal :approved-action) verified) (setf approved verified))))) ;; Step 2: Actuation based on signal type (case type ;; Explicit requests go directly to dispatch (:REQUEST (dispatch-action signal context)) ;; Log messages also dispatch (:LOG (dispatch-action signal context)) ;; Events with approved actions dispatch to their target (:EVENT (if approved (let* ((target (getf approved :target)) (result (dispatch-action approved context))) ;; Determine feedback based on actuator response (cond ;; Actuator returned a signal - use it as feedback ((and (listp result) (member (getf result :type) '(:EVENT :LOG))) (setf feedback result)) ;; Non-silent actuator with result - format as tool-output ((and result (not (member target *silent-actuators*))) (setf feedback (list :type :EVENT :depth (1+ (getf signal :depth 0)) :meta meta :payload (list :sensor :tool-output :result result :tool approved)))))) ;; No approved action, but have source - might be raw event (when source (dispatch-action signal context))))) ;; Step 3: Update signal status (setf (getf signal :status) :acted) feedback)) #+end_src * Test Suite These tests verify the Act pipeline. Run with: ~(fiveam:run! 'pipeline-act-suite)~ #+begin_src lisp :tangle pipeline-act-tests.lisp" (concat (concat (or (getenv "INSTALL_DIR ". "/harness "/tests) (defpackage :opencortex-pipeline-act-tests (:use :cl :fiveam :opencortex) (:export #:pipeline-act-suite)) (in-package :opencortex-pipeline-act-tests) (def-suite pipeline-act-suite :description "Test suite for Act pipeline (in-suite pipeline-act-suite) (test test-act-gate-symbolic-guard-bypass "Verify that act-gate proceeds normally when no skill intercepts." (clrhash opencortex::*skills-registry*) (let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :cli :payload (:text "Hello))) (result (opencortex:act-gate signal))) (is (eq :acted (getf signal :status))) (is (null result)))) (test test-act-gate-symbolic-guard-interception "Verify that act-gate intercepts actions when a skill returns a LOG/EVENT." (clrhash opencortex::*skills-registry*) (opencortex::defskill :mock-bouncer :priority 200 :trigger (lambda (ctx) (declare (ignore ctx)) t) :deterministic (lambda (action ctx) (declare (ignore action ctx)) (list :type :LOG :payload (list :text "BLOCKED BY SYMBOLIC GUARD))) (let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :shell :payload (:cmd "ls))) (result (opencortex:act-gate signal))) (is (eq :acted (getf signal :status))) (is (not (null result))) (is (eq :LOG (getf result :type))) (let ((msg (getf (getf result :payload) :text))) (is (search "BLOCKED BY SYMBOLIC GUARD" msg))))) #+end_src